Action
The following Post Login action imports a custom module to fetch the user’s roles from an external service and sets them as a custom claim on the access token, denying access if the module call fails.- JavaScript
- TypeScript
test-an-action-module.js
/** @import {Event, PostLoginAPI} from "@auth0/actions/post-login/v3" */
const { getRoles } = require('actions:my-module');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
try {
const { roles } = await getRoles();
api.accessToken.setCustomClaim(`${CUSTOM_CLAIM_NAMESPACE}/roles`, roles);
} catch (err) {
api.access.deny(err.message);
}
}
my-module.js
module.exports = {
/**
* Returns the user's roles.
*
* @returns {{ roles: string[] }} The roles payload.
*/
getRoles: async () => {
const response = await fetch(actions.secrets.EVENT_SINK_URL, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-API-Key': actions.secrets.EVENT_SINK_API_KEY,
}
});
if (!response.ok) {
throw new Error(`External service responded with status ${response.status}`);
}
return response.json();
}
};
test-an-action-module.ts
import type { Event, PostLoginAPI } from '@auth0/actions/post-login/v3';
const { getRoles } = require('actions:my-module');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event: Event, api: PostLoginAPI) => {
try {
const { roles } = await getRoles();
api.accessToken.setCustomClaim(`${CUSTOM_CLAIM_NAMESPACE}/roles`, roles);
} catch (err) {
api.access.deny((err as Error).message);
}
};
my-module.ts
/**
* Returns the user's roles.
*
* @returns The roles payload.
*/
exports.getRoles = async (): Promise<{ roles: string[] }> => {
const response = await fetch(actions.secrets.EVENT_SINK_URL, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-API-Key': actions.secrets.EVENT_SINK_API_KEY,
},
});
if (!response.ok) {
throw new Error(`External service responded with status ${response.status}`);
}
return response.json();
};
Unit Test
The unit tests load the action alongside the custom module and mockfetch to verify roles are set on success, and that access is denied when the external service returns an error status or the request fails with a network error.
Jest
Jest
- JavaScript
- TypeScript
test-an-action-module.spec.js
const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
const path = require('path');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.js');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
[event, api] = getDefaultArguments();
});
afterEach(() => {
jest.resetAllMocks();
});
it('uses a module to get roles and set them on the access token', async () => {
loader = await loadAction(ACTION_PATH, [
{ name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
);
jest.spyOn(api.accessToken, 'setCustomClaim');
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ roles: ['admin'] }),
});
await loader.execute('onExecutePostLogin', event, api);
expect(global.fetch).toHaveBeenCalled();
expect(api.accessToken.setCustomClaim).toHaveBeenCalledWith(`${CUSTOM_CLAIM_NAMESPACE}/roles`, ['admin']);
});
it('denies access when the external service responds with an error', async () => {
loader = await loadAction(ACTION_PATH, [
{ name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
);
jest.spyOn(api.access, 'deny');
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
ok: false,
status: 500,
});
await loader.execute('onExecutePostLogin', event, api);
expect(global.fetch).toHaveBeenCalled();
expect(api.access.deny).toHaveBeenCalledWith('External service responded with status 500');
});
it('denies access when fetching roles fails with a network error', async () => {
loader = await loadAction(ACTION_PATH, [
{ name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
);
jest.spyOn(api.access, 'deny');
jest.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
await loader.execute('onExecutePostLogin', event, api);
expect(global.fetch).toHaveBeenCalled();
expect(api.access.deny).toHaveBeenCalledWith('Network error');
});
});
package.json
{
"name": "actions-npm-example-js-jest",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"type": "commonjs",
"main": "module-usage.js",
"scripts": {
"test": "jest"
},
"devDependencies": {
"@auth0/actions": "^0.32.0",
"jest": "^30.4.2"
},
"jest": {
"testEnvironment": "node"
}
}
test-an-action-module.test.ts
const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
const path = require('path');
const { compileActionModules } = require('./test-utils/load-compiled-action');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.ts');
const MODULE_PATH = path.resolve(DIRNAME, './src/my-module.ts');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
[event, api] = getDefaultArguments();
});
afterEach(() => {
jest.resetAllMocks();
});
it('uses a module to get roles and set them on the access token', async () => {
const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
{ name: 'my-module', filename: MODULE_PATH },
]);
loader = await loadAction(compiledActionPath, compiledModules);
jest.spyOn(api.accessToken, 'setCustomClaim');
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ roles: ['admin'] }),
} as any);
await loader.execute('onExecutePostLogin', event, api);
expect(global.fetch).toHaveBeenCalled();
expect(api.accessToken.setCustomClaim).toHaveBeenCalledWith(`${CUSTOM_CLAIM_NAMESPACE}/roles`, ['admin']);
});
it('denies access when the external service responds with an error', async () => {
const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
{ name: 'my-module', filename: MODULE_PATH },
]);
loader = await loadAction(compiledActionPath, compiledModules);
jest.spyOn(api.access, 'deny');
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
ok: false,
status: 500,
} as any);
await loader.execute('onExecutePostLogin', event, api);
expect(global.fetch).toHaveBeenCalled();
expect(api.access.deny).toHaveBeenCalledWith('External service responded with status 500');
});
it('denies access when fetching roles fails with a network error', async () => {
const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
{ name: 'my-module', filename: MODULE_PATH },
]);
loader = await loadAction(compiledActionPath, compiledModules);
jest.spyOn(api.access, 'deny');
jest.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
await loader.execute('onExecutePostLogin', event, api);
expect(global.fetch).toHaveBeenCalled();
expect(api.access.deny).toHaveBeenCalledWith('Network error');
});
});
package.json
{
"name": "actions-npm-example-ts-jest",
"version": "1.0.0",
"description": "Actions TS",
"main": "example.ts",
"scripts": {
"test": "jest"
},
"author": "John Doe",
"license": "ISC",
"devDependencies": {
"@auth0/actions": "^0.32.0",
"@types/jest": "^29.5.12",
"@types/node": "22.14.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"typescript": "^5.9.2"
}
}
Mocha
Mocha
- JavaScript
- TypeScript
test-an-action-module.spec.js
const sinon = require('sinon');
const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
const path = require('path');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.js');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
[event, api] = getDefaultArguments();
});
afterEach(() => {
sinon.restore();
});
it('uses a module to get roles and set them on the access token', async () => {
loader = await loadAction(ACTION_PATH, [
{ name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
);
sinon.spy(api.accessToken, 'setCustomClaim');
sinon.stub(global, 'fetch').resolves({
ok: true,
status: 200,
json: async () => ({ roles: ['admin'] }),
});
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.called(global.fetch);
sinon.assert.calledWith(api.accessToken.setCustomClaim, `${CUSTOM_CLAIM_NAMESPACE}/roles`, ['admin']);
});
it('denies access when the external service responds with an error', async () => {
loader = await loadAction(ACTION_PATH, [
{ name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
);
sinon.spy(api.access, 'deny');
sinon.stub(global, 'fetch').resolves({
ok: false,
status: 500,
});
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.called(global.fetch);
sinon.assert.calledWith(api.access.deny, 'External service responded with status 500');
});
it('denies access when fetching roles fails with a network error', async () => {
loader = await loadAction(ACTION_PATH, [
{ name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
);
sinon.spy(api.access, 'deny');
sinon.stub(global, 'fetch').rejects(new Error('Network error'));
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.called(global.fetch);
sinon.assert.calledWith(api.access.deny, 'Network error');
});
});
package.json
{
"name": "actions-npm-example-js-mocha",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"type": "commonjs",
"main": "module-usage.js",
"scripts": {
"test": "mocha"
},
"devDependencies": {
"@auth0/actions": "^0.32.0",
"chai": "^4.5.0",
"mocha": "^11.0.0",
"sinon": "^19.0.0"
}
}
.mocharc.json
{
"spec": "src/**/*.spec.js"
}
test-an-action-module.test.ts
import * as path from 'path';
import sinon from 'sinon';
import { compileActionModules } from './test-utils/load-compiled-action';
const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.ts');
const MY_MODULE_PATH = path.resolve(DIRNAME, './src/my-module.ts');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
[event, api] = getDefaultArguments();
});
afterEach(() => {
sinon.restore();
});
it('uses a module to get roles and set them on the access token', async () => {
const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
{ name: 'my-module', filename: MY_MODULE_PATH },
]);
loader = await loadAction(compiledActionPath, compiledModules);
sinon.spy(api.accessToken, 'setCustomClaim');
sinon.stub(global, 'fetch').resolves({
ok: true,
status: 200,
json: async () => ({ roles: ['admin'] }),
} as any);
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.called(global.fetch as any);
sinon.assert.calledWith(api.accessToken.setCustomClaim, `${CUSTOM_CLAIM_NAMESPACE}/roles`, ['admin']);
});
it('denies access when the external service responds with an error', async () => {
const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
{ name: 'my-module', filename: MY_MODULE_PATH },
]);
loader = await loadAction(compiledActionPath, compiledModules);
sinon.spy(api.access, 'deny');
sinon.stub(global, 'fetch').resolves({
ok: false,
status: 500,
} as any);
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.called(global.fetch as any);
sinon.assert.calledWith(api.access.deny, 'External service responded with status 500');
});
it('denies access when fetching roles fails with a network error', async () => {
const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
{ name: 'my-module', filename: MY_MODULE_PATH },
]);
loader = await loadAction(compiledActionPath, compiledModules);
sinon.spy(api.access, 'deny');
sinon.stub(global, 'fetch').rejects(new Error('Network error'));
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.called(global.fetch as any);
sinon.assert.calledWith(api.access.deny, 'Network error');
});
});
package.json
{
"name": "actions-npm-example-ts-mocha",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"scripts": {
"test": "NODE_OPTIONS=--no-experimental-strip-types mocha"
},
"devDependencies": {
"@auth0/actions": "^0.32.0",
"@types/chai": "^4.3.16",
"@types/mocha": "^10.0.6",
"@types/node": "22.14.0",
"@types/sinon": "^17.0.3",
"chai": "^4.5.0",
"mocha": "^11.0.0",
"sinon": "^19.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
}
}
Node.js Test Runner
Node.js Test Runner
- JavaScript
- TypeScript
test-an-action-module.spec.js
const assert = require('node:assert');
const { describe, it, beforeEach, afterEach, mock } = require('node:test');
const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
const path = require('path');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.js');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
[event, api] = getDefaultArguments();
});
afterEach(() => {
mock.reset();
});
it('uses a module to get roles and set them on the access token', async () => {
loader = await loadAction(ACTION_PATH, [
{ name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
);
mock.method(api.accessToken, 'setCustomClaim');
mock.method(global, 'fetch', async () => ({
ok: true,
status: 200,
json: async () => ({ roles: ['admin'] }),
}));
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(global.fetch.mock.calls.length, 1);
assert.deepEqual(api.accessToken.setCustomClaim.mock.calls[0].arguments, [
`${CUSTOM_CLAIM_NAMESPACE}/roles`,
['admin'],
]);
});
it('denies access when the external service responds with an error', async () => {
loader = await loadAction(ACTION_PATH, [
{ name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
);
mock.method(api.access, 'deny');
mock.method(global, 'fetch', async () => ({
ok: false,
status: 500,
}));
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(global.fetch.mock.calls.length, 1);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['External service responded with status 500']);
});
it('denies access when fetching roles fails with a network error', async () => {
loader = await loadAction(ACTION_PATH, [
{ name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
);
mock.method(api.access, 'deny');
mock.method(global, 'fetch', async () => {
throw new Error('Network error');
});
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(global.fetch.mock.calls.length, 1);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Network error']);
});
});
package.json
{
"name": "actions-npm-example-js-node-test",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"type": "commonjs",
"main": "module-usage.js",
"scripts": {
"test": "node --test src/*.spec.js"
},
"devDependencies": {
"@auth0/actions": "^0.32.0"
}
}
test-an-action-module.test.ts
const assert = require('node:assert');
const { describe, it, beforeEach, afterEach, mock } = require('node:test');
const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
const path = require('path');
const { compileActionModules } = require('./test-utils/load-compiled-action.ts');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.ts');
const MY_MODULE_PATH = path.resolve(DIRNAME, './src/my-module.ts');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
[event, api] = getDefaultArguments();
});
afterEach(() => {
mock.reset();
});
it('uses a module to get roles and set them on the access token', async () => {
const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
{ name: 'my-module', filename: MY_MODULE_PATH },
]);
loader = await loadAction(compiledActionPath, compiledModules);
mock.method(api.accessToken, 'setCustomClaim');
mock.method(global, 'fetch', async () => ({
ok: true,
status: 200,
json: async () => ({ roles: ['admin'] }),
}));
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(global.fetch.mock.calls.length, 1);
assert.deepEqual(api.accessToken.setCustomClaim.mock.calls[0].arguments, [
`${CUSTOM_CLAIM_NAMESPACE}/roles`,
['admin'],
]);
});
it('denies access when the external service responds with an error', async () => {
const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
{ name: 'my-module', filename: MY_MODULE_PATH },
]);
loader = await loadAction(compiledActionPath, compiledModules);
mock.method(api.access, 'deny');
mock.method(global, 'fetch', async () => ({
ok: false,
status: 500,
}));
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(global.fetch.mock.calls.length, 1);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['External service responded with status 500']);
});
it('denies access when fetching roles fails with a network error', async () => {
const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
{ name: 'my-module', filename: MY_MODULE_PATH },
]);
loader = await loadAction(compiledActionPath, compiledModules);
mock.method(api.access, 'deny');
mock.method(global, 'fetch', async () => {
throw new Error('Network error');
});
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(global.fetch.mock.calls.length, 1);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Network error']);
});
});
package.json
{
"name": "actions-npm-example-ts-node-test",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"scripts": {
"test": "node --test src/*.test.ts"
},
"devDependencies": {
"@auth0/actions": "^0.32.0",
"@types/node": "22.14.0",
"typescript": "^5.9.2"
}
}