Skip to content

Commit

Permalink
feat: improvements to IDX client
Browse files Browse the repository at this point in the history
OKTA-466786
<<<Jenkins Check-In of Tested SHA: f416467 for [email protected]>>>
Artifact: okta-auth-js
Files changed count: 92
PR Link:
  • Loading branch information
aarongranick-okta authored and eng-prod-CI-bot-okta committed Mar 23, 2022
1 parent f06eac1 commit 71c3352
Show file tree
Hide file tree
Showing 92 changed files with 2,078 additions and 2,304 deletions.
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,25 @@
"env": {
"DEBUG": "1"
}
},

{
"type": "node",
"protocol": "inspector",
"request": "launch",
"name": "Debug Webpack",
"program": "${workspaceRoot}/node_modules/.bin/webpack",
"runtimeExecutable": "${env:HOME}/.nvm/versions/node/v12.13.0/bin/node",
"args": [
"--config",
"${workspaceRoot}/webpack.dev.config.js"
],
"autoAttachChildProcesses": true,
"console": "integratedTerminal",
"cwd": "${workspaceRoot}",
"env": {
"NODE_ENV": "development"
}
}
]
}
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
# Changelog

## 6.3.0

### Features

- [#1090](https://github.com/okta/okta-auth-js/pull/1090)
- An `authenticator` can be provided to IDX methods as either a string (representing the authenticator key) or an authenticator object
- IDX functions will accept the "canonical" name for inputs (as defined by server response). For example a `credentials` object can be passed to satisfy an "identify" remediation instead of `username` and `password`
- `idx.proceed` will continue without saved transaction meta if a `stateHandle` is available
- Unknown remediations/values will proceed if the proper data is supplied by the caller
- IDX response object has a new field `requestDidSucceed` which will be false if the XHR was returned with a non-2xx HTTP status

### Fixes

- [#1090](https://github.com/okta/okta-auth-js/pull/1090)
- Fixes concurrency issue with `transformAuthState`. Concurrent auth state updates will now enqueue calls to `transformAuthState` so that they execute sequentially
- Fixes issue with in-memory storage provider, where storage was shared between AuthJS instances in the same page/process. In-memory storage will now be unique per AuthJS instance.
- Fixes issue with the `step` option in IDX flows: it will only be used for a single remediation cycle
- [#1136](https://github.com/okta/okta-auth-js/pull/1136) Fixes typo in security question enrollment

### Other

- [#1090](https://github.com/okta/okta-auth-js/pull/1090) Removes runtime regenerator for development builds

## 6.2.0

### Features
Expand Down
7 changes: 6 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
const presets = ['@babel/preset-env'];
const plugins = ['@babel/plugin-transform-runtime'];
const plugins = [];

// Do not include async generator in development bundle (debug on modern browser)
if (process.env.NODE_ENV !== 'development') {
plugins.unshift('@babel/plugin-transform-runtime');
}

// Process typescript when running in jest
if (process.env.NODE_ENV === 'test') {
Expand Down
12 changes: 10 additions & 2 deletions lib/AuthStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { AuthState, AuthStateLogOptions } from './types';
import { OktaAuth } from '.';
import { getConsole } from './util';
import { EVENT_ADDED, EVENT_REMOVED } from './TokenManager';
import PromiseQueue from './PromiseQueue';

export const INITIAL_AUTH_STATE = null;
const DEFAULT_PENDING = {
Expand All @@ -40,6 +41,7 @@ const isSameAuthState = (prevState: AuthState | null, state: AuthState) => {
&& prevState.error === state.error;
};


export class AuthStateManager {
_sdk: OktaAuth;
_pending: {
Expand All @@ -49,6 +51,7 @@ export class AuthStateManager {
_authState: AuthState | null;
_prevAuthState: AuthState | null;
_logOptions: AuthStateLogOptions;
_transformQueue: PromiseQueue;

constructor(sdk: OktaAuth) {
if (!sdk.emitter) {
Expand All @@ -60,7 +63,10 @@ export class AuthStateManager {
this._authState = INITIAL_AUTH_STATE;
this._logOptions = {};
this._prevAuthState = null;

this._transformQueue = new PromiseQueue({
quiet: true
});

// Listen on tokenManager events to start updateState process
// "added" event is emitted in both add and renew process
// Only listen on "added" event to update auth state
Expand Down Expand Up @@ -169,8 +175,10 @@ export class AuthStateManager {
refreshToken,
isAuthenticated: !!(accessToken && idToken)
};

// Enqueue transformAuthState so that it does not run concurrently
const promise: Promise<AuthState> = transformAuthState
? transformAuthState(this._sdk, authState)
? this._transformQueue.push(transformAuthState, null, this._sdk, authState) as Promise<AuthState>
: Promise.resolve(authState);

promise
Expand Down
21 changes: 15 additions & 6 deletions lib/PromiseQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,34 @@ interface QueueItem {
reject: (reason?: unknown) => void;
}

interface PromiseQueueOptions {
quiet?: boolean; // if false, concurrrency warnings will not be logged
}
class PromiseQueue {
queue: QueueItem[];
running: boolean;
options: PromiseQueueOptions;

constructor() {
constructor(options: PromiseQueueOptions = { quiet: false }) {
this.queue = [];
this.running = false;
this.options = options;
}

// Returns a promise
// If the method is synchronous, it will resolve when the method completes
// If the method returns a promise, it will resolve (or reject) with the value from the method's promise
push(method: () => void, thisObject: object, ...args: any[]) {
push(method: (...args: any) => any, thisObject: any, ...args: any[]) {
return new Promise((resolve, reject) => {
if (this.queue.length > 0) {
warn(
'Async method is being called but another async method is already running. ' +
'The new method will be delayed until the previous method completes.'
);
// There is at least one other pending call.
// The PromiseQueue will prevent these methods from running concurrently.
if (this.options.quiet !== false) {
warn(
'Async method is being called but another async method is already running. ' +
'The new method will be delayed until the previous method completes.'
);
}
}
this.queue.push({
method,
Expand Down
11 changes: 6 additions & 5 deletions lib/TransactionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ import {
OAuthTransactionMeta,
TransactionMetaOptions,
TransactionManagerOptions,
CookieStorage
CookieStorage,
SavedIdxResponse
} from './types';
import { RawIdxResponse, isRawIdxResponse } from './idx/types/idx-js';
import { isRawIdxResponse } from './idx/types/idx-js';
import { warn } from './util';
import {
clearTransactionFromSharedStorage,
Expand Down Expand Up @@ -306,18 +307,18 @@ export default class TransactionManager {
// throw new AuthSdkError('Unable to parse the ' + REDIRECT_OAUTH_PARAMS_NAME + ' value from storage');
}

saveIdxResponse(idxResponse: RawIdxResponse): void {
saveIdxResponse({ rawIdxResponse, requestDidSucceed }: SavedIdxResponse): void {
if (!this.saveLastResponse) {
return;
}
const storage = this.storageManager.getIdxResponseStorage();
if (!storage) {
return;
}
storage.setStorage(idxResponse);
storage.setStorage({ rawIdxResponse, requestDidSucceed });
}

loadIdxResponse(): RawIdxResponse | null {
loadIdxResponse(): SavedIdxResponse | null {
if (!this.saveLastResponse) {
return null;
}
Expand Down
38 changes: 19 additions & 19 deletions lib/browser/browserStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ var storageUtil: BrowserStorageUtil = {
// https://connect.microsoft.com/IE/Feedback/Details/1496040
browserHasLocalStorage: function() {
try {
var storage = storageUtil.getLocalStorage();
return storageUtil.testStorage(storage);
var storage = this.getLocalStorage();
return this.testStorage(storage);
} catch (e) {
return false;
}
},

browserHasSessionStorage: function() {
try {
var storage = storageUtil.getSessionStorage();
return storageUtil.testStorage(storage);
var storage = this.getSessionStorage();
return this.testStorage(storage);
} catch (e) {
return false;
}
Expand All @@ -62,10 +62,10 @@ var storageUtil: BrowserStorageUtil = {
var supported = false;
switch (storageType) {
case 'sessionStorage':
supported = storageUtil.browserHasSessionStorage();
supported = this.browserHasSessionStorage();
break;
case 'localStorage':
supported = storageUtil.browserHasLocalStorage();
supported = this.browserHasLocalStorage();
break;
case 'cookie':
case 'memory':
Expand All @@ -82,16 +82,16 @@ var storageUtil: BrowserStorageUtil = {
let storageProvider;
switch (storageType) {
case 'sessionStorage':
storageProvider = storageUtil.getSessionStorage();
storageProvider = this.getSessionStorage();
break;
case 'localStorage':
storageProvider = storageUtil.getLocalStorage();
storageProvider = this.getLocalStorage();
break;
case 'cookie':
storageProvider = storageUtil.getCookieStorage(options);
storageProvider = this.getCookieStorage(options);
break;
case 'memory':
storageProvider = storageUtil.getInMemoryStorage();
storageProvider = this.getInMemoryStorage();
break;
default:
throw new AuthSdkError(`Unrecognized storage option: ${storageType}`);
Expand All @@ -111,15 +111,15 @@ var storageUtil: BrowserStorageUtil = {
return curType;
}

if (storageUtil.testStorageType(curType)) {
if (this.testStorageType(curType)) {
return curType;
}

// preferred type was unsupported.
warn(`This browser doesn't support ${curType}. Switching to ${nextType}.`);

// fallback to the next type. this is a recursive call
return storageUtil.findStorageType(types);
return this.findStorageType(types);
},

getLocalStorage: function() {
Expand All @@ -139,17 +139,17 @@ var storageUtil: BrowserStorageUtil = {
throw new AuthSdkError('getCookieStorage: "secure" and "sameSite" options must be provided');
}
const storage: CookieStorage = {
getItem: storageUtil.storage.get,
setItem: function(key, value, expiresAt = '2200-01-01T00:00:00.000Z') {
getItem: this.storage.get,
setItem: (key, value, expiresAt = '2200-01-01T00:00:00.000Z') => {
// By defauilt, cookie shouldn't expire
expiresAt = (sessionCookie ? null : expiresAt) as string;
storageUtil.storage.set(key, value, expiresAt, {
this.storage.set(key, value, expiresAt, {
secure: secure,
sameSite: sameSite,
});
},
removeItem: function(key) {
storageUtil.storage.delete(key);
removeItem: (key) => {
this.storage.delete(key);
}
};

Expand Down Expand Up @@ -196,7 +196,7 @@ var storageUtil: BrowserStorageUtil = {
},

// Provides an in-memory solution
inMemoryStore: {},
inMemoryStore: {}, // override this for a unique memory store per instance
getInMemoryStorage: function() {
return {
getItem: (key) => {
Expand Down Expand Up @@ -241,7 +241,7 @@ var storageUtil: BrowserStorageUtil = {
}

Cookies.set(name, value, cookieOptions);
return storageUtil.storage.get(name);
return this.get(name);
},

get: function(name?: string): string {
Expand Down
2 changes: 1 addition & 1 deletion lib/idx/authenticator/Authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export abstract class Authenticator<Values> {

abstract canVerify(values: Values): boolean;

abstract mapCredentials(values: Values): Credentials;
abstract mapCredentials(values: Values): Credentials | undefined;

abstract getInputs(idxRemediationValue: IdxRemediationValue): any; // TODO: add type
}
11 changes: 8 additions & 3 deletions lib/idx/authenticator/OktaPassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ import { Authenticator, Credentials } from './Authenticator';

export interface OktaPasswordInputValues {
password?: string;
credentials?: Credentials;
}

export class OktaPassword extends Authenticator<OktaPasswordInputValues> {
canVerify(values: OktaPasswordInputValues) {
return !!values.password;
return !!(values.credentials || values.password);
}

mapCredentials(values: OktaPasswordInputValues): Credentials {
return { passcode: values.password };
mapCredentials(values: OktaPasswordInputValues): Credentials | undefined {
const { credentials, password } = values;
if (!credentials && !password) {
return;
}
return credentials || { passcode: password };
}

getInputs(idxRemediationValue) {
Expand Down
8 changes: 6 additions & 2 deletions lib/idx/authenticator/OktaVerifyTotp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ interface TotpCredentials extends Credentials {
}

export class OktaVerifyTotp extends VerificationCodeAuthenticator {
mapCredentials(values): TotpCredentials {
return { totp: values.verificationCode };
mapCredentials(values): TotpCredentials | undefined {
const { verificationCode } = values;
if (!verificationCode) {
return;
}
return { totp: verificationCode };
}
}
10 changes: 9 additions & 1 deletion lib/idx/authenticator/SecurityQuestionEnrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ export interface SecurityQuestionEnrollValues {
questionKey?: string;
question?: string;
answer?: string;
credentials?: Credentials;
}

export class SecurityQuestionEnrollment extends Authenticator<SecurityQuestionEnrollValues> {
canVerify(values: SecurityQuestionEnrollValues) {
const { credentials } = values;
if (credentials && credentials.questionKey && credentials.answer) {
return true;
}
const { questionKey, question, answer } = values;
return !!(questionKey && answer) || !!(question && answer);
}

mapCredentials(values: SecurityQuestionEnrollValues): Credentials {
mapCredentials(values: SecurityQuestionEnrollValues): Credentials | undefined {
const { questionKey, question, answer } = values;
if (!questionKey && !question && !answer) {
return;
}
return {
questionKey: question ? 'custom' : questionKey,
question,
Expand Down
8 changes: 6 additions & 2 deletions lib/idx/authenticator/SecurityQuestionVerification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ export class SecurityQuestionVerification extends Authenticator<SecurityQuestion
return !!values.answer;
}

mapCredentials(values: SecurityQuestionVerificationValues): Credentials {
mapCredentials(values: SecurityQuestionVerificationValues): Credentials | undefined {
const { answer } = values;
if (!answer) {
return;
}
return {
questionKey: this.meta.contextualData!.enrolledQuestion!.questionKey,
answer: values.answer
answer
};
}

Expand Down
Loading

0 comments on commit 71c3352

Please sign in to comment.