Skip to main content

Custom JWT Claims (buildTokenPayload)

By default, node-auth embeds these standard claims in every JWT:

{
"sub": "userId",
"email": "user@example.com",
"role": "user",
"loginProvider": "local",
"isEmailVerified": true,
"isTotpEnabled": false,
"iat": 1700000000,
"exp": 1700000900
}

Use buildTokenPayload in AuthConfig to merge additional custom claims — tenant IDs, permissions, plan tier, feature flags, or any project-specific data — into every issued token pair.


Flow


Configuration

import { AuthConfigurator, BaseUser } from '@nik2208/node-auth';

// Extend BaseUser with your project-specific fields
interface AppUser extends BaseUser {
tenantId?: string;
plan?: 'free' | 'pro' | 'enterprise';
}

const auth = new AuthConfigurator({
accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!,
refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!,
accessTokenExpiresIn: '15m',
refreshTokenExpiresIn: '7d',

buildTokenPayload: (user: AppUser) => ({
tenantId: user.tenantId,
plan: user.plan,
}),
}, userStore);

The returned object is merged on top of the standard claims. You cannot override sub, email, role, iat, or exp.


Reading custom claims server-side

After auth.middleware() runs, custom claims are available on req.user:

app.get('/api/data', auth.middleware(), (req, res) => {
const { sub, email, tenantId, plan } = req.user as any;
res.json({ tenantId, plan });
});

Reading custom claims client-side (NestJS example)

// @CurrentUser() injects req.user which includes all JWT claims
@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@CurrentUser() user: { sub: string; email: string; tenantId: string; plan: string }) {
return user;
}

Async buildTokenPayload (loading RBAC from DB)

buildTokenPayload can be async. Use it to load permissions at token-issue time:

buildTokenPayload: async (user) => {
const permissions = await rbacStore.getPermissionsForUser(user.id);
const roles = await rbacStore.getRolesForUser(user.id);
return { permissions, roles };
},
Token size

JWT tokens are sent in every request (as cookies). Keep custom claims small — embed IDs and flags, not full objects. A token exceeding ~4 KB may be rejected by some cookie jars.


Common patterns

Multi-tenant ID

buildTokenPayload: (user) => ({ tenantId: user.tenantId }),

Role + permissions

buildTokenPayload: async (user) => ({
permissions: await rbacStore.getPermissionsForUser(user.id),
}),

Feature flags

buildTokenPayload: (user) => ({
features: {
analytics: user.plan === 'pro' || user.plan === 'enterprise',
export: user.plan === 'enterprise',
},
}),