Skip to Content
🚀 APSO is now in public beta. Get started →

Bring Your Own Auth (BYOA)

APSO supports any authentication provider that issues JWT tokens. This guide shows how to integrate popular auth services.

How It Works

  1. User authenticates with your auth provider
  2. Auth provider issues a JWT token
  3. Client includes token in API requests
  4. APSO backend verifies the token
  5. User context is extracted for authorization

JWT Requirements

Your JWT tokens must include:

{ "sub": "user-id", // Required: User identifier "email": "user@example.com", // Recommended "organizationId": "org-id", // Required for multi-tenancy "iat": 1699000000, // Issued at "exp": 1699086400 // Expiration }

Auth0

Configuration

// src/config/auth.config.ts export const authConfig = { issuer: `https://${process.env.AUTH0_DOMAIN}/`, audience: process.env.AUTH0_AUDIENCE, };

JWT Verification

// src/common/guards/auth.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { expressjwt, GetVerificationKey } from 'express-jwt'; import { expressJwtSecret } from 'jwks-rsa'; @Injectable() export class AuthGuard implements CanActivate { private jwtCheck = expressjwt({ secret: expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`, }) as GetVerificationKey, audience: process.env.AUTH0_AUDIENCE, issuer: `https://${process.env.AUTH0_DOMAIN}/`, algorithms: ['RS256'], }); async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const response = context.switchToHttp().getResponse(); return new Promise((resolve, reject) => { this.jwtCheck(request, response, (err) => { if (err) { reject(new UnauthorizedException()); } resolve(true); }); }); } }

Adding Organization ID

Use Auth0 Actions to add organizationId to tokens:

// Auth0 Action: Add Organization to Token exports.onExecutePostLogin = async (event, api) => { const organizationId = event.user.app_metadata?.organizationId; if (organizationId) { api.accessToken.setCustomClaim('organizationId', organizationId); } };

Firebase Auth

Configuration

// src/config/firebase.config.ts import * as admin from 'firebase-admin'; admin.initializeApp({ credential: admin.credential.cert({ projectId: process.env.FIREBASE_PROJECT_ID, clientEmail: process.env.FIREBASE_CLIENT_EMAIL, privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'), }), }); export const firebaseAuth = admin.auth();

JWT Verification

// src/common/guards/firebase-auth.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { firebaseAuth } from '../../config/firebase.config'; @Injectable() export class FirebaseAuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const token = this.extractToken(request); if (!token) { throw new UnauthorizedException(); } try { const decodedToken = await firebaseAuth.verifyIdToken(token); // Map Firebase claims to APSO user request.user = { id: decodedToken.uid, email: decodedToken.email, organizationId: decodedToken.organizationId, // Custom claim }; return true; } catch { throw new UnauthorizedException(); } } private extractToken(request: any): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; } }

Adding Custom Claims

// Set organization on user creation await admin.auth().setCustomUserClaims(uid, { organizationId: 'org-uuid', });

Supabase Auth

Configuration

// src/config/supabase.config.ts import { createClient } from '@supabase/supabase-js'; export const supabase = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY! );

JWT Verification

// src/common/guards/supabase-auth.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { supabase } from '../../config/supabase.config'; @Injectable() export class SupabaseAuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const token = this.extractToken(request); if (!token) { throw new UnauthorizedException(); } const { data: { user }, error } = await supabase.auth.getUser(token); if (error || !user) { throw new UnauthorizedException(); } request.user = { id: user.id, email: user.email, organizationId: user.user_metadata?.organizationId, }; return true; } private extractToken(request: any): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; } }

Clerk

Configuration

// src/config/clerk.config.ts import { createClerkClient } from '@clerk/clerk-sdk-node'; export const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY, });

JWT Verification

// src/common/guards/clerk-auth.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { clerk } from '../../config/clerk.config'; @Injectable() export class ClerkAuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const token = this.extractToken(request); if (!token) { throw new UnauthorizedException(); } try { const session = await clerk.sessions.verifySession( request.cookies.__session, token ); const user = await clerk.users.getUser(session.userId); request.user = { id: user.id, email: user.emailAddresses[0]?.emailAddress, organizationId: user.publicMetadata?.organizationId as string, }; return true; } catch { throw new UnauthorizedException(); } } private extractToken(request: any): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; } }

Custom JWT Provider

Generic Implementation

// src/common/guards/jwt-auth.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import * as jwt from 'jsonwebtoken'; @Injectable() export class JwtAuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const token = this.extractToken(request); if (!token) { throw new UnauthorizedException(); } try { const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; email: string; organizationId: string; }; request.user = { id: payload.sub, email: payload.email, organizationId: payload.organizationId, }; return true; } catch { throw new UnauthorizedException(); } } private extractToken(request: any): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; } }

User Context Decorator

// src/common/decorators/current-user.decorator.ts import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export interface UserContext { id: string; email: string; organizationId: string; } export const CurrentUser = createParamDecorator( (data: unknown, ctx: ExecutionContext): UserContext => { const request = ctx.switchToHttp().getRequest(); return request.user; }, );

Using in Controllers

// src/modules/projects/projects.controller.ts @Controller('projects') @UseGuards(AuthGuard) // Use your chosen guard export class ProjectsController { @Get() findAll(@CurrentUser() user: UserContext) { // user.organizationId is available for scoping return this.projectsService.findAll(user.organizationId); } }
Last updated on