UNPKG

@okta-dfuhriman/okta-auth-js

Version:
372 lines (342 loc) 10.4 kB
/* 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 max-statements, complexity, max-depth */ import { interact } from './interact'; import { introspect } from './introspect'; import { remediate } from './remediate'; import { getFlowSpecification } from './flow'; import * as remediators from './remediators'; import { OktaAuthIdxInterface, IdxStatus, IdxTransaction, IdxFeature, NextStep, RunOptions, IdxTransactionMeta, } from './types'; import { IdxMessage, IdxResponse } from './types/idx-js'; import { getSavedTransactionMeta, saveTransactionMeta } from './transactionMeta'; import { getAvailableSteps, getEnabledFeatures, getMessagesFromResponse, isTerminalResponse } from './util'; import { Tokens } from '../oidc/types'; import { APIError } from '../errors/types'; declare interface RunData { options: RunOptions; values: remediators.RemediationValues; status?: IdxStatus; tokens?: Tokens; nextStep?: NextStep; messages?: IdxMessage[]; error?: APIError | IdxResponse; meta?: IdxTransactionMeta; enabledFeatures?: IdxFeature[]; availableSteps?: NextStep[]; idxResponse?: IdxResponse; canceled?: boolean; interactionCode?: string; shouldSaveResponse?: boolean; shouldClearTransaction?: boolean; clearSharedStorage?: boolean; terminal?: boolean; } function initializeValues(options: RunOptions) { // remove known options, everything else is assumed to be a value const knownOptions = [ 'flow', 'remediators', 'actions', 'withCredentials', 'step', 'useGenericRemediator', 'exchangeCodeForTokens', ]; const values = { ...options }; knownOptions.forEach(option => { delete values[option]; }); return values; } function initializeData(authClient, data: RunData): RunData { let { options } = data; options = { ...authClient.options.idx, ...options }; let { flow, withCredentials, remediators, actions, } = options; const status = IdxStatus.PENDING; // certain options can be set by the flow specification flow = flow || authClient.idx.getFlow() || 'default'; if (flow) { authClient.idx.setFlow(flow); const flowSpec = getFlowSpecification(authClient, flow); // Favor option values over flow spec withCredentials = (typeof withCredentials !== 'undefined') ? withCredentials : flowSpec.withCredentials; remediators = remediators || flowSpec.remediators; actions = actions || flowSpec.actions; } return { ...data, options: { ...options, flow, withCredentials, remediators, actions, }, status }; } async function getDataFromIntrospect(authClient, data: RunData): Promise<RunData> { const { options } = data; const { stateHandle, withCredentials, version, state, scopes, recoveryToken, activationToken, maxAge, acrValues, nonce, } = options; let idxResponse; let meta = getSavedTransactionMeta(authClient, { state, recoveryToken, activationToken }); // may be undefined if (stateHandle) { idxResponse = await introspect(authClient, { withCredentials, version, stateHandle }); } else { let interactionHandle = meta?.interactionHandle; // may be undefined if (!interactionHandle) { // start a new transaction authClient.transactionManager.clear(); const interactResponse = await interact(authClient, { withCredentials, state, scopes, activationToken, recoveryToken, maxAge, acrValues, nonce, }); interactionHandle = interactResponse.interactionHandle; meta = interactResponse.meta; } // Introspect to get idx response idxResponse = await introspect(authClient, { withCredentials, version, interactionHandle }); } return { ...data, idxResponse, meta }; } async function getDataFromRemediate(authClient, data: RunData): Promise<RunData> { let { idxResponse, options, values } = data; const { autoRemediate, remediators, actions, flow, step, useGenericRemediator, } = options; const shouldRemediate = (autoRemediate !== false && (remediators || actions || step)); if (!shouldRemediate) { return data; } values = { ...values, stateHandle: idxResponse!.rawIdxState.stateHandle }; // Can we handle the remediations? const { idxResponse: idxResponseFromRemediation, nextStep, canceled, } = await remediate( authClient, idxResponse!, values, { remediators, actions, flow, step, useGenericRemediator, } ); idxResponse = idxResponseFromRemediation; return { ...data, idxResponse, nextStep, canceled }; } async function getTokens(authClient, data: RunData): Promise<Tokens> { let { meta, idxResponse } = data; const { interactionCode } = idxResponse as IdxResponse; const { clientId, codeVerifier, ignoreSignature, redirectUri, urls, scopes, } = meta as IdxTransactionMeta; const tokenResponse = await authClient.token.exchangeCodeForTokens({ interactionCode, clientId, codeVerifier, ignoreSignature, redirectUri, scopes }, urls); return tokenResponse.tokens; } async function finalizeData(authClient, data: RunData): Promise<RunData> { let { options, idxResponse, canceled, status, } = data; const { exchangeCodeForTokens } = options; let shouldSaveResponse = false; let shouldClearTransaction = false; let clearSharedStorage = true; let interactionCode; let tokens; let enabledFeatures; let availableSteps; let messages; let terminal; if (idxResponse) { shouldSaveResponse = !!(idxResponse.requestDidSucceed || idxResponse.stepUp); enabledFeatures = getEnabledFeatures(idxResponse); availableSteps = getAvailableSteps(authClient, idxResponse, options.useGenericRemediator); messages = getMessagesFromResponse(idxResponse, options); terminal = isTerminalResponse(idxResponse); } if (terminal) { status = IdxStatus.TERMINAL; // In most cases a terminal response should not clear transaction data. The user should cancel or skip to continue. // A terminal "success" is a non-error response with no further actions available. // In these narrow cases, saved transaction data should be cleared. // One example of a terminal success is when the email verify flow is continued in another tab const hasActions = Object.keys(idxResponse!.actions).length > 0; const hasErrors = !!messages.find(msg => msg.class === 'ERROR'); const isTerminalSuccess = !hasActions && !hasErrors && idxResponse!.requestDidSucceed === true; if (isTerminalSuccess) { shouldClearTransaction = true; } else { // save response if there are actions available (ignore messages) shouldSaveResponse = !!hasActions; } // leave shared storage intact so the transaction can be continued in another tab clearSharedStorage = false; } else if (canceled) { status = IdxStatus.CANCELED; shouldClearTransaction = true; } else if (idxResponse?.interactionCode) { interactionCode = idxResponse.interactionCode; if (exchangeCodeForTokens === false) { status = IdxStatus.SUCCESS; shouldClearTransaction = false; } else { tokens = await getTokens(authClient, data); status = IdxStatus.SUCCESS; shouldClearTransaction = true; } } return { ...data, status, interactionCode, tokens, shouldSaveResponse, shouldClearTransaction, clearSharedStorage, enabledFeatures, availableSteps, messages, terminal }; } export async function run( authClient: OktaAuthIdxInterface, options: RunOptions = {}, ): Promise<IdxTransaction> { let data: RunData = { options, values: initializeValues(options) }; data = initializeData(authClient, data); data = await getDataFromIntrospect(authClient, data); data = await getDataFromRemediate(authClient, data); data = await finalizeData(authClient, data); const { idxResponse, meta, shouldSaveResponse, shouldClearTransaction, clearSharedStorage, status, enabledFeatures, availableSteps, tokens, nextStep, messages, error, interactionCode } = data; if (shouldClearTransaction) { authClient.transactionManager.clear({ clearSharedStorage }); } else { // ensures state is saved to sessionStorage saveTransactionMeta(authClient, { ...meta }); if (shouldSaveResponse) { // Save intermediate idx response in storage to reduce introspect call const { rawIdxState: rawIdxResponse, requestDidSucceed } = idxResponse!; authClient.transactionManager.saveIdxResponse({ rawIdxResponse, requestDidSucceed, stateHandle: idxResponse!.context?.stateHandle, interactionHandle: meta?.interactionHandle }); } } // copy all fields from idxResponse which are needed by the widget const { actions, context, neededToProceed, proceed, rawIdxState, requestDidSucceed, stepUp } = idxResponse || {}; return { status: status!, ...(meta && { meta }), ...(enabledFeatures && { enabledFeatures }), ...(availableSteps && { availableSteps }), ...(tokens && { tokens }), ...(nextStep && { nextStep }), ...(messages && messages.length && { messages }), ...(error && { error }), ...(stepUp && { stepUp }), interactionCode, // if options.exchangeCodeForTokens is false // from idx-js actions: actions!, context: context!, neededToProceed: neededToProceed!, proceed: proceed!, rawIdxState: rawIdxState!, requestDidSucceed }; }