npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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

  • POST /auth/register — create user + session
  • POST /auth/login — validate credentials, create session
  • POST /auth/logout — revoke current session
  • POST /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-doorkeeper

Peer dependencies (must already be installed in your app):

npm install @nestjs/common @nestjs/core @nestjs/jwt typeorm reflect-metadata

Quick Start (CLI)

Run the init command inside your NestJS project:

npx @lgerma/nestjs-doorkeeper init

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

  1. Detect your ORM (TypeORM supported)
  2. Ask a few config questions (with defaults)
  3. Generate a TypeORM migration file in the detected (or chosen) migrations folder
  4. Print exact next steps

Then run the migration:

npx typeorm migration:run -d src/data-source.ts

Module 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: UserEntity

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

UserEntityauth_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 |

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

  1. Old session row is deleted
  2. New session row is created with fresh tokens and updated lastUsedAt
  3. 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:

  1. Client Hints (sec-ch-ua-platform, sec-ch-ua-mobile) — modern Chromium browsers
  2. 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