Skip to main content

Authorization

nauth-toolkit handles authentication — proving who the caller is. Deciding what they may do is your app's job, and you almost certainly already do it: a role column, a permissions table, CASL, a policy service. Nothing on this page replaces that.

This page is only about one thing: the ready-made admin routes. They need to know who is allowed to use them, and nauth-toolkit has no way to know that on its own.

Do you need to do anything?​

You want to…Then…
Mount the ready-made admin routes (groups: ['admin'])Yes — give nauth-toolkit a small provider that answers "may this user do this?" from whatever your app already has. Usually 15–20 lines.
Write your own admin controllers and call AdminAuthService / MFAService from themNo. Guard them the way you guard everything else. No provider, nothing to configure.

The provider is not a second permission system. It is a bridge: nauth-toolkit asks a question, your existing roles or permissions answer it. If the bridge ever feels like more work than it is worth, write your own controllers instead — that is a fully supported path, shown in Admin Operations.

If you do configure a provider, it applies everywhere

The check runs inside the services, so it also covers controllers you write and scripts. With your own guards in front, that is simply a second check — harmless, but worth knowing.

Writing the provider​

One class implementing IAuthorizationProvider, one method. nauth-toolkit calls it with the signed-in user and an action such as 'admin.user.delete'; you return { allow: true } or { allow: false, reason }. The reason is what the caller sees in the FORBIDDEN 403, and it goes into the audit log.

Pick whichever matches what your app already has. Each example injects one of your services — swap in the real name.

If your app has roles​

Any user with the admin role may use all admin routes:

src/auth/role.authorizer.ts
import { Injectable } from '@nestjs/common';
import type {
AuthorizationContext,
AuthorizationDecision,
IAuthorizationProvider,
} from '@nauth-toolkit/nestjs';
import { UsersService } from '../users/users.service';

@Injectable()
export class RoleAuthorizer implements IAuthorizationProvider {
constructor(private readonly users: UsersService) {}

async authorize({ actor }: AuthorizationContext): Promise<AuthorizationDecision> {
if (!actor) return { allow: false, reason: 'Authentication required' };

const role = await this.users.getRole(actor.sub);
return role === 'admin'
? { allow: true }
: { allow: false, reason: 'Requires the admin role' };
}
}

If your app has permissions​

Map each action to one of your permissions. Anything not in the map is refused, so a support role holding users:create and users:reset-password gets a 403 on every MFA route and on delete — no extra code:

src/auth/role.authorizer.ts
import { Injectable } from '@nestjs/common';
import type {
AuthAction,
AuthorizationContext,
AuthorizationDecision,
IAuthorizationProvider,
} from '@nauth-toolkit/nestjs';
import { PermissionsService } from '../permissions/permissions.service';

const REQUIRED: Partial<Record<AuthAction, string>> = {
'admin.user.create': 'users:create',
'admin.user.read': 'users:read',
'admin.user.list': 'users:read',
'admin.user.resetPassword': 'users:reset-password',
'admin.user.delete': 'users:delete',
'admin.mfa.removeDevice': 'mfa:manage',
// add the rest as you need them — full list under Reference > Actions below
};

@Injectable()
export class RoleAuthorizer implements IAuthorizationProvider {
constructor(private readonly permissions: PermissionsService) {}

async authorize({ actor, action }: AuthorizationContext): Promise<AuthorizationDecision> {
if (!actor) return { allow: false, reason: 'Authentication required' };

const needed = REQUIRED[action];
if (!needed) return { allow: false, reason: `${action} is not enabled` };

// Using CASL, accesscontrol or similar? Ask your ability here instead,
// e.g. ability.can('delete', 'User') for 'admin.user.delete'.
const ok = await this.permissions.has(actor.sub, needed);
return ok ? { allow: true } : { allow: false, reason: `Requires ${needed}` };
}
}

Two things to know while writing it:

  • Look the user up by actor.sub. actor carries only nauth-toolkit's own IUser fields — a column you added to your user entity will not be on it.
  • Do not read the role from user.metadata. Users can write their own metadata through POST /auth/signup and PUT /auth/profile, so anyone could make themselves an admin.

Registering it​

src/auth/auth.module.ts
AuthModule.forRoot({ ...authConfig, authorization: RoleAuthorizer })

Pass the class to AuthModule.forRoot() — it becomes a normal NestJS provider, so it can inject your services.

That is it — mount the admin routes and they are protected. See Admin Operations for mounting, and Configuration > Authorization for the option itself.

Your first admin​

If nobody has the admin role yet, no admin route can grant it. Do it once from a seed script. runAsSystem() skips the provider for the code inside it:

scripts/seed.ts
import { runAsSystem } from '@nauth-toolkit/core';

await runAsSystem(async () => {
const { user } = await adminAuthService.signup({ email: 'owner@example.com', password: seed });
await usersService.setRole(user.sub, 'admin'); // however your app does it
});

adminAuthService.signup() creates the account; the role is written by your own code. Only use runAsSystem() in seeds, migrations, cron jobs and tests — never inside a route.

Reference​

Actions​

The action passed to your provider, and the service method that enforces it. It is the same action whatever route or code path reached the method, and AuthAction is a closed union, so a typo in your provider is a compile error.

ActionWhat it allowsEnforced by
admin.user.createCreate a user directly, bypassing signupAdminAuthService.signup()
admin.user.createSocialCreate a user from a social identity, bypassing the OAuth flowAdminAuthService.signupSocial()
admin.user.readRead another user's profile, by sub or by emailgetUserById(), getUserByEmail()
admin.user.listList or search usersAdminAuthService.getUsers()
admin.user.updateEdit another user's attributesAdminAuthService.updateUserAttributes()
admin.user.deletePermanently delete a userAdminAuthService.deleteUser()
admin.user.disableDisable a user, blocking sign-inAdminAuthService.disableUser()
admin.user.enableRe-enable a disabled userAdminAuthService.enableUser()
admin.user.forcePasswordChangeForce a password change on next sign-inAdminAuthService.setMustChangePassword()
admin.user.updateVerifiedStatusOverride email/phone verified flagsAdminAuthService.updateVerifiedStatus()
admin.user.resetPasswordBegin an admin-initiated password reset (sends the user a code)AdminAuthService.resetPassword()
admin.user.setPasswordSet a user's password outright, without their involvementAdminAuthService.setPassword()
admin.session.listList another user's active sessionsAdminAuthService.getUserSessions()
admin.session.revokeRevoke one of another user's sessionsAdminAuthService.revokeUserSession()
admin.session.revokeAllRevoke every session belonging to another userAdminAuthService.logoutAll()
admin.trustedDevice.listList another user's trusted devicesAdminAuthService.getUserTrustedDevices()
admin.trustedDevice.revokeRevoke one of another user's trusted devicesAdminAuthService.revokeUserTrustedDevice()
admin.trustedDevice.revokeAllRevoke every trusted device belonging to another userAdminAuthService.revokeAllUserTrustedDevices()
admin.mfa.readStatusRead another user's MFA enrolment statusMFAService.adminGetMfaStatus()
admin.mfa.listDevicesList another user's MFA devicesMFAService.adminGetUserDevices()
admin.mfa.removeDeviceRemove one of another user's MFA devicesMFAService.adminRemoveDevice()
admin.mfa.setPreferredChange which of another user's MFA devices is preferredMFAService.adminSetPreferredDevice()
admin.mfa.setExemptionGrant or revoke an exemption from MFA enforcementMFAService.setMFAExemption()
admin.audit.readRead audit history beyond the caller's ownAuthAuditService.getUserAuthHistory(), getEventsByType(), getSuspiciousActivity(), getRiskAssessmentHistory()
admin.apiKey.createIssue an API key on another user's behalfApiKeyService.adminCreateKey()
admin.apiKey.listList another user's API keysApiKeyService.adminListKeys()
admin.apiKey.updateModify another user's API keyApiKeyService.adminUpdateKey()
admin.apiKey.revokeRevoke another user's API keyApiKeyService.adminRevokeKey()
admin.apiKey.deleteDelete another user's API keyApiKeyService.adminDeleteKey()

What authorize() receives​

The full AuthorizationContext:

FieldTypeDescription
actorIUser | undefinedThe signed-in caller.
actionAuthActionThe operation being attempted.
targetSubstring | undefinedThe user being acted on, when there is one — handy for "admins cannot edit other admins".
requestNAuthRequest | undefinedThe live request. request.body lets you go finer than per-action if you ever need to.
viaApiKeyboolean | undefinedTrue when the caller used an API key.

When your provider is not called​

SituationResult
No provider configuredAllowed. Admin routes will not mount, but your own controllers work.
Inside runAsSystem()Allowed.
No signed-in userDenied.
Your provider throwsDenied — a failing policy service never counts as a yes.

Good to know​

What's Next​