Session verification during server side rendering
init
function on your web server#
1) Call the backend If your web server is a different process than the API server, then you should call the SuperTokens backend SDK init
function with the Session recipe initialised.
If the web server domain is on a different sub domain than the api domain, then be sure to enable cookie sharing across backend sub domains
getSession
function#
2) Use the For server side rendering, we can utilise the getSession
function for session verification. The browser will send session cookies along with the request which will be verified by this function resulting in one of these states:
- Successful verification: This will yield a session object using which you can get the userId of the user.
- Try refresh token error: This means that the access token has expired and that you should trigger the refresh flow (more on this below).
- Unauthorised error: This means that the session does not exist, was revoked, or was compromised. Unintuitively, this should also trigger the refresh flow (more on this below) - which will fail and eventually redirect the user to the login screen.
Below is code for how you can use getSession
to achieve SSR and get the user's ID from the session:
- NodeJS
- GoLang
- Python
- Express
- Hapi
- Fastify
- Koa
- Loopback
- AWS Lambda / Netlify
- Next.js
- NestJS
import express from "express";import Session from "supertokens-node/recipe/session";import { Error as SuperTokensError } from "supertokens-node";
let app = express();
app.get("/dashboard", async (req, res, next) => { try { let session = await Session.getSession(req, res, { overrideGlobalClaimValidators: () => { // this makes it so that no custom session claims are checked return [] } });
let userId = session.getUserId(); //... } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN || err.type === Session.Error.UNAUTHORISED) { res.redirect("/refresh-session?redirectBack=/dashboard"); } else { next(err); } } else { next(err) } }});
import Hapi from "@hapi/hapi";import Session from "supertokens-node/recipe/session";import { Error as SuperTokensError } from "supertokens-node";
let server = Hapi.server({ port: 8000 });
server.route({ path: "/dashboard", method: "get", handler: async (req, res) => { try { let session = await Session.getSession(req, res, { overrideGlobalClaimValidators: () => { // this makes it so that no custom session claims are checked return [] } });
let userId = session.getUserId(); //... } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN || err.type === Session.Error.UNAUTHORISED) { return res.redirect("/refresh-session?redirectBack=/dashboard"); } } throw err; } }})
import Fastify from "fastify";import Session from "supertokens-node/recipe/session";import { Error as SuperTokensError } from "supertokens-node";
let fastify = Fastify();
fastify.get("/dashboard", async (req, res) => { try { let session = await Session.getSession(req, res, { overrideGlobalClaimValidators: () => { // this makes it so that no custom session claims are checked return [] } });
let userId = session.getUserId(); //... } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN || err.type === Session.Error.UNAUTHORISED) { return res.redirect("/refresh-session?redirectBack=/dashboard"); } } throw err; }});
import Session from "supertokens-node/recipe/session";import { middleware } from "supertokens-node/framework/awsLambda";import { SessionEvent } from "supertokens-node/framework/awsLambda";import { Error as SuperTokensError } from "supertokens-node";
async function viewDashboard(awsEvent: SessionEvent, context: any, callback: any) { try { let session = await Session.getSession(awsEvent, awsEvent, { overrideGlobalClaimValidators: () => { // this makes it so that no custom session claims are checked return [] } });
let userId = session.getUserId(); //... } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN || err.type === Session.Error.UNAUTHORISED) { return { statusCode: 302, headers: { Location: '/refresh-session?redirectBack=/dashboard', }, } } } throw err; }};
exports.handler = middleware(viewDashboard);
import KoaRouter from "koa-router";import Session from "supertokens-node/recipe/session";import { Error as SuperTokensError } from "supertokens-node";
let router = new KoaRouter();
router.get("/dashboard", async (ctx, next) => { try { let session = await Session.getSession(ctx, ctx, { overrideGlobalClaimValidators: () => { // this makes it so that no custom session claims are checked return [] } });
let userId = session.getUserId(); //... } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN || err.type === Session.Error.UNAUTHORISED) { return ctx.redirect("/refresh-session?redirectBack=/dashboard"); } } throw err; }});
import { inject } from "@loopback/core";import { RestBindings, MiddlewareContext, get, response } from "@loopback/rest";import Session from "supertokens-node/recipe/session";import { Error as SuperTokensError } from "supertokens-node";
class Dashboard { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @get("/dashboard") @response(200) async handler() { try { let session = await Session.getSession(this.ctx, this.ctx, { overrideGlobalClaimValidators: () => { // this makes it so that no custom session claims are checked return [] } });
let userId = session.getUserId(); //... } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN || err.type === Session.Error.UNAUTHORISED) { return this.ctx.response.redirect("/refresh-session?redirectBack=/dashboard"); } } throw err; } }}
important
Please visit the Integrations -> NextJS -> Session verification -> getServerSideProps
guide
import { Controller, Get, UseGuards, Req, Res } from "@nestjs/common";import type { Request, Response } from "express";import Session from "supertokens-node/recipe/session";import { Error as SuperTokensError } from "supertokens-node";
@Controller()export class DashboardController { @Get('dashboard') async getDashboard(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise<void> { try { let session = await Session.getSession(req, res, { overrideGlobalClaimValidators: () => { // this makes it so that no custom session claims are checked return [] } });
let userId = session.getUserId(); //... } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN || err.type === Session.Error.UNAUTHORISED) { return res.redirect("/refresh-session?redirectBack=/dashboard"); } } throw err; } }}
import ( "fmt" "net/http"
defaultErrors "errors"
"github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/claims" "github.com/supertokens/supertokens-golang/recipe/session/errors" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens")
func showDashboard(w http.ResponseWriter, r *http.Request) { sessionContainer, err := session.GetSession(r, w, &sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { // this makes it so that no custom session claims are checked return []claims.SessionClaimValidator{}, nil }, })
if err != nil { err = supertokens.ErrorHandler(err, r, w) if err != nil { if defaultErrors.As(err, &errors.TryRefreshTokenError{}) || defaultErrors.As(err, &errors.UnauthorizedError{}) { http.RedirectHandler("/refresh-session?redirectBack=/dashboard", http.StatusFound).ServeHTTP(w, r) return } } // TODO: send 500 error to the frontend return }
userID := sessionContainer.GetUserID() fmt.Println(userID) // ...}
- FastAPI
- Flask
- Django
from supertokens_python.recipe.session.asyncio import get_sessionfrom fastapi.requests import Requestfrom fastapi.responses import RedirectResponsefrom supertokens_python.recipe.session.exceptions import ( UnauthorisedError, TryRefreshTokenError)
@app.get('/dashboard') async def dashboard(request: Request): try: session = await get_session(request, override_global_claim_validators=lambda global_validators, session, user_context: [])
if session is None: raise Exception("Should never come here")
user_id = session.get_user_id()
print(user_id) # TODO except Exception as e: if isinstance(e, TryRefreshTokenError) or isinstance(e, UnauthorisedError): return RedirectResponse( '/refresh-session?redirectBack=/dashboard', status_code=302) raise e
from supertokens_python.recipe.session.syncio import get_sessionfrom flask.wrappers import Requestfrom flask import redirectfrom supertokens_python.recipe.session.exceptions import ( UnauthorisedError, TryRefreshTokenError)
@app.route('/dashboard', methods=['GET']) def dashboard(request: Request): try: session = get_session(request, override_global_claim_validators=lambda global_validators, session, user_context: [])
if session is None: raise Exception("Should never come here")
user_id = session.get_user_id()
print(user_id) # TODO except Exception as e: if isinstance(e, TryRefreshTokenError) or isinstance(e, UnauthorisedError): return redirect( '/refresh-session?redirectBack=/dashboard', code=302) raise e
from supertokens_python.recipe.session.asyncio import get_sessionfrom django.http import HttpRequestfrom django.shortcuts import redirectfrom supertokens_python.recipe.session.exceptions import ( UnauthorisedError, TryRefreshTokenError)
async def dashboard(request: HttpRequest): try: session = await get_session(request, override_global_claim_validators=lambda global_validators, session, user_context: []) if session is None: raise Exception("Should never come here")
user_id = session.get_user_id()
print(user_id) # TODO except Exception as e: if isinstance(e, TryRefreshTokenError) or isinstance(e, UnauthorisedError): return redirect( '/refresh-session?redirectBack=/dashboard', code=302) raise e
important
On successful verification, please do not update the access token payload during SSR because:
- This is
GET
request, which usually means no state change. - The change to the access token (due to payload update) will not be correctly reflected on the frontend.
#
3) Implementing the refresh session flowFrom the above code snippets, if the getSession
function throws an unauthorised or try refresh token error, we redirect the user to the /refresh-session
path on the client side. On this path, we want to attempt refreshing the session which can yield:
- Success: The frontend will get new access and refresh tokens. Post this, we want to redirect the user to the path mentioned on the
redirectBack
query param. - Failure: This would happen if the session has expired or has been revoked from the backend. Either way, you want to redirect the user to the login page.
Below is the code snippet that you can use on the /refresh-session
path on the frontend
- ReactJS
- Angular
- Vue
import Session from 'supertokens-web-js/recipe/session';
function attemptRefresh() { Session.attemptRefreshingSession().then(success => { if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); window.location.href = urlParams.get('redirectBack')!; } else { // we redirect to the login page since the user // is now logged out window.location.href = "/login" } })}
import React from "react";import Session from "supertokens-auth-react/recipe/session"import SuperTokens from "supertokens-auth-react";
export function AttemptRefresh(props: any) { React.useEffect(() => { let cancel = false; Session.attemptRefreshingSession().then(success => { if (cancel) { // component has unmounted somehow.. return; } if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); window.location.href = urlParams.get('redirectBack')!; } else { // we redirect to the login page since the user // is now logged out SuperTokens.redirectToAuth(); } }) return () => { cancel = true; } }, []); return null;}
import Session from 'supertokens-web-js/recipe/session';
function attemptRefresh() { Session.attemptRefreshingSession().then(success => { if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); window.location.href = urlParams.get('redirectBack')!; } else { // we redirect to the login page since the user // is now logged out window.location.href = "/login" } })}
UNAUTHORISED
error?#
Why do we trigger the refresh session flow on There are two reasons why getSession
throws an UNAUTHORISED
error:
- The session tokens were not passed from the frontend: This can happen if the route is called when the user has logged out.
- The session tokens are passed from the frontend, but has been revoked from the backend: This can happen if the session was somehow revoked in offline mode. Here, whilst the frontend "thinks" that the session exists, it doesn't actually exist.
When this error is thrown, if we redirect the user to the login page directly, then in the second case, the frontend will redirect the user back to the current route (since it thinks that the session exists) causing an infinite loop. To counteract this issue, we redirect the user to refreshing a session page which will attempt a session refresh and fail. On failure, we will then redirect the user to the login page on the frontend. This will also cause the frontend state to be corrected so that it knows that the session doesn't exist anymore.
#
How can I do optional session verification during SSR?Checkout this section on how to pass the optional config when calling the getSession
function.