Skip to content

Instantly share code, notes, and snippets.

@cayblood
Created November 22, 2023 23:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cayblood/6d631327d08f21c8faef755467fae310 to your computer and use it in GitHub Desktop.
Save cayblood/6d631327d08f21c8faef755467fae310 to your computer and use it in GitHub Desktop.
// app/services/auth.server.ts
import { parse } from "cookie";
import { verify } from "jsonwebtoken";
import { findOrCreateUser } from "~/models/user.server";
import { createUserSession, logout } from "~/services/session.server";
import type { HankoAuthInfo } from "~/routes/login";
import { JwksClient } from "jwks-rsa";
export const extractHankoCookie = (request: Request) => {
const cookies = parse(request.headers.get("Cookie") || "");
return cookies.hanko;
};
export async function requireValidJwt(request: Request) {
const hankoBackend = `${process.env.HANKO_BACKEND_URL}/.well-known/jwks.json`;
const token = extractHankoCookie(request);
const client = new JwksClient({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: hankoBackend,
});
const key = await client.getSigningKey();
const publicKey = key.getPublicKey();
try {
return verify(token, publicKey, { complete: true });
} catch (err) {
throw await logout(request);
}
}
export async function loginUserFromSuccessfulHankoAuth(
request: Request,
authInfo: HankoAuthInfo,
redirectTo: string
) {
const user = await findOrCreateUser(authInfo.hankoId, authInfo.email);
return createUserSession({
request,
userId: user.id,
remember: false,
redirectTo,
});
}
// app/utils/hanko.client.ts
import { register } from "@teamhanko/hanko-elements";
import type { RegisterResult } from "@teamhanko/hanko-elements/dist/Elements";
import { Hanko } from "@teamhanko/hanko-frontend-sdk";
export { register, Hanko, RegisterResult };
// app/routes/login.tsx
import {
type ReactElement,
Suspense,
useCallback,
useEffect,
useState,
} from "react";
import {
register,
type Hanko,
type RegisterResult,
} from "~/utils/hanko.client.ts";
import type { ActionArgs } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";
import { loginUserFromSuccessfulHankoAuth } from "~/services/auth.server";
import { z } from "zod";
import { useMatchesData } from "~/utils/utils";
const hankoAuthInfo = z
.object({
hankoId: z.string().uuid(),
email: z.string().email(),
})
.required();
export type HankoAuthInfo = z.infer<typeof hankoAuthInfo>;
export const action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const authInfo: HankoAuthInfo = hankoAuthInfo.parse(
Object.fromEntries(formData),
);
const url = new URL(request.url);
const redirectTo = url.searchParams.get("redirectTo") || "/";
return loginUserFromSuccessfulHankoAuth(request, authInfo, redirectTo);
};
const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
});
const LoginForm = () => {
const [hanko, setHanko] = useState<Hanko>();
const fetcher = useFetcher();
const data = useMatchesData("root");
const schema = z.object({
ENV: z.object({
HANKO_URL: z.string().url(),
}),
});
const hankoUrl = schema.parse(data).ENV.HANKO_URL;
const redirectAfterLogin = useCallback(
async (hanko: Hanko) => {
// successfully logged in, redirect to a page in your application
if (hanko) {
const user = userSchema.parse(await hanko.user.getCurrent());
const d = { hankoId: user.id, email: user.email };
fetcher.submit(d, { method: "post" });
}
},
[fetcher],
);
useEffect(() => {
if (hanko) {
hanko.onAuthFlowCompleted(() => {
redirectAfterLogin(hanko);
});
}
}, [hanko, redirectAfterLogin]);
useEffect(() => {
register(hankoUrl)
.catch((error: Error) => {
console.error(error.message);
})
.then((result: RegisterResult | void) => {
if (result) {
setHanko(result.hanko);
}
});
}, [hankoUrl]);
return (
<div className="">
<Suspense fallback={"Loading..."}>
<hanko-auth />
</Suspense>
</div>
);
};
export default function Login() {
return (
<div className="fixed left-0 right-0 top-0 z-10 h-full">
<div className="flex min-h-full flex-col justify-center p-4">
<div className="w-full self-center rounded-lg bg-stone-100/80 py-8 backdrop-blur-sm sm:w-3/4 md:w-1/2 lg:w-1/3">
<LoginForm />
</div>
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment