Skip to content

Instantly share code, notes, and snippets.

@ikovic
Last active May 17, 2023 14:42
Show Gist options
  • Save ikovic/25dbd76189ade54f0e655395493a59f5 to your computer and use it in GitHub Desktop.
Save ikovic/25dbd76189ade54f0e655395493a59f5 to your computer and use it in GitHub Desktop.
Server side auth with Remix
// login route action handler receives the token as form data and persists it as a cookie
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const token = formData.get("token") as string;
const redirectTo = getRedirectToFromForm(formData);
if (!token) return handleFailure(request);
await handleSignInSuccess(token);
return createUserSession({ request, token, redirectTo });
}
// inside the default export that renders the login form
// create an onSuccess handler that reads the token and submits it as form data
const handleSuccess = async ({ token }: User) => {
submit(
{
token,
redirectTo: getRedirectToFromParams(searchParams),
action: Action.SIGN_IN,
},
{ action: LOGIN_PATH, method: "post", replace: true }
);
};
// SlashID <Form>
<Form onSuccess={handleSuccess} />
// in any route you can get the token from the cookie in the request object
import { SSR } from "@slashid/slashid";
export async function loader({ request }: LoaderArgs): LoaderResponseData {
// you can validate the token here
// strict option is to call validateToken(request) (imported from session.server.tsx)
// this will call the SlashID API => adds a roundtrip
// alternatively you can use a JWT validation library and validate offline against our JWKS: https://api.sandbox.slashid.com/.well-known/jwks.json
const token = await getToken(request);
const user = new SSR.User(token);
const attributes = {} // use the user to fetch attributes or just use the ID to fetch data from the DB
return json({
token,
attributes
});
}
import { SSR } from "@slashid/slashid";
import { createCookieSessionStorage, redirect } from "@vercel/remix";
import type { Token } from "~/domain/user/types";
import { HOME_PATH, LOGIN_PATH } from "../domain/paths";
export function getCurrentPath(request: Request) {
return new URL(request.url).pathname;
}
const USER_SESSION_KEY = "token";
const MAX_COOKIE_AGE = 60 * 60 * 24 * 7; // 7 days -> should actually be in sync with the SlashID token expiry,
const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
path: HOME_PATH,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
},
});
function composeLoginPath(redirectTo: string) {
const searchParams = new URLSearchParams([["redirectTo", redirectTo]]);
return `${LOGIN_PATH}?${searchParams}`;
}
async function getSession(request: Request) {
const cookie = request.headers.get("Cookie");
return sessionStorage.getSession(cookie);
}
export async function createUserSession({
request,
token,
redirectTo,
}: {
request: Request;
token: string;
redirectTo: string;
}) {
const session = await getSession(request);
session.set(USER_SESSION_KEY, token);
return redirect(redirectTo, {
headers: {
"Set-Cookie": await sessionStorage.commitSession(session, {
maxAge: MAX_COOKIE_AGE,
}),
},
});
}
export async function handleFailure(request: Request) {
const session = await getSession(request);
session.flash("error", "Missing or invalid token");
// Redirect back to the login page with errors.
return redirect(LOGIN_PATH, {
headers: {
"Set-Cookie": await sessionStorage.commitSession(session, {
maxAge: MAX_COOKIE_AGE,
}),
},
});
}
export async function logOutFromSession(
request: Request,
redirectTo: string = getCurrentPath(request)
) {
const session = await getSession(request);
// TODO before destroying the session we should get the token, create the SSR.User(token) with it and then call user.logout() to invalidate the token
return redirect(composeLoginPath(redirectTo), {
headers: {
"Set-Cookie": await sessionStorage.destroySession(session),
},
});
}
export async function getToken(request: Request): Promise<Token> {
const session = await getSession(request);
const token = session.get(USER_SESSION_KEY);
return token;
}
export async function validateToken(request: Request) {
const token = await getToken(request);
if (!token) return false;
const user = new SSR.User(token);
try {
const { valid } = await user.validateToken();
return valid;
} catch (e) {
console.error(e);
}
return false;
}
export async function validateRequest(
request: Request,
redirectTo: string = getCurrentPath(request)
) {
const valid = await validateToken(request);
if (!valid) throw redirect(composeLoginPath(redirectTo));
}
// TODO - at the moment we also store the token in local storage for uninterrupted client side navigation
// in the next couple of days we'll publish an update to the React SDK so this is no longer needed
export const SlashID = ({ children }: Props) => {
return (
<SlashIDProvider
baseApiUrl="https://api.sandbox.slashid.com"
oid="YOUR_OID"
tokenStorage="localStorage"
>
{children}
</SlashIDProvider>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment