Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
  • Loading branch information
denysoblohin-okta committed Apr 25, 2022
1 parent 4770c86 commit 635d025
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 279 deletions.
1 change: 1 addition & 0 deletions jest.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const config = Object.assign({}, baseConfig, {
'oidc/renewTokens.ts',
'TokenManager/browser',
'SyncStorageService',
'LeaderElectionService',
'ServiceManager'
])
});
Expand Down
24 changes: 16 additions & 8 deletions lib/ServiceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ import {
import { OktaAuth } from '.';
import { AutoRenewService, SyncStorageService, LeaderElectionService } from './services';

const AUTO_RENEW = 'autoRenew';
const SYNC_STORAGE = 'syncStorage';
const LEADER_ELECTION = 'leaderElection';

export class ServiceManager implements ServiceManagerInterface {
private sdk: OktaAuth;
private options: ServiceManagerOptions;
private services: Map<string, ServiceInterface>;
private started: boolean;

private static knownServices = ['autoRenew', 'syncStorage', 'leaderElection'];
private static knownServices = [AUTO_RENEW, SYNC_STORAGE, LEADER_ELECTION];

private static defaultOptions = {
autoRenew: true,
Expand All @@ -41,7 +45,11 @@ export class ServiceManager implements ServiceManagerInterface {
const { autoRenew, autoRemove, syncStorage } = sdk.tokenManager.getOptions();
this.options = Object.assign({},
ServiceManager.defaultOptions,
{ autoRenew, autoRemove, syncStorage, electionChannelName: sdk.options.clientId },
{ autoRenew, autoRemove, syncStorage },
{
electionChannelName: `${sdk.options.clientId}-election`,
syncChannelName: `${sdk.options.clientId}-sync`,
},
options
);

Expand All @@ -64,11 +72,11 @@ export class ServiceManager implements ServiceManagerInterface {
}

isLeader() {
return (this.getService('leaderElection') as LeaderElectionService)?.isLeader();
return (this.getService(LEADER_ELECTION) as LeaderElectionService)?.isLeader();
}

isLeaderRequired() {
return [...this.services.values()].some(srv => srv.requiresLeadership());
return [...this.services.values()].some(srv => srv.canStart() && srv.requiresLeadership());
}

start() {
Expand Down Expand Up @@ -106,7 +114,7 @@ export class ServiceManager implements ServiceManagerInterface {
private canStartService(name: string, srv: ServiceInterface): boolean {
let canStart = srv.canStart() && !srv.isStarted();
// only start election if a leader is required
if (name == 'leaderElection') {
if (name === LEADER_ELECTION) {
canStart &&= this.isLeaderRequired();
} else if (srv.requiresLeadership()) {
canStart &&= this.isLeader();
Expand All @@ -119,13 +127,13 @@ export class ServiceManager implements ServiceManagerInterface {

let service: ServiceInterface | undefined;
switch (name) {
case 'leaderElection':
case LEADER_ELECTION:
service = new LeaderElectionService({...this.options, onLeader: this.onLeader});
break;
case 'autoRenew':
case AUTO_RENEW:
service = new AutoRenewService(tokenManager, {...this.options});
break;
case 'syncStorage':
case SYNC_STORAGE:
service = new SyncStorageService(tokenManager, {...this.options});
break;
default:
Expand Down
37 changes: 2 additions & 35 deletions lib/TokenManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import { removeNils, clone } from './util';
import { AuthSdkError } from './errors';
import { validateToken } from './oidc/util';
import { isLocalhost, isIE11OrLess } from './features';
import { isLocalhost } from './features';
import SdkClock from './clock';
import {
EventEmitter,
Expand Down Expand Up @@ -47,8 +47,7 @@ const DEFAULT_OPTIONS = {
clearPendingRemoveTokens: true,
storage: undefined, // will use value from storageManager config
expireEarlySeconds: 30,
storageKey: TOKEN_STORAGE_NAME,
_storageEventDelay: 0
storageKey: TOKEN_STORAGE_NAME
};
export const EVENT_EXPIRED = 'expired';
export const EVENT_RENEWED = 'renewed';
Expand Down Expand Up @@ -86,9 +85,6 @@ export class TokenManager implements TokenManagerInterface {
}

options = Object.assign({}, DEFAULT_OPTIONS, removeNils(options));
if (isIE11OrLess()) {
options._storageEventDelay = options._storageEventDelay || 1000;
}
if (!isLocalhost()) {
options.expireEarlySeconds = DEFAULT_OPTIONS.expireEarlySeconds;
}
Expand Down Expand Up @@ -160,25 +156,6 @@ export class TokenManager implements TokenManagerInterface {
this.emitter.emit(EVENT_ERROR, error);
}

emitEventsForCrossTabsStorageUpdate(newValue, oldValue) {
const oldTokens = this.getTokensFromStorageValue(oldValue);
const newTokens = this.getTokensFromStorageValue(newValue);
Object.keys(newTokens).forEach(key => {
const oldToken = oldTokens[key];
const newToken = newTokens[key];
if (JSON.stringify(oldToken) !== JSON.stringify(newToken)) {
this.emitAdded(key, newToken);
}
});
Object.keys(oldTokens).forEach(key => {
const oldToken = oldTokens[key];
const newToken = newTokens[key];
if (!newToken) {
this.emitRemoved(key, oldToken);
}
});
}

clearExpireEventTimeout(key) {
clearTimeout(this.state.expireTimeouts[key] as any);
delete this.state.expireTimeouts[key];
Expand Down Expand Up @@ -447,16 +424,6 @@ export class TokenManager implements TokenManagerInterface {
}
});
}

getTokensFromStorageValue(value) {
let tokens;
try {
tokens = JSON.parse(value) || {};
} catch (e) {
tokens = {};
}
return tokens;
}

updateRefreshToken(token: RefreshToken) {
const key = this.getStorageKeyByType('refreshToken') || REFRESH_TOKEN_STORAGE_KEY;
Expand Down
22 changes: 9 additions & 13 deletions lib/services/LeaderElectionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ import {
} from 'broadcast-channel';
import { isBrowser } from '../features';

type OnLeaderHandler = (() => void);
type Options = ServiceManagerOptions & {
declare type OnLeaderHandler = (() => void);
declare type ServiceOptions = ServiceManagerOptions & {
onLeader?: OnLeaderHandler;
};

export class LeaderElectionService implements ServiceInterface {
private options: Options;
private options: ServiceOptions;
private channel?: BroadcastChannel;
private elector?: LeaderElector;
private started = false;

constructor(options: Options = {}) {
constructor(options: ServiceOptions = {}) {
this.options = options;
this.onLeaderDuplicate = this.onLeaderDuplicate.bind(this);
this.onLeader = this.onLeader.bind(this);
Expand All @@ -54,15 +54,11 @@ export class LeaderElectionService implements ServiceInterface {
start() {
this.stop();
if (this.canStart()) {
if (!this.channel) {
const { electionChannelName } = this.options;
this.channel = new BroadcastChannel(electionChannelName as string);
}
if (!this.elector) {
this.elector = createLeaderElection(this.channel);
this.elector.onduplicate = this.onLeaderDuplicate;
this.elector.awaitLeadership().then(this.onLeader);
}
const { electionChannelName } = this.options;
this.channel = new BroadcastChannel(electionChannelName as string);
this.elector = createLeaderElection(this.channel);
this.elector.onduplicate = this.onLeaderDuplicate;
this.elector.awaitLeadership().then(this.onLeader);
this.started = true;
}
}
Expand Down
106 changes: 70 additions & 36 deletions lib/services/SyncStorageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,30 @@
* See the License for the specific language governing permissions and limitations under the License.
*/


/* global window */
import { TokenManager } from '../TokenManager';
import { TokenManager, EVENT_ADDED, EVENT_REMOVED, EVENT_RENEWED } from '../TokenManager';
import { BroadcastChannel } from 'broadcast-channel';
import { isBrowser } from '../features';
import { ServiceManagerOptions, ServiceInterface } from '../types';

import { ServiceManagerOptions, ServiceInterface, Token } from '../types';

export type SyncMessage = {
type: string;
key: string;
token: Token;
oldToken?: Token;
};
export class SyncStorageService implements ServiceInterface {
private tokenManager: TokenManager;
private options: ServiceManagerOptions;
private syncTimeout: unknown;
private channel?: BroadcastChannel<SyncMessage>;
private started = false;

constructor(tokenManager: TokenManager, options: ServiceManagerOptions = {}) {
this.tokenManager = tokenManager;
this.options = options;
this.storageListener = this.storageListener.bind(this);
}

// Sync authState cross multiple tabs when localStorage is used as the storageProvider
// A StorageEvent is sent to a window when a storage area it has access to is changed
// within the context of another document.
// https://developer.mozilla.org/en-US/docs/Web/API/StorageEvent
private storageListener({ key, newValue, oldValue }: StorageEvent) {
const opts = this.tokenManager.getOptions();

const handleCrossTabsStorageChange = () => {
this.tokenManager.resetExpireEventTimeoutAll();
this.tokenManager.emitEventsForCrossTabsStorageUpdate(newValue, oldValue);
};

// Skip if:
// not from localStorage.clear (event.key is null)
// event.key is not the storageKey
// oldValue === newValue
if (key && (key !== opts.storageKey || newValue === oldValue)) {
return;
}

// LocalStorage cross tabs update is not synced in IE, set a 1s timer by default to read latest value
// https://stackoverflow.com/questions/24077117/localstorage-in-win8-1-ie11-does-not-synchronize
this.syncTimeout = setTimeout(() => handleCrossTabsStorageChange(), opts._storageEventDelay);
this.onTokenAddedHandler = this.onTokenAddedHandler.bind(this);
this.onTokenRemovedHandler = this.onTokenRemovedHandler.bind(this);
this.onTokenRenewedHandler = this.onTokenRenewedHandler.bind(this);
this.onSyncMessageHandler = this.onSyncMessageHandler.bind(this);
}

requiresLeadership() {
Expand All @@ -69,16 +51,68 @@ export class SyncStorageService implements ServiceInterface {
start() {
if (this.canStart()) {
this.stop();
window.addEventListener('storage', this.storageListener);
const { syncChannelName } = this.options;
this.channel = new BroadcastChannel(syncChannelName as string);
this.tokenManager.on(EVENT_ADDED, this.onTokenAddedHandler);
this.tokenManager.on(EVENT_REMOVED, this.onTokenRemovedHandler);
this.tokenManager.on(EVENT_RENEWED, this.onTokenRenewedHandler);
this.channel.addEventListener('message', this.onSyncMessageHandler);
this.started = true;
}
}

private onTokenAddedHandler(key: string, token: Token) {
this.channel?.postMessage({
type: EVENT_ADDED,
key,
token
});
}

private onTokenRemovedHandler(key: string, token: Token) {
this.channel?.postMessage({
type: EVENT_REMOVED,
key,
token
});
}

private onTokenRenewedHandler(key: string, token: Token, oldToken?: Token) {
this.channel?.postMessage({
type: EVENT_RENEWED,
key,
token,
oldToken
});
}

private onSyncMessageHandler(msg: SyncMessage) {
switch (msg.type) {
case EVENT_ADDED:
this.tokenManager.emitAdded(msg.key, msg.token);
this.tokenManager.setExpireEventTimeout(msg.key, msg.token);
break;
case EVENT_REMOVED:
this.tokenManager.clearExpireEventTimeout(msg.key);
this.tokenManager.emitRemoved(msg.key, msg.token);
break;
case EVENT_RENEWED:
this.tokenManager.clearExpireEventTimeout(msg.key);
this.tokenManager.emitRenewed(msg.key, msg.token, msg.oldToken);
this.tokenManager.setExpireEventTimeout(msg.key, msg.token);
break;
default:
throw new Error(`Unknown message type ${msg.type}`);
}
}

stop() {
if (this.started) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
window.removeEventListener('storage', this.storageListener!);
clearTimeout(this.syncTimeout as any);
this.tokenManager.off(EVENT_ADDED, this.onTokenAddedHandler);
this.tokenManager.off(EVENT_REMOVED, this.onTokenRemovedHandler);
this.channel?.removeEventListener('message', this.onSyncMessageHandler);
this.channel?.close();
this.channel = undefined;
this.started = false;
}
}
Expand Down
1 change: 0 additions & 1 deletion lib/types/OktaAuthOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export interface TokenManagerOptions {
storageKey?: string;
expireEarlySeconds?: number;
syncStorage?: boolean;
_storageEventDelay?: number;
}

export interface CustomUrls {
Expand Down
1 change: 1 addition & 0 deletions lib/types/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface AutoRenewServiceOptions {

export interface SyncStorageServiceOptions {
syncStorage?: boolean;
syncChannelName?: string;
}

export interface LeaderElectionServiceOptions {
Expand Down
Loading

0 comments on commit 635d025

Please sign in to comment.