|
| 1 | +import jose from "jose"; |
| 2 | + |
| 3 | +import SuperTokens from "../../../superTokens"; |
| 4 | + |
| 5 | +import type { AccessTokenPayload, LoadedSessionContext } from "../types"; |
| 6 | + |
| 7 | +const COOKIE_ACCESS_TOKEN_NAME = "sAccessToken"; |
| 8 | +const HEADER_ACCESS_TOKEN_NAME = "st-access-token"; |
| 9 | +const FRONT_TOKEN_NAME = "sFrontToken"; |
| 10 | + |
| 11 | +type CookiesStore = { |
| 12 | + get: (name: string) => { value: string }; |
| 13 | +}; |
| 14 | + |
| 15 | +function isCookiesStore(obj: unknown): obj is CookiesStore { |
| 16 | + return typeof obj === "object" && obj !== null && "get" in obj && typeof (obj as CookiesStore).get === "function"; |
| 17 | +} |
| 18 | + |
| 19 | +type CookiesObject = Record<string, string>; |
| 20 | + |
| 21 | +type GetServerSidePropsRedirect = { |
| 22 | + redirect: { destination: string; permanent: boolean }; |
| 23 | +}; |
| 24 | +type GetServerSidePropsReturnValue = |
| 25 | + | { |
| 26 | + props: { session: LoadedSessionContext }; |
| 27 | + } |
| 28 | + | GetServerSidePropsRedirect; |
| 29 | + |
| 30 | +type SSRSessionState = |
| 31 | + | "front-token-not-found" |
| 32 | + | "front-token-expired" |
| 33 | + | "access-token-not-found" |
| 34 | + | "tokens-do-not-match" |
| 35 | + | "tokens-match"; |
| 36 | + |
| 37 | +export async function getSSRSession( |
| 38 | + cookies: CookiesStore, |
| 39 | + redirect: (url: string) => never |
| 40 | +): Promise<LoadedSessionContext>; |
| 41 | +export async function getSSRSession(cookies: CookiesObject): Promise<GetServerSidePropsReturnValue>; |
| 42 | +export async function getSSRSession( |
| 43 | + cookies: CookiesObject | CookiesStore, |
| 44 | + redirect?: (url: string) => never |
| 45 | +): Promise<LoadedSessionContext | GetServerSidePropsReturnValue> { |
| 46 | + if (isCookiesStore(cookies)) { |
| 47 | + if (!redirect) { |
| 48 | + throw new Error("Undefined redirect function"); |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + const { state, session } = await getSSRSessionState(cookies); |
| 53 | + switch (state) { |
| 54 | + case "front-token-not-found": |
| 55 | + if (!redirect) { |
| 56 | + return { redirect: { destination: getWebsiteBasePath(), permanent: false } }; |
| 57 | + } else { |
| 58 | + return redirect(getWebsiteBasePath()); |
| 59 | + } |
| 60 | + case "front-token-expired": |
| 61 | + case "access-token-not-found": |
| 62 | + case "tokens-do-not-match": |
| 63 | + if (!redirect) { |
| 64 | + return { redirect: { destination: `${getApiBasePath()}/refresh`, permanent: false } }; |
| 65 | + } else { |
| 66 | + return redirect(`${getApiBasePath()}/refresh`); |
| 67 | + } |
| 68 | + case "tokens-match": |
| 69 | + if (!redirect) { |
| 70 | + return { props: { session: session as LoadedSessionContext } }; |
| 71 | + } |
| 72 | + return session as LoadedSessionContext; |
| 73 | + default: |
| 74 | + // This is here just to prevent typescript from complaining |
| 75 | + // about the function not returning a value |
| 76 | + throw new Error(`Unknown state: ${state}`); |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +function getCookieValue(cookieStore: CookiesStore | CookiesObject, name: string): string | undefined { |
| 81 | + if (isCookiesStore(cookieStore)) { |
| 82 | + return cookieStore.get(name)?.value; |
| 83 | + } |
| 84 | + return cookieStore[name]; |
| 85 | +} |
| 86 | + |
| 87 | +async function getSSRSessionState( |
| 88 | + cookies: CookiesObject | CookiesStore |
| 89 | +): Promise<{ state: SSRSessionState; session?: LoadedSessionContext }> { |
| 90 | + const frontToken = getCookieValue(cookies, FRONT_TOKEN_NAME); |
| 91 | + if (!frontToken) { |
| 92 | + return { state: "front-token-not-found" }; |
| 93 | + } |
| 94 | + |
| 95 | + const parsedFrontToken = parseFrontToken(frontToken); |
| 96 | + if (parsedFrontToken.up?.exp && parsedFrontToken.up.exp < Date.now()) { |
| 97 | + return { state: "front-token-expired" }; |
| 98 | + } |
| 99 | + |
| 100 | + const accessToken = |
| 101 | + getCookieValue(cookies, COOKIE_ACCESS_TOKEN_NAME) || getCookieValue(cookies, HEADER_ACCESS_TOKEN_NAME); |
| 102 | + if (!accessToken) { |
| 103 | + return { state: "access-token-not-found" }; |
| 104 | + } |
| 105 | + |
| 106 | + const parsedAccessToken = await parseAccessToken(accessToken); |
| 107 | + if (!comparePayloads(parsedFrontToken, parsedAccessToken)) { |
| 108 | + return { state: "tokens-do-not-match" }; |
| 109 | + } |
| 110 | + |
| 111 | + return { |
| 112 | + state: "tokens-match", |
| 113 | + session: { |
| 114 | + userId: parsedAccessToken.up.sub, |
| 115 | + accessTokenPayload: parsedAccessToken, |
| 116 | + doesSessionExist: true, |
| 117 | + loading: false, |
| 118 | + invalidClaims: [], |
| 119 | + accessDeniedValidatorError: undefined, |
| 120 | + }, |
| 121 | + }; |
| 122 | +} |
| 123 | + |
| 124 | +const getApiBasePath = () => { |
| 125 | + return SuperTokens.getInstanceOrThrow().appInfo.apiBasePath.getAsStringDangerous(); |
| 126 | +}; |
| 127 | + |
| 128 | +const getWebsiteBasePath = () => { |
| 129 | + return SuperTokens.getInstanceOrThrow().appInfo.websiteBasePath.getAsStringDangerous(); |
| 130 | +}; |
| 131 | + |
| 132 | +function parseFrontToken(frontToken: string): AccessTokenPayload { |
| 133 | + return JSON.parse(decodeURIComponent(escape(atob(frontToken)))); |
| 134 | +} |
| 135 | + |
| 136 | +async function parseAccessToken(token: string): Promise<AccessTokenPayload> { |
| 137 | + const JWKS = jose.createRemoteJWKSet(new URL(`${getApiBasePath()}/authjwt/jwks.json`)); |
| 138 | + const { payload } = await jose.jwtVerify(token, JWKS); |
| 139 | + return payload; |
| 140 | +} |
| 141 | + |
| 142 | +function comparePayloads(payload1: AccessTokenPayload, payload2: AccessTokenPayload): boolean { |
| 143 | + return JSON.stringify(payload1) === JSON.stringify(payload2); |
| 144 | +} |
0 commit comments