Skip to content

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

typescript
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

PropertyTypeRequiredDescription
authSubjectType<AuthSubjectPort>YesUser lookup for JWT, Local, and Anonymous strategies
mfaSubjectType<MfaSubjectPort>YesMFA state persistence (secrets, channels, backup codes)
sessionPersistenceType<SessionPersistencePort>YesSession storage, querying, and invalidation
verificationSubjectType<VerificationSubjectPort>YesEmail/PIN verification lifecycle
socialAuthType<SocialAuthPort>NoOAuth user creation/linking. When provided, GitHub/Google/Microsoft/Apple strategies are registered.
tokenEnricherType<TokenEnricherPort>NoCustom JWT claim injection. When provided, additional claims are merged into access tokens.

Environment Variables

VariableDescriptionRequired
AUTH_JWT_SECRETJWT signing secretYes
AUTH_JWT_EXPIRES_INToken expiration (e.g. '15m', '1h', '7d')Yes
AUTH_GITHUB_CLIENT_IDGitHub OAuth client IDIf using GitHub OAuth
AUTH_GITHUB_CLIENT_SECRETGitHub OAuth client secretIf using GitHub OAuth
AUTH_GOOGLE_CLIENT_IDGoogle OAuth client IDIf using Google OAuth
AUTH_GOOGLE_CLIENT_SECRETGoogle OAuth client secretIf using Google OAuth
AUTH_MICROSOFT_CLIENT_IDMicrosoft OAuth client IDIf using Microsoft OAuth
AUTH_MICROSOFT_CLIENT_SECRETMicrosoft OAuth client secretIf using Microsoft OAuth
AUTH_APPLE_CLIENT_IDApple OAuth client IDIf using Apple OAuth
AUTH_APPLE_TEAM_IDApple team IDIf using Apple OAuth
AUTH_APPLE_KEY_IDApple key IDIf using Apple OAuth
AUTH_APPLE_PRIVATE_KEYApple private key (PEM)If using Apple OAuth
SEED_ANONYMOUS_USERNAMEUsername for anonymous/seed userIf using Anonymous strategy

Ports (Contracts)

AuthSubjectPort

Resolves authentication subjects for JWT validation, local login, and anonymous access.

typescript
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:

typescript
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

typescript
@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.

typescript
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:

typescript
interface IStoreSessionArgs {
  readonly token: string;
  readonly userId: string;
  readonly ipAddress: string;
  readonly deviceInfo?: IDeviceInfo;
  readonly location?: string;
  readonly clientName?: string;
}

ISessionRecord:

typescript
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

typescript
@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.

typescript
abstract class MfaSubjectPort {
  abstract findById(id: string): Promise<IMfaSubject | null>;
  abstract updateMfaState(id: string, update: IMfaSubjectUpdate): Promise<void>;
}

IMfaSubject:

typescript
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:

typescript
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.

typescript
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:

typescript
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.

typescript
abstract class SocialAuthPort {
  abstract findOrCreateByProfile(profile: ISocialProfile): Promise<IAuthSubject>;
}

ISocialProfile:

typescript
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

typescript
@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.

typescript
abstract class TokenEnricherPort {
  abstract enrichClaims(subject: IAuthSubject): Promise<Record<string, unknown>>;
}

Real-World Adapter

typescript
@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.

typescript
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.

typescript
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.

typescript
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.

typescript
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

GuardStrategyDescription
JwtAuthGuardJWTValidates Bearer token in Authorization header
LocalAuthGuardLocalUsername/password authentication
AnonymousAuthGuardAnonymousSeed/anonymous user access
GithubAuthGuardGitHubGitHub OAuth redirect
RolesGuardRBAC guard checking IAuthSubject.roles

Using Guards

typescript
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:

ChannelClassDescription
TOTPTotpMfaChannelTime-based one-time password (Google Authenticator, Authy)
SMSSmsMfaChannelOne-time code via SMS
Email OTPEmailOtpMfaChannelOne-time code via email
PushPushMfaChannelPush notification approval

All channels use the MfaSubjectPort for state persistence and MfaEncryptionService for secret encryption at rest.


Authentication Strategies

StrategyTriggerPort Used
JwtStrategyBearer token in headerAuthSubjectPort.findById()
LocalStrategyUsername + password in bodyAuthSubjectPort.findByLogin()
AnonymousStrategyAnonymous access endpointAuthSubjectPort.findAnonymous()
GithubStrategyGitHub OAuth callbackSocialAuthPort.findOrCreateByProfile()
GoogleStrategyGoogle OAuth callbackSocialAuthPort.findOrCreateByProfile()
MicrosoftStrategyMicrosoft OAuth callbackSocialAuthPort.findOrCreateByProfile()
AppleStrategyApple OAuth callbackSocialAuthPort.findOrCreateByProfile()

JwtPayloadBase

Abstract base class for JWT payloads. Provides toJSON() / fromJSON() serialization and a DefaultJwtPayload subclass.

typescript
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

ExportTypeDescription
AuthModuleNestJS ModuleMain module with register()
AuthSubjectPortPortUser lookup
MfaSubjectPortPortMFA state
SessionPersistencePortPortSession storage
VerificationSubjectPortPortVerification lifecycle
SocialAuthPortPortOAuth user linking
TokenEnricherPortPortJWT claim enrichment
AuthTokenServiceServiceToken creation
SessionServiceServiceSession management
VerificationServiceServiceEmail/PIN verification
MfaServiceServiceMFA orchestration
ChallengeServiceServiceAuth challenge flows
JwtAuthGuardGuardJWT validation
LocalAuthGuardGuardLocal login
AnonymousAuthGuardGuardAnonymous access
RolesGuardGuardRBAC enforcement
SessionMappingProfileMapperSession entity → response
JwtPayloadBaseModelJWT payload abstraction

Released under the MIT License.