Action
The following Password Reset Post Challenge action redirects the user to an external identity verification page when their risk score exceeds a threshold, then validates the returned session token and verification result before allowing the password reset to continue.- JavaScript
- TypeScript
mock-redirects.js
/** @import {Event, PasswordResetPostChallengeAPI} from "@auth0/actions/password-reset-post-challenge/v1" */
const VERIFICATION_URL = 'https://verify.example.com/identity';
const RISK_SCORE_THRESHOLD = 50;
/**
* Handler that will be called during the execution of a PasswordResetPostChallenge flow.
* Redirects the user to an external identity verification page when the supplemental
* risk assessment score exceeds the configured threshold.
*
* @param {Event} event - Details about the user and the password reset transaction.
* @param {PasswordResetPostChallengeAPI} api - Interface whose methods can be used to change the behavior of the password reset.
*/
exports.onExecutePostChallenge = async (event, api) => {
const riskScore = event.authentication.riskAssessment?.supplemental?.akamai?.akamaiUserRisk?.score ?? 0;
if (riskScore < RISK_SCORE_THRESHOLD) {
return;
}
const sessionToken = api.redirect.encodeToken({
secret: event.secrets.REDIRECT_SECRET,
payload: { userId: event.user.user_id },
});
api.redirect.sendUserTo(VERIFICATION_URL, {
query: { session_token: sessionToken },
});
};
/**
* Handler that will be called when the user returns from the redirect issued by
* onExecutePostChallenge. Validates the session token before allowing the password
* reset flow to proceed.
*
* @param {Event} event - Details about the user and the password reset transaction.
* @param {PasswordResetPostChallengeAPI} api - Interface whose methods can be used to change the behavior of the password reset.
*/
exports.onContinuePostChallenge = async (event, api) => {
let payload;
try {
payload = api.redirect.validateToken({ secret: event.secrets.REDIRECT_SECRET });
} catch (err) {
api.access.deny(`Unable to verify redirect response: ${err.message}`);
return;
}
if (payload.userId !== event.user.user_id) {
api.access.deny('Verification response does not match the current user.');
return;
}
if (event.request.query.verified !== 'true') {
api.access.deny('Identity verification was not completed.');
}
};
mock-redirects.ts
import type { Event, PasswordResetPostChallengeAPI } from '@auth0/actions/password-reset-post-challenge/v1';
const VERIFICATION_URL = 'https://verify.example.com/identity';
const RISK_SCORE_THRESHOLD = 50;
/**
* Handler that will be called during the execution of a PasswordResetPostChallenge flow.
* Redirects the user to an external identity verification page when the supplemental
* risk assessment score exceeds the configured threshold.
*
* @param {Event} event - Details about the user and the password reset transaction.
* @param {PasswordResetPostChallengeAPI} api - Interface whose methods can be used to change the behavior of the password reset.
*/
exports.onExecutePostChallenge = async (event: Event, api: PasswordResetPostChallengeAPI) => {
const riskScore = event.authentication.riskAssessment?.supplemental?.akamai?.akamaiUserRisk?.score ?? 0;
if (riskScore < RISK_SCORE_THRESHOLD) {
return;
}
const sessionToken = api.redirect.encodeToken({
secret: event.secrets.REDIRECT_SECRET,
payload: { userId: event.user.user_id },
});
api.redirect.sendUserTo(VERIFICATION_URL, {
query: { session_token: sessionToken },
});
};
/**
* Handler that will be called when the user returns from the redirect issued by
* onExecutePostChallenge. Validates the session token before allowing the password
* reset flow to proceed.
*
* @param {Event} event - Details about the user and the password reset transaction.
* @param {PasswordResetPostChallengeAPI} api - Interface whose methods can be used to change the behavior of the password reset.
*/
exports.onContinuePostChallenge = async (event: Event, api: PasswordResetPostChallengeAPI) => {
let payload;
try {
payload = api.redirect.validateToken({ secret: event.secrets.REDIRECT_SECRET });
} catch (err) {
api.access.deny(`Unable to verify redirect response: ${(err as Error).message}`);
return;
}
if (payload.userId !== event.user.user_id) {
api.access.deny('Verification response does not match the current user.');
return;
}
if (event.request.query.verified !== 'true') {
api.access.deny('Identity verification was not completed.');
}
};
Unit Test
The unit tests mock theevent and api objects, along with the redirect token encoding and validation, to verify the redirect only fires above the risk threshold and that access is denied for invalid tokens, mismatched users, or incomplete verification.
Jest
Jest
- JavaScript
- TypeScript
mock-redirects.spec.js
const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/test');
const path = require('path');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.js');
const VERIFICATION_URL = 'https://verify.example.com/identity';
describe('onExecutePostChallenge', () => {
let loader;
let event;
let api;
beforeEach(async () => {
jest.resetAllMocks();
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
jest.spyOn(api.redirect, 'encodeToken').mockReturnValue('signed.jwt.token');
jest.spyOn(api.redirect, 'sendUserTo');
});
afterEach(() => {
jest.resetAllMocks();
});
it('does not redirect when the risk score is below the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;
await loader.execute('onExecutePostChallenge', event, api);
expect(api.redirect.encodeToken).not.toHaveBeenCalled();
expect(api.redirect.sendUserTo).not.toHaveBeenCalled();
});
it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;
await loader.execute('onExecutePostChallenge', event, api);
expect(api.redirect.encodeToken).toHaveBeenCalledWith({
secret: 'test-secret',
payload: { userId: event.user.user_id },
});
expect(api.redirect.sendUserTo).toHaveBeenCalledWith(VERIFICATION_URL, {
query: { session_token: 'signed.jwt.token' },
});
});
});
describe('onContinuePostChallenge', () => {
let loader;
let event;
let api;
beforeEach(async () => {
jest.resetAllMocks();
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
jest.spyOn(api.access, 'deny');
});
afterEach(() => {
jest.resetAllMocks();
});
it('denies access when the redirect token cannot be validated', async () => {
jest.spyOn(api.redirect, 'validateToken').mockImplementation(() => {
throw new Error('invalid signature');
});
await loader.execute('onContinuePostChallenge', event, api);
expect(api.redirect.validateToken).toHaveBeenCalledWith({ secret: 'test-secret' });
expect(api.access.deny).toHaveBeenCalledWith('Unable to verify redirect response: invalid signature');
});
it('denies access when the token belongs to a different user', async () => {
jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: 'someone-else' });
await loader.execute('onContinuePostChallenge', event, api);
expect(api.access.deny).toHaveBeenCalledWith('Verification response does not match the current user.');
});
it('denies access when the verification was not completed', async () => {
jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: event.user.user_id });
event.request.query.verified = 'false';
await loader.execute('onContinuePostChallenge', event, api);
expect(api.access.deny).toHaveBeenCalledWith('Identity verification was not completed.');
});
it('allows the flow to continue when verification succeeded', async () => {
jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: event.user.user_id });
event.request.query.verified = 'true';
await loader.execute('onContinuePostChallenge', event, api);
expect(api.access.deny).not.toHaveBeenCalled();
});
});
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"
}
}
mock-redirects.test.ts
const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/test');
const path = require('path');
const { compileActionModules } = require('./test-utils/load-compiled-action');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.ts');
const VERIFICATION_URL = 'https://verify.example.com/identity';
describe('onExecutePostChallenge', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
jest.resetAllMocks();
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
jest.spyOn(api.redirect, 'encodeToken').mockReturnValue('signed.jwt.token');
jest.spyOn(api.redirect, 'sendUserTo');
});
afterEach(() => {
jest.resetAllMocks();
});
it('does not redirect when the risk score is below the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;
await loader.execute('onExecutePostChallenge', event, api);
expect(api.redirect.encodeToken).not.toHaveBeenCalled();
expect(api.redirect.sendUserTo).not.toHaveBeenCalled();
});
it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;
await loader.execute('onExecutePostChallenge', event, api);
expect(api.redirect.encodeToken).toHaveBeenCalledWith({
secret: 'test-secret',
payload: { userId: event.user.user_id },
});
expect(api.redirect.sendUserTo).toHaveBeenCalledWith(VERIFICATION_URL, {
query: { session_token: 'signed.jwt.token' },
});
});
});
describe('onContinuePostChallenge', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
jest.resetAllMocks();
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
jest.spyOn(api.access, 'deny');
});
afterEach(() => {
jest.resetAllMocks();
});
it('denies access when the redirect token cannot be validated', async () => {
jest.spyOn(api.redirect, 'validateToken').mockImplementation(() => {
throw new Error('invalid signature');
});
await loader.execute('onContinuePostChallenge', event, api);
expect(api.redirect.validateToken).toHaveBeenCalledWith({ secret: 'test-secret' });
expect(api.access.deny).toHaveBeenCalledWith('Unable to verify redirect response: invalid signature');
});
it('denies access when the token belongs to a different user', async () => {
jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: 'someone-else' });
await loader.execute('onContinuePostChallenge', event, api);
expect(api.access.deny).toHaveBeenCalledWith('Verification response does not match the current user.');
});
it('denies access when the verification was not completed', async () => {
jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: event.user.user_id });
event.request.query.verified = 'false';
await loader.execute('onContinuePostChallenge', event, api);
expect(api.access.deny).toHaveBeenCalledWith('Identity verification was not completed.');
});
it('allows the flow to continue when verification succeeded', async () => {
jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: event.user.user_id });
event.request.query.verified = 'true';
await loader.execute('onContinuePostChallenge', event, api);
expect(api.access.deny).not.toHaveBeenCalled();
});
});
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
mock-redirects.spec.js
const { expect } = require('chai');
const sinon = require('sinon');
const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/test');
const path = require('path');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.js');
const VERIFICATION_URL = 'https://verify.example.com/identity';
describe('onExecutePostChallenge', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
sinon.stub(api.redirect, 'encodeToken').returns('signed.jwt.token');
sinon.spy(api.redirect, 'sendUserTo');
});
afterEach(() => {
sinon.restore();
});
it('does not redirect when the risk score is below the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;
await loader.execute('onExecutePostChallenge', event, api);
sinon.assert.notCalled(api.redirect.encodeToken);
sinon.assert.notCalled(api.redirect.sendUserTo);
});
it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;
await loader.execute('onExecutePostChallenge', event, api);
sinon.assert.calledWith(api.redirect.encodeToken, {
secret: 'test-secret',
payload: { userId: event.user.user_id },
});
sinon.assert.calledWith(api.redirect.sendUserTo, VERIFICATION_URL, {
query: { session_token: 'signed.jwt.token' },
});
});
});
describe('onContinuePostChallenge', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
sinon.spy(api.access, 'deny');
});
afterEach(() => {
sinon.restore();
});
it('denies access when the redirect token cannot be validated', async () => {
sinon.stub(api.redirect, 'validateToken').callsFake(() => {
throw new Error('invalid signature');
});
await loader.execute('onContinuePostChallenge', event, api);
sinon.assert.calledWith(api.redirect.validateToken, { secret: 'test-secret' });
sinon.assert.calledWith(api.access.deny, 'Unable to verify redirect response: invalid signature');
});
it('denies access when the token belongs to a different user', async () => {
sinon.stub(api.redirect, 'validateToken').returns({ userId: 'someone-else' });
await loader.execute('onContinuePostChallenge', event, api);
sinon.assert.calledWith(api.access.deny, 'Verification response does not match the current user.');
});
it('denies access when the verification was not completed', async () => {
sinon.stub(api.redirect, 'validateToken').returns({ userId: event.user.user_id });
event.request.query.verified = 'false';
await loader.execute('onContinuePostChallenge', event, api);
sinon.assert.calledWith(api.access.deny, 'Identity verification was not completed.');
});
it('allows the flow to continue when verification succeeded', async () => {
sinon.stub(api.redirect, 'validateToken').returns({ userId: event.user.user_id });
event.request.query.verified = 'true';
await loader.execute('onContinuePostChallenge', event, api);
sinon.assert.notCalled(api.access.deny);
});
});
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"
}
mock-redirects.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/password-reset-post-challenge/v1/test');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.ts');
const VERIFICATION_URL = 'https://verify.example.com/identity';
describe('onExecutePostChallenge', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
sinon.stub(api.redirect, 'encodeToken').returns('signed.jwt.token');
sinon.spy(api.redirect, 'sendUserTo');
});
afterEach(() => {
sinon.restore();
});
it('does not redirect when the risk score is below the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;
await loader.execute('onExecutePostChallenge', event, api);
sinon.assert.notCalled(api.redirect.encodeToken);
sinon.assert.notCalled(api.redirect.sendUserTo);
});
it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;
await loader.execute('onExecutePostChallenge', event, api);
sinon.assert.calledWith(api.redirect.encodeToken, {
secret: 'test-secret',
payload: { userId: event.user.user_id },
});
sinon.assert.calledWith(api.redirect.sendUserTo, VERIFICATION_URL, {
query: { session_token: 'signed.jwt.token' },
});
});
});
describe('onContinuePostChallenge', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
sinon.spy(api.access, 'deny');
});
afterEach(() => {
sinon.restore();
});
it('denies access when the redirect token cannot be validated', async () => {
sinon.stub(api.redirect, 'validateToken').callsFake(() => {
throw new Error('invalid signature');
});
await loader.execute('onContinuePostChallenge', event, api);
sinon.assert.calledWith(api.redirect.validateToken, { secret: 'test-secret' });
sinon.assert.calledWith(api.access.deny, 'Unable to verify redirect response: invalid signature');
});
it('denies access when the token belongs to a different user', async () => {
sinon.stub(api.redirect, 'validateToken').returns({ userId: 'someone-else' });
await loader.execute('onContinuePostChallenge', event, api);
sinon.assert.calledWith(api.access.deny, 'Verification response does not match the current user.');
});
it('denies access when the verification was not completed', async () => {
sinon.stub(api.redirect, 'validateToken').returns({ userId: event.user.user_id });
event.request.query.verified = 'false';
await loader.execute('onContinuePostChallenge', event, api);
sinon.assert.calledWith(api.access.deny, 'Identity verification was not completed.');
});
it('allows the flow to continue when verification succeeded', async () => {
sinon.stub(api.redirect, 'validateToken').returns({ userId: event.user.user_id });
event.request.query.verified = 'true';
await loader.execute('onContinuePostChallenge', event, api);
sinon.assert.notCalled(api.access.deny);
});
});
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
mock-redirects.spec.js
const assert = require('node:assert');
const { describe, it, beforeEach, afterEach, mock } = require('node:test');
const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/test');
const path = require('path');
const DIRNAME = path.dirname('../../../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.js');
const VERIFICATION_URL = 'https://verify.example.com/identity';
describe('onExecutePostChallenge', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
mock.method(api.redirect, 'encodeToken', () => 'signed.jwt.token');
mock.method(api.redirect, 'sendUserTo');
});
afterEach(() => {
mock.reset();
});
it('does not redirect when the risk score is below the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;
await loader.execute('onExecutePostChallenge', event, api);
assert.strictEqual(api.redirect.encodeToken.mock.calls.length, 0);
assert.strictEqual(api.redirect.sendUserTo.mock.calls.length, 0);
});
it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;
await loader.execute('onExecutePostChallenge', event, api);
assert.deepEqual(api.redirect.encodeToken.mock.calls[0].arguments, [{
secret: 'test-secret',
payload: { userId: event.user.user_id },
}]);
assert.deepEqual(api.redirect.sendUserTo.mock.calls[0].arguments, [VERIFICATION_URL, {
query: { session_token: 'signed.jwt.token' },
}]);
});
});
describe('onContinuePostChallenge', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
mock.method(api.access, 'deny');
});
afterEach(() => {
mock.reset();
});
it('denies access when the redirect token cannot be validated', async () => {
mock.method(api.redirect, 'validateToken', () => {
throw new Error('invalid signature');
});
await loader.execute('onContinuePostChallenge', event, api);
assert.deepEqual(api.redirect.validateToken.mock.calls[0].arguments, [{ secret: 'test-secret' }]);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Unable to verify redirect response: invalid signature']);
});
it('denies access when the token belongs to a different user', async () => {
mock.method(api.redirect, 'validateToken', () => ({ userId: 'someone-else' }));
await loader.execute('onContinuePostChallenge', event, api);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Verification response does not match the current user.']);
});
it('denies access when the verification was not completed', async () => {
mock.method(api.redirect, 'validateToken', () => ({ userId: event.user.user_id }));
event.request.query.verified = 'false';
await loader.execute('onContinuePostChallenge', event, api);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Identity verification was not completed.']);
});
it('allows the flow to continue when verification succeeded', async () => {
mock.method(api.redirect, 'validateToken', () => ({ userId: event.user.user_id }));
event.request.query.verified = 'true';
await loader.execute('onContinuePostChallenge', event, api);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
});
});
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"
}
}
mock-redirects.test.ts
const assert = require('node:assert');
const { describe, it, beforeEach, afterEach, mock } = require('node:test');
const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/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/mock-redirects.ts');
const VERIFICATION_URL = 'https://verify.example.com/identity';
describe('onExecutePostChallenge', () => {
let loader;
let event;
let api;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
mock.method(api.redirect, 'encodeToken', () => 'signed.jwt.token');
mock.method(api.redirect, 'sendUserTo');
});
afterEach(() => {
mock.reset();
});
it('does not redirect when the risk score is below the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;
await loader.execute('onExecutePostChallenge', event, api);
assert.strictEqual(api.redirect.encodeToken.mock.calls.length, 0);
assert.strictEqual(api.redirect.sendUserTo.mock.calls.length, 0);
});
it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;
await loader.execute('onExecutePostChallenge', event, api);
assert.deepEqual(api.redirect.encodeToken.mock.calls[0].arguments, [{
secret: 'test-secret',
payload: { userId: event.user.user_id },
}]);
assert.deepEqual(api.redirect.sendUserTo.mock.calls[0].arguments, [VERIFICATION_URL, {
query: { session_token: 'signed.jwt.token' },
}]);
});
});
describe('onContinuePostChallenge', () => {
let loader;
let event;
let api;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
event.secrets.REDIRECT_SECRET = 'test-secret';
mock.method(api.access, 'deny');
});
afterEach(() => {
mock.reset();
});
it('denies access when the redirect token cannot be validated', async () => {
mock.method(api.redirect, 'validateToken', () => {
throw new Error('invalid signature');
});
await loader.execute('onContinuePostChallenge', event, api);
assert.deepEqual(api.redirect.validateToken.mock.calls[0].arguments, [{ secret: 'test-secret' }]);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Unable to verify redirect response: invalid signature']);
});
it('denies access when the token belongs to a different user', async () => {
mock.method(api.redirect, 'validateToken', () => ({ userId: 'someone-else' }));
await loader.execute('onContinuePostChallenge', event, api);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Verification response does not match the current user.']);
});
it('denies access when the verification was not completed', async () => {
mock.method(api.redirect, 'validateToken', () => ({ userId: event.user.user_id }));
event.request.query.verified = 'false';
await loader.execute('onContinuePostChallenge', event, api);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Identity verification was not completed.']);
});
it('allows the flow to continue when verification succeeded', async () => {
mock.method(api.redirect, 'validateToken', () => ({ userId: event.user.user_id }));
event.request.query.verified = 'true';
await loader.execute('onContinuePostChallenge', event, api);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
});
});
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"
}
}