Action
The following Post Login action renders a marketing consent Form to users who haven’t answered it yet, then handles the submitted response on the following login attempt: denying access if the Form was skipped, or storing the consent choice inuser_metadata otherwise.
- JavaScript
- TypeScript
mock-form-render.js
/** @import {Event, PostLoginAPI} from "@auth0/actions/post-login/v3" */
const CONSENT_FORM_ID = 'marketing-consent-form';
/**
* Handler that will be called during the execution of a PostLogin flow.
* Renders a custom Form asking for marketing consent when the user hasn't
* answered it yet. `api.prompt.render` cannot be combined with
* `api.redirect.sendUserTo` in the same execution - pick one per action.
*
* @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) => {
const hasAnsweredConsent = typeof event.user.user_metadata.marketing_consent === 'boolean';
if (hasAnsweredConsent) {
return;
}
api.prompt.render(CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name ?? event.user.name },
});
};
/**
* Handler that will be called when the user submits the Form rendered by
* onExecutePostLogin.
*
* @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.onContinuePostLogin = async (event, api) => {
const consent = event.prompt?.fields?.consent;
if (typeof consent !== 'boolean') {
api.access.deny('Marketing consent response was not submitted.');
return;
}
api.user.setUserMetadata('marketing_consent', consent);
};
mock-form-render.ts
import type { Event, PostLoginAPI } from '@auth0/actions/post-login/v3';
const CONSENT_FORM_ID = 'marketing-consent-form';
/**
* Handler that will be called during the execution of a PostLogin flow.
* Renders a custom Form asking for marketing consent when the user hasn't
* answered it yet. `api.prompt.render` cannot be combined with
* `api.redirect.sendUserTo` in the same execution - pick one per action.
*
* @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) => {
const hasAnsweredConsent = typeof event.user.user_metadata.marketing_consent === 'boolean';
if (hasAnsweredConsent) {
return;
}
api.prompt.render(CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name ?? event.user.name },
});
};
/**
* Handler that will be called when the user submits the Form rendered by
* onExecutePostLogin.
*
* @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.onContinuePostLogin = async (event: Event, api: PostLoginAPI) => {
const consent = event.prompt?.fields?.consent;
if (typeof consent !== 'boolean') {
api.access.deny('Marketing consent response was not submitted.');
return;
}
api.user.setUserMetadata('marketing_consent', consent);
};
Unit Test
The unit tests mock theevent and api objects to verify the Form renders only when consent hasn’t been answered, and that both accepted and declined consent submissions are stored correctly while missing submissions are denied.
Jest
Jest
- JavaScript
- TypeScript
mock-form-render.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/mock-form-render.js');
const CONSENT_FORM_ID = 'marketing-consent-form';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
jest.resetAllMocks();
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
jest.spyOn(api.prompt, 'render');
});
afterEach(() => {
jest.resetAllMocks();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).toHaveBeenCalledWith(CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
});
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).not.toHaveBeenCalled();
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).not.toHaveBeenCalled();
});
});
describe('onContinuePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
jest.resetAllMocks();
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
jest.spyOn(api.access, 'deny');
jest.spyOn(api.user, 'setUserMetadata');
});
afterEach(() => {
jest.resetAllMocks();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
expect(api.access.deny).toHaveBeenCalledWith('Marketing consent response was not submitted.');
expect(api.user.setUserMetadata).not.toHaveBeenCalled();
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', true);
expect(api.access.deny).not.toHaveBeenCalled();
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', false);
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-form-render.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/mock-form-render.ts');
const CONSENT_FORM_ID = 'marketing-consent-form';
describe('onExecutePostLogin', () => {
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();
jest.spyOn(api.prompt, 'render');
});
afterEach(() => {
jest.resetAllMocks();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).toHaveBeenCalledWith(CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
});
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).not.toHaveBeenCalled();
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).not.toHaveBeenCalled();
});
});
describe('onContinuePostLogin', () => {
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();
jest.spyOn(api.access, 'deny');
jest.spyOn(api.user, 'setUserMetadata');
});
afterEach(() => {
jest.resetAllMocks();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
expect(api.access.deny).toHaveBeenCalledWith('Marketing consent response was not submitted.');
expect(api.user.setUserMetadata).not.toHaveBeenCalled();
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', true);
expect(api.access.deny).not.toHaveBeenCalled();
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', false);
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-form-render.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/mock-form-render.js');
const CONSENT_FORM_ID = 'marketing-consent-form';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
sinon.spy(api.prompt, 'render');
});
afterEach(() => {
sinon.restore();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.calledWith(api.prompt.render, CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
});
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.prompt.render);
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.prompt.render);
});
});
describe('onContinuePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
sinon.spy(api.access, 'deny');
sinon.spy(api.user, 'setUserMetadata');
});
afterEach(() => {
sinon.restore();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.access.deny, 'Marketing consent response was not submitted.');
sinon.assert.notCalled(api.user.setUserMetadata);
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', true);
sinon.assert.notCalled(api.access.deny);
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', false);
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-form-render.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/mock-form-render.ts');
const CONSENT_FORM_ID = 'marketing-consent-form';
describe('onExecutePostLogin', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
sinon.spy(api.prompt, 'render');
});
afterEach(() => {
sinon.restore();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.calledWith(api.prompt.render, CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
});
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.prompt.render);
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.prompt.render);
});
});
describe('onContinuePostLogin', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
sinon.spy(api.access, 'deny');
sinon.spy(api.user, 'setUserMetadata');
});
afterEach(() => {
sinon.restore();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.access.deny, 'Marketing consent response was not submitted.');
sinon.assert.notCalled(api.user.setUserMetadata);
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', true);
sinon.assert.notCalled(api.access.deny);
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', false);
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-form-render.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/mock-form-render.js');
const CONSENT_FORM_ID = 'marketing-consent-form';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
mock.method(api.prompt, 'render');
});
afterEach(() => {
mock.reset();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
assert.deepEqual(api.prompt.render.mock.calls[0].arguments, [CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
}]);
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.prompt.render.mock.calls.length, 0);
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.prompt.render.mock.calls.length, 0);
});
});
describe('onContinuePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
mock.method(api.access, 'deny');
mock.method(api.user, 'setUserMetadata');
});
afterEach(() => {
mock.reset();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Marketing consent response was not submitted.']);
assert.strictEqual(api.user.setUserMetadata.mock.calls.length, 0);
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', true]);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', false]);
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-form-render.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/mock-form-render.ts');
const CONSENT_FORM_ID = 'marketing-consent-form';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
mock.method(api.prompt, 'render');
});
afterEach(() => {
mock.reset();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
assert.deepEqual(api.prompt.render.mock.calls[0].arguments, [CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
}]);
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.prompt.render.mock.calls.length, 0);
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.prompt.render.mock.calls.length, 0);
});
});
describe('onContinuePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
mock.method(api.access, 'deny');
mock.method(api.user, 'setUserMetadata');
});
afterEach(() => {
mock.reset();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Marketing consent response was not submitted.']);
assert.strictEqual(api.user.setUserMetadata.mock.calls.length, 0);
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', true]);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', false]);
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"
}
}