Skip to main content

Authentication Routes

This is not an exhaustive and complete guide but shows how to implement the most common authentication endpoints with DTO validation, error handling, and multi-step authentication flows.

Accuracy and Adaptation

The examples on this page may not compile or run accurately if directly copied. Please use this to build your own flows. Don't assume input validation and do your input santisation, and application specific logic where possible.

For framework-specific integration details, see:

Overview

nauth-toolkit provides a comprehensive set of authentication endpoints that support:

  • Primary authentication flows - Signup, login, challenge responses, token refresh, logout
  • Password management - Change password, forgot password, password reset
  • Multi-factor authentication - MFA setup, verification, device management
  • Social authentication - OAuth flows, account linking
  • Session management - Device trust, session revocation
  • Audit logging - Authentication history

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": "totp"
}

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

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.