Skip to main content

Authentication Routes

The toolkit ships every auth endpoint as a mountable bundle. Mount it and you write no controllers; exclude a key and write only the route you want to change.

The rest of this page documents each endpoint's request and response shape, and shows the hand-written form for when you need to override one.

Full route tableShipped Routes
Framework integrationNestJS · Express · Fastify
Sample apps

examples/starter-nestjs, examples/starter-express and examples/starter-fastify all mount the shipped routes. examples/demo-nestjs hand-writes its controllers, as a reference for overriding.

Mounting the shipped routes​

src/config/auth.config.ts
export const authConfig: NAuthModuleConfig = {
routes: [{ prefix: 'auth' }],
};

AuthModule.forRoot() builds the controllers. No controllers array needed.

Choosing which routes to mount​

By group​

groups selects bundles. The admin and apiKeysAdmin groups are never mounted by default. Route Groups lists every route in each one.

{ prefix: 'auth', groups: ['core', 'profile'] }

By key​

exclude drops individual routes. It serves two different purposes.

Override — replace a route with your own:

{ prefix: 'auth', exclude: ['login'] }

Then declare just that route (see the sections below for the hand-written form).

Suppress — never expose a capability at all:

{ prefix: 'admin', groups: ['admin'], exclude: ['adminSetPassword', 'adminDeleteUser'] }
exclude removes the endpoint, not the capability

AdminAuthService.setPassword is still a public method, still callable from a controller you write, a script, or a background job. If the intent is "nobody may do this, however they reach it", deny the action in your authorization provider as well:

if (action === 'admin.user.setPassword') {
return { allow: false, reason: 'Password changes go through the email reset flow' };
}

The service then refuses regardless of caller. See Authorization.

An unknown key in exclude throws at mount time, so a typo cannot silently re-expose a route you meant to suppress.

Web and mobile from one backend​

Mount the same routes twice, differing only in transport. Requires tokenDelivery.method: 'hybrid' — the toolkit refuses at startup otherwise.

routes: [
{ prefix: 'auth', delivery: 'cookies' },
{ prefix: 'mobile/auth', delivery: 'json', groups: ['core', 'social'] },
]

See Token Management for what each mode changes.

Writing your own​

Everything below shows the request and response shape of each endpoint, and the hand-written form. You need this only for routes you exclude.

All endpoints use DTOs for request validation and return structured responses. The Challenge System handles multi-step authentication flows automatically.

Primary Authentication Flow​

The core authentication flow consists of five primary endpoints that handle most authentication scenarios.

Signup​

Creates a new user account. May return a challenge if email/phone verification is required.

import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService, SignupDTO, AuthResponseDTO, Public } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Public()
@Post('signup')
@HttpCode(HttpStatus.CREATED)
async signup(@Body() dto: SignupDTO): Promise<AuthResponseDTO> {
return await this.authService.signup(dto);
}
}

Request DTO: SignupDTO

{
"email": "user@example.com",
"password": "SecurePass123!",
"firstName": "John",
"lastName": "Doe"
}

Response: AuthResponseDTO - Contains tokens or challenge

See Challenge System for challenge handling.

Login​

Authenticates user with email/password. May return a challenge if MFA or verification is required.

import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService, LoginDTO, AuthResponseDTO, Public } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Public()
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() dto: LoginDTO): Promise<AuthResponseDTO> {
return await this.authService.login(dto);
}
}

Request DTO: LoginDTO

{
"identifier": "user@example.com",
"password": "SecurePass123!"
}

Response: AuthResponseDTO - Contains tokens or challenge

See Challenge System for MFA and challenge handling.

Respond to Challenge​

Unified endpoint for completing any challenge type (email verification, phone verification, MFA, password change).

import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService, RespondChallengeDTO, AuthResponseDTO, Public } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Public()
@Post('respond-challenge')
@HttpCode(HttpStatus.OK)
async respondToChallenge(@Body() dto: RespondChallengeDTO): Promise<AuthResponseDTO> {
return await this.authService.respondToChallenge(dto);
}
}

Request DTO: RespondChallengeDTO

Email Verification:

{
"session": "challenge-session-token",
"type": "VERIFY_EMAIL",
"code": "123456"
}

Phone Verification (Collection/Update):

{
"session": "challenge-session-token",
"type": "VERIFY_PHONE",
"phone": "+1234567890"
}

Note: The phone field can be used to:

  • Collect a phone number when user has none (e.g., social signup)
  • Update an existing phone number if user entered wrong number during signup

After submitting phone, backend sends verification SMS and returns the same challenge for code verification.

Phone Verification (Code):

{
"session": "challenge-session-token",
"type": "VERIFY_PHONE",
"code": "123456"
}

MFA Verification:

{
"session": "challenge-session-token",
"type": "MFA_REQUIRED",
"method": "totp",
"code": "123456"
}

Response: AuthResponseDTO - Contains tokens or next challenge

See Challenge System for complete challenge flow documentation.

Refresh Token​

Issues a new access token using a valid refresh token.

import { Controller, Post, Body, HttpCode, HttpStatus, Req } from '@nestjs/common';
import { AuthService, RefreshTokenDTO, TokenResponse, Public } from '@nauth-toolkit/nestjs';
import type { Request } from 'express';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Public()
@Post('refresh')
@HttpCode(HttpStatus.OK)
async refresh(@Body() dto: RefreshTokenDTO | undefined, @Req() req: Request): Promise<TokenResponse> {
const dtoToUse: RefreshTokenDTO = dto ?? ({} as RefreshTokenDTO);
if (!dtoToUse.refreshToken) {
dtoToUse.refreshToken = req?.cookies?.['nauth_refresh_token'];
}
return await this.authService.refreshToken(dtoToUse);
}
}

Request DTO: RefreshTokenDTO

{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Response: TokenResponse - New access and refresh tokens

See Token Delivery for token rotation details.

Logout​

Revokes the current session and clears authentication cookies. User identity is automatically resolved from the JWT context.

import { Controller, Get, Query, UseGuards, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService, LogoutDTO, LogoutResponseDTO, AuthGuard } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@UseGuards(AuthGuard)
@Get('logout')
@HttpCode(HttpStatus.OK)
async logout(@Query() dto: LogoutDTO): Promise<LogoutResponseDTO> {
return await this.authService.logout(dto);
}
}

Request DTO: LogoutDTO

Query Parameters:

  • forgetMe (optional) - If true, untrusts the device

Response: LogoutResponseDTO

{
"success": true
}

Password Management​

Change Password​

Allows authenticated users to change their password.

import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService, ChangePasswordDTO, ChangePasswordResponseDTO, AuthGuard } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@UseGuards(AuthGuard)
@Post('change-password')
@HttpCode(HttpStatus.OK)
async changePassword(@Body() dto: ChangePasswordDTO): Promise<ChangePasswordResponseDTO> {
return await this.authService.changePassword(dto);
}
}

Request DTO: ChangePasswordDTO

{
"oldPassword": "OldPass123!",
"newPassword": "NewSecurePass123!"
}

Response: ChangePasswordResponseDTO

{
"success": true
}

Forgot Password​

Request a password reset code via email or SMS. The baseUrl field is used by the toolkit to construct the reset link in the email.

import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService, ForgotPasswordDTO, ForgotPasswordResponseDTO, Public } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Public()
@Post('forgot-password')
@HttpCode(HttpStatus.OK)
async forgotPassword(@Body() dto: ForgotPasswordDTO): Promise<ForgotPasswordResponseDTO> {
dto.baseUrl = `${process.env.FRONTEND_BASE_URL || 'http://localhost:4200'}/auth/reset-password`;
return await this.authService.forgotPassword(dto);
}
}

Request DTO: ForgotPasswordDTO

{
"identifier": "user@example.com"
}

Response: ForgotPasswordResponseDTO - Response details in same file

Confirm Forgot Password​

Confirm password reset code and set new password.

import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService, ConfirmForgotPasswordDTO, ConfirmForgotPasswordResponseDTO, Public } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Public()
@Post('forgot-password/confirm')
@HttpCode(HttpStatus.OK)
async confirmForgotPassword(@Body() dto: ConfirmForgotPasswordDTO): Promise<ConfirmForgotPasswordResponseDTO> {
return await this.authService.confirmForgotPassword(dto);
}
}

Request DTO: ConfirmForgotPasswordDTO - DTO details in same file

{
"identifier": "user@example.com",
"code": "123456",
"newPassword": "NewSecurePass123!"
}

Helper Endpoints​

Get Setup Data​

Get MFA setup data during MFA_SETUP_REQUIRED challenge.

import { Controller, Post, Body, HttpCode, HttpStatus, Inject, BadRequestException } from '@nestjs/common';
import { MFAService, GetSetupDataDTO, GetSetupDataResponseDTO, Public } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
constructor(
@Inject(MFAService)
private readonly mfaService?: MFAService,
) {}

@Public()
@Post('challenge/setup-data')
@HttpCode(HttpStatus.OK)
async getSetupData(@Body() dto: GetSetupDataDTO): Promise<GetSetupDataResponseDTO> {
if (!this.mfaService) {
throw new BadRequestException('MFA service is not available');
}
return await this.mfaService.getSetupData(dto);
}
}

Request DTO: GetSetupDataDTO

{
"session": "challenge-session-token",
"method": "sms",
"setupData": { "phoneNumber": "+14155552671" }
}

setupData is optional and method-specific. For SMS it carries the phone number when the account has none (the challenge sets requiresPhoneCollection: "true"), see SMS MFA > Forced Setup.

Response: GetSetupDataResponseDTO - Provider-specific setup data

See MFA Guide for complete MFA setup flow.

Resend Code​

Resend verification code for the current challenge.

import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService, ResendCodeDTO, ResendCodeResponseDTO, Public } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Public()
@Post('challenge/resend')
@HttpCode(HttpStatus.OK)
async resendCode(@Body() dto: ResendCodeDTO): Promise<ResendCodeResponseDTO> {
return await this.authService.resendCode(dto);
}
}

Request DTO: ResendCodeDTO

{
"session": "challenge-session-token"
}

Response: ResendCodeResponseDTO

{
"destination": "u***r@example.com"
}

MFA Management​

Get MFA Status​

Get MFA configuration status for the current user.

import { Controller, Get, UseGuards } from '@nestjs/common';
import { MFAService, AuthGuard } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
constructor(private readonly mfaService: MFAService) {}

@UseGuards(AuthGuard)
@Get('mfa/status')
async getMfaStatus() {
return await this.mfaService.getMfaStatus();
}
}

Response:

{
"enabled": true,
"required": false,
"methods": ["totp", "sms"],
"availableMethods": ["totp", "sms", "passkey"],
"hasBackupCodes": true,
"preferredMethod": "totp"
}

See Managing MFA Devices for complete MFA device management.

Social Authentication​

Redirect-first social login. The backend owns the OAuth callback, sets cookies (or issues an exchangeToken), then redirects back to the frontend.

Start social login redirect​

GET /auth/social/:provider/redirect?returnTo=/auth/callback&appState=12345

import { Controller, Get, Param, Query, Redirect } from '@nestjs/common';
import { Public, SocialRedirectHandler, StartSocialRedirectQueryDTO, StartSocialRedirectResponseDTO } from '@nauth-toolkit/nestjs';

@Controller('auth/social')
export class SocialRedirectController {
constructor(private readonly socialRedirect: SocialRedirectHandler) {}

@Public()
@Redirect()
@Get(':provider/redirect')
async start(
@Param('provider') provider: string,
@Query() dto: StartSocialRedirectQueryDTO,
): Promise<StartSocialRedirectResponseDTO> {
return await this.socialRedirect.start(provider, dto);
}
}

Provider callback (backend)​

Provider redirects back to:

  • GET /auth/social/:provider/callback (Google/Facebook)
  • POST /auth/social/:provider/callback (Apple form_post)

Backend responds with:

  • 302 → frontend returnTo?appState=... (cookies success path), and sets httpOnly cookies in the same response
  • 302 → frontend returnTo?appState=...&exchangeToken=... (json/hybrid, and cookies-with-challenge)

NestJS: simplified controller​

The handler reads delivery and deviceToken from ContextStorage and applies cookies via HTTP_RESPONSE. The controller only passes provider and DTO.

import { Controller, Get, Post, Param, Query, Body, Redirect } from '@nestjs/common';
import { Public, SocialRedirectHandler, SocialCallbackQueryDTO, SocialCallbackFormDTO, SocialRedirectCallbackResponseDTO } from '@nauth-toolkit/nestjs';

@Controller('auth/social')
export class SocialRedirectController {
constructor(private readonly socialRedirect: SocialRedirectHandler) {}

@Public()
@Redirect()
@Get(':provider/callback')
async callbackGet(
@Param('provider') provider: string,
@Query() dto: SocialCallbackQueryDTO,
): Promise<SocialRedirectCallbackResponseDTO> {
return await this.socialRedirect.callback(provider, dto);
}

@Public()
@Redirect()
@Post(':provider/callback')
async callbackPost(
@Param('provider') provider: string,
@Body() dto: SocialCallbackFormDTO,
): Promise<SocialRedirectCallbackResponseDTO> {
return await this.socialRedirect.callback(provider, dto);
}
}

Exchange exchangeToken​

POST /auth/social/exchange

{ "exchangeToken": "..." }

Response: AuthResponseDTO

OpenID Connect Interaction​

When your application is also an OpenID Connect provider, four more routes bridge the provider and your login. OIDCProviderModule.forRoot() registers them for you at oidc/interaction/:uid, under any global prefix — the paths below assume none.

EndpointMethodAuthPurpose
/oidc/interaction/:uidGETOptionalWhat the pending authorization request needs
/oidc/interaction/:uid/abortPOSTOptionalAbandon the request with access_denied
/oidc/interaction/:uid/confirmPOSTOptionalRecord the consent decision
/oidc/interaction/:uid/loginPOSTOptionalComplete the login step

"Optional" means the routes run inside the guard chain so a signed-in caller is identified, but an anonymous caller is answered, not rejected — a signed-out response is what tells your page to send the user to login. If you write these routes yourself, reproduce that: @UseGuards(AuthGuard) on the class and @Public() on every route.

Each returns JSON rather than a 302; the consent screen navigates itself with window.location.assign(redirectTo). See createOIDCInteractionController for the shipped implementation and Building the Consent Screen for the frontend.

User Profile​

Get Current User​

Get the authenticated user's profile. Use UserResponseDTO.fromEntity() to sanitize the response and exclude sensitive fields like password hashes.

import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard, CurrentUser, UserResponseDTO } from '@nauth-toolkit/nestjs';
import type { IUser } from '@nauth-toolkit/nestjs';

@Controller('auth')
export class AuthController {
@UseGuards(AuthGuard)
@Get('profile')
async getProfile(@CurrentUser() user: IUser): Promise<UserResponseDTO> {
return UserResponseDTO.fromEntity(user);
}
}

Response: UserResponseDTO - Sanitized user profile

Error Handling​

All endpoints use NAuthException for structured error responses. Handle errors appropriately:

import { NAuthHttpExceptionFilter } from '@nauth-toolkit/nestjs';

// In main.ts
app.useGlobalFilters(new NAuthHttpExceptionFilter());

See Error Handling for complete error handling guide.