Guide
#
OverviewIntegrating SuperTokens into a NestJS backend is a bit different than the quick setup guide shows. We will add a few things:
- A module to house all authorization related code
- A service to initialize the SDK
- A middleware to add the authorization endpoints
- A global error handler to pass SuperTokens related errors to the SDK
- A guard to protect your API endpoints
- A parameter decorator to access the session in your code
We will cover each of these in the following few sections. Then, you can do the rest of the customizations by following the "Common customizations" section.
Please look here to see how to get started with your NestJS backend.
#
1) Installing SuperTokensnpm i -s supertokens-node
#
2) Adding a new moduleYou can scaffold a module using the nest CLI by running this in the root folder of the application:
nest g module auth
The result should be a new auth
folder with auth.module.ts
in it. We should convert this into a dynamic module so we can set parts of the SuperTokens configuration in the App module. Centralizing settings like this can be helpful for things like using a separate connection URI for testing.
#
Add config type and injection tokenAdd a config.interface.ts
file next into the auth
folder. We will put the type and injection token for the SuperTokens config here.
import { AppInfo } from "supertokens-node/types";
export const ConfigInjectionToken = "ConfigInjectionToken";
export type AuthModuleConfig = { appInfo: AppInfo; connectionURI: string; apiKey?: string;}
#
Convert to a dynamic moduleWe want to configure this module in the App module, so we add a static forRoot
method and convert it into a dynamic module.
import { MiddlewareConsumer, Module, NestModule, DynamicModule,} from '@nestjs/common';
import { AuthMiddleware } from './auth.middleware';import { ConfigInjectionToken, AuthModuleConfig } from './config.interface';
@Module({ providers: [], exports: [], controllers: [],})export class AuthModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(AuthMiddleware).forRoutes('*'); }
static forRoot({ connectionURI, apiKey, appInfo }: AuthModuleConfig): DynamicModule { return { providers: [ { useValue: { appInfo, connectionURI, apiKey, }, provide: ConfigInjectionToken, }, ], exports: [], imports: [], module: AuthModule, }; }}
#
Adding the module to the applicationYou need to update the App
module to use the new dynamic module by importing the result of forRoot
instead of the class itself.
// ...import { Module} from '@nestjs/common';import { AuthModule } from './auth/auth.module';
@Module({ imports: [ AuthModule.forRoot({ connectionURI: "", apiKey: "", appInfo: { // Learn more about this on https://supertokens.com/docs/thirdparty/appinfo appName: "<YOUR_APP_NAME>", apiDomain: "<YOUR_API_DOMAIN>", websiteDomain: "<YOUR_WEBSITE_DOMAIN>", apiBasePath: "/auth", websiteBasePath: "/auth" }, }), ], controllers: [/* ... */], providers: [/* ... */],})export class AppModule {}
#
3) Adding a serviceYou can scaffold this service using the nest CLI by running this in the root folder of the application:
nest g service supertokens auth
#
Move the new service into the dynamic moduleimport { MiddlewareConsumer, Module, NestModule, DynamicModule,} from '@nestjs/common';
import { AuthMiddleware } from './auth.middleware';import { ConfigInjectionToken, AuthModuleConfig } from './config.interface';import { SupertokensService } from './supertokens/supertokens.service';
@Module({ providers: [], exports: [], controllers: [],})export class AuthModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(AuthMiddleware).forRoutes('*'); }
static forRoot({ connectionURI, apiKey, appInfo }: AuthModuleConfig): DynamicModule { return { providers: [ { useValue: { appInfo, connectionURI, apiKey, }, provide: ConfigInjectionToken, }, SupertokensService, ], exports: [], imports: [], module: AuthModule, }; }}
#
Add service codeWe initialize the SDK in a service so that you can have access to injected services in event handlers. Edit the supertokens.service.ts
to match:
import { Inject, Injectable } from '@nestjs/common';import supertokens from "supertokens-node";import Session from 'supertokens-node/recipe/session';import ThirdParty from 'supertokens-node/recipe/thirdparty';
import { ConfigInjectionToken, AuthModuleConfig } from "../config.interface";
@Injectable()export class SupertokensService { constructor(@Inject(ConfigInjectionToken) private config: AuthModuleConfig) { supertokens.init({ appInfo: config.appInfo, supertokens: { connectionURI: config.connectionURI, apiKey: config.apiKey, }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ // We have provided you with development keys which you can use for testing. // IMPORTANT: Please replace them with your own OAuth keys for production use. ThirdParty.Google({ clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", clientSecret: "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" }), ThirdParty.Github({ clientId: "467101b197249757c71f", clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd" }), ThirdParty.Apple({ clientId: "4398792-io.supertokens.example.service", clientSecret: { keyId: "7M48Y4RYDL", privateKey: "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", teamId: "YWQCXGJRJL", }, }), // ThirdParty.Facebook({ // clientSecret: "FACEBOOK_CLIENT_SECRET", // clientId: "FACEBOOK_CLIENT_ID" // }) ] } }), Session.init(), ] }); }}
When you want to generate your own keys, please refer to the corresponding documentation to get your client ids and client secrets for each of the below providers:
- Generate your client ID and secret by following the docs here
- Set the authorisation callback URL to
<YOUR_WEBSITE_DOMAIN>/auth/callback/google
Github
- Generate your client ID and secret by following the docs here
- Set the authorisation callback URL to
<YOUR_WEBSITE_DOMAIN>/auth/callback/github
- Generate your client ID and secret by following the docs here
- Set the authorisation callback URL to
<YOUR_WEBSITE_DOMAIN>/auth/callback/facebook
Note
Make sure to enable https
to be able to use the test users of the Facebook app. On http://localhost
, the login flow can be verified only with the app's admin user.
Apple
- Generate your client ID and secret by following this article
- Set the authorisation callback URL to
<YOUR_API_DOMAIN>/auth/callback/apple
. Note that Apple doesn't allowlocalhost
in the URL. So if you are in dev mode, you can use the dev keys we have provided above.
middleware
#
4) Exposing SuperTokens APIs using its #
The middleware fileYou can scaffold the middleware by running nest g middleware auth auth
in the application's root folder.
The result should be in the auth module, called auth.middleware.ts
. Next, we need to edit this to use the middleware from supertokens.
import { Injectable, NestMiddleware } from "@nestjs/common";import { middleware } from 'supertokens-node/framework/express';
@Injectable()export class AuthMiddleware implements NestMiddleware { supertokensMiddleware: any;
constructor() { this.supertokensMiddleware = middleware(); }
use(req: Request, res: any, next: () => void) { return this.supertokensMiddleware(req, res, next); }}
#
Registering the middlewareWe need to edit the module file to register the middleware. You can achieve this by implementing a configure
method in the AuthModule
class.
import { DynamicModule, MiddlewareConsumer, Module, NestModule } from "@nestjs/common";import { AuthMiddleware } from "./auth.middleware";// ...
export class AuthModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(AuthMiddleware).forRoutes('*'); } // ...}
#
5) Update CORS settingsYou should enable and update your CORS settings in main.ts
:
import { NestFactory } from '@nestjs/core';// ...import supertokens from 'supertokens-node';import { AppModule } from './app.module';
async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ origin: ['<YOUR_WEBSITE_DOMAIN>'], allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()], credentials: true, });
await app.listen(3000);}
bootstrap()
#
6) Add the SuperTokens error handlerWe add the SuperTokens error handler through a NestJS exception filter.
#
Exception filterYou can scaffold the exception filter using the CLI by: nest g filter auth auth
. This will result in a new auth.filter.ts
file next to the auth module. We need to edit this to add our error handler.
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';import { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
import { errorHandler } from 'supertokens-node/framework/express';import { Error as STError } from 'supertokens-node';
@Catch(STError)export class SupertokensExceptionFilter implements ExceptionFilter { handler: ErrorRequestHandler;
constructor() { this.handler = errorHandler(); }
catch(exception: Error, host: ArgumentsHost) { const ctx = host.switchToHttp();
const resp = ctx.getResponse<Response>(); if (resp.headersSent) { return; }
this.handler( exception, ctx.getRequest<Request>(), resp, ctx.getNext<NextFunction>(), ); }}
#
Registering the filterWe need to add this filter as a global exception filter. You can do this in main.ts
, right after the updated cors settings.
import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';
import supertokens from 'supertokens-node';import { SupertokensExceptionFilter } from './auth/auth.filter';
async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ origin: ['http://localhost:3001'], // TODO: URL of the website domain allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()], credentials: true, });
app.useGlobalFilters(new SupertokensExceptionFilter());
await app.listen(3000);}
bootstrap();
#
7) Add session verification guardNow that the library is set up, you can add a guard to protect your API. You can scaffold this nest g guard auth auth
. This results in auth.guard.ts
that we can edit to implement session verification.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';import { Error as STError } from "supertokens-node";
import { verifySession } from 'supertokens-node/recipe/session/framework/express';import { VerifySessionOptions } from 'supertokens-node/recipe/session';
@Injectable()export class AuthGuard implements CanActivate { constructor(private readonly verifyOptions?: VerifySessionOptions) {}
async canActivate(context: ExecutionContext): Promise<boolean> { const ctx = context.switchToHttp();
let err = undefined; const resp = ctx.getResponse(); // You can create an optional version of this by passing {sessionRequired: false} to verifySession await verifySession(this.verifyOptions)( ctx.getRequest(), resp, (res) => { err = res; }, );
if (resp.headersSent) { throw new STError({ message: "RESPONSE_SENT", type: "RESPONSE_SENT", }); }
if (err) { throw err; }
return true; }}
#
8) Add a parameter decoratorNow you can add a parameter decorator to access the already verified session in your APIs. You can generate an empty decorator by running nest g decorator session auth
. Edit session.decorator.ts
to return the session attached to the request:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const Session = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.session; },);
#
9) Combine the decorator and the guard to authenticate usersYou can add a protected method into a controller (e.g.: App.controller.ts
) that receives the verified session as a parameter by:
import { Controller, Get, UseGuards } from '@nestjs/common';// ...import { SessionContainer } from "supertokens-node/recipe/session";import { AuthGuard } from './auth/auth.guard';import { Session } from './auth/session.decorator';// ...
@Controller()export class AppController { // ... @Get('test') @UseGuards(new AuthGuard()) async getTest(@Session() session: SessionContainer): Promise<string> { // TODO: magic return "magic"; }}
You should look at the Sessions section under Common Customizations to see how you can use the session object.
#
10) Setup the SuperTokens corehttps://try.supertokens.com
as the connection URI in the init function?