@okta-dfuhriman/okta-auth-js
Version:
The Okta Auth SDK
224 lines (195 loc) • 7.07 kB
text/typescript
/*!
* 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-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// Do not use this type in code, so it won't be emitted in the declaration output
import PCancelable from 'p-cancelable';
import { AuthSdkError } from '../errors';
import {
EVENT_ADDED,
EVENT_REMOVED,
OAuthStorageManagerInterface,
OAuthTransactionMeta,
OktaAuthOAuthInterface
} from '../oidc';
import {
AuthState,
AuthStateLogOptions,
OktaAuthCoreOptions,
} from './types';
import { PromiseQueue, getConsole } from '../util';
export const INITIAL_AUTH_STATE = null;
const DEFAULT_PENDING = {
updateAuthStatePromise: null,
canceledTimes: 0
};
const EVENT_AUTH_STATE_CHANGE = 'authStateChange';
const MAX_PROMISE_CANCEL_TIMES = 10;
// only compare first level of authState
const isSameAuthState = (prevState: AuthState | null, state: AuthState) => {
// initial state is null
if (!prevState) {
return false;
}
return prevState.isAuthenticated === state.isAuthenticated
&& JSON.stringify(prevState.idToken) === JSON.stringify(state.idToken)
&& JSON.stringify(prevState.accessToken) === JSON.stringify(state.accessToken)
&& prevState.error === state.error;
};
export class AuthStateManager
<
M extends OAuthTransactionMeta,
S extends OAuthStorageManagerInterface<M>,
O extends OktaAuthCoreOptions
>
{
_sdk: OktaAuthOAuthInterface<M, S, O>;
_pending: {
updateAuthStatePromise: any;
canceledTimes: number;
};
_authState: AuthState | null;
_prevAuthState: AuthState | null;
_logOptions: AuthStateLogOptions;
_transformQueue: PromiseQueue;
constructor(sdk: OktaAuthOAuthInterface<M, S, O>) {
if (!sdk.emitter) {
throw new AuthSdkError('Emitter should be initialized before AuthStateManager');
}
this._sdk = sdk;
this._pending = { ...DEFAULT_PENDING };
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
sdk.tokenManager.on(EVENT_ADDED, (key, token) => {
this._setLogOptions({ event: EVENT_ADDED, key, token });
this.updateAuthState();
});
sdk.tokenManager.on(EVENT_REMOVED, (key, token) => {
this._setLogOptions({ event: EVENT_REMOVED, key, token });
this.updateAuthState();
});
}
_setLogOptions(options) {
this._logOptions = options;
}
getAuthState(): AuthState | null {
return this._authState;
}
getPreviousAuthState(): AuthState | null {
return this._prevAuthState;
}
async updateAuthState(): Promise<AuthState> {
const { transformAuthState, devMode } = this._sdk.options;
const log = (status) => {
const { event, key, token } = this._logOptions;
getConsole().group(`OKTA-AUTH-JS:updateAuthState: Event:${event} Status:${status}`);
getConsole().log(key, token);
getConsole().log('Current authState', this._authState);
getConsole().groupEnd();
// clear log options after logging
this._logOptions = {};
};
const emitAuthStateChange = (authState) => {
if (isSameAuthState(this._authState, authState)) {
devMode && log('unchanged');
return;
}
this._prevAuthState = this._authState;
this._authState = authState;
// emit new authState object
this._sdk.emitter.emit(EVENT_AUTH_STATE_CHANGE, { ...authState });
devMode && log('emitted');
};
const finalPromise = (origPromise) => {
return this._pending.updateAuthStatePromise.then(() => {
const curPromise = this._pending.updateAuthStatePromise;
if (curPromise && curPromise !== origPromise) {
return finalPromise(curPromise);
}
return this.getAuthState();
});
};
if (this._pending.updateAuthStatePromise) {
if (this._pending.canceledTimes >= MAX_PROMISE_CANCEL_TIMES) {
// stop canceling then starting a new promise
// let existing promise finish to prevent running into loops
devMode && log('terminated');
return finalPromise(this._pending.updateAuthStatePromise);
} else {
this._pending.updateAuthStatePromise.cancel();
}
}
/* eslint-disable complexity */
const cancelablePromise = new PCancelable((resolve, _, onCancel) => {
onCancel.shouldReject = false;
onCancel(() => {
this._pending.updateAuthStatePromise = null;
this._pending.canceledTimes = this._pending.canceledTimes + 1;
devMode && log('canceled');
});
const emitAndResolve = (authState) => {
if (cancelablePromise.isCanceled) {
resolve();
return;
}
// emit event and resolve promise
emitAuthStateChange(authState);
resolve();
// clear pending states after resolve
this._pending = { ...DEFAULT_PENDING };
};
this._sdk.isAuthenticated()
.then(() => {
if (cancelablePromise.isCanceled) {
resolve();
return;
}
const { accessToken, idToken, refreshToken } = this._sdk.tokenManager.getTokensSync();
const authState = {
accessToken,
idToken,
refreshToken,
isAuthenticated: !!(accessToken && idToken)
};
// Enqueue transformAuthState so that it does not run concurrently
const promise: Promise<AuthState> = transformAuthState
? this._transformQueue.push(transformAuthState, null, this._sdk, authState) as Promise<AuthState>
: Promise.resolve(authState);
promise
.then(authState => emitAndResolve(authState))
.catch(error => emitAndResolve({
accessToken,
idToken,
refreshToken,
isAuthenticated: false,
error
}));
});
});
/* eslint-enable complexity */
this._pending.updateAuthStatePromise = cancelablePromise;
return finalPromise(cancelablePromise);
}
subscribe(handler): void {
this._sdk.emitter.on(EVENT_AUTH_STATE_CHANGE, handler);
}
unsubscribe(handler?): void {
this._sdk.emitter.off(EVENT_AUTH_STATE_CHANGE, handler);
}
}