Skip to content

Commit de72c25

Browse files
committed
feat(12820): implement crossTab auth events
1 parent 88113b6 commit de72c25

File tree

13 files changed

+355
-22
lines changed

13 files changed

+355
-22
lines changed

packages/auth/__tests__/providers/cognito/tokenProvider/tokenStore.test.ts

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { KeyValueStorageInterface } from '@aws-amplify/core';
5-
import { decodeJWT } from '@aws-amplify/core/internals/utils';
4+
import { Hub, KeyValueStorageInterface } from '@aws-amplify/core';
5+
import { AMPLIFY_SYMBOL, decodeJWT } from '@aws-amplify/core/internals/utils';
66

7-
import { DefaultTokenStore } from '../../../../src/providers/cognito/tokenProvider';
7+
import {
8+
AUTH_KEY_PREFIX,
9+
DefaultTokenStore,
10+
} from '../../../../src/providers/cognito/tokenProvider';
811

912
const userPoolId = 'us-west-1:0000523';
1013
const userPoolClientId = 'mockCognitoUserPoolsId';
@@ -21,6 +24,9 @@ jest.mock(
2124
TokenProviderErrorCode: 'mockErrorCode',
2225
}),
2326
);
27+
jest.mock('../../../../src/providers/cognito/apis/getCurrentUser', () => ({
28+
getCurrentUser: () => Promise.resolve({}),
29+
}));
2430

2531
const mockedDecodeJWT = jest.mocked(decodeJWT);
2632
mockedDecodeJWT.mockReturnValue({
@@ -72,6 +78,7 @@ const mockKeyValueStorage: jest.Mocked<KeyValueStorageInterface> = {
7278
getItem: jest.fn(),
7379
removeItem: jest.fn(),
7480
clear: jest.fn(),
81+
addListener: jest.fn(),
7582
};
7683

7784
describe('TokenStore', () => {
@@ -403,4 +410,83 @@ describe('TokenStore', () => {
403410
expect(finalTokens?.refreshToken).toBe(newMockAuthToken.refreshToken);
404411
});
405412
});
413+
414+
describe('setupNotify', () => {
415+
it('should setup a KeyValueStorageEvent listener', async () => {
416+
tokenStore.setupNotify();
417+
418+
const spy = jest.spyOn(keyValStorage, 'addListener');
419+
const hubSpy = jest.spyOn(Hub, 'dispatch');
420+
421+
expect(spy).toHaveBeenCalledWith(expect.any(Function));
422+
423+
const listener = spy.mock.calls[0][0];
424+
425+
// does nothing if key does not match
426+
await listener({
427+
key: 'foo.bar',
428+
oldValue: null,
429+
newValue: null,
430+
});
431+
432+
expect(hubSpy).not.toHaveBeenCalled();
433+
434+
// does nothing if both values are null
435+
await listener({
436+
key: `${AUTH_KEY_PREFIX}.someid.someotherId.refreshToken`,
437+
oldValue: null,
438+
newValue: null,
439+
});
440+
441+
expect(hubSpy).not.toHaveBeenCalled();
442+
443+
// dispatches signedIn on new value
444+
await listener({
445+
key: `${AUTH_KEY_PREFIX}.someid.someotherId.refreshToken`,
446+
newValue: '123',
447+
oldValue: null,
448+
});
449+
450+
expect(hubSpy).toHaveBeenCalledWith(
451+
'auth',
452+
{ event: 'signedIn', data: {} },
453+
'Auth',
454+
AMPLIFY_SYMBOL,
455+
true,
456+
);
457+
hubSpy.mockClear();
458+
459+
// dispatches signedOut on null newValue
460+
await listener({
461+
key: `${AUTH_KEY_PREFIX}.someid.someotherId.refreshToken`,
462+
newValue: null,
463+
oldValue: '123',
464+
});
465+
466+
expect(hubSpy).toHaveBeenCalledWith(
467+
'auth',
468+
{ event: 'signedOut' },
469+
'Auth',
470+
AMPLIFY_SYMBOL,
471+
true,
472+
);
473+
hubSpy.mockClear();
474+
475+
// dispatches tokenRefresh for changed value
476+
await listener({
477+
key: `${AUTH_KEY_PREFIX}.someid.someotherId.refreshToken`,
478+
newValue: '456',
479+
oldValue: '123',
480+
});
481+
482+
expect(hubSpy).toHaveBeenCalledWith(
483+
'auth',
484+
{ event: 'tokenRefresh' },
485+
'Auth',
486+
AMPLIFY_SYMBOL,
487+
true,
488+
);
489+
hubSpy.mockClear();
490+
});
491+
});
406492
});

packages/auth/src/providers/cognito/tokenProvider/CognitoUserPoolsTokenProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export class CognitoUserPoolsTokenProvider
2323
constructor() {
2424
this.authTokenStore = new DefaultTokenStore();
2525
this.authTokenStore.setKeyValueStorage(defaultStorage);
26+
this.authTokenStore.setupNotify();
2627
this.tokenOrchestrator = new TokenOrchestrator();
2728
this.tokenOrchestrator.setAuthTokenStore(this.authTokenStore);
2829
this.tokenOrchestrator.setTokenRefresher(refreshAuthTokens);

packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import { AuthConfig, KeyValueStorageInterface } from '@aws-amplify/core';
3+
import { AuthConfig, Hub, KeyValueStorageInterface } from '@aws-amplify/core';
44
import {
5+
AMPLIFY_SYMBOL,
56
assertTokenProviderConfig,
67
decodeJWT,
78
} from '@aws-amplify/core/internals/utils';
9+
import { KeyValueStorageEvent } from '@aws-amplify/core/src/types';
810

911
import { AuthError } from '../../../errors/AuthError';
12+
import { getCurrentUser } from '../apis/getCurrentUser';
1013

1114
import {
1215
AuthKeys,
@@ -42,6 +45,47 @@ export class DefaultTokenStore implements AuthTokenStore {
4245
this.authConfig = authConfig;
4346
}
4447

48+
setupNotify() {
49+
this.keyValueStorage?.addListener?.(async (e: KeyValueStorageEvent) => {
50+
const [key, , , id] = (e.key || '').split('.');
51+
if (key === AUTH_KEY_PREFIX && id === 'refreshToken') {
52+
const { newValue, oldValue } = e;
53+
if (newValue && oldValue === null) {
54+
Hub.dispatch(
55+
'auth',
56+
{
57+
event: 'signedIn',
58+
data: await getCurrentUser(),
59+
},
60+
'Auth',
61+
AMPLIFY_SYMBOL,
62+
true,
63+
);
64+
} else if (newValue === null && oldValue) {
65+
Hub.dispatch(
66+
'auth',
67+
{
68+
event: 'signedOut',
69+
},
70+
'Auth',
71+
AMPLIFY_SYMBOL,
72+
true,
73+
);
74+
} else if (newValue && oldValue) {
75+
Hub.dispatch(
76+
'auth',
77+
{
78+
event: 'tokenRefresh',
79+
},
80+
'Auth',
81+
AMPLIFY_SYMBOL,
82+
true,
83+
);
84+
}
85+
}
86+
});
87+
}
88+
4589
async loadTokens(): Promise<CognitoAuthTokens | null> {
4690
// TODO(v6): migration logic should be here
4791
// Reading V5 tokens old format

packages/auth/src/providers/cognito/utils/signInWithRedirectStore.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import {
77
} from '@aws-amplify/core';
88
import { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils';
99

10+
import { AUTH_KEY_PREFIX } from '../tokenProvider/constants';
1011
import { getAuthStorageKeys } from '../tokenProvider/TokenStore';
1112

1213
import { OAuthStorageKeys, OAuthStore } from './types';
1314

1415
const V5_HOSTED_UI_KEY = 'amplify-signin-with-hostedUI';
1516

16-
const name = 'CognitoIdentityServiceProvider';
1717
export class DefaultOAuthStore implements OAuthStore {
1818
keyValueStorage: KeyValueStorageInterface;
1919
cognitoConfig?: CognitoUserPoolConfig;
@@ -26,7 +26,7 @@ export class DefaultOAuthStore implements OAuthStore {
2626
assertTokenProviderConfig(this.cognitoConfig);
2727

2828
const authKeys = createKeysForAuthStorage(
29-
name,
29+
AUTH_KEY_PREFIX,
3030
this.cognitoConfig.userPoolClientId,
3131
);
3232
await Promise.all([
@@ -39,7 +39,7 @@ export class DefaultOAuthStore implements OAuthStore {
3939
async clearOAuthData(): Promise<void> {
4040
assertTokenProviderConfig(this.cognitoConfig);
4141
const authKeys = createKeysForAuthStorage(
42-
name,
42+
AUTH_KEY_PREFIX,
4343
this.cognitoConfig.userPoolClientId,
4444
);
4545
await this.clearOAuthInflightData();
@@ -52,7 +52,7 @@ export class DefaultOAuthStore implements OAuthStore {
5252
assertTokenProviderConfig(this.cognitoConfig);
5353

5454
const authKeys = createKeysForAuthStorage(
55-
name,
55+
AUTH_KEY_PREFIX,
5656
this.cognitoConfig.userPoolClientId,
5757
);
5858

@@ -63,7 +63,7 @@ export class DefaultOAuthStore implements OAuthStore {
6363
assertTokenProviderConfig(this.cognitoConfig);
6464

6565
const authKeys = createKeysForAuthStorage(
66-
name,
66+
AUTH_KEY_PREFIX,
6767
this.cognitoConfig.userPoolClientId,
6868
);
6969

@@ -74,7 +74,7 @@ export class DefaultOAuthStore implements OAuthStore {
7474
assertTokenProviderConfig(this.cognitoConfig);
7575

7676
const authKeys = createKeysForAuthStorage(
77-
name,
77+
AUTH_KEY_PREFIX,
7878
this.cognitoConfig.userPoolClientId,
7979
);
8080

@@ -85,7 +85,7 @@ export class DefaultOAuthStore implements OAuthStore {
8585
assertTokenProviderConfig(this.cognitoConfig);
8686

8787
const authKeys = createKeysForAuthStorage(
88-
name,
88+
AUTH_KEY_PREFIX,
8989
this.cognitoConfig.userPoolClientId,
9090
);
9191

@@ -100,7 +100,7 @@ export class DefaultOAuthStore implements OAuthStore {
100100
assertTokenProviderConfig(this.cognitoConfig);
101101

102102
const authKeys = createKeysForAuthStorage(
103-
name,
103+
AUTH_KEY_PREFIX,
104104
this.cognitoConfig.userPoolClientId,
105105
);
106106

@@ -112,7 +112,7 @@ export class DefaultOAuthStore implements OAuthStore {
112112
async storeOAuthInFlight(inflight: boolean): Promise<void> {
113113
assertTokenProviderConfig(this.cognitoConfig);
114114
const authKeys = createKeysForAuthStorage(
115-
name,
115+
AUTH_KEY_PREFIX,
116116
this.cognitoConfig.userPoolClientId,
117117
);
118118

@@ -126,7 +126,7 @@ export class DefaultOAuthStore implements OAuthStore {
126126
assertTokenProviderConfig(this.cognitoConfig);
127127

128128
const authKeys = createKeysForAuthStorage(
129-
name,
129+
AUTH_KEY_PREFIX,
130130
this.cognitoConfig.userPoolClientId,
131131
);
132132

@@ -151,7 +151,7 @@ export class DefaultOAuthStore implements OAuthStore {
151151
assertTokenProviderConfig(this.cognitoConfig);
152152

153153
const authKeys = createKeysForAuthStorage(
154-
name,
154+
AUTH_KEY_PREFIX,
155155
this.cognitoConfig.userPoolClientId,
156156
);
157157

packages/core/__tests__/Hub.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,76 @@ describe('Hub', () => {
140140
expect(loggerSpy).not.toHaveBeenCalled();
141141
});
142142
});
143+
144+
describe('crossTab', () => {
145+
const crossTabListener = jest.fn();
146+
const uiCrossTabListener = jest.fn();
147+
const sameTabListener = jest.fn();
148+
beforeAll(() => {
149+
Hub.listen('auth', crossTabListener, {
150+
enableCrossTabEvents: true,
151+
});
152+
Hub.listen('ui' as 'auth', uiCrossTabListener, {
153+
enableCrossTabEvents: true,
154+
});
155+
Hub.listen('auth', sameTabListener);
156+
});
157+
158+
beforeEach(() => {
159+
crossTabListener.mockClear();
160+
sameTabListener.mockClear();
161+
});
162+
163+
it('should not call crossTab listeners on sameTab events', () => {
164+
Hub.dispatch(
165+
'auth',
166+
{
167+
event: 'signedIn',
168+
data: {},
169+
},
170+
'Auth',
171+
Symbol.for('amplify_default'),
172+
);
173+
174+
expect(crossTabListener).not.toHaveBeenCalled();
175+
expect(sameTabListener).toHaveBeenCalled();
176+
});
177+
178+
it('should call crossTab listeners on crossTab events', () => {
179+
Hub.dispatch(
180+
'auth',
181+
{
182+
event: 'signedIn',
183+
data: {
184+
username: 'foo',
185+
userId: '123',
186+
},
187+
},
188+
'Auth',
189+
Symbol.for('amplify_default'),
190+
true,
191+
);
192+
193+
expect(crossTabListener).toHaveBeenCalled();
194+
expect(sameTabListener).not.toHaveBeenCalled();
195+
});
196+
197+
it('should not allow crossTab dispatch in other channels', () => {
198+
Hub.dispatch(
199+
// this looks weird but is only used to mute TS.
200+
// becase the API can be called this way.
201+
// and we want to check the logic, not the types
202+
'ui' as 'auth',
203+
{
204+
event: 'tokenRefresh',
205+
message: 'whooza',
206+
},
207+
'Auth',
208+
Symbol.for('amplify_default'),
209+
true,
210+
);
211+
212+
expect(uiCrossTabListener).not.toHaveBeenCalled();
213+
});
214+
});
143215
});

packages/core/__tests__/storage/DefaultStorage.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DefaultStorage } from '../../src/storage/DefaultStorage';
22
import { InMemoryStorage } from '../../src/storage/InMemoryStorage';
3+
import * as utils from '../../src/utils';
34

45
const key = 'k';
56
const value = 'value';
@@ -57,4 +58,16 @@ describe('DefaultStorage', () => {
5758
value: originalLocalStorage,
5859
});
5960
});
61+
62+
it('should setup listeners, when in browser', () => {
63+
jest.spyOn(utils, 'isBrowser').mockImplementation(() => true);
64+
const windowSpy = jest.spyOn(window, 'addEventListener');
65+
66+
defaultStorage = new DefaultStorage();
67+
expect(windowSpy).toHaveBeenCalledWith(
68+
'storage',
69+
expect.any(Function),
70+
false,
71+
);
72+
});
6073
});

0 commit comments

Comments
 (0)