Last active
May 17, 2023 14:42
-
-
Save ikovic/25dbd76189ade54f0e655395493a59f5 to your computer and use it in GitHub Desktop.
Server side auth with Remix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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} /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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