Skip to main content

AuthService

Package: @nauth-toolkit/core Type: Service

Main authentication service that orchestrates all authentication operations including user signup, login, password management, session management, and token generation.

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

Overview

Central service for all authentication operations including signup, login, password management, session handling, and token generation.

note

Automatically injected by your framework adapter. No manual instantiation required.

Methods

changePassword()

Change user's password. Requires current password verification. All user sessions are revoked on successful password change.

async changePassword(dto: ChangePasswordDTO): Promise<ChangePasswordResponseDTO>

Parameters

  • dto - ChangePasswordDTO - No sub field required - user is automatically derived from authenticated context

Returns

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
NOT_FOUNDUser not found or no passwordundefined
PASSWORD_INCORRECTCurrent password incorrectundefined
WEAK_PASSWORDPolicy violation{ errors: string[] }
PASSWORD_REUSEDOnly if password.historyCount is configured AND password reusedundefined
PASSWORD_CHANGE_NOT_ALLOWEDOnly if hooks.beforePasswordChange hook returns falseundefined

WEAK_PASSWORD details

When password validation fails, details includes an array of error strings:

{
"errors": [
"Password must be at least 8 characters long",
"Password must contain at least one uppercase letter",
"Password must contain at least one number",
"Password must contain at least one special character !@#$%^&*()_+=[{}|;:,.<>?-]"
]
}
Social-only users

This method requires an existing password. Social-only users (users who signed up via OAuth and have no password) cannot use this method.

For social-only users:

An administrator can also assign a password using the AdminAuthService.setPassword() method.

Example

await authService.changePassword({
oldPassword: 'OldPass123!',
newPassword: 'NewPass456!',
});

confirmForgotPassword()

Confirm password reset code and set a new password. All user sessions are revoked on successful password reset.

async confirmForgotPassword(dto: ConfirmForgotPasswordDTO): Promise<ConfirmForgotPasswordResponseDTO>

Parameters

Returns

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
SERVICE_UNAVAILABLEPassword reset service not available (framework adapter setup issue - email provider required)undefined
PASSWORD_RESET_CODE_INVALIDCode invalid, user not found, or no active reset tokenundefined
PASSWORD_RESET_CODE_EXPIREDCode expiredundefined
PASSWORD_RESET_MAX_ATTEMPTSOnly if password.passwordReset.maxAttempts exceeded (default: 3)undefined
WEAK_PASSWORDPolicy violation{ errors: string[] }
PASSWORD_REUSEDOnly if password.historyCount is configured AND password reusedundefined

SERVICE_UNAVAILABLE details

This error indicates that PasswordResetService was not injected into AuthService during framework adapter initialization. This typically occurs when:

  • The framework adapter (NestJS/Express/Fastify) was not properly configured with an email provider
  • AuthService was manually instantiated without providing passwordResetService
  • There is a framework adapter setup issue

Note: This is a framework adapter configuration issue, not a consumer application configuration. If you encounter this error, check your framework adapter setup and ensure email provider packages are installed and configured.

WEAK_PASSWORD details

When password validation fails, details includes an array of error strings:

{
"errors": [
"Password must be at least 8 characters long",
"Password must contain at least one uppercase letter",
"Password must contain at least one number",
"Password must contain at least one special character !@#$%^&*()_+=[{}|;:,.<>?-]"
]
}

Example

await authService.confirmForgotPassword({
identifier: 'user@example.com',
code: '123456',
newPassword: 'NewSecurePass123!',
});

forgotPassword()

Request a password reset code (account recovery). This method is non-enumerating—it always returns success even if the user doesn't exist, to prevent account enumeration attacks.

Social accounts

This flow can also be used by social-only (social-first) accounts to set a first password after proving ownership via the reset code.

async forgotPassword(dto: ForgotPasswordDTO): Promise<ForgotPasswordResponseDTO>

Parameters

Returns

  • ForgotPasswordResponseDTO - { success: boolean, destination?: string, deliveryMedium?: 'email' | 'sms', expiresIn?: number }

Response Behavior

  • Always returns { success: true } when the request is accepted (non-enumerating)
  • destination, deliveryMedium, and expiresIn are only included when a reset code is successfully sent
  • Returns success without sending if:
    • passwordResetService is not configured
    • User not found
    • Identifier type doesn't match configuration
    • No verified delivery channel available (email/phone) based on signup.verificationMethod

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
RATE_LIMIT_PASSWORD_RESETOnly if password.passwordReset.rateLimitMax exceeded (default: 3 requests per rateLimitWindow seconds, default: 3600){ retryAfter: number, maxAttempts: number }

RATE_LIMIT_PASSWORD_RESET details

When rate limit is exceeded, details includes:

{
"retryAfter": 3600,
"maxAttempts": 3
}
  • retryAfter: Seconds until the rate limit window resets
  • maxAttempts: Maximum number of requests allowed per window

Example

await authService.forgotPassword({
identifier: 'user@example.com',
});

getUserAuthHistory()

Get paginated authentication audit history for the current authenticated user. Returns login attempts, password changes, MFA events, device trust events, and risk factors.

async getUserAuthHistory(dto?: GetUserAuthHistoryDTO): Promise<GetUserAuthHistoryResponseDTO>

Parameters

  • dto - GetUserAuthHistoryDTO (optional) - Filtering and pagination options. No sub field required - user is automatically derived from the authenticated user's context.

Returns

Behavior

  • Automatically uses the authenticated user's context (no sub needed in DTO)
  • Supports filtering by event types, status, and date ranges
  • Supports pagination with configurable page size (max 500)

Example

const result = await authService.getUserAuthHistory({
page: 1,
limit: 50,
eventTypes: [AuthAuditEventType.LOGIN_SUCCESS],
startDate: new Date('2025-01-01'),
});
// result.data - IAuthAudit[]
// result.total - number
// result.page - number
// result.limit - number
// result.totalPages - number

getUserForAuthContext()

Get user for authentication context with sensitive fields removed. This method ensures consistent user object shape across platforms (core + NestJS) with sensitive fields removed and hasPasswordHash flag added.

async getUserForAuthContext(sub: string): Promise<IUser>

Parameters

  • sub - External user identifier (UUID v4)

Returns

  • IUser - User object with hasPasswordHash flag, without sensitive fields (passwordHash, totpSecret, backupCodes, passwordHistory)

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
NOT_FOUNDUser not foundundefined
ACCOUNT_INACTIVEAccount disabledundefined
Internal use

This method is primarily used by AuthHandler and AuthGuard to load authenticated users. It ensures consistent user object shape across platforms (core + NestJS) with sensitive fields removed.

Example

const user = await authService.getUserForAuthContext('user-uuid-123');
// user.hasPasswordHash === true/false
// user.passwordHash === undefined (removed)

getUserSessions()

Get all active sessions for the current authenticated user. Returns session details including device info, location, authentication method, and timestamps. Current session is marked with isCurrent: true.

async getUserSessions(): Promise<GetUserSessionsResponseDTO>

Parameters

None - user is automatically derived from authenticated context

Returns

Behavior

  • Returns all active sessions for the specified user
  • Current session (if called from authenticated context) is marked with isCurrent: true
  • Includes device information (name, type, platform, browser) when available
  • Includes location information (IP address, country, city) when available
  • Includes authentication method (password, social, admin) and OAuth provider for social logins

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
NOT_FOUNDUser not foundundefined
Authentication Required

This method requires authentication. For user endpoints, extract sub from authenticated user context. For admin endpoints, protect with admin guards and accept sub from route parameter.

Example

@UseGuards(AuthGuard)
@Get('sessions')
async getSessions(@CurrentUser() user: IUser) {
return this.authService.getUserSessions();
}

isTrustedDevice()

Check whether the current device is trusted (eligible for trusted-device MFA bypass). Requires an authenticated session (sessionId must be present in request context).

async isTrustedDevice(): Promise<IsTrustedDeviceResponseDTO>

Returns

Behavior

  • Returns { trusted: false } if trustedDeviceService is not configured
  • Returns { trusted: false } if no device token is present
  • Returns { trusted: true } if device token is valid and trust has not expired
  • Requires authenticated session (sessionId from JWT token in request context)

Device Token Delivery

The device token is read from request context, which varies by token delivery mode:

  • Cookies mode: Device token is automatically sent via nauth_device_token httpOnly cookie (no client action required)
  • JSON mode: Client must send device token in X-Device-Token header (default header name, configurable via deviceTrust.headerName). The frontend SDK automatically handles this.

The method behavior is identical in both modes—the difference is only in how the device token is transmitted to the server.

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
SESSION_NOT_FOUNDSession ID not found in request context OR session not found or revokedundefined
NOT_FOUNDUser not foundundefined

Example

const result = await authService.isTrustedDevice();
// result.trusted === true | false

login()

Authenticate user with email, username, or phone. Returns tokens on success or challenge information when verification/MFA is required. Response body format varies by tokenDelivery.method configuration.

async login(dto: LoginDTO): Promise<AuthResponseDTO>

Parameters

Returns

  • AuthResponseDTO - Response format depends on outcome and tokenDelivery.method:
    • Success (JSON mode): Contains accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt, user, authMethod, trusted, deviceToken (if trusted)
    • Success (Cookies mode): Contains user, authMethod, trusted, deviceToken (if trusted). Tokens are delivered via httpOnly cookies only.
    • Challenge: Contains challengeName, session, challengeParameters, sub (same format regardless of tokenDelivery method)
    • Blocked: Throws exception (no response body)

Response Variations by Token Delivery Mode

ModeSuccess Response BodyChallenge Response BodyNotes

| JSON (tokenDelivery.method: 'json') | { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt, user, authMethod, trusted?, deviceToken? } | { challengeName, session, challengeParameters, sub } | Tokens present in response body; client must store securely | | Cookies (tokenDelivery.method: 'cookies') | { user, authMethod, trusted?, deviceToken? } (tokens removed) | { challengeName, session, challengeParameters, sub } | Tokens NOT in body (httpOnly cookies only); client reads via secure context | | Hybrid (tokenDelivery.method: 'hybrid') | Depends on hybridPolicy: web=cookies, mobile=json | { challengeName, session, challengeParameters, sub } | Policy-driven: web clients get cookies, mobile/API gets JSON tokens |

Token Delivery

If client checks result.accessToken, behavior differs by tokenDelivery.method. In cookies mode, tokens are NOT in the response body—they're in httpOnly cookies set by framework adapters.

Possible Outcomes

OutcomeWhenResponse Body
SuccessCredentials valid, no challenges required, risk assessment passesTokens + user data (format depends on tokenDelivery.method)
Email verification challengeEmail not verified AND emailVerification.required = true{ challengeName: 'VERIFY_EMAIL', session, challengeParameters, sub }
Phone verification challengePhone not verified AND phoneVerification.required = true{ challengeName: 'VERIFY_PHONE', session, challengeParameters, sub }
MFA setup challengeMFA required AND user has no MFA device configured{ challengeName: 'MFA_SETUP_REQUIRED', session, challengeParameters, sub }
MFA verification challengeMFA required AND user has MFA device configured{ challengeName: 'MFA_REQUIRED', session, challengeParameters, sub }
Force password changePassword expired (>= password.expiryDays old) OR mustChangePassword flag set{ challengeName: 'FORCE_CHANGE_PASSWORD', session, challengeParameters, sub }
Blocked (adaptive risk)mfa.enforcement = 'ADAPTIVE' AND adaptive risk evaluation resolves to action = 'block_signin' (via mfa.adaptive.riskLevels)Throws SIGNIN_BLOCKED_HIGH_RISK (no body returned)

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
INVALID_CREDENTIALSInvalid password/user not found OR identifier type mismatch (if login.identifierType is set)undefined | { suggestedProvider: string }
ACCOUNT_INACTIVEUser account isActive = falseundefined
RATE_LIMIT_LOGINOnly if lockout.enabled = true AND IP has exceeded max failed attemptsundefined
SIGNIN_BLOCKED_HIGH_RISKOnly if mfa.adaptive.enabled = true AND risk score exceeds threshold{ expiresAt?: Date }

INVALID_CREDENTIALS details

When the account exists but has no password (social-first account), details may include a suggested provider:

{
"suggestedProvider": "Google"
}

SIGNIN_BLOCKED_HIGH_RISK details

Only thrown if adaptive MFA is enabled (mfa.adaptive.enabled = true) AND risk evaluation determines the login is too risky. Includes optional expiry time:

{
"expiresAt": "2026-01-01T00:00:00.000Z"
}

Example

@Post('login')
async login(@Body() dto: LoginDTO) {
return await this.authService.login(dto);
// Framework adapter handles token delivery based on tokenDelivery.method config
}

Example Response (JSON mode)

Success:

{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"accessTokenExpiresAt": 1730000000,
"refreshTokenExpiresAt": 1732592000,
"authMethod": "password",
"trusted": true,
"deviceToken": "a21b654c-2746-4168-acee-c175083a65cd",
"user": {
"sub": "b32c765d-3857-5279-bdff-d286194b76de",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe"
}
}

Challenge:

{
"challengeName": "MFA_REQUIRED",
"session": "a21b654c-2746-4168-acee-c175083a65cd",
"challengeParameters": {
"methods": ["totp", "sms"]
},
"sub": "b32c765d-3857-5279-bdff-d286194b76de"
}

Example Response (Cookies mode)

Success (tokens in httpOnly cookies, not in body):

{
"authMethod": "password",
"trusted": true,
"deviceToken": "a21b654c-2746-4168-acee-c175083a65cd",
"user": {
"sub": "b32c765d-3857-5279-bdff-d286194b76de",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe"
}
}

Challenge (same format as JSON mode):

{
"challengeName": "MFA_REQUIRED",
"session": "a21b654c-2746-4168-acee-c175083a65cd",
"challengeParameters": {
"methods": ["totp", "sms"]
},
"sub": "b32c765d-3857-5279-bdff-d286194b76de"
}

logout()

Logout user from current session. Revokes the session and optionally removes trusted device if forgetMe is true.

async logout(dto: LogoutDTO): Promise<LogoutResponseDTO>

Parameters

  • dto - LogoutDTO - No sub field required - user is automatically derived from authenticated context. Contains optional forgetMe flag

Returns

Behavior

  • Revokes the current authenticated session
  • If forgetMe is true and trusted device feature is enabled, also revokes the trusted device token
  • Session ID is automatically extracted from JWT token in request context

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
SESSION_NOT_FOUNDSession ID not found in request context (request not authenticated)undefined
Authentication Required

This method requires the user to be authenticated. The endpoint is protected and cannot be called publicly. The session ID is automatically extracted from the authenticated user's JWT token by framework adapters.

Example

// Normal logout (device remains trusted)
await authService.logout({
forgetMe: false,
});

// Logout and forget device (device untrusted, MFA required on next login)
await authService.logout({
forgetMe: true,
});

logoutAll()

Logout user from all sessions across all devices (global signout). Optionally revokes all trusted devices if forgetDevices is true.

async logoutAll(dto: LogoutAllDTO): Promise<LogoutAllResponseDTO>

Parameters

  • dto - LogoutAllDTO - No sub field required - user is automatically derived from authenticated context. Contains optional forgetDevices flag

Returns

Behavior

  • Revokes all sessions for the user across all devices
  • If forgetDevices is true and trusted device feature is enabled, also revokes all trusted devices for the user
  • Returns the count of revoked sessions
  • Device revocation errors are non-blocking (logged but operation continues)

Usage Patterns

  • User-initiated: User logs out from all their own sessions (protected endpoint, user provides their own sub)
  • Admin-initiated: Admin force-logs out any user (admin-protected endpoint, admin provides target user's sub) See logoutAll in AdminAuthService

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
NOT_FOUNDUser not foundundefined
Authentication Required

This method requires authentication. For user endpoints, extract sub from authenticated user context. For admin endpoints, protect with admin guards and accept sub from route parameter.

Example

// User-initiated (user context)
@UseGuards(AuthGuard)
@Post('logout/all')
async logoutAll(@Body() body: { forgetDevices?: boolean }) {
return this.authService.logoutAll({ forgetDevices: body.forgetDevices });
}

logoutSession()

Logout from a specific session by session ID. Validates session ownership for security. Automatically clears cookies if logging out the current session.

async logoutSession(dto: LogoutSessionDTO): Promise<LogoutSessionResponseDTO>

Parameters

  • dto - LogoutSessionDTO - No sub field required - user is automatically derived from authenticated context. Contains sessionId

Returns

Behavior

  • Revokes the specified session for the user
  • Validates session belongs to user (prevents unauthorized session revocation)
  • Automatically clears cookies if the revoked session was the current session
  • Returns wasCurrentSession: true if the revoked session was the current session

Usage Patterns

  • User logging out own session: User revokes specific session (protected endpoint, user provides their own sub)
  • Admin revoking any user's session: Admin revokes specific session for any user (admin-protected endpoint, admin provides target user's sub)

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
NOT_FOUNDUser not foundundefined
SESSION_NOT_FOUNDSession not foundundefined
FORBIDDENSession does not belong to userundefined
Authentication Required

This method requires authentication. For user endpoints, extract sub from authenticated user context. For admin endpoints, protect with admin guards and accept sub from route parameter. Session ownership is validated automatically.

Example

// User logging out own session
@UseGuards(AuthGuard)
@Delete('sessions/:sessionId')
async logoutSession(@Param('sessionId') sessionId: string) {
return this.authService.logoutSession({ sessionId });
}

refreshToken()

Generate new access token using refresh token. Implements secure token rotation with distributed locking and reuse detection to prevent race conditions and replay attacks.

async refreshToken(dto: RefreshTokenDTO): Promise<TokenResponse>

Parameters

Returns

  • TokenResponse - { accessToken: string, refreshToken: string, accessTokenExpiresAt: number, refreshTokenExpiresAt: number }

Response Variations by Token Delivery Mode

ModeResponse BodyNotes
JSON (tokenDelivery.method: 'json'){ accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt }Tokens present in response body; client must store securely
Cookies (tokenDelivery.method: 'cookies'){} (empty object - tokens removed)Tokens NOT in body (httpOnly cookies only); client reads via secure context
Hybrid (tokenDelivery.method: 'hybrid')Depends on hybridPolicy: web=cookies (empty body), mobile=json (tokens in body)Policy-driven: web clients get cookies, mobile/API gets JSON tokens. See Token Delivery Modes and Token Management guides.

Behavior

  • Uses distributed locking to prevent concurrent refresh attempts for the same session
  • Implements token rotation (old tokens are invalidated when new ones are issued)
  • Detects token reuse attempts and revokes affected sessions
  • Returns current tokens if cookie race condition is detected (same session, legitimate duplicate request)

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
TOKEN_INVALIDInvalid/malformed token, expired token, or token already used (reuse detected)undefined
SESSION_NOT_FOUNDSession not found/revokedundefined
RATE_LIMIT_LOGINOnly if distributed lock cannot be acquired (refresh already in progress for same session){ retryAfter: number }
NOT_FOUNDUser not found (internal error)undefined

Example

const tokens = await authService.refreshToken({
refreshToken: 'eyJhbGci...',
});

resendCode()

Resend verification code via email or SMS for the current challenge session. Supports VERIFY_EMAIL, VERIFY_PHONE, and MFA_REQUIRED (SMS/Email methods only).

async resendCode(dto: ResendCodeDTO): Promise<ResendCodeResponseDTO>

Parameters

Returns

  • ResendCodeResponseDTO - { destination: string } - Masked destination where code was sent (e.g., u***r@example.com or +1***5678)

Behavior

  • Resends code for the challenge type specified in the session
  • For VERIFY_PHONE: Requires phone number to be provided first (via respondToChallenge with phone field)
  • For MFA_REQUIRED: Only supports SMS and Email methods (TOTP/Passkey methods don't support code resending)
  • Enforces resend delay (default: 60 seconds) to prevent abuse
  • Returns masked destination for privacy

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
CHALLENGE_INVALIDChallenge session not found or invalidundefined
CHALLENGE_EXPIREDChallenge session expiredundefined
VALIDATION_FAILEDChallenge session has no user, phone not provided (VERIFY_PHONE), method not specified (MFA), or unsupported challenge typeundefined
RATE_LIMIT_RESENDOnly if resend delay not met (default: 60 seconds since last code sent){ retryAfter: number, resendDelay?: number }

RATE_LIMIT_RESEND details

When resend delay is not met, details includes:

{
"retryAfter": 45,
"resendDelay": 60
}
  • retryAfter: Seconds until resend is allowed
  • resendDelay: Configured resend delay in seconds (default: 60)

Example

const result = await authService.resendCode({
session: 'a21b654c-2746-4168-acee-c175083a65cd',
});

respondToChallenge()

Respond to authentication challenge (MFA, email verification, phone verification, password change, MFA setup).

Supports multiple challenge types:

  • VERIFY_EMAIL: Verify email address with code
  • VERIFY_PHONE: Collect phone number or verify with code
  • MFA_REQUIRED: Verify MFA code (SMS, Email, TOTP, Passkey, Backup)
  • FORCE_CHANGE_PASSWORD: Change password when forced
  • MFA_SETUP_REQUIRED: Complete MFA device setup
async respondToChallenge(dto: RespondChallengeDTO): Promise<AuthResponseDTO>

Parameters

Returns

  • AuthResponseDTO - Response format depends on outcome and tokenDelivery.method:
    • Success (JSON mode): Contains accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt, user, authMethod, trusted, deviceToken (if trusted)
    • Success (Cookies mode): Contains user, authMethod, trusted, deviceToken (if trusted). Tokens are delivered via httpOnly cookies only.
    • Challenge: Contains challengeName, session, challengeParameters, sub (same format regardless of tokenDelivery method)

Response Variations by Token Delivery Mode

ModeSuccess Response BodyChallenge Response BodyNotes
JSON (tokenDelivery.method: 'json'){ accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt, user, authMethod, trusted?, deviceToken? }{ challengeName, session, challengeParameters, sub }Tokens present in response body; client must store securely
Cookies (tokenDelivery.method: 'cookies'){ user, authMethod, trusted?, deviceToken? } (tokens removed){ challengeName, session, challengeParameters, sub }Tokens NOT in body (httpOnly cookies only); client reads via secure context
Hybrid (tokenDelivery.method: 'hybrid')Depends on hybridPolicy: web=cookies, mobile=json{ challengeName, session, challengeParameters, sub }Policy-driven: web clients get cookies, mobile/API gets JSON tokens

Phone Verification Notes:

For VERIFY_PHONE challenges, 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

The backend accepts phone updates unconditionally during the challenge, regardless of whether the user already has a phone number. When a phone is provided, the backend:

  1. Updates the user's phone number in the database
  2. Sends a verification SMS to the new/updated phone number
  3. Returns the same VERIFY_PHONE challenge for code verification

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
VALIDATION_FAILEDInvalid format/missing fields, challenge type mismatch, or unknown challenge type{ field?: string, fields?: string[] }
CHALLENGE_INVALIDChallenge session not found, user not found in session, or passkey challenge missing (MFA)undefined
CHALLENGE_EXPIREDChallenge session expiredundefined
CHALLENGE_ALREADY_COMPLETEDChallenge session already completedundefined
VERIFICATION_CODE_INVALIDVerification code incorrect (email, phone, or MFA)undefined (email/MFA) or { attemptsRemaining?: number } (phone)
VERIFICATION_CODE_EXPIREDVerification code expired (from email/phone verification services)undefined
VERIFICATION_TOO_MANY_ATTEMPTSToo many failed verification attempts (phone verification only){ maxAttempts: number, currentAttempts: number }
INVALID_PHONE_FORMATPhone number format invalid (E.164 format required)undefined
PHONE_REQUIREDPhone number required but not provided (from phone verification service)undefined
NOT_FOUNDUser not found after verification/setup or during MFA verificationundefined
WEAK_PASSWORDPassword policy violation (FORCE_CHANGE_PASSWORD challenge only){ errors: string[] }
PASSWORD_REUSEDPassword reused (FORCE_CHANGE_PASSWORD challenge only, conditional on password.historyCount)undefined

VERIFICATION_CODE_INVALID details

For phone verification, details includes attemptsRemaining:

{
"attemptsRemaining": 2
}

For email and MFA verification, details is undefined.

VERIFICATION_TOO_MANY_ATTEMPTS details

When max attempts exceeded for phone verification:

{
"maxAttempts": 3,
"currentAttempts": 3
}

WEAK_PASSWORD details

When password policy validation fails:

{
"errors": ["Password must be at least 8 characters", "Password must contain at least one uppercase letter"]
}

Example

const dto = Object.assign(new RespondChallengeDTO(), {
session: challengeSession,
type: 'MFA_REQUIRED',
code: '123456',
});
const result = await authService.respondToChallenge(dto);

signup()

Register new user account.

async signup(dto: SignupDTO): Promise<AuthResponseDTO>

Parameters

Returns

  • AuthResponseDTO - Response format depends on outcome and tokenDelivery.method:
    • Success (JSON mode): Contains accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt, user, authMethod, trusted, deviceToken (if trusted)
    • Success (Cookies mode): Contains user, authMethod, trusted, deviceToken (if trusted). Tokens are delivered via httpOnly cookies only.
    • Challenge: Contains challengeName, session, challengeParameters, sub (same format regardless of tokenDelivery method)

Response Variations by Token Delivery Mode

ModeSuccess Response BodyChallenge Response BodyNotes
JSON (tokenDelivery.method: 'json'){ accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt, user, authMethod, trusted?, deviceToken? }{ challengeName, session, challengeParameters, sub }Tokens present in response body; client must store securely
Cookies (tokenDelivery.method: 'cookies'){ user, authMethod, trusted?, deviceToken? } (tokens removed){ challengeName, session, challengeParameters, sub }Tokens NOT in body (httpOnly cookies only); client reads via secure context
Hybrid (tokenDelivery.method: 'hybrid')Depends on hybridPolicy: web=cookies, mobile=json{ challengeName, session, challengeParameters, sub }Policy-driven: web clients get cookies, mobile/API gets JSON tokens

Errors

CodeWhenDetails
SIGNUP_DISABLEDSignups disabledundefined
EMAIL_EXISTSEmail already registeredundefined
USERNAME_EXISTSUsername takenundefined
PHONE_EXISTSPhone already registeredundefined
WEAK_PASSWORDPolicy violation{ errors: string[] }
PHONE_REQUIREDPhone required but missing{ verificationMethod: 'phone' | 'both' }

Throws NAuthException with the codes listed above.

WEAK_PASSWORD errors

Example strings returned in errors:

{
"errors": [
"Password must be at least 8 characters long",
"Password must contain at least one uppercase letter",
"Password must contain at least one number",
"Password must contain at least one special character !@#$%^&*()_+=[{}|;:,.<>?-]"
]
}

Example

const result = await authService.signup({
email: 'user@example.com',
password: 'SecurePass123!',
firstName: 'John',
lastName: 'Doe',
});

trustDevice()

Mark current device as trusted for MFA bypass. Only available when mfa.rememberDevices is set to 'user_opt_in' mode. Requires an authenticated session.

If the device is already trusted, returns the existing device token without creating a new one.

async trustDevice(): Promise<TrustDeviceResponseDTO>

Returns

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
FORBIDDENOnly if mfa.rememberDevices is not 'user_opt_in' (feature only available in user opt-in mode)undefined
SESSION_NOT_FOUNDSession ID not found in request context, or session not found/revokedundefined
NOT_FOUNDUser not foundundefined

Example

const result = await authService.trustDevice();

updateUserAttributes()

Update user profile information (firstName, lastName, username, email, phone, metadata, preferredMfaMethod).

Important behaviors:

  • When email changes: Email verification is reset (unless retainVerification: true), and all Email MFA devices are deleted
  • When phone changes: Phone verification is reset (unless retainVerification: true), and all SMS MFA devices are deleted
  • If deleted MFA devices were the only active methods, MFA is automatically disabled
  • Metadata is merged with existing metadata (set key to null to delete)
async updateUserAttributes(dto: UpdateUserAttributesDTO): Promise<UserResponseDTO>

Parameters

Returns

Errors

Throws NAuthException with the codes listed below.

CodeWhenDetails
NOT_FOUNDUser not found (by sub identifier) or user not found after updateundefined
VALIDATION_FAILEDUniqueness violation (email, phone, or username already exists for another user){ conflicts: string[] }

VALIDATION_FAILED details

When uniqueness constraints are violated, details includes a conflicts array:

{
"conflicts": ["Email already exists", "Phone number already exists", "Username already exists"]
}

Example

// Update basic profile fields
const updatedUser = await authService.updateUserAttributes({
username: 'newusername',
firstName: 'John',
lastName: 'Doe',
});

// Add or update metadata
await authService.updateUserAttributes({
metadata: {
department: 'Engineering',
role: 'Senior Developer',
},
});

// Delete metadata keys by setting to null
await authService.updateUserAttributes({
metadata: {
temporaryField: null,
oldKey: null,
},
});

// Mix profile updates with metadata operations
await authService.updateUserAttributes({
firstName: 'Jane',
metadata: {
department: 'Product',
oldDepartment: null,
},
});
MFA Device Deletion

When updating email or phone, associated MFA devices are automatically deleted (cannot be reactivated):

  • Email change: All Email MFA devices are permanently deleted (requires re-setup)
  • Phone change: All SMS MFA devices with the old number are permanently deleted (requires re-setup)

If the deleted device(s) were the only MFA method(s), MFA is disabled for the user. They will need to set up MFA again at next login if MFA is required.

Audit events are logged for all device deletions with reason email_changed or phone_changed.

Best Practices

  • Set retainVerification: true only when transferring between trusted systems
  • Notify users when their MFA devices are removed due to profile changes
  • Guide users through MFA setup after email/phone changes if MFA is required

validateAccessToken()

Validate JWT access token and decode payload. Returns validation result with decoded payload if valid, or error information if invalid.

async validateAccessToken(dto: ValidateAccessTokenDTO): Promise<ValidateAccessTokenResponseDTO>

Parameters

Returns

Behavior

  • Verifies token signature using configured secret/public key
  • Validates expiration timestamp
  • Ensures token type is 'access' (not refresh)
  • Checks issuer and audience claims
  • Returns decoded payload if valid
  • Returns error information if invalid

Use Cases

  • Manual token validation in consumer applications
  • Token introspection for debugging
  • Custom authorization logic requiring token payload
  • API gateway token validation
  • Microservice authentication

Errors

This method does not throw errors. All validation failures are returned in the response DTO with valid: false and appropriate error and errorType fields.

Example

@Injectable()
export class TokenService {
constructor(private readonly authService: AuthService) {}

async validateToken(token: string) {
const result = await this.authService.validateAccessToken({
accessToken: token,
});

if (result.valid) {
console.log('User ID:', result.payload.sub);
console.log('Session ID:', result.payload.sessionId);
console.log('Expires at:', new Date(result.payload.exp * 1000));
return result.payload;
} else {
console.error('Validation failed:', result.error);
console.error('Error type:', result.errorType);
return null;
}
}
}

Response Examples

Valid token:

{
"valid": true,
"payload": {
"sub": "a21b654c-2746-4168-acee-c175083a65cd",
"email": "user@example.com",
"type": "access",
"sessionId": "session-uuid-123",
"iat": 1704067200,
"exp": 1704070800,
"iss": "nauth-toolkit",
"aud": "my-app"
}
}

Invalid token:

{
"valid": false,
"error": "Token expired",
"errorType": "expired"
}

Error Handling

All methods throw NAuthException with structured error data.

try {
await authService.login(dto);
} catch (error) {
if (error instanceof NAuthException) {
console.log(error.code); // AuthErrorCode enum
console.log(error.message); // Human-readable
console.log(error.details); // Optional metadata
}
}

See Error Handling Guide for complete patterns.