UNPKG

@okta-dfuhriman/okta-auth-js

Version:
298 lines (262 loc) 9.87 kB
import { warn, split2 } from '../util'; import * as remediators from './remediators'; import { RemediationValues, Remediator, RemediatorConstructor } from './remediators'; import { GenericRemediator } from './remediators/GenericRemediator'; import { OktaAuthIdxInterface, IdxFeature, NextStep, RemediateOptions, RemediationResponse, RunOptions } from './types'; import { IdxMessage, IdxRemediation, IdxRemediationValue, IdxResponse } from './types/idx-js'; export function isTerminalResponse(idxResponse: IdxResponse) { const { neededToProceed, interactionCode } = idxResponse; return !neededToProceed.length && !interactionCode; } export function canSkipFn(idxResponse: IdxResponse) { return idxResponse.neededToProceed.some(({ name }) => name === 'skip'); } export function canResendFn(idxResponse: IdxResponse) { return Object.keys(idxResponse.actions).some(actionName => actionName.includes('resend')); } export function getMessagesFromIdxRemediationValue( value?: IdxRemediationValue[] ): IdxMessage[] | undefined { if (!value || !Array.isArray(value)) { return; } return value.reduce((messages, value) => { if (value.messages) { messages = [...messages, ...value.messages.value] as never; } if (value.form) { const messagesFromForm = getMessagesFromIdxRemediationValue(value.form.value) || []; messages = [...messages, ...messagesFromForm] as never; } if (value.options) { let optionValues = []; value.options.forEach(option => { if (!option.value || typeof option.value === 'string') { return; } optionValues = [...optionValues, option.value] as never; }); const messagesFromOptions = getMessagesFromIdxRemediationValue(optionValues) || []; messages = [...messages, ...messagesFromOptions] as never; } return messages; }, []); } export function getMessagesFromResponse(idxResponse: IdxResponse, options: RunOptions): IdxMessage[] { let messages: IdxMessage[] = []; const { rawIdxState, neededToProceed } = idxResponse; // Handle global messages const globalMessages = rawIdxState.messages?.value.map(message => message); if (globalMessages) { messages = [...messages, ...globalMessages] as never; } // Handle field messages for current flow // Preserve existing logic for general cases, remove in the next major version // Follow ion response format for top level messages when useGenericRemediator is true if (!options.useGenericRemediator) { for (let remediation of neededToProceed) { const fieldMessages = getMessagesFromIdxRemediationValue(remediation.value); if (fieldMessages) { messages = [...messages, ...fieldMessages] as never; } } } // API may return identical error on same field, filter by i18n key const seen = {}; messages = messages.reduce((filtered, message) => { const key = message.i18n?.key; if (key && seen[key]) { return filtered; } seen[key] = message; filtered = [...filtered, message] as never; return filtered; }, []); return messages; } export function getEnabledFeatures(idxResponse: IdxResponse): IdxFeature[] { const res = []; const { actions, neededToProceed } = idxResponse; if (actions['currentAuthenticator-recover']) { res.push(IdxFeature.PASSWORD_RECOVERY as never); } if (neededToProceed.some(({ name }) => name === 'select-enroll-profile')) { res.push(IdxFeature.REGISTRATION as never); } if (neededToProceed.some(({ name }) => name === 'redirect-idp')) { res.push(IdxFeature.SOCIAL_IDP as never); } if (neededToProceed.some(({ name }) => name === 'unlock-account')) { res.push(IdxFeature.ACCOUNT_UNLOCK as never); } return res; } export function getAvailableSteps( authClient: OktaAuthIdxInterface, idxResponse: IdxResponse, useGenericRemediator?: boolean ): NextStep[] { const res: NextStep[] = []; const remediatorMap: Record<string, RemediatorConstructor> = Object.values(remediators) .reduce((map, remediatorClass) => { // Only add concrete subclasses to the map if (remediatorClass.remediationName) { map[remediatorClass.remediationName] = remediatorClass; } return map; }, {}); for (let remediation of idxResponse.neededToProceed) { const T = getRemediatorClass(remediation, { useGenericRemediator, remediators: remediatorMap }); if (T) { const remediator: Remediator = new T(remediation); res.push (remediator.getNextStep(authClient, idxResponse.context) as never); } } for (const [name] of Object.entries((idxResponse.actions || {}))) { let stepObj = { name, action: async (params?) => { return authClient.idx.proceed({ actions: [{ name, params }] }); } }; if (name.startsWith('currentAuthenticator')) { const [part1, part2] = split2(name, '-'); const actionObj = idxResponse.rawIdxState[part1].value[part2]; /* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */ const { href, method, rel, accepts, produces, ...rest } = actionObj; /* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */ const value = actionObj.value?.filter(item => item.name !== 'stateHandle'); stepObj = { ...rest, ...(value && { value }), ...stepObj, }; } res.push(stepObj); } return res; } export function filterValuesForRemediation( idxResponse: IdxResponse, remediationName: string, values: RemediationValues ): RemediationValues { const remediations = idxResponse.neededToProceed || []; const remediation = remediations.find(r => r.name === remediationName); if (!remediation) { // step was specified, but remediation was not found. This is unexpected! warn(`filterValuesForRemediation: "${remediationName}" did not match any remediations`); return values; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const valuesForRemediation = remediation.value!.reduce((res, entry) => { const { name, value } = entry; if (name === 'stateHandle') { res[name] = value; // use the stateHandle value in the remediation } else { res[name] = values[name]; // use the value provided by the caller } return res; }, {}); return valuesForRemediation; } function getRemediatorClass(remediation: IdxRemediation, options: RemediateOptions) { const { useGenericRemediator, remediators } = options; if (!remediation) { return undefined; } if (useGenericRemediator) { return GenericRemediator; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return remediators![remediation.name]; } // Return first match idxRemediation in allowed remediators // eslint-disable-next-line complexity export function getRemediator( idxResponse: IdxResponse, values: RemediationValues, options: RemediateOptions, ): Remediator | undefined { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const remediators = options.remediators!; const useGenericRemediator = options.useGenericRemediator; const {neededToProceed: idxRemediations, context} = idxResponse; let remediator: Remediator; // remediation name specified by caller - fast-track remediator lookup if (options.step) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const remediation = idxRemediations.find(({ name }) => name === options.step)!; if (remediation) { const T = getRemediatorClass(remediation, options); return T ? new T(remediation, values, options) : undefined; } else { // step was specified, but remediation was not found. This is unexpected! warn(`step "${options.step}" did not match any remediations`); return; } } const remediatorCandidates: Remediator[] = []; if (useGenericRemediator) { // always pick the first remediation for when use GenericRemediator remediatorCandidates.push(new GenericRemediator(idxRemediations[0], values, options)); } else { for (let remediation of idxRemediations) { const isRemeditionInFlow = Object.keys(remediators as object).includes(remediation.name); if (!isRemeditionInFlow) { continue; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const T = getRemediatorClass(remediation, options)!; remediator = new T(remediation, values, options); if (remediator.canRemediate(context)) { // found the remediator return remediator; } // remediator cannot handle the current values // maybe return for next step remediatorCandidates.push(remediator); } } return remediatorCandidates[0]; } export function getNextStep( authClient: OktaAuthIdxInterface, remediator: Remediator, idxResponse: IdxResponse ): NextStep { const nextStep = remediator.getNextStep(authClient, idxResponse.context); const canSkip = canSkipFn(idxResponse); const canResend = canResendFn(idxResponse); return { ...nextStep, ...(canSkip && {canSkip}), ...(canResend && {canResend}), }; } export function handleFailedResponse( authClient: OktaAuthIdxInterface, idxResponse: IdxResponse, options = {} ): RemediationResponse { const terminal = isTerminalResponse(idxResponse); const messages = getMessagesFromResponse(idxResponse, options); if (terminal) { return { idxResponse, terminal, messages }; } else { const remediator = getRemediator(idxResponse, {}, options); const nextStep = remediator && getNextStep(authClient, remediator, idxResponse); return { idxResponse, messages, ...(nextStep && { nextStep }), }; } }