@okta-dfuhriman/okta-auth-js
Version:
The Okta Auth SDK
263 lines (235 loc) • 8.95 kB
text/typescript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/*!
* Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/
/* eslint-disable complexity */
import { OktaAuthIdxInterface, NextStep, IdxMessage, Authenticator, Input, RemediateOptions } from '../../types';
import { IdxAuthenticator, IdxRemediation, IdxContext } from '../../types/idx-js';
import { getAllValues, getRequiredValues, titleCase, getAuthenticatorFromRemediation } from '../util';
import { formatAuthenticator, compareAuthenticators } from '../../authenticator/util';
// A map from IDX data values (server spec) to RemediationValues (client spec)
export type IdxToRemediationValueMap = Record<string, string[]>;
export interface RemediationValues {
stateHandle?: string;
authenticators?: (Authenticator | string)[];
authenticator?: string | Authenticator;
authenticatorsData?: Authenticator[];
resend?: boolean;
}
export interface RemediatorConstructor {
new<T extends RemediationValues>(
remediation: IdxRemediation,
values?: T,
options?: RemediateOptions
): any;
}
// Base class - DO NOT expose static remediationName
export class Remediator<T extends RemediationValues = RemediationValues> {
static remediationName: string;
remediation: IdxRemediation;
values: T;
options: RemediateOptions;
map?: IdxToRemediationValueMap;
constructor(
remediation: IdxRemediation,
values: T = {} as T,
options: RemediateOptions = {}
) {
// assign fields to the instance
this.values = { ...values };
this.options = { ...options };
this.formatAuthenticators();
this.remediation = remediation;
}
private formatAuthenticators() {
this.values.authenticators = (this.values.authenticators || []) as Authenticator[];
// ensure authenticators are in the correct format
this.values.authenticators = this.values.authenticators.map(authenticator => {
return formatAuthenticator(authenticator);
});
// add authenticator (if any) to "authenticators"
if (this.values.authenticator) {
const authenticator = formatAuthenticator(this.values.authenticator);
const hasAuthenticatorInList = this.values.authenticators.some(existing => {
return compareAuthenticators(authenticator, existing);
});
if (!hasAuthenticatorInList) {
this.values.authenticators.push(authenticator);
}
}
// save non-key meta to "authenticatorsData" field
// authenticators will be removed after selection to avoid select-authenticator loop
this.values.authenticatorsData = this.values.authenticators.reduce((acc, authenticator) => {
if (typeof authenticator === 'object' && Object.keys(authenticator).length > 1) {
// save authenticator meta into authenticator data
acc.push(authenticator);
}
return acc;
}, this.values.authenticatorsData || []);
}
getName(): string {
return this.remediation.name;
}
// Override this method to provide custom check
/* eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars */
canRemediate(context?: IdxContext): boolean {
const required = getRequiredValues(this.remediation);
const needed = required!.find((key) => !this.hasData(key));
if (needed) {
return false; // missing data for a required field
}
return true; // all required fields have available data
}
// returns an object for the entire remediation form, or just a part
getData(key?: string) {
if (!key) {
let allValues = getAllValues(this.remediation);
let res = allValues!.reduce((data, key) => {
data[key] = this.getData(key); // recursive
return data;
}, {});
return res;
}
// Map value by "map${Property}" function in each subClass
if (typeof this[`map${titleCase(key)}`] === 'function') {
const val = this[`map${titleCase(key)}`](
this.remediation.value!.find(({name}) => name === key)
);
if (val) {
return val;
}
}
// If a map is defined for this key, return the first aliased property that returns a truthy value
if (this.map && this.map[key]) {
const entry = this.map[key];
for (let i = 0; i < entry.length; i++) {
let val = this.values[entry[i]];
if (val) {
return val;
}
}
}
// fallback: return the value by key
return this.values[key];
}
hasData(
key: string // idx name
): boolean
{
// no attempt to format, we want simple true/false
return !!this.getData(key);
}
getNextStep(_authClient: OktaAuthIdxInterface, _context?: IdxContext): NextStep {
const name = this.getName();
const inputs = this.getInputs();
const authenticator = this.getAuthenticator();
// TODO: remove type field in the next major version change
// https://oktainc.atlassian.net/browse/OKTA-431749
const type = authenticator?.type;
return {
name,
inputs,
...(type && { type }),
...(authenticator && { authenticator }),
};
}
// Get inputs for the next step
getInputs(): Input[] {
const inputs: Input[] = [];
const inputsFromRemediation = this.remediation.value || [];
inputsFromRemediation.forEach(inputFromRemediation => {
let input;
let { name, type, visible, messages } = inputFromRemediation;
if (visible === false) {
return; // Filter out invisible inputs, like stateHandle
}
if (typeof this[`getInput${titleCase(name)}`] === 'function') {
input = this[`getInput${titleCase(name)}`](inputFromRemediation);
} else if (type !== 'object') {
// handle general primitive types
let alias;
const aliases = (this.map ? this.map[name] : null) || [];
if (aliases.length === 1) {
alias = aliases[0];
} else {
// try find key from values
alias = aliases.find(name => Object.keys(this.values).includes(name));
}
if (alias) {
input = { ...inputFromRemediation, name: alias };
}
}
if (!input) {
input = inputFromRemediation;
}
if (Array.isArray(input)) {
input.forEach(i => inputs.push(i));
} else {
// guarantees field-level messages are passed back
if (messages) {
input.messages = messages;
}
inputs.push(input);
}
});
return inputs;
}
static getMessages(remediation: IdxRemediation): IdxMessage[] | undefined {
if (!remediation.value) {
return;
}
return remediation.value[0]?.form?.value.reduce((messages: IdxMessage[], field) => {
if (field.messages) {
messages = [...messages, ...field.messages.value];
}
return messages;
}, []);
}
// Prepare values for the next remediation
// In general, remove used values from inputs for the current remediation
// Override this method if special cases need be handled
getValuesAfterProceed(): T {
const inputsFromRemediation = this.remediation.value || []; // "raw" inputs from server response
const inputsFromRemediator = this.getInputs(); // "aliased" inputs from SDK remediator
const inputs = [
...inputsFromRemediation,
...inputsFromRemediator
];
// scrub all values related to this remediation
for (const input of inputs) {
delete this.values[input.name];
}
return this.values;
}
protected getAuthenticator(): IdxAuthenticator | undefined {
// relatesTo value may be an authenticator or an authenticatorEnrollment
const relatesTo = this.remediation.relatesTo?.value;
if (!relatesTo) {
return;
}
const authenticatorFromRemediation = getAuthenticatorFromRemediation(this.remediation);
if (!authenticatorFromRemediation) {
// Hopefully value is an authenticator
return relatesTo;
}
// If relatesTo is an authenticatorEnrollment, the id is actually the enrollmentId
// Let's get the correct authenticator id from the form value
const id = authenticatorFromRemediation.form!.value
.find(({ name }) => name === 'id')!.value as string;
const enrollmentId = authenticatorFromRemediation.form!.value
.find(({ name }) => name === 'enrollmentId')?.value as string;
return {
...relatesTo,
id,
enrollmentId
};
}
}