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 them | No. 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.
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:
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:
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.actorcarries only nauth-toolkit's ownIUserfields — 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 ownmetadatathroughPOST /auth/signupandPUT /auth/profile, so anyone could make themselves an admin.
Registering it
- NestJS
- Express
- Fastify
AuthModule.forRoot({ ...authConfig, authorization: RoleAuthorizer })
Pass the class to AuthModule.forRoot() — it becomes a normal
NestJS provider, so it can inject your services.
Same class without @Injectable(), importing the types from @nauth-toolkit/core:
const nauth = await NAuth.create({
config: authConfig,
dataSource,
adapter: new ExpressAdapter(),
authorization: new RoleAuthorizer(usersService),
});
Pass an instance to NAuth.create().
Same class without @Injectable(), importing the types from @nauth-toolkit/core:
const nauth = await NAuth.create({
config: authConfig,
dataSource,
adapter: new FastifyAdapter(),
authorization: new RoleAuthorizer(usersService),
});
Pass an instance to NAuth.create().
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:
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.
| Action | What it allows | Enforced by |
|---|---|---|
admin.user.create | Create a user directly, bypassing signup | AdminAuthService.signup() |
admin.user.createSocial | Create a user from a social identity, bypassing the OAuth flow | AdminAuthService.signupSocial() |
admin.user.read | Read another user's profile, by sub or by email | getUserById(), getUserByEmail() |
admin.user.list | List or search users | AdminAuthService.getUsers() |
admin.user.update | Edit another user's attributes | AdminAuthService.updateUserAttributes() |
admin.user.delete | Permanently delete a user | AdminAuthService.deleteUser() |
admin.user.disable | Disable a user, blocking sign-in | AdminAuthService.disableUser() |
admin.user.enable | Re-enable a disabled user | AdminAuthService.enableUser() |
admin.user.forcePasswordChange | Force a password change on next sign-in | AdminAuthService.setMustChangePassword() |
admin.user.updateVerifiedStatus | Override email/phone verified flags | AdminAuthService.updateVerifiedStatus() |
admin.user.resetPassword | Begin an admin-initiated password reset (sends the user a code) | AdminAuthService.resetPassword() |
admin.user.setPassword | Set a user's password outright, without their involvement | AdminAuthService.setPassword() |
admin.session.list | List another user's active sessions | AdminAuthService.getUserSessions() |
admin.session.revoke | Revoke one of another user's sessions | AdminAuthService.revokeUserSession() |
admin.session.revokeAll | Revoke every session belonging to another user | AdminAuthService.logoutAll() |
admin.trustedDevice.list | List another user's trusted devices | AdminAuthService.getUserTrustedDevices() |
admin.trustedDevice.revoke | Revoke one of another user's trusted devices | AdminAuthService.revokeUserTrustedDevice() |
admin.trustedDevice.revokeAll | Revoke every trusted device belonging to another user | AdminAuthService.revokeAllUserTrustedDevices() |
admin.mfa.readStatus | Read another user's MFA enrolment status | MFAService.adminGetMfaStatus() |
admin.mfa.listDevices | List another user's MFA devices | MFAService.adminGetUserDevices() |
admin.mfa.removeDevice | Remove one of another user's MFA devices | MFAService.adminRemoveDevice() |
admin.mfa.setPreferred | Change which of another user's MFA devices is preferred | MFAService.adminSetPreferredDevice() |
admin.mfa.setExemption | Grant or revoke an exemption from MFA enforcement | MFAService.setMFAExemption() |
admin.audit.read | Read audit history beyond the caller's own | AuthAuditService.getUserAuthHistory(), getEventsByType(), getSuspiciousActivity(), getRiskAssessmentHistory() |
admin.apiKey.create | Issue an API key on another user's behalf | ApiKeyService.adminCreateKey() |
admin.apiKey.list | List another user's API keys | ApiKeyService.adminListKeys() |
admin.apiKey.update | Modify another user's API key | ApiKeyService.adminUpdateKey() |
admin.apiKey.revoke | Revoke another user's API key | ApiKeyService.adminRevokeKey() |
admin.apiKey.delete | Delete another user's API key | ApiKeyService.adminDeleteKey() |
What authorize() receives
The full AuthorizationContext:
| Field | Type | Description |
|---|---|---|
actor | IUser | undefined | The signed-in caller. |
action | AuthAction | The operation being attempted. |
targetSub | string | undefined | The user being acted on, when there is one — handy for "admins cannot edit other admins". |
request | NAuthRequest | undefined | The live request. request.body lets you go finer than per-action if you ever need to. |
viaApiKey | boolean | undefined | True when the caller used an API key. |
When your provider is not called
| Situation | Result |
|---|---|
| No provider configured | Allowed. Admin routes will not mount, but your own controllers work. |
Inside runAsSystem() | Allowed. |
| No signed-in user | Denied. |
| Your provider throws | Denied — a failing policy service never counts as a yes. |
Good to know
- Denying an action leaves the endpoint mounted, answering
403. To remove it from the app entirely, useexcludewhen mounting. - Every denial is written to the audit log as
AUTHORIZATION_DENIEDwith the action, actor, target and reason. - API keys are rejected on the shipped admin routes before your provider is asked.
AdminAuthService.confirmResetPassword(),MFAService.getSetupData()andMFAService.getChallengeData()are never authorized — their callers are not signed in yet.
What's Next
- Admin Operations — mounting the routes, or writing your own controllers
- IAuthorizationProvider — full type reference
- Authentication Routes — route groups and
exclude - Audit Logs — reading the trail, including denials