@okta-dfuhriman/okta-auth-js
Version:
The Okta Auth SDK
298 lines (262 loc) • 9.87 kB
text/typescript
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 }),
};
}
}