Skip to content

Repository files navigation

nestjs-oauth2-password

build coverage mutation score version zero dependencies ESM TypeScript license

OAuth2 Resource Owner Password Credentials (ROPC) for NestJS: opaque, server-stored, revocable access + refresh tokens, an RFC 6749 token endpoint with RFC 7009 revocation, and a default-deny bearer guard — wired with a single forRoot().

🔆 ESM-only. Requires Node ≥ 20 and NestJS 10 / 11 / 12.

This is the opaque-token half. It owns the OAuth2 flow and delegates the credential check to the token-agnostic companion (nestjs-credentials) and token storage to your database. For stateless self-contained JWTs instead, use the sibling nestjs-jwt-guard.

Why

Unlike a self-contained JWT, an opaque token carries no claims — it's just a high-entropy string looked up on every request. That one store read per request is the price of what JWTs can't do: instant revocation and always-live user state. This package mints the tokens and runs the grant flow; you own persistence (so revocation is real) and the credential check (so there are no user-model opinions).

ROPC hands the user's password straight to the client, so it's justified for first-party apps (your own web/mobile client) or legacy interop — not third-party delegation. What's valuable here is the opaque + revocable + refresh + client-id machinery, not the grant itself.

Install

npm install nestjs-oauth2-password nestjs-credentials

@nestjs/common, @nestjs/core, and reflect-metadata are peer dependencies (already in any Nest app). nestjs-credentials is optional but recommended — it's the natural validateUser seam.

Quick start

Register once. It mounts POST /oauth/token + POST /oauth/revoke and registers the bearer guard globally:

import { Module } from '@nestjs/common';
import { CredentialsModule, CredentialsService } from 'nestjs-credentials';
import { OAuth2PasswordModule } from 'nestjs-oauth2-password';
import { TokenStoreService } from './token-store.service';

@Module({
  imports: [
    CredentialsModule.register({ useFactory: () => userStore }), // user lookup + password verify
    OAuth2PasswordModule.forRootAsync({
      inject: [CredentialsService, TokenStoreService],
      useFactory: (creds: CredentialsService, tokens: TokenStoreService) => ({
        validateUser: (username, password) => creds.verify(username, password),
        tokenStore: tokens,
        accessTokenTtl: 60 * 60,           // 1h
        refreshTokenTtl: 60 * 60 * 24 * 14, // 14d
      }),
    }),
  ],
})
export class AppModule {}

Get a token — the password grant:

POST /oauth/token
Content-Type: application/json

{ "grant_type": "password", "username": "ada@example.com", "password": "secret" }
{ "access_token": "v4y…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "8Qb…" }

Refresh it — the old access + refresh tokens are revoked and a fresh pair is issued (rotation):

POST /oauth/token
{ "grant_type": "refresh_token", "refresh_token": "8Qb…" }

Call a protected route with Authorization: Bearer <access_token>. Every route requires a valid token; mark exceptions with @Public():

import { Public, type AuthenticatedRequest } from 'nestjs-oauth2-password';

@Public()
@Get('health')
health() { return { ok: true }; }

@Get('me')
me(@Req() req: AuthenticatedRequest) {
  return req.user; // whatever your validateUser returned; req.client too
}

Revoke a token (logout) — RFC 7009, always 200:

POST /oauth/revoke
{ "token": "8Qb…", "token_type_hint": "refresh_token" }

Using nestjs-http-envelope? Mark the OAuth2 routes @SkipEnvelope() so the RFC token/error JSON isn't wrapped: SkipEnvelope()(OAuth2PasswordController) before bootstrap, or set registerController: false and mount your own. The error half needs nestjs-http-envelope v1.0.2 or later; before it, @SkipEnvelope() skipped the success envelope only, and errors lost their error_description.

The seams

The package is stateless — you supply three seams. Only the first two are required.

Seam Shape Role
validateUser (username, password, client) => user | null The ROPC credential check. Wire it to nestjs-credentials (creds.verify). Return null ⇒ invalid_grant.
tokenStore save / find / revoke × access / refresh Opaque-token persistence. Back it with Prisma/TypeORM/Redis.
clientStore findClient, verifySecret Client identity & secret — required only when requireClient/confidentialClients is on.

A minimal TokenStore (the package mints the token and computes expiryDate; you persist it — hash at rest with the provided hashToken):

import { Injectable } from '@nestjs/common';
import {
  hashToken,
  type IssuedAccessToken,
  type StoredAccessToken,
  type TokenStore,
} from 'nestjs-oauth2-password';

@Injectable()
export class TokenStoreService implements TokenStore<User> {
  constructor(private readonly db: PrismaService) {}

  saveAccessToken(t: IssuedAccessToken<User>) {
    return this.db.accessToken.create({
      data: { tokenHash: hashToken(t.token), userId: t.user.id, expiryDate: t.expiryDate },
    });
  }

  async findAccessToken(token: string): Promise<StoredAccessToken<User> | null> {
    const row = await this.db.accessToken.findUnique({
      where: { tokenHash: hashToken(token) },
      include: { user: true },
    });
    return row ? { user: row.user, client: null, expiryDate: row.expiryDate } : null;
  }

  revokeAccessToken(token: string) {
    return this.db.accessToken.deleteMany({ where: { tokenHash: hashToken(token) } });
  }

  // …saveRefreshToken / findRefreshToken / revokeRefreshToken follow the same shape.
  // A refresh row stores the access token it was issued with, so rotation revokes both.
}

Grants

The token endpoint dispatches on grant_type:

grant_type Body fields Result
password username, password (+ client_id/client_secret if required) A new access token (+ refresh token unless disabled).
refresh_token refresh_token (+ client_id/client_secret if required) Revokes the presented pair, issues a fresh one.

Protocol errors use the RFC 6749 §5.2 shape — { "error": "invalid_grant", "error_description": "…" } — via OAuth2Exception (invalid_client ⇒ 401, the rest ⇒ 400).

Configuration

OAuth2PasswordModule.forRoot({
  validateUser: (username, password, client) => credentials.verify(username, password), // required
  tokenStore,                  // required
  clientStore,                 // required if requireClient/confidentialClients
  accessTokenTtl: 3600,        // seconds (default)
  refreshTokenTtl: 1209600,    // seconds (default — 14d)
  enableRefreshToken: true,    // issue refresh tokens (default)
  requireClient: false,        // require client_id on every grant (default)
  confidentialClients: false,  // verify client_secret — implies requireClient (default)
  tokenBytes: 32,              // entropy of generated opaque tokens (default — 256-bit)
  attachTo: 'user',            // request property the principal is attached to (default)
  registerGuard: true,         // register the bearer guard globally via APP_GUARD (default)
  registerController: true,    // mount /oauth/token + /oauth/revoke (default)
  isGlobal: true,              // module is global (default)
});
Option Default Description
validateUser — The ROPC credential check (required).
tokenStore — Opaque-token persistence (required).
clientStore — Client lookup/auth — required if requireClient/confidentialClients.
accessTokenTtl 3600 Access-token lifetime, seconds.
refreshTokenTtl 1209600 Refresh-token lifetime, seconds.
enableRefreshToken true Issue refresh tokens alongside access tokens.
requireClient false Require a client_id on every grant.
confidentialClients false Verify client_secret (implies requireClient).
tokenBytes 32 Entropy (bytes) of generated opaque tokens.
attachTo 'user' Request property the principal is attached to.
registerGuard true Register the bearer guard globally via APP_GUARD.
registerController true Mount the built-in token & revoke controller.
isGlobal true Register the module globally.

Per-route use

Disable the global guard (registerGuard: false) and apply per-controller instead — the guard is exported:

@UseGuards(OAuth2PasswordGuard)
@Controller('admin')
export class AdminController {}

API

Module

Export Description
OAuth2PasswordModule.forRoot(options) Configure the seams + behavior synchronously. See Configuration.
OAuth2PasswordModule.forRootAsync(options) Build the seams from injected deps (useFactory).

Flow & enforcement

Export Description
OAuth2PasswordController The /oauth/token + /oauth/revoke endpoints. Opt out with registerController: false.
OAuth2PasswordGuard The bearer guard. Registered globally by default; exported for per-route @UseGuards.
OAuth2PasswordService token(body), revoke(body), verifyAccessToken(token) — the grant engine, if you wire your own controller.
@Public() Marks a route/controller as exempt from the guard.
IS_PUBLIC_KEY The metadata key @Public() sets (for custom reflection).

Helpers & advanced

Export Description
generateToken(bytes?) Mint a high-entropy opaque token (default 256-bit).
hashToken(token) SHA-256 hex digest — hash tokens at rest in your store.
expiryDate(ttlSeconds, now?) · isExpired(date, now?) TTL/expiry helpers.
OAuth2Exception RFC 6749 §5.2 protocol error (OAuth2ErrorCode).
OAUTH2_PASSWORD_OPTIONS DI token holding the resolved options.
resolveOptions(options) Merge options over the defaults → ResolvedOAuth2PasswordOptions.

Types

Export Description
ValidateUser, ClientStore, TokenStore The three seams.
IssuedAccessToken, IssuedRefreshToken, StoredAccessToken, StoredRefreshToken Token records handed to / returned from the store.
TokenRequest, TokenResponse, RevokeRequest, AuthenticatedRequest HTTP contract types.
OAuth2PasswordOptions, OAuth2PasswordAsyncOptions, OAuth2PasswordBehavior, OAuth2PasswordProviders, ResolvedOAuth2PasswordOptions Option types.

Related Projects

  • nestjs-credentials — Token-agnostic username/password verification: a UserStore seam + pluggable PasswordHasher. The natural validateUser companion.
  • nestjs-jwt-guard — The stateless, self-contained-JWT alternative: a configurable bearer guard, @Public(), and a token-issuance helper.
  • nestjs-accesscontrol — The official NestJS integration for AccessControl v3: RBAC + ABAC with fluent CRUD decorators and attribute filtering.
  • nestjs-http-envelope — A uniform, configurable response & error envelope for NestJS.
  • nestjs-configuard — The NestJS integration for configuard: DB-backed, typed, ABAC-filtered runtime config.

License

MIT © Onur Yıldırım

About

OAuth2 Resource Owner Password Credentials (ROPC) for NestJS — opaque, server-stored, revocable access + refresh tokens, an RFC 6749 token endpoint, and a bearer guard.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages