Skip to content

Repository files navigation

NestAuth — JWT, Local, Google & Facebook Authentication for NestJS

npm version npm downloads license

@next-nest-auth/nestauth is a drop-in authentication module for NestJS applications. Register one module, implement one interface, and get a working login, refresh-token, Google OAuth2, and Facebook OAuth2 flow — all issuing short-lived JWT access tokens and long-lived JWT refresh tokens — without hand-wiring Passport strategies, guards, and controllers yourself.

It's built for NestJS REST APIs consumed by any frontend — Next.js, React, Vue, mobile apps, or another backend — since everything it exposes is plain JSON over HTTP.

Table of Contents

Why NestAuth

Features

  • Local login — username/password (or any credential shape you define) via a validateUser() method you implement.
  • JWT access + refresh tokens — short-lived access tokens, long-lived refresh tokens, cryptographically tagged so one can never be used in place of the other.
  • Google OAuth2 and Facebook OAuth2 login, wired up automatically — implement google()/facebook() on your UserService and the redirect/callback routes just work.
  • A ready-made NestAuthJwtGuard to protect any route in your own app with one decorator.
  • Consistent JSON error responses (statusCode, message, path, app) across every auth endpoint, for both Express and Fastify.
  • Multiple isolated auth instances in one app — mount two independent NestAuthModule.forRoot() calls under different route prefixes (e.g. separate customer vs. admin auth) without route or strategy collisions.
  • You own the data layer. NestAuth never touches a database or hashes a password itself — you implement NestAuthInterface against whatever user store and hashing library you already use, so it doesn't fight your existing schema.

Who this is for

  • Teams building a NestJS REST/GraphQL backend for a separate frontend (Next.js, React Native, a SPA) that need email/password login plus "Sign in with Google/Facebook" without assembling Passport strategies by hand.
  • Projects that need refresh-token rotation semantics (access vs. refresh tokens that can't be swapped for one another) out of the box.
  • Multi-tenant or multi-surface backends that want two separately configured auth realms (e.g. /nestauth for end users, /admin-auth for staff) in the same Nest application.

Prerequisites

  • Node.js >= 18
  • A NestJS application (v11+) with @nestjs/common, @nestjs/config, @nestjs/jwt, and @nestjs/passport installed — these are peerDependencies, so your app supplies them.

Installation

1. Install NestJS (if you don't already have an app)

npm i -g @nestjs/cli
nest new nestauth-app
cd nestauth-app

2. Install @next-nest-auth/nestauth

npm install @next-nest-auth/nestauth

Quick Start

1. Register NestAuthModule

// app.module.ts
import { Module } from "@nestjs/common";
import { NestAuthModule } from "@next-nest-auth/nestauth";
import { UserModule } from "./user.module";
import { UserService } from "./user.service";

@Module({
    imports: [
        NestAuthModule.forRoot({
            UserModule: UserModule,
            UserService: UserService,
            jwtSecret: process.env.JWT_SECRET, // required — throws on startup if missing
            jwtExpiresIn: "15m",
            jwtRefreshTokenExpiresIn: "7d",
            routePrefix: "nestauth",
        }),
    ],
})
export class AppModule {}

2. Implement NestAuthInterface on your UserService

validateUser() and getUserById() are required. google()/facebook() are optional — implement them only if you enable those login methods; calling the corresponding endpoint without an implementation returns a 501 Not Implemented instead of crashing.

// user.service.ts
import { Injectable } from "@nestjs/common";
import {
    JwtPayloadType,
    NestAuthInterface,
    GoogleProfileType,
    FacebookProfileType,
} from "@next-nest-auth/nestauth";

@Injectable()
export class UserService implements NestAuthInterface {
    async validateUser(params: { username: string; password: string }): Promise<JwtPayloadType> {
        // Look up the user and verify the password (e.g. with bcrypt) against your own store.
        // Return null to reject the login attempt.
        return { sub: 1, name: "John Doe", email: "john@example.com", role: "user" };
    }

    async getUserById(id: number | string): Promise<JwtPayloadType> {
        // Used to re-hydrate the user when a refresh token is redeemed.
        return { sub: id, name: "John Doe", email: "john@example.com", role: "user" };
    }

    // Optional — only needed if you use Google login.
    async google(profile: GoogleProfileType): Promise<JwtPayloadType> {
        // Find-or-create a user from profile.email, return the JWT payload for them.
        return { sub: profile.id, name: profile.firstName, email: profile.email };
    }

    // Optional — only needed if you use Facebook login.
    async facebook(profile: FacebookProfileType): Promise<JwtPayloadType> {
        return { sub: profile.id, name: profile.firstName, email: profile.email };
    }
}

3. Log in

curl -X POST http://localhost:3000/nestauth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"john","password":"secret"}'

Whatever body you send is passed straight to validateUser() — use any field names your app needs (username/password, email/OTP, mobile/PIN, etc.).

{
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsIm5hbWUiOiJKb2huIERvZSIsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIiwidHlwZSI6ImFjY2VzcyIsImlhdCI6MTc4Njk0OTI5MCwiZXhwIjoxNzg2OTUwMTkwfQ.V6itfqLh6hB1-Yj6S88ozBfLp6iTESVhC0Vyroe-Ghw",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsIm5hbWUiOiJKb2huIERvZSIsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIiwidHlwZSI6InJlZnJlc2giLCJpYXQiOjE3ODY5NDkyOTAsImV4cCI6MTc4NzU1NDA5MH0.Wt2zC32nO1e16Wa_FrJeyNDCnggwhgc9l840DUpE7rk",
    "accessTokenExpiresIn": "15m",
    "refreshTokenExpiresIn": "7d"
}

Decoded, the access token's payload is:

{ "sub": 1, "name": "John Doe", "email": "john@example.com", "role": "user", "type": "access", "iat": 1786949290, "exp": 1786950190 }

The refresh token carries the same claims with "type": "refresh" — the type claim is what keeps the two from being interchangeable (see Security Notes).

Configuration Reference

Options passed to NestAuthModule.forRoot(options):

Option Type Required Description
UserModule Type<any> Yes The Nest module that provides your UserService.
UserService Type<NestAuthInterface> Yes Your class implementing NestAuthInterface.
jwtSecret string Yes Secret used to sign/verify all tokens. forRoot() throws if omitted — there is no default.
jwtExpiresIn string | number No Access token lifetime (e.g. "15m"). Defaults to 15m.
jwtRefreshTokenExpiresIn string | number No Refresh token lifetime (e.g. "7d"). Defaults to 7d.
routePrefix string No Mounts the auth controller at <routePrefix>/nestauth instead of /nestauth, and also namespaces DI tokens and Passport strategy names — see Multiple Auth Instances.

API Reference

All routes below are mounted under /nestauth by default, or /<routePrefix>/nestauth if routePrefix is set.

Method Path Guard Purpose
POST /login local Authenticate with validateUser(); returns an access + refresh token pair.
POST /refresh-token none (token itself is the credential) Exchange a refresh token for a new access + refresh token pair.
GET /google Google OAuth Starts the Google login redirect.
GET /google-redirect Google OAuth Google's callback target; calls UserService.google() and returns tokens.
GET /facebook Facebook OAuth Starts the Facebook login redirect.
GET /facebook-redirect Facebook OAuth Facebook's callback target; calls UserService.facebook() and returns tokens.
GET /logout JWT (access token) Stateless logout — confirms the token is valid and tells the client to discard it.
ALL / none Returns a welcome string; useful as a health check for the auth routes.

Local Login

POST /nestauth/login
Content-Type: application/json

{ "username": "john", "password": "secret" }

The request body is whatever validateUser() expects — NestAuth doesn't impose a schema on it.

Refresh Token

POST /nestauth/refresh-token
Content-Type: application/json

{ "refreshToken": "your-refresh-token" }

The legacy key refresh_token is also accepted. Only tokens issued with type: "refresh" are accepted — presenting an access token here fails, and vice versa on protected routes.

On success: a new { accessToken, refreshToken, accessTokenExpiresIn, refreshTokenExpiresIn } pair, same shape as login.

On failure:

{
    "statusCode": 401,
    "message": "Invalid or expired refresh token",
    "path": "/nestauth/refresh-token",
    "app": "nestauth"
}

Google OAuth

Send the browser to GET /nestauth/google to start the flow. On success, Google redirects to GET /nestauth/google-redirect, which calls UserService.google(profile) and returns the same token pair shape as login. Requires GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and BASE_URL to be set (see Environment Variables), and google() implemented on your UserService — otherwise this returns 501 Not Implemented.

Facebook OAuth

Same shape as Google: GET /nestauth/facebook starts the flow, GET /nestauth/facebook-redirect is the callback, and it calls UserService.facebook(profile). Requires FACEBOOK_APP_ID, FACEBOOK_APP_SECRET, BASE_URL, and a facebook() implementation.

Logout

GET /nestauth/logout
Authorization: Bearer <accessToken>

NestAuth is stateless — there's no server-side session or token store to invalidate. This endpoint just validates the token and returns { "message": "Logged out. Discard the access and refresh tokens on the client." }; your client is responsible for discarding both tokens. If you need server-side revocation (e.g. for a "log out everywhere" feature), you'll need to add a token blacklist or a tokenVersion check in getUserById() yourself.

Protecting Your Own Routes

Use the exported NestAuthJwtGuard on any route in your own application:

import { Controller, Get, UseGuards, Request } from "@nestjs/common";
import { NestAuthJwtGuard } from "@next-nest-auth/nestauth";

@Controller("user")
export class AppController {
    @Get("/profile")
    @UseGuards(NestAuthJwtGuard)
    getProfile(@Request() req) {
        return req.user; // the JWT payload, minus iat/exp/type
    }
}

Error Response Shape

Every error from a NestAuth route (login, refresh, OAuth, logout) is normalized to:

{ "statusCode": 401, "message": "...", "path": "/nestauth/...", "app": "nestauth" }

This normalization only applies to NestAuth's own controller. Errors from NestAuthJwtGuard used on your own routes use Nest's default exception format unless you apply your own filter.

Multiple Auth Instances

Because routePrefix namespaces both the mounted path and the internal DI tokens/Passport strategy names, you can register NestAuthModule.forRoot() more than once in the same app for independent auth realms:

@Module({
    imports: [
        NestAuthModule.forRoot({
            UserModule: CustomerModule,
            UserService: CustomerAuthService,
            jwtSecret: process.env.CUSTOMER_JWT_SECRET,
            routePrefix: "customer",
        }),
        NestAuthModule.forRoot({
            UserModule: AdminModule,
            UserService: AdminAuthService,
            jwtSecret: process.env.ADMIN_JWT_SECRET,
            routePrefix: "admin",
        }),
    ],
})
export class AppModule {}

This mounts routes at /customer/nestauth/... and /admin/nestauth/..., each with its own local/Google/Facebook strategies (namespaced per instance) and its own login/refresh logic.

Known limitation: NestAuthJwtGuard/NestAuthJwtStrategy are not namespaced per instance — Passport registers strategies in a single global registry keyed by name, and every forRoot() call registers its JWT strategy under the same name ("jwt"). With two instances, whichever one is instantiated last silently wins that registry entry, and NestAuthJwtGuard app-wide ends up validating tokens against only that instance's secret. Don't rely on two different jwtSecrets being independently enforced by NestAuthJwtGuard today — use distinct route prefixes for organizing endpoints, not for cryptographic isolation between realms, until this is namespaced (tracked in the changelog as a known issue, not yet fixed).

Environment Variables

jwtSecret is passed explicitly via forRoot() — how you source it (env var, secrets manager) is up to you. A typical .env:

JWT_SECRET=replace-with-a-long-random-secret

If you use the Google or Facebook strategies, also set:

BASE_URL=https://your-app.example.com
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
FACEBOOK_APP_ID=...
FACEBOOK_APP_SECRET=...

Security Notes

  • Rate limiting is your responsibility. /login has no built-in throttling; pair it with @nestjs/throttler or an upstream WAF/gateway to mitigate credential stuffing.
  • Password hashing is your responsibility. validateUser() receives the raw request body — hash and compare passwords yourself (e.g. with bcrypt) before returning a payload.
  • No built-in refresh-token revocation. Refresh tokens are stateless JWTs valid until they expire; there is no server-side blacklist. If you need immediate revocation, check a tokenVersion/sessionId claim against your user store inside getUserById().
  • Validate your own request bodies. NestAuth doesn't apply a ValidationPipe or DTOs to validateUser()'s input — add your own validation if the body shape matters for your app's security.

Changelog

See CHANGELOG.md for release notes, including breaking changes.

License

MIT — see LICENSE.

About

A nestjs package for authentication

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages