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
- User authenticates with your auth provider
- Auth provider issues a JWT token
- Client includes token in API requests
- APSO backend verifies the token
- 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);
}
}Related
Last updated on