@okta-dfuhriman/okta-auth-js
Version:
The Okta Auth SDK
372 lines (342 loc) • 10.4 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 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
};
}