@lgerma/nestjs-doorkeeper
v1.0.1
Published
Drop-in NestJS authentication library with JWT, refresh tokens, session tracking, and device detection — inspired by Rails Doorkeeper
Maintainers
Readme
@lgerma/nestjs-doorkeeper
A drop-in authentication module for NestJS inspired by Rails' Doorkeeper. Handles sessions, access tokens, refresh tokens, and device tracking out of the box.
Table of Contents
- Features
- Installation
- Quick Start (CLI)
- Module Registration
- Auth Routes
- Guards
- Decorators
- Current User Modes
- Configuration Reference
- Sub-path Exports
- Entities
- Token Strategy
- Device & Browser Tracking
Features
POST /auth/register— create user + sessionPOST /auth/login— validate credentials, create sessionPOST /auth/logout— revoke current sessionPOST /auth/refresh— rotate both tokens- Global
JwtAuthGuard— all routes protected by default @Public()— opt-out individual routes from auth@CurrentUser()— inject authenticated user anywhere- Session-based refresh tokens — opaque, stored in DB, rotated on use
- Device & browser metadata captured on every login/refresh
- Configurable table prefix, route prefix, and token TTLs
- No Passport.js dependency
Installation
npm install @lgerma/nestjs-doorkeeperPeer dependencies (must already be installed in your app):
npm install @nestjs/common @nestjs/core @nestjs/jwt typeorm reflect-metadataQuick Start (CLI)
Run the init command inside your NestJS project:
npx @lgerma/nestjs-doorkeeper initThe CLI will prompt you for:
| Prompt | Default |
|--------|---------|
| Access token TTL | 15m |
| Refresh token TTL | 30d |
| JWT secret env var | JWT_SECRET |
| Table prefix | auth |
| Route prefix | auth |
| Migrations output folder | auto-detected from data-source.ts location (e.g. src/database/migrations), falls back to src/migrations |
The migrations folder is detected automatically by searching for a data-source.ts file under src/. If found, the migration is placed as a sibling in a migrations/ subfolder next to it. You can override the path at the prompt.
This will:
- Detect your ORM (TypeORM supported)
- Ask a few config questions (with defaults)
- Generate a TypeORM migration file in the detected (or chosen) migrations folder
- Print exact next steps
Then run the migration:
npx typeorm migration:run -d src/data-source.tsModule Registration
Minimal (synchronous)
import { AuthModule } from '@lgerma/nestjs-doorkeeper';
@Module({
imports: [
AuthModule.forRoot({
jwt: { secret: process.env.JWT_SECRET },
}),
],
})
export class AppModule {}With full config
AuthModule.forRoot({
tablePrefix: 'auth', // default: 'auth' → tables: auth_users, auth_sessions
routePrefix: 'auth', // default: 'auth' → routes: /auth/register, /auth/login, ...
global: true, // default: true → JwtAuthGuard applied globally
currentUser: 'subset', // default: 'subset' → see Current User Modes
jwt: {
secret: process.env.JWT_SECRET,
accessTokenTtl: '15m', // default: '15m'
refreshTokenTtl: '30d', // default: '30d'
},
})Async (with ConfigService)
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthModule } from '@lgerma/nestjs-doorkeeper';
AuthModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
jwt: {
secret: config.get<string>('JWT_SECRET'),
accessTokenTtl: '15m',
refreshTokenTtl: '30d',
},
tablePrefix: 'auth',
routePrefix: 'auth',
}),
})Auth Routes
All routes are registered automatically. The prefix is configurable via routePrefix (default: auth).
| Method | Path | Auth | Body | Response |
|---|---|---|---|---|
| POST | /auth/register | Public | { email, password } | { accessToken, refreshToken } |
| POST | /auth/login | Public | { email, password } | { accessToken, refreshToken } |
| POST | /auth/logout | JWT required | — | 204 No Content |
| POST | /auth/refresh | Public | { refreshToken } | { accessToken, refreshToken } |
Register
curl -X POST http://localhost:3000/auth/register \
-H 'Content-Type: application/json' \
-d '{ "email": "[email protected]", "password": "secret123" }'Login
curl -X POST http://localhost:3000/auth/login \
-H 'Content-Type: application/json' \
-d '{ "email": "[email protected]", "password": "secret123" }'Logout
curl -X POST http://localhost:3000/auth/logout \
-H 'Authorization: Bearer <access_token>'Refresh
curl -X POST http://localhost:3000/auth/refresh \
-H 'Content-Type: application/json' \
-d '{ "refreshToken": "<refresh_token>" }'Guards
JwtAuthGuard (global)
Applied globally when global: true (the default). Every route requires a valid Authorization: Bearer <token> header unless decorated with @Public().
To apply it manually instead of globally:
AuthModule.forRoot({ global: false, jwt: { secret: '...' } })
// Then on specific routes or controllers:
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile() {}RefreshGuard
Used internally on POST /auth/refresh. Can also be used manually if you need to protect a custom refresh endpoint:
import { RefreshGuard } from '@lgerma/nestjs-doorkeeper/guards';
@UseGuards(RefreshGuard)
@Post('custom-refresh')
customRefresh(@Req() req: any) {
// req.refreshToken is set by RefreshGuard
}Decorators
@Public()
Skips JwtAuthGuard on a route even when the guard is applied globally.
import { Public } from '@lgerma/nestjs-doorkeeper/decorators';
@Public()
@Get('health')
healthCheck() {
return { status: 'ok' };
}Can also be applied at the controller level to make all its routes public:
@Public()
@Controller('webhooks')
export class WebhookController {}@CurrentUser()
Injects the authenticated user into a route handler. The shape depends on currentUser mode.
import { CurrentUser } from '@lgerma/nestjs-doorkeeper/decorators';
@Get('profile')
getProfile(@CurrentUser() user: { id: string; email: string }) {
return user;
}Current User Modes
Configured via currentUser in forRoot. Controls what @CurrentUser() returns and whether a DB query is made on each request.
| Mode | DB hit | Injects | Use when |
|---|---|---|---|
| subset | No | { id, email } | Default — most routes only need user identity |
| payload | No | Full JWT payload including iat, exp | You need token timestamps in handlers |
| entity | Yes | Full UserEntity from DB | You frequently need DB fields beyond what the JWT carries |
// subset (default)
@CurrentUser() user: { id: string; email: string }
// payload
@CurrentUser() user: { sub: string; email: string; iat: number; exp: number }
// entity
@CurrentUser() user: UserEntityConfiguration Reference
AuthModuleOptions
| Option | Type | Default | Description |
|---|---|---|---|
| jwt.secret | string | required | JWT signing secret |
| jwt.accessTokenTtl | string | '15m' | Access token lifetime (e.g. '15m', '1h') |
| jwt.refreshTokenTtl | string | '30d' | Refresh token lifetime (e.g. '7d', '30d') |
| tablePrefix | string | 'auth' | Prefix for DB tables (auth_users, auth_sessions) |
| routePrefix | string | 'auth' | Prefix for HTTP routes (/auth/login, etc.) |
| global | boolean | true | Register JwtAuthGuard globally |
| currentUser | 'subset' \| 'payload' \| 'entity' | 'subset' | Shape of @CurrentUser() injection |
TTL format: a number followed by a unit — s (seconds), m (minutes), h (hours), d (days).
Examples: '30s', '15m', '2h', '7d'.
Sub-path Exports
| Import path | Exports |
|---|---|
| @lgerma/nestjs-doorkeeper | AuthModule |
| @lgerma/nestjs-doorkeeper/guards | JwtAuthGuard, RefreshGuard |
| @lgerma/nestjs-doorkeeper/decorators | @CurrentUser, @Public |
| @lgerma/nestjs-doorkeeper/services | AuthService, SessionService, TokenService |
| @lgerma/nestjs-doorkeeper/entities | UserEntity, SessionEntity |
| @lgerma/nestjs-doorkeeper/adapters | TypeOrmAdapter, IDoorkeeperAdapter |
Entities
Both entities are automatically registered — no need to add them to your TypeORM entities array.
UserEntity → auth_users
| Column | Type | Notes |
|---|---|---|
| id | uuid | Primary key, auto-generated |
| email | varchar | Unique, not null |
| passwordHash | varchar | bcrypt hash |
| isActive | boolean | Default true. Set to false to disable login |
| createdAt | timestamp | Auto |
| updatedAt | timestamp | Auto |
SessionEntity → auth_sessions
| Column | Type | Notes |
|---|---|---|
| id | uuid | Primary key |
| userId | uuid | FK → auth_users.id, CASCADE DELETE |
| accessToken | varchar | Stored JWT |
| refreshToken | varchar | Opaque random token, unique, indexed |
| ipAddress | varchar | Captured on login/refresh |
| userAgent | varchar | Raw User-Agent string |
| deviceType | varchar | desktop, mobile, or tablet |
| deviceName | varchar | e.g. iPhone, Mac, Pixel 7 |
| browserName | varchar | e.g. Chrome, Firefox, Safari |
| osName | varchar | e.g. macOS, Windows, Android, iOS |
| osVersion | varchar | e.g. 14.4, 11 |
| createdAt | timestamp | Auto |
| lastUsedAt | timestamp | Updated on token rotation |
| expiresAt | timestamp | Derived from refreshTokenTtl |
Token Strategy
| Token | Type | Default TTL | Notes |
|---|---|---|---|
| Access token | JWT (signed) | 15m | Sent in Authorization: Bearer header |
| Refresh token | Opaque hex string | 30d | Sent in request body, stored in DB |
Rotation — on every /auth/refresh call:
- Old session row is deleted
- New session row is created with fresh tokens and updated
lastUsedAt - New
{ accessToken, refreshToken }pair is returned
Revocation — logout deletes the session row. No revoked_at column — absence of the row means the token is invalid.
One user can have multiple active sessions (multiple devices/browsers simultaneously).
Device & Browser Tracking
Device metadata is captured automatically on register, login, and refresh from request headers — no configuration needed.
Detection order:
- Client Hints (
sec-ch-ua-platform,sec-ch-ua-mobile) — modern Chromium browsers - User-Agent string — regex fallback for all other browsers
Fields captured per session:
| Field | Example values |
|---|---|
| deviceType | desktop, mobile, tablet |
| deviceName | iPhone, iPad, Mac, Pixel 7, Samsung Galaxy S21 |
| browserName | Chrome, Firefox, Safari, Edge, Opera |
| osName | macOS, Windows, iOS, Android, Linux |
| osVersion | 14.4, 11/10, 17.0 |
License
MIT
