Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SyncStorageService using broadcast-channel #1197

Closed
wants to merge 35 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
419224d
Added LeaderElectionService
denysoblohin-okta Apr 24, 2022
d795662
broadcastChannelName -> electionChannelName
denysoblohin-okta Apr 24, 2022
20c5d10
New impl of SyncStorageService
denysoblohin-okta Apr 25, 2022
0f461e8
fix message loop
denysoblohin-okta Apr 26, 2022
5991101
allow syncStorage for localStorage and cookie
denysoblohin-okta Apr 26, 2022
bd6ac2d
.
denysoblohin-okta Apr 26, 2022
e890015
fixes
denysoblohin-okta Apr 27, 2022
ca08809
fix
denysoblohin-okta Apr 27, 2022
095faf4
support IE11
denysoblohin-okta May 2, 2022
714ed25
IE11 workaround
denysoblohin-okta May 2, 2022
a4adcfe
Use event set_storage for IE11
denysoblohin-okta May 3, 2022
5850351
Added polyfills for IE11
denysoblohin-okta May 3, 2022
3453ebb
lint fix
denysoblohin-okta May 3, 2022
85138a4
fix Channel is closed
denysoblohin-okta May 3, 2022
e442b2c
added tests LES
denysoblohin-okta May 4, 2022
34acc6f
added test for starting leaderElection service
denysoblohin-okta May 4, 2022
246b2af
added tests for services
denysoblohin-okta May 4, 2022
34e6ff4
added test
denysoblohin-okta May 5, 2022
f9294ee
added tests
denysoblohin-okta May 5, 2022
b156e62
added SM tests
denysoblohin-okta May 5, 2022
353ba5e
fix: post set_storage event when storage is cleared on logout
denysoblohin-okta May 5, 2022
3949afb
fix: SM should be started before TM
denysoblohin-okta May 6, 2022
0cf1982
fix test 'can use memory token storage' (OKTA-464122)
denysoblohin-okta May 9, 2022
f8ecc6e
Support broadcastChannelName as old name for electionChannelName (OKT…
denysoblohin-okta May 10, 2022
afa1461
adress comments
denysoblohin-okta May 23, 2022
19de428
Overload token manager on/off
denysoblohin-okta May 24, 2022
c336d3f
fix
denysoblohin-okta May 24, 2022
4e4a02f
fix
denysoblohin-okta May 24, 2022
97eb8d9
async fix after rebase
denysoblohin-okta Jun 1, 2022
049f7ed
fix clearPendingRemoveTokens
denysoblohin-okta Jun 2, 2022
32faade
test
denysoblohin-okta Jun 2, 2022
fce8c44
fix
denysoblohin-okta Jun 2, 2022
35be52a
chlog
denysoblohin-okta Jun 2, 2022
45fc202
await updateAuthState in start
denysoblohin-okta Jun 2, 2022
8897c98
fix test memory storage
denysoblohin-okta Jun 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Use event set_storage for IE11
denysoblohin-okta committed Jun 2, 2022
commit a4adcfef8420e531a7edc24137ac29445933738c
29 changes: 23 additions & 6 deletions lib/TokenManager.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
import { removeNils, clone } from './util';
import { AuthSdkError } from './errors';
import { validateToken } from './oidc/util';
import { isLocalhost } from './features';
import { isLocalhost, isIE11OrLess } from './features';
import SdkClock from './clock';
import {
EventEmitter,
@@ -28,8 +28,7 @@ import {
StorageType,
OktaAuthInterface,
StorageProvider,
TokenManagerErrorEventHandler,
TokenManagerEventHandler,
TokenManagerAnyEventHandler,
TokenManagerInterface,
RefreshToken,
AccessTokenCallback,
@@ -54,6 +53,7 @@ export const EVENT_RENEWED = 'renewed';
export const EVENT_ADDED = 'added';
export const EVENT_REMOVED = 'removed';
export const EVENT_ERROR = 'error';
export const EVENT_SET_STORAGE = 'set_storage';

interface TokenManagerState {
expireTimeouts: Record<string, unknown>;
@@ -73,8 +73,8 @@ export class TokenManager implements TokenManagerInterface {
private state: TokenManagerState;
private options: TokenManagerOptions;

on: (event: string, handler: TokenManagerErrorEventHandler | TokenManagerEventHandler, context?: object) => void;
off: (event: string, handler?: TokenManagerErrorEventHandler | TokenManagerEventHandler) => void;
on: (event: string, handler: TokenManagerAnyEventHandler, context?: object) => void;
off: (event: string, handler?: TokenManagerAnyEventHandler) => void;

// eslint-disable-next-line complexity
constructor(sdk: OktaAuthInterface, options: TokenManagerOptions = {}) {
@@ -219,6 +219,7 @@ export class TokenManager implements TokenManagerInterface {
validateToken(token);
tokenStorage[key] = token;
this.storage.setStorage(tokenStorage);
this.emitSetStorageEvent();
this.emitAdded(key, token);
this.setExpireEventTimeout(key, token);
}
@@ -276,6 +277,19 @@ export class TokenManager implements TokenManagerInterface {
throw new AuthSdkError('Unknown token type');
}

// for synchronization of LocalStorage cross tabs for IE11
private emitSetStorageEvent() {
if (isIE11OrLess()) {
const storage = this.storage.getStorage();
this.emitter.emit(EVENT_SET_STORAGE, storage);
}
}

// used in `SyncStorageService` for synchronization of LocalStorage cross tabs for IE11
public getStorage() {
return this.storage;
}

setTokens(
tokens: Tokens,
// TODO: callbacks can be removed in the next major version OKTA-407224
@@ -331,7 +345,8 @@ export class TokenManager implements TokenManagerInterface {
return storage;
}, {});
this.storage.setStorage(storage);

this.emitSetStorageEvent();

// emit event and start expiration timer
types.forEach(type => {
const newToken = tokens[type];
@@ -358,6 +373,7 @@ export class TokenManager implements TokenManagerInterface {
var removedToken = tokenStorage[key];
delete tokenStorage[key];
this.storage.setStorage(tokenStorage);
this.emitSetStorageEvent();

this.emitRemoved(key, removedToken);
}
@@ -437,6 +453,7 @@ export class TokenManager implements TokenManagerInterface {
validateToken(token);
tokenStorage[key] = token;
this.storage.setStorage(tokenStorage);
this.emitSetStorageEvent();
}

removeRefreshToken () {
43 changes: 25 additions & 18 deletions lib/services/SyncStorageService.ts
Original file line number Diff line number Diff line change
@@ -10,16 +10,17 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import { TokenManager, EVENT_ADDED, EVENT_REMOVED, EVENT_RENEWED } from '../TokenManager';
import { TokenManager, EVENT_ADDED, EVENT_REMOVED, EVENT_RENEWED, EVENT_SET_STORAGE } from '../TokenManager';
import { BroadcastChannel } from 'broadcast-channel';
import { isBrowser, isIE11OrLess } from '../features';
import { ServiceManagerOptions, ServiceInterface, Token } from '../types';
import { isBrowser } from '../features';
import { ServiceManagerOptions, ServiceInterface, Token, Tokens } from '../types';

export type SyncMessage = {
type: string;
key: string;
token: Token;
key?: string;
token?: Token;
oldToken?: Token;
storage?: Tokens;
};
export class SyncStorageService implements ServiceInterface {
private tokenManager: TokenManager;
@@ -34,6 +35,7 @@ export class SyncStorageService implements ServiceInterface {
this.onTokenAddedHandler = this.onTokenAddedHandler.bind(this);
this.onTokenRemovedHandler = this.onTokenRemovedHandler.bind(this);
this.onTokenRenewedHandler = this.onTokenRenewedHandler.bind(this);
this.onSetStorageHandler = this.onSetStorageHandler.bind(this);
this.onSyncMessageHandler = this.onSyncMessageHandler.bind(this);
}

@@ -57,6 +59,7 @@ export class SyncStorageService implements ServiceInterface {
this.tokenManager.on(EVENT_ADDED, this.onTokenAddedHandler);
this.tokenManager.on(EVENT_REMOVED, this.onTokenRemovedHandler);
this.tokenManager.on(EVENT_RENEWED, this.onTokenRenewedHandler);
this.tokenManager.on(EVENT_SET_STORAGE, this.onSetStorageHandler);
this.channel.addEventListener('message', this.onSyncMessageHandler);
this.started = true;
}
@@ -67,6 +70,7 @@ export class SyncStorageService implements ServiceInterface {
this.tokenManager.off(EVENT_ADDED, this.onTokenAddedHandler);
this.tokenManager.off(EVENT_REMOVED, this.onTokenRemovedHandler);
this.tokenManager.off(EVENT_RENEWED, this.onTokenRenewedHandler);
this.tokenManager.off(EVENT_SET_STORAGE, this.onSetStorageHandler);
this.channel?.removeEventListener('message', this.onSyncMessageHandler);
this.channel?.close();
this.channel = undefined;
@@ -108,32 +112,35 @@ export class SyncStorageService implements ServiceInterface {
});
}

private onSetStorageHandler(storage: Tokens) {
this.channel?.postMessage({
type: EVENT_SET_STORAGE,
storage
});
}

/* eslint-disable complexity */
private onSyncMessageHandler(msg: SyncMessage) {
// Notes:
// 1. Using `enablePostMessage` flag here to prevent sync message loop.
// If this flag is on, tokenManager event handlers do not post sync message.
// 2. IE11 has known issue with synchronization of LocalStorage cross tabs.
// One workaround is to set empty event handler for `window.onstorage`.
// But it's not 100% working, sometimes you still get old value from LocalStorage.
// Better approch is to explicitly udpate LocalStorage with tokenManager's add/remove.
// Better approch is to explicitly udpate LocalStorage with `setStorage`.

this.enablePostMessage = false;
switch (msg.type) {
case EVENT_SET_STORAGE:
this.tokenManager.getStorage().setStorage(msg.storage);
break;
case EVENT_ADDED:
if (!isIE11OrLess()) {
this.tokenManager.emitAdded(msg.key, msg.token);
this.tokenManager.setExpireEventTimeout(msg.key, msg.token);
} else {
this.tokenManager.add(msg.key, msg.token);
}
this.tokenManager.emitAdded(msg.key, msg.token);
this.tokenManager.setExpireEventTimeout(msg.key, msg.token);
break;
case EVENT_REMOVED:
if (!isIE11OrLess()) {
this.tokenManager.clearExpireEventTimeout(msg.key);
this.tokenManager.emitRemoved(msg.key, msg.token);
} else {
this.tokenManager.remove(msg.key);
}
this.tokenManager.clearExpireEventTimeout(msg.key);
this.tokenManager.emitRemoved(msg.key, msg.token);
break;
case EVENT_RENEWED:
this.tokenManager.emitRenewed(msg.key, msg.token, msg.oldToken);
7 changes: 4 additions & 3 deletions lib/types/TokenManager.ts
Original file line number Diff line number Diff line change
@@ -11,16 +11,17 @@ export interface TokenManagerError {

export declare type TokenManagerErrorEventHandler = (error: TokenManagerError) => void;
export declare type TokenManagerEventHandler = (key: string, token: Token, oldtoken?: Token) => void;

export declare type TokenManagerSetStorageEventHandler = (storage: Tokens) => void;
export declare type TokenManagerAnyEventHandler = TokenManagerEventHandler | TokenManagerErrorEventHandler | TokenManagerSetStorageEventHandler;

export declare type AccessTokenCallback = (key: string, token: AccessToken) => void;
export declare type IDTokenCallback = (key: string, token: IDToken) => void;
export declare type RefreshTokenCallback = (key: string, token: RefreshToken) => void;

// only add methods needed internally
export interface TokenManagerInterface {
on: (event: string, handler: TokenManagerErrorEventHandler | TokenManagerEventHandler, context?: object) => void;
off: (event: string, handler?: TokenManagerErrorEventHandler | TokenManagerEventHandler) => void;
on: (event: string, handler: TokenManagerAnyEventHandler, context?: object) => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we could handle this better with function overloads, like we are doing on SIW: https://github.com/okta/okta-signin-widget/blob/okta-signin-widget-6.3.3/src/types/api.ts#L10
Then it will be clear which type of event has which type of callback. As is, I'm not sure as a developer how to implement my handler function - it seems to just have one of 3 shapes, so I have to be prepared for any of the variations. Would be better I think to know which type of callback I should implement for a specific event.

off: (event: string, handler?: TokenManagerAnyEventHandler) => void;
getTokensSync(): Tokens;
setTokens({ accessToken, idToken, refreshToken }: Tokens, accessTokenCb?: AccessTokenCallback, idTokenCb?: IDTokenCallback, refreshTokenCb?: RefreshTokenCallback): void;
getStorageKeyByType(type: TokenType): string;