@devoven/authn
v0.1.1
Published
AuthN module for NestJS — hexagonal architecture
Readme
@devoven/authn
Authentication module for NestJS. Covers registration, login, JWT token issuance and refresh, email verification, and password reset.
Installation
npm install @devoven/authn
# or
pnpm add @devoven/authnbcrypt is a regular dependency and is installed automatically.
Peer dependencies
This package requires the standard NestJS peer dependencies plus @nestjs/jwt, class-validator, and class-transformer. If you do not already have them:
npm install @nestjs/jwt class-validator class-transformerQuick Start
import { Module } from '@nestjs/common';
import { AuthNModule } from '@devoven/authn';
// Your implementations of the four required repository ports:
import { PrismaUserRepository } from './auth/prisma-user.repository';
import { PrismaRefreshTokenRepository } from './auth/prisma-refresh-token.repository';
import { PrismaVerificationTokenRepository } from './auth/prisma-verification-token.repository';
import { PrismaResetTokenRepository } from './auth/prisma-reset-token.repository';
@Module({
imports: [
AuthNModule.register({
jwtSecret: process.env.JWT_SECRET,
userRepository: PrismaUserRepository,
refreshTokenRepository: PrismaRefreshTokenRepository,
verificationTokenRepository: PrismaVerificationTokenRepository,
resetTokenRepository: PrismaResetTokenRepository,
}),
],
})
export class AppModule {}The four repository options are required. All other options are optional with sensible defaults.
Module Options
interface AuthNModuleOptions {
// Required
userRepository: Type<UserRepositoryPort> | UserRepositoryPort;
refreshTokenRepository: Type<RefreshTokenRepositoryPort> | RefreshTokenRepositoryPort;
verificationTokenRepository: Type<VerificationTokenRepositoryPort> | VerificationTokenRepositoryPort;
resetTokenRepository: Type<ResetTokenRepositoryPort> | ResetTokenRepositoryPort;
// Optional — infrastructure
jwtSecret?: string;
tokenProvider?: Type<TokenProviderPort> | TokenProviderPort;
passwordHasher?: Type<PasswordHasherPort> | PasswordHasherPort;
// Optional — TTL overrides (seconds)
accessTokenTtlSeconds?: number;
refreshTokenTtlSeconds?: number;
emailVerificationTokenTtlSeconds?: number;
passwordResetTokenTtlSeconds?: number;
}| Option | Type | Default | Description |
|--------|------|---------|-------------|
| userRepository | Class or instance | — | Required. Persistence adapter for users |
| refreshTokenRepository | Class or instance | — | Required. Persistence adapter for refresh tokens |
| verificationTokenRepository | Class or instance | — | Required. Persistence adapter for email verification tokens |
| resetTokenRepository | Class or instance | — | Required. Persistence adapter for password reset tokens |
| jwtSecret | string | — | Required when using the default JwtTokenProvider |
| tokenProvider | Class or instance | JwtTokenProvider | Override JWT signing/verification |
| passwordHasher | Class or instance | BcryptPasswordHasher | Override password hashing |
| accessTokenTtlSeconds | number | 900 (15 min) | Access token lifetime |
| refreshTokenTtlSeconds | number | 604800 (7 days) | Refresh token lifetime |
| emailVerificationTokenTtlSeconds | number | 86400 (24 hours) | Email verification token lifetime |
| passwordResetTokenTtlSeconds | number | 1800 (30 min) | Password reset token lifetime |
Async Registration
Use registerAsync when options depend on injected services:
import { AuthNModule } from '@devoven/authn';
import { ConfigService } from '@nestjs/config';
@Module({
imports: [
AuthNModule.registerAsync({
useFactory: (config: ConfigService) => ({
jwtSecret: config.getOrThrow('JWT_SECRET'),
userRepository: PrismaUserRepository,
refreshTokenRepository: PrismaRefreshTokenRepository,
verificationTokenRepository: PrismaVerificationTokenRepository,
resetTokenRepository: PrismaResetTokenRepository,
accessTokenTtlSeconds: config.get('ACCESS_TOKEN_TTL'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}REST API
All endpoints are served under the /auth prefix. The controller applies AuthGuard globally — routes marked @Public() skip token validation.
| Method | Path | Auth | Description | Status |
|--------|------|------|-------------|--------|
| POST | /auth/register | Public | Register a new user | 204 |
| POST | /auth/login | Public | Log in and receive tokens | 200 |
| POST | /auth/verify-email | Public | Verify email using a token | 204 |
| POST | /auth/reset-password | Public | Set a new password using a reset token | 204 |
| POST | /auth/refresh | Public | Exchange a refresh token for a new token pair | 200 |
| POST | /auth/change-password | Bearer | Change password for the authenticated user | 204 |
| POST | /auth/revoke | Bearer | Revoke a specific refresh token | 204 |
POST /auth/register
Request body:
{
"identifier": "[email protected]",
"password": "secret123",
"metadata": { "name": "Alice" }
}| Field | Type | Validation |
|-------|------|------------|
| identifier | string | Required |
| password | string | Required, min 8 characters |
| metadata | object | Optional |
Response 204 No Content.
POST /auth/login
Request body:
{
"identifier": "[email protected]",
"password": "secret123"
}Response 200 OK:
{
"accessToken": "<jwt>",
"refreshToken": "<uuid>"
}POST /auth/verify-email
Request body:
{ "token": "<verification-token>" }Response 204 No Content.
POST /auth/reset-password
Request body:
{
"token": "<reset-token>",
"newPassword": "newSecret123"
}Response 204 No Content.
POST /auth/refresh
Request body:
{ "refreshToken": "<uuid>" }Response 200 OK: same shape as /auth/login.
POST /auth/change-password
Requires Authorization: Bearer <accessToken>.
Request body:
{
"currentPassword": "secret123",
"newPassword": "newSecret123"
}Response 204 No Content.
POST /auth/revoke
Requires Authorization: Bearer <accessToken>.
Request body:
{ "refreshToken": "<uuid>" }Response 204 No Content.
Guards and Decorators
AuthGuard
AuthGuard validates the Authorization: Bearer <token> header on every request. On success it populates request.user with the decoded token payload (ITokenPayload, which always includes sub: string).
Apply it globally or per-controller:
import { AuthGuard } from '@devoven/authn';
// Global
app.useGlobalGuards(app.get(AuthGuard));
// Per-controller
@UseGuards(AuthGuard)
@Controller('orders')
export class OrdersController {}AuthGuard is exported from the module and can be injected directly.
@Public()
Skip AuthGuard for a specific route or controller:
import { Public } from '@devoven/authn';
@Public()
@Get('health')
healthCheck() { ... }@CurrentUser()
Inject the authenticated token payload into a route handler parameter:
import { CurrentUser } from '@devoven/authn';
import type { ITokenPayload } from '@devoven/authn';
@Get('profile')
getProfile(@CurrentUser() user: ITokenPayload) {
return user.sub; // user ID
}ITokenPayload is { sub: string; [key: string]: unknown }. Additional claims added during token signing are accessible as extra keys.
Architecture
The module follows hexagonal architecture. authn.module.ts is the composition root — the only place concrete adapters are wired to port tokens.
Driving Port / Token / Use Case Mapping
These are the inbound ports used by the controller and exported for injection in consuming apps:
| Port Interface | DI Token (TOKENS.*) | Use Case |
|----------------|----------------------|----------|
| RegisterPort | RegisterPort | RegisterUseCase |
| LoginPort | LoginPort | LoginUseCase |
| IssueTokenPairPort | IssueTokenPairPort | IssueTokenPairUseCase |
| RefreshTokenPairPort | RefreshTokenPairPort | RefreshTokenPairUseCase |
| RevokeRefreshTokenPort | RevokeRefreshTokenPort | RevokeRefreshTokenUseCase |
| ValidateAccessTokenPort | ValidateAccessTokenPort | ValidateAccessTokenUseCase |
| RequestEmailVerificationPort | RequestEmailVerificationPort | RequestEmailVerificationUseCase |
| RequestPasswordResetPort | RequestPasswordResetPort | RequestPasswordResetUseCase |
| ResetPasswordPort | ResetPasswordPort | ResetPasswordUseCase |
| VerifyEmailPort | VerifyEmailPort | VerifyEmailUseCase |
| ChangePasswordPort | ChangePasswordPort | ChangePasswordUseCase |
| LinkAccountPort | LinkAccountPort | LinkAccountUseCase |
Driven Port / Token Mapping
These are the outbound ports that consuming apps must implement:
| Port Interface | DI Token (TOKENS.*) | Default |
|----------------|----------------------|---------|
| UserRepositoryPort | UserRepositoryPort | — (required) |
| RefreshTokenRepositoryPort | RefreshTokenRepositoryPort | — (required) |
| VerificationTokenRepositoryPort | VerificationTokenRepositoryPort | — (required) |
| ResetTokenRepositoryPort | ResetTokenRepositoryPort | — (required) |
| TokenProviderPort | TokenProviderPort | JwtTokenProvider |
| PasswordHasherPort | PasswordHasherPort | BcryptPasswordHasher |
Import TOKENS from @devoven/authn to inject any of these by token string.
Custom Adapters
Implementing a User Repository
import { Injectable } from '@nestjs/common';
import { UserRepositoryPort, AuthUserEntity } from '@devoven/authn';
@Injectable()
export class PrismaUserRepository implements UserRepositoryPort {
constructor(private readonly prisma: PrismaService) {}
async save(user: AuthUserEntity<Record<string, unknown>>): Promise<void> {
await this.prisma.user.upsert({ /* ... */ });
}
async findByIdentifier(identifier: string) {
const row = await this.prisma.user.findUnique({ where: { email: identifier } });
if (!row) return null;
return AuthUserEntity.reconstitute(
row.id,
row.email,
row.passwordHash,
row.metadata,
[],
row.emailVerifiedAt,
row.createdAt,
);
}
async findById(id: string) { /* ... */ }
async findByProvider(provider: string, providerId: string) { /* ... */ }
async existsByIdentifier(identifier: string) { /* ... */ }
async existsByProvider(provider: string, providerId: string) { /* ... */ }
}Use AuthUserEntity.reconstitute(...) to rebuild entities from persistence. Never construct the entity directly.
RefreshTokenRepositoryPort
RefreshTokenRepositoryPort<T> is generic. The type parameter T represents optional extra metadata the consumer can attach to each refresh token (for example, device name, IP address, or any application-specific payload). If you do not need extra metadata, use RefreshTokenRepositoryPort<never> or RefreshTokenRepositoryPort<Record<string, unknown>>.
interface RefreshTokenRepositoryPort<T> {
save(token: RefreshTokenEntity<T>): Promise<void>;
findByToken(token: string): Promise<RefreshTokenEntity<T> | null>;
revokeByToken(token: string): Promise<void>;
revokeAllByUserId(userId: string): Promise<void>;
}VerificationTokenRepositoryPort
interface VerificationTokenRepositoryPort {
save(token: EmailVerificationTokenEntity): Promise<void>;
findByToken(token: string): Promise<EmailVerificationTokenEntity | null>;
markUsed(token: string): Promise<void>;
}ResetTokenRepositoryPort
interface ResetTokenRepositoryPort {
save(token: PasswordResetTokenEntity): Promise<void>;
findByToken(token: string): Promise<PasswordResetTokenEntity | null>;
markUsed(token: string): Promise<void>;
}PasswordHasherPort
Override the default BcryptPasswordHasher by implementing this interface and passing it via passwordHasher in module options:
interface PasswordHasherPort {
hash(plaintext: string): Promise<string>;
compare(plaintext: string, hashed: string): Promise<boolean>;
}Implementing a Token Provider
Replace JWT with any signing mechanism by implementing TokenProviderPort:
interface TokenProviderPort {
sign(payload: ITokenPayload, expiresInSeconds: number): Promise<string>;
verify(token: string): Promise<ITokenPayload>;
}Example:
import { Injectable } from '@nestjs/common';
import { TokenProviderPort, ITokenPayload } from '@devoven/authn';
@Injectable()
export class CustomTokenProvider implements TokenProviderPort {
async sign(payload: ITokenPayload, expiresInSeconds: number): Promise<string> { /* ... */ }
async verify(token: string): Promise<ITokenPayload> { /* ... */ }
}Pass it via module options:
AuthNModule.register({
tokenProvider: CustomTokenProvider,
// jwtSecret is not needed when tokenProvider is overridden
userRepository: ...,
/* ... */
})In-memory repositories (InMemoryUserRepository, InMemoryRefreshTokenRepository, InMemoryVerificationTokenRepository, InMemoryResetTokenRepository) are exported for use in tests.
