diff --git a/templates/add-persistence-attribute-PASSWORD_RESET_POST_CHALLENGE/code.js b/templates/add-persistence-attribute-PASSWORD_RESET_POST_CHALLENGE/code.js index dd8a85c..26a8c1c 100644 --- a/templates/add-persistence-attribute-PASSWORD_RESET_POST_CHALLENGE/code.js +++ b/templates/add-persistence-attribute-PASSWORD_RESET_POST_CHALLENGE/code.js @@ -1,10 +1,16 @@ -const { ManagementClient } = require('auth0'); +const { ManagementClient, AuthenticationClient } = require('auth0'); const HTTP_TIMEOUT = 1000; // 1s // --- AUTH0 ACTIONS TEMPLATE https://github.com/auth0/opensource-marketplace/blob/main/templates/add-persistence-attribute-PASSWORD_RESET_POST_CHALLENGE --- /** - * Handler that will be called during the execution of a Password Reset / Post Challenge Flow. + * Handler that will be called during the execution of a Password Reset / Post Challenge Flow. Makes use of a few SECRETS: + * METADATA_KEY - object key value to be set for the user + * METADATA_DEFAULT_VALUE - value to set for METADATA_KEY on initialization + * TENANT_DOMAIN - Domain used for auth in AuthenticationClientOptions (See https://auth0.github.io/node-auth0/interfaces/auth.AuthenticationClientOptions.html for deetails on object) + * CLIENT_ID - Client ID used for auth in AuthenticationClientOptions + * CLIENT_SECRET - Client Secret used for auth in AuthenticationClientOptions + * AUDIENCE - [OPTIONAL] Audience used * * @param {Event} event - Details about the post challenge request. * @param {PasswordResetPostChallengeAPI} api - Interface whose methods can be used to change the behavior of the post challenge flow. @@ -33,43 +39,89 @@ exports.onExecutePostChallenge = async (event, api) => { return api.access.deny('missing client id'); } - const clientId = event.secrets.CLIENT_ID; - if (!event.secrets.CLIENT_SECRET) { return api.access.deny('missing client secret'); } - const clientSecret = event.secrets.CLIENT_SECRET; - if (!event.secrets.TENANT_DOMAIN) { return api.access.deny('missing tenant domain'); } - const domain = event.secrets.TENANT_DOMAIN; + const authenticationClientOptions = { + domain: event.secrets.TENANT_DOMAIN, + clientId: event.secrets.CLIENT_ID, + clientSecret: event.secrets.CLIENT_SECRET, + timeoutDuration: HTTP_TIMEOUT, + }; - const management = new ManagementClient({ - domain, - clientId, - clientSecret, - httpTimeout: HTTP_TIMEOUT, - }); + // if event.secrets.AUDIENCE is set, make use of the parameter for validating JWT is being used by intended Audience + if (event.secrets.AUDIENCE) { + authenticationClientOptions.audience = event.secrets.AUDIENCE; + } + + const management = await getManagementApiClient( + api.cache, + authenticationClientOptions + ); await management.users.update( { id: event.user.user_id }, { user_metadata: { - [metadataKey]: metadataValue || metadataDefaultValue, + [metadataKey]: metadataDefaultValue, }, } ); }; /** - * Handler that will be invoked when this action is resuming after an external redirect. If your - * onExecutePostChallenge function does not perform a redirect, this function can be safely ignored. + * Get an AccessToken * - * @param {Event} event - Details about the user and the context in which they are logging in. - * @param {PasswordResetPostChallengeAPI} api - Interface whose methods can be used to change the behavior of the post challenge flow. + * @param {CacheAPI} cache + * @param {{domain: string, clientId: string, clientSecret: string, audience?: string, timeoutDuration: number}} options - AuthenticationClient options to fetch the token + * @return {Promise} + */ +async function getAccessToken(cache, options) { + let key = `access_token_${options.clientId}`; + + if (options.audience) { + key += `_${options.audience}`; + } + + // Check the cache if we have a valid entry + const record = cache.get(key); + if (record && record.expires_at > Date.now()) { + return record.value; + } + + // Get the AccessToken using a client_credential grant. + const authClient = new AuthenticationClient(options); + + const { + data: { access_token, expires_in }, + } = await authClient.oauth.clientCredentialsGrant({ + audience: options.audience ?? `https://${options.domain}/api/v2/`, + }); + + // Try to cache it + const cacheSetResult = cache.set(key, access_token, { ttl: expires_in }); + if (cacheSetResult.type === 'error') { + console.error(`Failed to set ${key}: ${cacheSetResult.code}`); + } + + return access_token; +} + +/** + * @param {CacheAPI} cache + * @param {{domain: string, clientId: string, clientSecret: string, audience?: string, timeoutDuration: number}} options - AuthenticationClient options to fetch the token */ -// exports.onContinuePostChallenge = async (event, api) => { -// }; +async function getManagementApiClient(cache, options) { + const token = await getAccessToken(cache, options); + + return new ManagementClient({ + domain: options.domain, + token, + httpTimeout: HTTP_TIMEOUT, + }); +} diff --git a/templates/add-persistence-attribute-POST_CHANGE_PASSWORD/code.js b/templates/add-persistence-attribute-POST_CHANGE_PASSWORD/code.js index 099be66..1c4d29c 100644 --- a/templates/add-persistence-attribute-POST_CHANGE_PASSWORD/code.js +++ b/templates/add-persistence-attribute-POST_CHANGE_PASSWORD/code.js @@ -1,10 +1,16 @@ -const { ManagementClient } = require('auth0'); +const { ManagementClient, AuthenticationClient } = require('auth0'); -const HTTP_TIMEOUT = 1000; // 1s +const HTTP_TIMEOUT = 1000; // 1s time to timeout on web requests to auth // --- AUTH0 ACTIONS TEMPLATE https://github.com/auth0/opensource-marketplace/blob/main/templates/add-persistence-attribute-POST_CHANGE_PASSWORD --- /** - * Handler that will be called during the execution of a PostChangePassword flow. + * Handler that will be called during the execution of a PostChangePassword flow. Makes use of a few SECRETS: + * METADATA_KEY - object key value to be set for the user + * METADATA_DEFAULT_VALUE - value to set for METADATA_KEY on initialization + * TENANT_DOMAIN - Domain used for auth in AuthenticationClientOptions (See https://auth0.github.io/node-auth0/interfaces/auth.AuthenticationClientOptions.html for deetails on object) + * CLIENT_ID - Client ID used for auth in AuthenticationClientOptions + * CLIENT_SECRET - Client Secret used for auth in AuthenticationClientOptions + * AUDIENCE - [OPTIONAL] Audience used * * @param {Event} event - Details about the user and the context in which the change password is happening. * @param {PostChangePasswordAPI} api - Methods and utilities to help change the behavior after a user changes their password. @@ -36,35 +42,91 @@ exports.onExecutePostChangePassword = async (event, api) => { return; } - const clientId = event.secrets.CLIENT_ID; - if (!event.secrets.CLIENT_SECRET) { console.log('missing event.secrets.CLIENT_SECRET'); return; } - const clientSecret = event.secrets.CLIENT_SECRET; - if (!event.secrets.TENANT_DOMAIN) { console.log('missing event.secrets.TENANT_DOMAIN'); return; } - const domain = event.secrets.TENANT_DOMAIN; + const authenticationClientOptions = { + domain: event.secrets.TENANT_DOMAIN, + clientId: event.secrets.CLIENT_ID, + clientSecret: event.secrets.CLIENT_SECRET, + timeoutDuration: HTTP_TIMEOUT, + }; - const management = new ManagementClient({ - domain, - clientId, - clientSecret, - httpTimeout: HTTP_TIMEOUT, - }); + // if event.secrets.AUDIENCE is set, make use of the parameter for validating JWT is being used by intended Audience + if (event.secrets.AUDIENCE) { + authenticationClientOptions.audience = event.secrets.AUDIENCE; + } + + const management = await getManagementApiClient( + api.cache, + authenticationClientOptions + ); await management.users.update( { id: event.user.user_id }, { user_metadata: { - [metadataKey]: metadataValue || metadataDefaultValue, + [metadataKey]: metadataDefaultValue, }, } ); }; + +/** + * Get an AccessToken + * + * @param {CacheAPI} cache + * @param {{domain: string, clientId: string, clientSecret: string, audience?: string, timeoutDuration: number}} options - AuthenticationClient options to fetch the token + * @return {Promise} + */ +async function getAccessToken(cache, options) { + let key = `access_token_${options.clientId}`; + + if (options.audience) { + key += `_${options.audience}`; + } + + // Check the cache if we have a valid entry + const record = cache.get(key); + if (record && record.expires_at > Date.now()) { + return record.value; + } + + // Get the AccessToken using a client_credential grant. + const authClient = new AuthenticationClient(options); + + const { + data: { access_token, expires_in }, + } = await authClient.oauth.clientCredentialsGrant({ + audience: options.audience ?? `https://${options.domain}/api/v2/`, + }); + + // Try to cache it + const cacheSetResult = cache.set(key, access_token, { ttl: expires_in }); + if (cacheSetResult.type === 'error') { + console.error(`Failed to set ${key}: ${cacheSetResult.code}`); + } + + return access_token; +} + +/** + * @param {CacheAPI} cache + * @param {{domain: string, clientId: string, clientSecret: string, audience?: string, timeoutDuration: number}} options - AuthenticationClient options to fetch the token + */ +async function getManagementApiClient(cache, options) { + const token = await getAccessToken(cache, options); + + return new ManagementClient({ + domain: options.domain, + token, + httpTimeout: HTTP_TIMEOUT, + }); +} diff --git a/templates/add-persistence-attribute-POST_USER_REGISTRATION/code.js b/templates/add-persistence-attribute-POST_USER_REGISTRATION/code.js index b15b46f..562c0de 100644 --- a/templates/add-persistence-attribute-POST_USER_REGISTRATION/code.js +++ b/templates/add-persistence-attribute-POST_USER_REGISTRATION/code.js @@ -1,10 +1,16 @@ -const { ManagementClient } = require('auth0'); +const { ManagementClient, AuthenticationClient } = require('auth0'); const HTTP_TIMEOUT = 1000; // 1s // --- AUTH0 ACTIONS TEMPLATE https://github.com/auth0/opensource-marketplace/blob/main/templates/add-persistence-attribute-POST_USER_REGISTRATION --- /** - * Handler that will be called during the execution of a PostUserRegistration flow. + * Handler that will be called during the execution of a PostUserRegistration flow.. Makes use of a few SECRETS: + * METADATA_KEY - object key value to be set for the user + * METADATA_DEFAULT_VALUE - value to set for METADATA_KEY on initialization + * TENANT_DOMAIN - Domain used for auth in AuthenticationClientOptions (See https://auth0.github.io/node-auth0/interfaces/auth.AuthenticationClientOptions.html for deetails on object) + * CLIENT_ID - Client ID used for auth in AuthenticationClientOptions + * CLIENT_SECRET - Client Secret used for auth in AuthenticationClientOptions + * AUDIENCE - [OPTIONAL] Audience used * * @param {Event} event - Details about the context and user that has registered. * @param {PostUserRegistrationAPI} api - Methods and utilities to help change the behavior after a signup. @@ -36,35 +42,91 @@ exports.onExecutePostUserRegistration = async (event, api) => { return; } - const clientId = event.secrets.CLIENT_ID; - if (!event.secrets.CLIENT_SECRET) { console.log('missing event.secrets.CLIENT_SECRET'); return; } - const clientSecret = event.secrets.CLIENT_SECRET; - if (!event.secrets.TENANT_DOMAIN) { console.log('missing event.secrets.TENANT_DOMAIN'); return; } - const domain = event.secrets.TENANT_DOMAIN; + const authenticationClientOptions = { + domain: event.secrets.TENANT_DOMAIN, + clientId: event.secrets.CLIENT_ID, + clientSecret: event.secrets.CLIENT_SECRET, + timeoutDuration: HTTP_TIMEOUT, + }; - const management = new ManagementClient({ - domain, - clientId, - clientSecret, - httpTimeout: HTTP_TIMEOUT, - }); + // if event.secrets.AUDIENCE is set, make use of the parameter for validating JWT is being used by intended Audience + if (event.secrets.AUDIENCE) { + authenticationClientOptions.audience = event.secrets.AUDIENCE; + } + + const management = await getManagementApiClient( + api.cache, + authenticationClientOptions + ); await management.users.update( { id: event.user.user_id }, { user_metadata: { - [metadataKey]: metadataValue || metadataDefaultValue, + [metadataKey]: metadataDefaultValue, }, } ); }; + +/** + * Get an AccessToken + * + * @param {CacheAPI} cache + * @param {{domain: string, clientId: string, clientSecret: string, audience?: string, timeoutDuration: number}} options - AuthenticationClient options to fetch the token + * @return {Promise} + */ +async function getAccessToken(cache, options) { + let key = `access_token_${options.clientId}`; + + if (options.audience) { + key += `_${options.audience}`; + } + + // Check the cache if we have a valid entry + const record = cache.get(key); + if (record && record.expires_at > Date.now()) { + return record.value; + } + + // Get the AccessToken using a client_credential grant. + const authClient = new AuthenticationClient(options); + + const { + data: { access_token, expires_in }, + } = await authClient.oauth.clientCredentialsGrant({ + audience: options.audience ?? `https://${options.domain}/api/v2/`, + }); + + // Try to cache it + const cacheSetResult = cache.set(key, access_token, { ttl: expires_in }); + if (cacheSetResult.type === 'error') { + console.error(`Failed to set ${key}: ${cacheSetResult.code}`); + } + + return access_token; +} + +/** + * @param {CacheAPI} cache + * @param {{domain: string, clientId: string, clientSecret: string, audience?: string, timeoutDuration: number}} options - AuthenticationClient options to fetch the token + */ +async function getManagementApiClient(cache, options) { + const token = await getAccessToken(cache, options); + + return new ManagementClient({ + domain: options.domain, + token, + httpTimeout: HTTP_TIMEOUT, + }); +} diff --git a/templates/add-persistence-attribute-SEND_PHONE_MESSAGE/code.js b/templates/add-persistence-attribute-SEND_PHONE_MESSAGE/code.js index 84297b1..3df639a 100644 --- a/templates/add-persistence-attribute-SEND_PHONE_MESSAGE/code.js +++ b/templates/add-persistence-attribute-SEND_PHONE_MESSAGE/code.js @@ -1,24 +1,30 @@ -const { ManagementClient } = require('auth0'); +const { ManagementClient, AuthenticationClient } = require('auth0'); const HTTP_TIMEOUT = 1000; // 1s // --- AUTH0 ACTIONS TEMPLATE https://github.com/auth0/opensource-marketplace/blob/main/templates/add-persistence-attribute-SEND_PHONE_MESSAGE --- /** - * Handler that will be called during the execution of a SendPhoneMessage flow. + * Handler that will be called during the execution of a SendPhoneMessage flow.. Makes use of a few SECRETS: + * METADATA_KEY - object key value to be set for the user + * METADATA_DEFAULT_VALUE - value to set for METADATA_KEY on initialization + * TENANT_DOMAIN - Domain used for auth in AuthenticationClientOptions (See https://auth0.github.io/node-auth0/interfaces/auth.AuthenticationClientOptions.html for deetails on object) + * CLIENT_ID - Client ID used for auth in AuthenticationClientOptions + * CLIENT_SECRET - Client Secret used for auth in AuthenticationClientOptions + * AUDIENCE - [OPTIONAL] Audience used * * @param {Event} event - Details about the user and the context in which they are logging in. * @param {SendPhoneMessageAPI} api - Methods and utilities to help change the behavior of sending a phone message. */ exports.onExecuteSendPhoneMessage = async (event, api) => { if (!event.secrets.METADATA_KEY) { - console.log('missing event.secrets.METADATA_KEY'); + console.log('missing metadata key'); return; } const metadataKey = event.secrets.METADATA_KEY; if (!event.secrets.METADATA_DEFAULT_VALUE) { - console.log('missing event.secrets.METADATA_DEFAULT_VALUE'); + console.log('missing metadata default value'); return; } @@ -36,35 +42,91 @@ exports.onExecuteSendPhoneMessage = async (event, api) => { return; } - const clientId = event.secrets.CLIENT_ID; - if (!event.secrets.CLIENT_SECRET) { console.log('missing event.secrets.CLIENT_SECRET'); return; } - const clientSecret = event.secrets.CLIENT_SECRET; - if (!event.secrets.TENANT_DOMAIN) { - console.log('missing event.secrets.TENANT_DOMAIN'); + console.log('missing tenant domain'); return; } - const domain = event.secrets.TENANT_DOMAIN; + const authenticationClientOptions = { + domain: event.secrets.TENANT_DOMAIN, + clientId: event.secrets.CLIENT_ID, + clientSecret: event.secrets.CLIENT_SECRET, + timeoutDuration: HTTP_TIMEOUT, + }; - const management = new ManagementClient({ - domain, - clientId, - clientSecret, - httpTimeout: HTTP_TIMEOUT, - }); + // if event.secrets.AUDIENCE is set, make use of the parameter for validating JWT is being used by intended Audience + if (event.secrets.AUDIENCE) { + authenticationClientOptions.audience = event.secrets.AUDIENCE; + } + + const management = await getManagementApiClient( + api.cache, + authenticationClientOptions + ); await management.users.update( { id: event.user.user_id }, { user_metadata: { - [metadataKey]: metadataValue || metadataDefaultValue, + [metadataKey]: metadataDefaultValue, }, } ); }; + +/** + * Get an AccessToken + * + * @param {CacheAPI} cache + * @param {{domain: string, clientId: string, clientSecret: string, audience?: string, timeoutDuration: number}} options - AuthenticationClient options to fetch the token + * @return {Promise} + */ +async function getAccessToken(cache, options) { + let key = `access_token_${options.clientId}`; + + if (options.audience) { + key += `_${options.audience}`; + } + + // Check the cache if we have a valid entry + const record = cache.get(key); + if (record && record.expires_at > Date.now()) { + return record.value; + } + + // Get the AccessToken using a client_credential grant. + const authClient = new AuthenticationClient(options); + + const { + data: { access_token, expires_in }, + } = await authClient.oauth.clientCredentialsGrant({ + audience: options.audience ?? `https://${options.domain}/api/v2/`, + }); + + // Try to cache it + const cacheSetResult = cache.set(key, access_token, { ttl: expires_in }); + if (cacheSetResult.type === 'error') { + console.error(`Failed to set ${key}: ${cacheSetResult.code}`); + } + + return access_token; +} + +/** + * @param {CacheAPI} cache + * @param {{domain: string, clientId: string, clientSecret: string, audience?: string, timeoutDuration: number}} options - AuthenticationClient options to fetch the token + */ +async function getManagementApiClient(cache, options) { + const token = await getAccessToken(cache, options); + + return new ManagementClient({ + domain: options.domain, + token, + httpTimeout: HTTP_TIMEOUT, + }); +}