platform-authentication
Full-featured authentication library supporting JWT tokens, OAuth social login (GitHub, Google, Microsoft, Apple), multi-factor authentication (TOTP, SMS, Email OTP, Push), session management, and email verification. Fully decoupled from user storage via 6 abstract ports.
Package: @breadstone/archipel-platform-authentication
Quick Start
import { Module } from '@nestjs/common';
import { AuthModule } from '@breadstone/archipel-platform-authentication';
@Module({
imports: [
AuthModule.register({
authSubject: PrismaAuthSubjectAdapter,
mfaSubject: PrismaMfaSubjectAdapter,
sessionPersistence: PrismaSessionAdapter,
verificationSubject: PrismaVerificationAdapter,
socialAuth: PrismaSocialAuthAdapter, // optional — enables OAuth
tokenEnricher: AppTokenEnricherAdapter, // optional — enriches JWT claims
}),
],
})
export class AppModule {}Module Configuration
IAuthModuleOptions
| Property | Type | Required | Description |
|---|---|---|---|
authSubject | Type<AuthSubjectPort> | Yes | User lookup for JWT, Local, and Anonymous strategies |
mfaSubject | Type<MfaSubjectPort> | Yes | MFA state persistence (secrets, channels, backup codes) |
sessionPersistence | Type<SessionPersistencePort> | Yes | Session storage, querying, and invalidation |
verificationSubject | Type<VerificationSubjectPort> | Yes | Email/PIN verification lifecycle |
socialAuth | Type<SocialAuthPort> | No | OAuth user creation/linking. When provided, GitHub/Google/Microsoft/Apple strategies are registered. |
tokenEnricher | Type<TokenEnricherPort> | No | Custom JWT claim injection. When provided, additional claims are merged into access tokens. |
Environment Variables
| Variable | Description | Required |
|---|---|---|
AUTH_JWT_SECRET | JWT signing secret | Yes |
AUTH_JWT_EXPIRES_IN | Token expiration (e.g. '15m', '1h', '7d') | Yes |
AUTH_GITHUB_CLIENT_ID | GitHub OAuth client ID | If using GitHub OAuth |
AUTH_GITHUB_CLIENT_SECRET | GitHub OAuth client secret | If using GitHub OAuth |
AUTH_GOOGLE_CLIENT_ID | Google OAuth client ID | If using Google OAuth |
AUTH_GOOGLE_CLIENT_SECRET | Google OAuth client secret | If using Google OAuth |
AUTH_MICROSOFT_CLIENT_ID | Microsoft OAuth client ID | If using Microsoft OAuth |
AUTH_MICROSOFT_CLIENT_SECRET | Microsoft OAuth client secret | If using Microsoft OAuth |
AUTH_APPLE_CLIENT_ID | Apple OAuth client ID | If using Apple OAuth |
AUTH_APPLE_TEAM_ID | Apple team ID | If using Apple OAuth |
AUTH_APPLE_KEY_ID | Apple key ID | If using Apple OAuth |
AUTH_APPLE_PRIVATE_KEY | Apple private key (PEM) | If using Apple OAuth |
SEED_ANONYMOUS_USERNAME | Username for anonymous/seed user | If using Anonymous strategy |
Ports (Contracts)
AuthSubjectPort
Resolves authentication subjects for JWT validation, local login, and anonymous access.
abstract class AuthSubjectPort {
abstract findById(id: string): Promise<IAuthSubject | null>;
abstract findByLogin(login: string): Promise<IAuthSubject | null>;
abstract findAnonymous(userName: string): Promise<IAuthSubject | null>;
}IAuthSubject Interface:
interface IAuthSubject {
readonly id: string;
readonly userName: string | null;
readonly email: string | null;
readonly password: string | null;
readonly isVerified: boolean;
readonly isAnonymous: boolean;
readonly roles: ReadonlyArray<string>;
readonly mfaEnabled: boolean;
}Real-World Adapter: Prisma
@Injectable()
export class PrismaAuthSubjectAdapter extends AuthSubjectPort {
private readonly _prisma: PrismaService;
constructor(prisma: PrismaService) {
super();
this._prisma = prisma;
}
public async findById(id: string): Promise<IAuthSubject | null> {
return this._prisma.user.findUnique({
where: { id },
select: {
id: true,
userName: true,
email: true,
password: true,
isVerified: true,
isAnonymous: true,
roles: true,
mfaEnabled: true,
},
});
}
public async findByLogin(login: string): Promise<IAuthSubject | null> {
return this._prisma.user.findFirst({
where: { OR: [{ userName: login }, { email: login }] },
select: {
id: true,
userName: true,
email: true,
password: true,
isVerified: true,
isAnonymous: true,
roles: true,
mfaEnabled: true,
},
});
}
public async findAnonymous(userName: string): Promise<IAuthSubject | null> {
return this._prisma.user.findFirst({
where: { userName, isAnonymous: true },
select: {
id: true,
userName: true,
email: true,
password: true,
isVerified: true,
isAnonymous: true,
roles: true,
mfaEnabled: true,
},
});
}
}SessionPersistencePort
Persists and queries user sessions.
abstract class SessionPersistencePort {
abstract store(args: IStoreSessionArgs): Promise<void>;
abstract findByToken(token: string): Promise<ISessionRecord | null>;
abstract findAllByUserId(userId: string): Promise<ReadonlyArray<ISessionRecord>>;
abstract updateLastActive(token: string, date: Date): Promise<void>;
abstract invalidate(token: string): Promise<boolean>;
abstract invalidateById(id: string, userId: string): Promise<boolean>;
}IStoreSessionArgs:
interface IStoreSessionArgs {
readonly token: string;
readonly userId: string;
readonly ipAddress: string;
readonly deviceInfo?: IDeviceInfo;
readonly location?: string;
readonly clientName?: string;
}ISessionRecord:
interface ISessionRecord {
readonly id: string;
readonly token: string;
readonly userId: string;
readonly ipAddress: string;
readonly deviceType: string | null;
readonly deviceName: string | null;
readonly deviceOs: string | null;
readonly browser: string | null;
readonly location: string | null;
readonly clientName: string | null;
readonly lastActive: Date;
readonly createdAt: Date;
}Real-World Adapter: Prisma
@Injectable()
export class PrismaSessionAdapter extends SessionPersistencePort {
private readonly _prisma: PrismaService;
constructor(prisma: PrismaService) {
super();
this._prisma = prisma;
}
public async store(args: IStoreSessionArgs): Promise<void> {
await this._prisma.session.upsert({
where: { token: args.token },
update: { lastActive: new Date() },
create: {
token: args.token,
userId: args.userId,
ipAddress: args.ipAddress,
deviceType: args.deviceInfo?.deviceType ?? null,
deviceName: args.deviceInfo?.deviceName ?? null,
deviceOs: args.deviceInfo?.os ?? null,
browser: args.deviceInfo?.browser ?? null,
location: args.location ?? null,
clientName: args.clientName ?? null,
lastActive: new Date(),
},
});
}
public async findByToken(token: string): Promise<ISessionRecord | null> {
return this._prisma.session.findUnique({ where: { token } });
}
public async findAllByUserId(userId: string): Promise<ReadonlyArray<ISessionRecord>> {
return this._prisma.session.findMany({
where: { userId },
orderBy: { lastActive: 'desc' },
});
}
public async updateLastActive(token: string, date: Date): Promise<void> {
await this._prisma.session.update({
where: { token },
data: { lastActive: date },
});
}
public async invalidate(token: string): Promise<boolean> {
const result = await this._prisma.session.deleteMany({ where: { token } });
return result.count > 0;
}
public async invalidateById(id: string, userId: string): Promise<boolean> {
const result = await this._prisma.session.deleteMany({ where: { id, userId } });
return result.count > 0;
}
}MfaSubjectPort
MFA state persistence for managing secrets, channels, and backup codes.
abstract class MfaSubjectPort {
abstract findById(id: string): Promise<IMfaSubject | null>;
abstract updateMfaState(id: string, update: IMfaSubjectUpdate): Promise<void>;
}IMfaSubject:
interface IMfaSubject {
readonly id: string;
readonly email: string | null;
readonly userName: string | null;
readonly mfaEnabled: boolean;
readonly mfaSecret: string | null;
readonly mfaBackupCodes: unknown;
readonly mfaPreferredMethod: string | null;
readonly mfaVerifiedAt: Date | null;
readonly mfaChannels: unknown;
readonly phoneNumber: string | null;
}IMfaSubjectUpdate:
interface IMfaSubjectUpdate {
readonly mfaChannels?: unknown;
readonly mfaEnabled?: boolean;
readonly mfaPreferredMethod?: string | null;
readonly mfaBackupCodes?: unknown;
readonly mfaVerifiedAt?: Date | null;
readonly mfaUpdatedAt?: Date | null;
readonly mfaSecret?: string | null;
readonly phoneNumber?: string | null;
}VerificationSubjectPort
Email and PIN verification lifecycle.
abstract class VerificationSubjectPort {
abstract findByLogin(login: string): Promise<IVerifiableSubject | null>;
abstract findByVerificationToken(token: string): Promise<IVerifiableSubject | null>;
abstract setVerificationToken(id: string, token: string | null): Promise<void>;
abstract markVerified(id: string): Promise<void>;
}IVerifiableSubject:
interface IVerifiableSubject {
readonly id: string;
readonly email: string | null;
readonly userName: string | null;
readonly isVerified: boolean;
readonly verificationToken: string | null;
}SocialAuthPort (Optional)
OAuth user creation and linking for social login providers.
abstract class SocialAuthPort {
abstract findOrCreateByProfile(profile: ISocialProfile): Promise<IAuthSubject>;
}ISocialProfile:
interface ISocialProfile {
readonly provider: string; // e.g. 'github', 'google', 'microsoft', 'apple'
readonly userName: string | null;
readonly email: string | null;
readonly displayName: string | null;
}Real-World Adapter
@Injectable()
export class PrismaSocialAuthAdapter extends SocialAuthPort {
private readonly _prisma: PrismaService;
constructor(prisma: PrismaService) {
super();
this._prisma = prisma;
}
public async findOrCreateByProfile(profile: ISocialProfile): Promise<IAuthSubject> {
// Try to find existing user by email
let user = await this._prisma.user.findFirst({
where: { email: profile.email },
});
if (!user) {
// Create new user from social profile
user = await this._prisma.user.create({
data: {
email: profile.email,
userName: profile.userName ?? profile.displayName,
isVerified: true, // Social accounts are pre-verified
isAnonymous: false,
roles: ['user'],
authProviders: {
create: { provider: profile.provider },
},
},
});
}
return {
id: user.id,
userName: user.userName,
email: user.email,
password: null,
isVerified: user.isVerified,
isAnonymous: false,
roles: user.roles,
mfaEnabled: user.mfaEnabled ?? false,
};
}
}TokenEnricherPort (Optional)
Injects additional claims into JWT access tokens.
abstract class TokenEnricherPort {
abstract enrichClaims(subject: IAuthSubject): Promise<Record<string, unknown>>;
}Real-World Adapter
@Injectable()
export class AppTokenEnricherAdapter extends TokenEnricherPort {
private readonly _prisma: PrismaService;
constructor(prisma: PrismaService) {
super();
this._prisma = prisma;
}
public async enrichClaims(subject: IAuthSubject): Promise<Record<string, unknown>> {
const subscription = await this._prisma.subscription.findFirst({
where: { userId: subject.id, status: 'active' },
select: { tier: true, expiresAt: true },
});
return {
tier: subscription?.tier ?? 'free',
subscriptionActive: subscription !== null,
};
}
}Services
AuthTokenService
Creates JWT access tokens with base claims and optional enrichment.
import { AuthTokenService } from '@breadstone/archipel-platform-authentication';
@Controller('auth')
export class AuthController {
constructor(private readonly _authToken: AuthTokenService) {}
@Post('login')
public async login(@Body() body: LoginDto): Promise<{ accessToken: string }> {
const subject: IAuthSubject = /* validated subject */;
const accessToken = await this._authToken.createAccessToken(subject);
return { accessToken };
}
}The token payload includes id, email, roles, iat, exp, plus any claims from TokenEnricherPort.
SessionService
Manages user sessions — creation, listing, activity tracking, and invalidation.
import { SessionService } from '@breadstone/archipel-platform-authentication';
@Controller('sessions')
export class SessionController {
constructor(private readonly _sessions: SessionService) {}
@Get()
public async listSessions(@Req() req: Request): Promise<ISessionRecord[]> {
return this._sessions.listSessions(req.user.id);
}
@Delete(':id')
public async revokeSession(@Param('id') id: string, @Req() req: Request): Promise<boolean> {
return this._sessions.invalidateById(id, req.user.id);
}
}VerificationService
Handles email verification via token or PIN.
import { VerificationService } from '@breadstone/archipel-platform-authentication';
// Token-based verification (email link)
const verified = await verificationService.verifyToken(token);
// PIN-based verification (6-digit code)
const verified = await verificationService.verifyPin(email, pin);MfaService
Orchestrates multi-factor authentication across all channels.
import { MfaService } from '@breadstone/archipel-platform-authentication';
// Initiate MFA setup (returns QR code URI for TOTP)
const setup = await mfaService.initSetup(userId, 'totp');
// Verify and enable MFA
const confirmed = await mfaService.verifyAndEnable(userId, 'totp', code);
// Issue challenge during login
const challenge = await mfaService.issueChallenge(userId, 'totp');
// Verify challenge response
const valid = await mfaService.verifyChallenge(userId, 'totp', code);ChallengeService
Manages authentication challenge flows (e.g., during login when MFA is required).
Guards
| Guard | Strategy | Description |
|---|---|---|
JwtAuthGuard | JWT | Validates Bearer token in Authorization header |
LocalAuthGuard | Local | Username/password authentication |
AnonymousAuthGuard | Anonymous | Seed/anonymous user access |
GithubAuthGuard | GitHub | GitHub OAuth redirect |
RolesGuard | — | RBAC guard checking IAuthSubject.roles |
Using Guards
import { JwtAuthGuard, RolesGuard } from '@breadstone/archipel-platform-authentication';
import { UseGuards } from '@nestjs/common';
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('dashboard')
@Roles('admin')
public async getDashboard(): Promise<DashboardResponse> {
// Only accessible with valid JWT containing 'admin' role
}
}MFA Channels
The MFA system supports four channels, all orchestrated by MfaService:
| Channel | Class | Description |
|---|---|---|
| TOTP | TotpMfaChannel | Time-based one-time password (Google Authenticator, Authy) |
| SMS | SmsMfaChannel | One-time code via SMS |
| Email OTP | EmailOtpMfaChannel | One-time code via email |
| Push | PushMfaChannel | Push notification approval |
All channels use the MfaSubjectPort for state persistence and MfaEncryptionService for secret encryption at rest.
Authentication Strategies
| Strategy | Trigger | Port Used |
|---|---|---|
JwtStrategy | Bearer token in header | AuthSubjectPort.findById() |
LocalStrategy | Username + password in body | AuthSubjectPort.findByLogin() |
AnonymousStrategy | Anonymous access endpoint | AuthSubjectPort.findAnonymous() |
GithubStrategy | GitHub OAuth callback | SocialAuthPort.findOrCreateByProfile() |
GoogleStrategy | Google OAuth callback | SocialAuthPort.findOrCreateByProfile() |
MicrosoftStrategy | Microsoft OAuth callback | SocialAuthPort.findOrCreateByProfile() |
AppleStrategy | Apple OAuth callback | SocialAuthPort.findOrCreateByProfile() |
JwtPayloadBase
Abstract base class for JWT payloads. Provides toJSON() / fromJSON() serialization and a DefaultJwtPayload subclass.
import { JwtPayloadBase } from '@breadstone/archipel-platform-authentication';
// Decoding a JWT
const payload = JwtPayloadBase.fromJSON(decodedToken);
console.log(payload.id, payload.email, payload.roles);Exports Summary
| Export | Type | Description |
|---|---|---|
AuthModule | NestJS Module | Main module with register() |
AuthSubjectPort | Port | User lookup |
MfaSubjectPort | Port | MFA state |
SessionPersistencePort | Port | Session storage |
VerificationSubjectPort | Port | Verification lifecycle |
SocialAuthPort | Port | OAuth user linking |
TokenEnricherPort | Port | JWT claim enrichment |
AuthTokenService | Service | Token creation |
SessionService | Service | Session management |
VerificationService | Service | Email/PIN verification |
MfaService | Service | MFA orchestration |
ChallengeService | Service | Auth challenge flows |
JwtAuthGuard | Guard | JWT validation |
LocalAuthGuard | Guard | Local login |
AnonymousAuthGuard | Guard | Anonymous access |
RolesGuard | Guard | RBAC enforcement |
SessionMappingProfile | Mapper | Session entity → response |
JwtPayloadBase | Model | JWT payload abstraction |