Skip to main content

Admin Operations

Build a complete admin panel for user management. By the end of this guide you will have these endpoints working:

User management:

EndpointMethodPurpose
/auth/admin/signupPOSTCreate a user with overrides (bypass verification, generate password)
/auth/admin/signup-socialPOSTImport a social user (migration from Cognito, Auth0, etc.)
/auth/admin/usersGETList users with filters and pagination
/auth/admin/users/:subGETGet a single user by UUID
/auth/admin/users/:subDELETEDelete a user (cascade cleanup)
/auth/admin/users/:sub/disablePOSTLock a user account
/auth/admin/users/:sub/enablePOSTUnlock a user account

Password management:

EndpointMethodPurpose
/auth/admin/set-passwordPOSTSet a user's password directly
/auth/admin/reset-password/initiatePOSTSend a password reset code via email or SMS
/auth/admin/users/:sub/force-password-changePOSTForce password change on next login

Session management:

EndpointMethodPurpose
/auth/admin/users/:sub/sessionsGETList all active sessions for a user
/auth/admin/users/:sub/logout-allPOSTSign out a user from all devices

MFA management:

EndpointMethodPurpose
/auth/admin/users/:sub/mfa/statusGETCheck a user's MFA enrollment
/auth/admin/users/:sub/mfa/devicesGETList a user's MFA devices
/auth/admin/mfa/devices/:deviceIdDELETERemove an MFA device
/auth/admin/users/:sub/mfa/devices/:deviceId/preferredPOSTSet preferred MFA device
/auth/admin/mfa/exemptionPOSTGrant or revoke MFA exemption

Audit:

EndpointMethodPurpose
/auth/admin/audit/historyGETGet authentication audit history for a user
Sample apps

Admin routes are fully implemented in the nauth example apps — see the Express and Fastify examples for admin routes, and the Angular example for admin.component.ts.

Authorization is your responsibility

nauth-toolkit is an authentication framework — it does not handle authorization or permissions. All admin endpoints below use requireAuth() to verify the caller has a valid JWT, but you must add your own authorization layer on top (role checks, permission guards, RBAC, etc.) to ensure only admins can access these routes.

Prerequisites

  • Basic Auth Flows are working (signup, login, challenge endpoints)
  • You have an authorization strategy for protecting admin routes (role-based, permission-based, etc.)

Step 1: Add Backend Routes

The admin API uses two services from nauth-toolkit:

  • adminAuthService — user CRUD, passwords, sessions
  • mfaService — MFA status, devices, exemptions

Both are available on the nauth instance (Express/Fastify) or injectable via the NestJS module.

User management routes

src/auth/admin.controller.ts
import {
Controller, Get, Post, Delete, Body, Param, Query,
UseGuards, HttpCode, HttpStatus,
} from '@nestjs/common';
import { AdminAuthService, AuthGuard } from '@nauth-toolkit/nestjs';

// WARNING: AuthGuard only verifies the JWT is valid (authentication).
// You MUST add your own authorization guard to restrict access to admins.
// Example: @UseGuards(AuthGuard, YourAdminGuard)
@UseGuards(AuthGuard)
@Controller('auth/admin')
export class AdminController {
constructor(private readonly adminAuthService: AdminAuthService) {}

@Post('signup')
@HttpCode(HttpStatus.CREATED)
async createUser(@Body() dto: any) {
return await this.adminAuthService.signup(dto);
}

@Post('signup-social')
@HttpCode(HttpStatus.CREATED)
async importSocialUser(@Body() dto: any) {
return await this.adminAuthService.signupSocial(dto);
}

@Get('users')
async listUsers(@Query() query: any) {
return await this.adminAuthService.getUsers(query);
}

@Get('users/:sub')
async getUser(@Param('sub') sub: string) {
return await this.adminAuthService.getUserById({ sub });
}

@Delete('users/:sub')
@HttpCode(HttpStatus.OK)
async deleteUser(@Param('sub') sub: string) {
return await this.adminAuthService.deleteUser({ sub });
}

@Post('users/:sub/disable')
@HttpCode(HttpStatus.OK)
async disableUser(@Param('sub') sub: string, @Body() body: any) {
return await this.adminAuthService.disableUser({ sub, reason: body?.reason });
}

@Post('users/:sub/enable')
@HttpCode(HttpStatus.OK)
async enableUser(@Param('sub') sub: string) {
return await this.adminAuthService.enableUser({ sub });
}

@Post('set-password')
@HttpCode(HttpStatus.OK)
async setPassword(@Body() dto: any) {
return await this.adminAuthService.setPassword(dto);
}

@Post('reset-password/initiate')
@HttpCode(HttpStatus.OK)
async resetPassword(@Body() dto: any) {
return await this.adminAuthService.resetPassword(dto);
}

@Post('users/:sub/force-password-change')
@HttpCode(HttpStatus.OK)
async forcePasswordChange(@Param('sub') sub: string) {
return await this.adminAuthService.setMustChangePassword({ sub });
}

@Get('users/:sub/sessions')
async getUserSessions(@Param('sub') sub: string) {
return await this.adminAuthService.getUserSessions({ sub });
}

@Post('users/:sub/logout-all')
@HttpCode(HttpStatus.OK)
async logoutAll(@Param('sub') sub: string, @Body() body: any) {
return await this.adminAuthService.logoutAll({ sub, forgetDevices: body?.forgetDevices });
}
}

Register the controller in your auth module.

Admin MFA routes

src/auth/admin-mfa.controller.ts
import {
Controller, Get, Post, Delete, Body, Param,
UseGuards, HttpCode, HttpStatus, Inject,
} from '@nestjs/common';
import { AuthGuard, MFAService } from '@nauth-toolkit/nestjs';

// WARNING: Add your own authorization guard alongside AuthGuard.
@UseGuards(AuthGuard)
@Controller('auth/admin')
export class AdminMfaController {
constructor(
@Inject(MFAService)
private readonly mfaService: MFAService,
) {}

@Get('users/:sub/mfa/status')
async getMfaStatus(@Param('sub') sub: string) {
return await this.mfaService.adminGetMfaStatus({ sub });
}

@Get('users/:sub/mfa/devices')
async getMfaDevices(@Param('sub') sub: string) {
return await this.mfaService.adminGetUserDevices({ sub });
}

@Delete('mfa/devices/:deviceId')
@HttpCode(HttpStatus.OK)
async removeDevice(@Param('deviceId') deviceId: string) {
return await this.mfaService.adminRemoveDevice({ deviceId: Number(deviceId) });
}

@Post('users/:sub/mfa/devices/:deviceId/preferred')
@HttpCode(HttpStatus.OK)
async setPreferredDevice(@Param('sub') sub: string, @Param('deviceId') deviceId: string) {
return await this.mfaService.adminSetPreferredDevice({ sub, deviceId: Number(deviceId) });
}

@Post('mfa/exemption')
@HttpCode(HttpStatus.OK)
async setMfaExemption(@Body() dto: any) {
return await this.mfaService.setMFAExemption(dto);
}
}

Register the controller in your auth module.

Admin audit route

src/auth/admin.controller.ts
// Add to your AdminController:

@Get('audit/history')
async getAuditHistory(@Query() query: any) {
return await this.auditService.getUserAuthHistory(query);
}

Inject AuthAuditService (or access it via the nauth instance) in the constructor.

Step 2: User Management

Create a user

POST /auth/admin/signup

Request body:

{
"email": "john@example.com",
"password": "SecurePass123!",
"firstName": "John",
"lastName": "Doe",
"isEmailVerified": true,
"mustChangePassword": false
}

Response:

{
"user": {
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"isEmailVerified": true,
"isPhoneVerified": false,
"isActive": true,
"isLocked": false,
"mfaEnabled": false,
"createdAt": "2025-01-15T10:30:00.000Z"
}
}

Create with auto-generated password

{
"email": "john@example.com",
"generatePassword": true,
"isEmailVerified": true,
"mustChangePassword": true
}

Response — includes the one-time generated password:

{
"user": { "sub": "...", "email": "john@example.com" },
"generatedPassword": "Kx9#mP2$vN7@qR4!"
}

The generated password is returned once and never stored in plain text. Deliver it securely to the user.

When mustChangePassword: true, the user's next login returns a FORCE_CHANGE_PASSWORD challenge — they must set a new password before receiving tokens. See Basic Auth Flows > Other challenge types for challenge handling.

All request fields

FieldTypeRequiredDefaultDescription
emailstringYesUser email address
passwordstringConditionalRequired unless generatePassword: true
usernamestringNoUnique username (3-50 chars)
firstNamestringNoFirst name
lastNamestringNoLast name
phonestringNoPhone in E.164 format (+14155552671)
metadataobjectNoCustom fields
isEmailVerifiedbooleanNofalseSkip email verification
isPhoneVerifiedbooleanNofalseSkip phone verification
mustChangePasswordbooleanNofalseForce password change on next login
generatePasswordbooleanNofalseAuto-generate a secure password

Import a social user

Use this to migrate users from Cognito, Auth0, Firebase, or any OAuth provider.

POST /auth/admin/signup-social

Request body:

{
"email": "jane@example.com",
"provider": "google",
"providerId": "google_12345",
"providerEmail": "jane@gmail.com",
"firstName": "Jane",
"lastName": "Smith"
}

Response:

{
"user": {
"sub": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Smith",
"isEmailVerified": true,
"hasSocialAuth": true,
"socialProviders": ["google"]
},
"socialAccount": {
"provider": "google",
"providerId": "google_12345",
"providerEmail": "jane@gmail.com"
}
}

For hybrid accounts (social + password), include the password field. The user can then log in with either method.

FieldTypeRequiredDescription
emailstringYesUser email (auto-verified for social imports)
providerstringYes'google' | 'apple' | 'facebook'
providerIdstringYesProvider's unique user ID
providerEmailstringNoEmail from the provider (may differ from user email)
passwordstringNoFor hybrid social + password accounts
socialMetadataobjectNoRaw OAuth profile data (stored for audit)
firstName, lastName, username, phone, metadataNoSame as admin signup
isPhoneVerifiedbooleanNoSkip phone verification
mustChangePasswordbooleanNoOnly relevant if password is provided

List users

GET /auth/admin/users?page=1&limit=20&isEmailVerified=true&sortBy=createdAt&sortOrder=DESC

Response:

{
"users": [
{
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"isEmailVerified": true,
"isPhoneVerified": false,
"isActive": true,
"isLocked": false,
"mfaEnabled": true,
"hasSocialAuth": false,
"createdAt": "2025-01-15T10:30:00.000Z",
"updatedAt": "2025-02-01T14:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 156,
"totalPages": 8
}
}

Filter parameters

ParameterTypeDescription
pagenumberPage number (1-indexed, default: 1)
limitnumberRecords per page (default: 10, max: 100)
emailstringPartial match on email
phonestringPartial match on phone
isEmailVerifiedbooleanFilter by email verification status
isPhoneVerifiedbooleanFilter by phone verification status
hasSocialAuthbooleanFilter by social auth presence
isLockedbooleanFilter by account lock status
mfaEnabledbooleanFilter by MFA enrollment
createdAt[operator]stringDate comparison: gt, gte, lt, lte, eq
createdAt[value]stringISO 8601 date
updatedAt[operator]stringSame operators as createdAt
updatedAt[value]stringISO 8601 date
sortBystring'email' | 'createdAt' | 'updatedAt' | 'username' | 'phone'
sortOrderstring'ASC' | 'DESC' (default: 'DESC')

Get a single user

GET /auth/admin/users/:sub

Returns the same user object as listed above.

Delete a user

DELETE /auth/admin/users/:sub

Response — cascade cleanup counts:

{
"success": true,
"deletedUserId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"deletedRecords": {
"sessions": 3,
"verificationTokens": 1,
"mfaDevices": 2,
"trustedDevices": 1,
"socialAccounts": 1,
"loginAttempts": 12,
"challengeSessions": 0,
"auditLogs": 47
}
}

Disable a user

Locks the account and revokes all active sessions immediately.

POST /auth/admin/users/:sub/disable

Request body (optional):

{
"reason": "Account compromised"
}

Response:

{
"success": true,
"user": {
"sub": "a1b2c3d4-...",
"isLocked": true,
"isActive": false
},
"revokedSessions": 3
}

Enable a user

POST /auth/admin/users/:sub/enable

Response:

{
"success": true,
"user": {
"sub": "a1b2c3d4-...",
"isLocked": false,
"isActive": true
}
}

Step 3: Password Management

Set password directly

Sets a new password without requiring the old one. Optionally revokes all sessions.

POST /auth/admin/set-password

Request body:

{
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"newPassword": "NewSecurePass123!",
"mustChangePassword": true,
"revokeSessions": true
}

Response:

{
"success": true,
"mustChangePassword": true,
"sessionsRevoked": 3
}

Initiate password reset

Sends a reset code via email or SMS. The user completes the reset via the standard /auth/forgot-password/confirm endpoint.

POST /auth/admin/reset-password/initiate

Request body:

{
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"deliveryMethod": "email",
"baseUrl": "https://myapp.com/reset-password",
"revokeSessions": true,
"reason": "User requested via support ticket"
}

Response:

{
"success": true,
"destination": "j***@example.com",
"deliveryMedium": "email",
"expiresIn": 3600,
"sessionsRevoked": 3
}
FieldTypeDefaultDescription
substringUser UUID (required)
deliveryMethodstring'email''email' or 'sms'
baseUrlstringBase URL for building the reset link (optional)
codeExpiresInnumber3600Code expiry in seconds (300–86400)
revokeSessionsbooleanfalseRevoke all sessions immediately
reasonstringAudit trail reason (max 500 chars)

Force password change

Flags the user to change their password on the next login. The next login returns a FORCE_CHANGE_PASSWORD challenge.

POST /auth/admin/users/:sub/force-password-change

Response:

{
"success": true
}

Step 4: Session Management

Get user sessions

GET /auth/admin/users/:sub/sessions

Response:

{
"sessions": [
{
"sessionId": "sess_abc123",
"deviceId": "dev_xyz789",
"deviceName": "Chrome on MacOS",
"deviceType": "desktop",
"platform": "macOS",
"browser": "Chrome 120",
"ipAddress": "203.0.113.42",
"ipCountry": "US",
"ipCity": "San Francisco",
"lastActivityAt": "2025-02-15T10:30:00.000Z",
"createdAt": "2025-02-01T08:00:00.000Z",
"expiresAt": "2025-03-01T08:00:00.000Z",
"isTrustedDevice": true,
"isCurrent": false,
"authMethod": "password",
"authProvider": null
}
]
}

Logout all sessions

Signs the user out from all devices. Optionally revokes trusted device status.

POST /auth/admin/users/:sub/logout-all

Request body (optional):

{
"forgetDevices": true
}

Response:

{
"revokedCount": 5
}

Step 5: Admin MFA Management

Get MFA status

GET /auth/admin/users/:sub/mfa/status

Response:

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

List MFA devices

GET /auth/admin/users/:sub/mfa/devices

Response:

{
"devices": [
{
"id": 1,
"type": "totp",
"name": "Google Authenticator",
"isPreferred": true,
"isActive": true,
"createdAt": "2025-01-10T12:00:00.000Z"
},
{
"id": 2,
"type": "passkey",
"name": "MacBook Pro Touch ID",
"isPreferred": false,
"isActive": true,
"createdAt": "2025-01-15T09:00:00.000Z"
}
]
}

Remove an MFA device

DELETE /auth/admin/mfa/devices/:deviceId

Response:

{
"removedDeviceId": 2,
"message": "Device removed successfully"
}

Set preferred MFA device

POST /auth/admin/users/:sub/mfa/devices/:deviceId/preferred

Response:

{
"message": "Preferred device updated"
}

Grant or revoke MFA exemption

Exempt a user from MFA requirements (e.g., service accounts).

POST /auth/admin/mfa/exemption

Request body:

{
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"exempt": true,
"reason": "Service account — no human login",
"grantedBy": "admin-user-uuid"
}

Response:

{
"message": "MFA exemption granted"
}

Step 6: Frontend SDK

The frontend SDK provides client.admin.* methods that call the admin endpoints above. This works with any frontend framework.

Configure

Enable admin operations by adding the admin block to your client config:

import { NAuthClient } from '@nauth-toolkit/client';

const auth = new NAuthClient({
baseUrl: 'https://api.example.com',
authPathPrefix: '/auth',
tokenDelivery: 'cookies',
admin: {
pathPrefix: '/admin', // Matches your backend route prefix
},
});

The SDK builds admin URLs as: baseUrl + authPathPrefix + admin.pathPrefix + endpoint path. With the config above, getUsers() calls https://api.example.com/auth/admin/users.

Configuration options

OptionTypeDefaultDescription
pathPrefixstring'/admin'Prefix prepended to all admin endpoint paths
endpointsobjectOverride individual endpoint paths (see below)
headersobjectAdditional headers sent with every admin request

Default endpoint paths

All paths are relative to authPathPrefix + admin.pathPrefix:

KeyDefault PathMethod
signup/signupPOST
signupSocial/signup-socialPOST
getUsers/usersGET
getUser/users/:subGET
deleteUser/users/:subDELETE
disableUser/users/:sub/disablePOST
enableUser/users/:sub/enablePOST
forcePasswordChange/users/:sub/force-password-changePOST
setPassword/set-passwordPOST
resetPasswordInitiate/reset-password/initiatePOST
getUserSessions/users/:sub/sessionsGET
logoutAll/users/:sub/logout-allPOST
getMfaStatus/users/:sub/mfa/statusGET
getMfaDevices/users/:sub/mfa/devicesGET
removeMfaDeviceById/mfa/devices/:deviceIdDELETE
setPreferredMfaDevice/users/:sub/mfa/devices/:deviceId/preferredPOST
setMfaExemption/mfa/exemptionPOST
getAuditHistory/audit/historyGET

Override individual paths if your backend uses different URLs:

admin: {
pathPrefix: '/admin',
endpoints: {
getUsers: '/users/list', // Override just this one
},
},

User management

// Create a user
const { user, generatedPassword } = await auth.admin.createUser({
email: 'john@example.com',
generatePassword: true,
isEmailVerified: true,
mustChangePassword: true,
});

// Import a social user
const { user, socialAccount } = await auth.admin.importSocialUser({
email: 'jane@example.com',
provider: 'google',
providerId: 'google_12345',
providerEmail: 'jane@gmail.com',
});

// List users with filters
const { users, pagination } = await auth.admin.getUsers({
page: 1,
limit: 20,
isEmailVerified: true,
mfaEnabled: false,
sortBy: 'createdAt',
sortOrder: 'DESC',
});

// Date range filter
const { users } = await auth.admin.getUsers({
createdAt: { operator: 'gte', value: new Date('2025-01-01') },
});

// Get a single user
const user = await auth.admin.getUser('a1b2c3d4-...');

// Delete a user
const { deletedRecords } = await auth.admin.deleteUser('a1b2c3d4-...');

// Disable / enable
const { revokedSessions } = await auth.admin.disableUser('a1b2c3d4-...', 'Suspicious activity');
await auth.admin.enableUser('a1b2c3d4-...');

Password management

// Set password directly
await auth.admin.setPassword('john@example.com', 'NewSecurePass123!');

// Initiate password reset (sends email/SMS)
const { destination } = await auth.admin.initiatePasswordReset({
sub: 'a1b2c3d4-...',
deliveryMethod: 'email',
baseUrl: 'https://myapp.com/reset-password',
revokeSessions: true,
});

// Force password change on next login
await auth.admin.forcePasswordChange('a1b2c3d4-...');

Session management

// Get all sessions
const { sessions } = await auth.admin.getUserSessions('a1b2c3d4-...');

// Logout all sessions + revoke trusted devices
const { revokedCount } = await auth.admin.logoutAllSessions('a1b2c3d4-...', true);

MFA management

// Check MFA status
const status = await auth.admin.getMfaStatus('a1b2c3d4-...');
console.log(status.enabled, status.configuredMethods);

// List MFA devices
const { devices } = await auth.admin.getMfaDevices('a1b2c3d4-...');

// Remove a device
await auth.admin.removeMfaDeviceById(123);

// Set preferred device
await auth.admin.setPreferredMfaDevice('a1b2c3d4-...', 123);

// Grant MFA exemption
await auth.admin.setMfaExemption('a1b2c3d4-...', true, 'Service account');

// Revoke MFA exemption
await auth.admin.setMfaExemption('a1b2c3d4-...', false);

Audit history

const history = await auth.admin.getAuditHistory({
sub: 'a1b2c3d4-...',
page: 1,
limit: 50,
eventType: 'LOGIN_SUCCESS',
});

Error Codes

Common errors returned by admin endpoints:

Error CodeDescriptionResolution
NOT_FOUNDUser not foundCheck the sub UUID
EMAIL_EXISTSEmail already registeredUse a different email
USERNAME_EXISTSUsername already takenUse a different username
PHONE_EXISTSPhone already registeredUse a different phone number
WEAK_PASSWORDPassword doesn't meet policyUse a stronger password
SOCIAL_ACCOUNT_EXISTSProvider + providerId already linkedThis social account is already imported
VALIDATION_FAILEDInvalid request formatCheck required fields and formats

What's Next

  • Basic Auth Flows — Core authentication (signup, login, challenges)
  • MFA — Multi-factor authentication setup
  • Audit Logs — Query user activity history and display login events
  • Lifecycle Hooks — Hook into auth events for custom logic (e.g., send welcome email after admin signup)
  • Challenge System — How FORCE_CHANGE_PASSWORD and other challenges work