Skip to content

Commit e8fb997

Browse files
Samaritan1011001israxAllanZhengYP
authored
feat(auth): HostedUI oidc signout (#13512)
* chore(auth): add oauth metadata into token orchestrator (#13712) (#13736) * chore: add oauth metadata into token orchestrator * chore: add unit tests * chore: address feedback * wip: hardcode signout uri for poc * chore: expose the prefferedRedirectSignOutUrl * chore: add prefered url change to native file * chore: correct param name * chore: update getRedirectUrl function to consider preferred url * chore: add unit test for the feature * chore: update input type to use the accepted format * chore: review comments * fix: address npm audit issues * chore: update comments, bundle size and rn version * chore: update bundle size limit * chore: update bundle size limit * chore: address coments and rename a param to getRedirecturl funciton * chore: make preid release ready * chore: update yarn.lock * chore: add test and update push-integ branch * chore: revert preid release updates * chore: update sample name * chore: enable react native tests with localhost server * chore: enable subdomain test * chore: update some function calls in tests * chore: minor reverts * fix: unit tests fail on mehtod params * chore: revert ppush branch * chore: remove subdomain test rdundant * chore: upadte step name * chore: reflect API changes and clean up * chore: revert unintented change glob * chore: bundle size minor adjustments * chore: move localhost page hosting to RN script in the app * chore: revert unintended change * chore: revert branch name for integ test --------- Co-authored-by: israx <[email protected]> Co-authored-by: AllanZhengYP <[email protected]>
1 parent a363362 commit e8fb997

22 files changed

+463
-199
lines changed

.github/integ-config/detox-integ-all.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@
1616
- test_name: 'integ_rn_ios_api_v6_rn_72_detox_cli'
1717
working_directory: amplify-js-samples-staging/samples/react-native/api/v6/ApiGRAPHQL
1818
timeout_minutes: 120
19+
- test_name: 'integ_rn_ios_oidc_signout'
20+
working_directory: amplify-js-samples-staging/samples/react-native/auth/HosteduiApp
21+
timeout_minutes: 120
22+
host_signout_page: true

.github/integ-config/integ-all.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,13 @@ tests:
514514
sample_name: [sign-in-with-oauth]
515515
spec: sign-in-with-oauth
516516
browser: [chrome]
517+
- test_name: integ_vue_sign_out_of_oidc_provider
518+
desc: 'Sign-out of OIDC provider'
519+
framework: vue
520+
category: auth
521+
sample_name: [sign-in-with-oauth]
522+
spec: sign-out-oidc-provider
523+
browser: [chrome]
517524

518525
# AUTH GEN2
519526
- test_name: integ_react_javascript_authentication_gen2

.github/workflows/callable-e2e-test-detox.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ on:
1313
timeout_minutes:
1414
required: true
1515
type: number
16+
host_signout_page:
17+
required: false
18+
type: boolean
19+
default: false
1620

1721
jobs:
1822
e2e-test:
@@ -70,6 +74,11 @@ jobs:
7074
JEST_JUNIT_OUTPUT_NAME: detox-test-results.xml
7175
working-directory: ${{ inputs.working_directory }}
7276
shell: bash
77+
- name: Start the http-server and host the oidc signout page locally (background).
78+
if: ${{ inputs.host_signout_page }}
79+
run: yarn host:signout
80+
working-directory: ${{ inputs.working_directory }}
81+
shell: bash
7382
- name: Detox run
7483
run: |
7584
$GITHUB_WORKSPACE/amplify-js/scripts/retry-yarn-script.sh -s 'detox test -c ios.sim.debug -u' -n 3

.github/workflows/callable-e2e-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,4 @@ jobs:
7474
test_name: ${{ matrix.integ-config.test_name }}
7575
working_directory: ${{ matrix.integ-config.working_directory }}
7676
timeout_minutes: ${{ matrix.integ-config.timeout_minutes || 45 }}
77+
host_signout_page: ${{ matrix.integ-config.host_signout_page || false }}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,8 @@
137137
"nx": "16.7.0",
138138
"xml2js": "0.5.0",
139139
"tar": "6.2.1"
140+
},
141+
"overrides": {
142+
"tar": "6.2.1"
140143
}
141144
}

packages/auth/__tests__/providers/cognito/signOut.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ describe('signOut', () => {
221221
cognitoConfigWithOauth,
222222
mockDefaultOAuthStoreInstance,
223223
mockTokenOrchestrator,
224+
undefined,
224225
);
225226
// In cases of OAuth, token removal and Hub dispatch should be performed by the OAuth handling since
226227
// these actions can be deferred or canceled out of altogether.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { invalidAppSchemeException } from '../../../../../src/errors/constants';
2+
import { getRedirectUrl } from '../../../../../src/providers/cognito/utils/oauth/getRedirectUrl.native';
3+
4+
describe('getRedirectUrl (native)', () => {
5+
const mockRedirectUrls = [
6+
'myDevApp://',
7+
'myProdApp://',
8+
'https://intermidiateSite.com',
9+
];
10+
11+
it('should return the first non http/s url from the array when redirectUrl is not provided', () => {
12+
expect(getRedirectUrl(mockRedirectUrls)).toStrictEqual(mockRedirectUrls[0]);
13+
});
14+
15+
it('should return redirectUrl if it matches at least one of the redirect urls from config', () => {
16+
const configRedirectUrl = mockRedirectUrls[2];
17+
18+
expect(getRedirectUrl(mockRedirectUrls, configRedirectUrl)).toStrictEqual(
19+
configRedirectUrl,
20+
);
21+
});
22+
23+
it('should throw an exception when there is no url with no http nor https as prefix irrespective of a redirectUrl is given or not', () => {
24+
const mockRedirectUrlsWithNoAppScheme = ['https://intermidiateSite.com'];
25+
expect(() =>
26+
getRedirectUrl(
27+
mockRedirectUrlsWithNoAppScheme,
28+
mockRedirectUrlsWithNoAppScheme[0],
29+
),
30+
).toThrow(invalidAppSchemeException);
31+
expect(() => getRedirectUrl(mockRedirectUrlsWithNoAppScheme)).toThrow(
32+
invalidAppSchemeException,
33+
);
34+
});
35+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { getRedirectUrl } from '../../../../../src/providers/cognito/utils/oauth';
2+
import {
3+
invalidOriginException,
4+
invalidPreferredRedirectUrlException,
5+
invalidRedirectException,
6+
} from '../../../../../src/errors/constants';
7+
8+
describe('getRedirectUrl', () => {
9+
const mockRedirectUrls = ['https://example.com/app'];
10+
let windowSpy: jest.SpyInstance;
11+
12+
beforeEach(() => {
13+
windowSpy = jest.spyOn(window, 'window', 'get');
14+
});
15+
16+
afterEach(() => {
17+
windowSpy.mockRestore();
18+
});
19+
20+
it('should return the redirect url that has the same origin and same pathName', () => {
21+
windowSpy.mockReturnValue({
22+
location: {
23+
origin: 'https://example.com/',
24+
pathname: 'app',
25+
},
26+
});
27+
expect(getRedirectUrl(mockRedirectUrls)).toStrictEqual(mockRedirectUrls[0]);
28+
});
29+
30+
it('should throw an invalid origin exception if there is no url that is the same origin and pathname', () => {
31+
windowSpy.mockReturnValue({
32+
location: {
33+
origin: 'https://differentOrigin.com/',
34+
pathname: 'differentApp',
35+
},
36+
});
37+
expect(() => getRedirectUrl(mockRedirectUrls)).toThrow(
38+
invalidOriginException,
39+
);
40+
});
41+
42+
it('should throw an invalid redirect exception if there is no url that is the same origin/pathname and is also not http or https', () => {
43+
const mockNonHttpRedirectUrls = ['test-non-http-string'];
44+
windowSpy.mockReturnValue({
45+
location: {
46+
origin: 'https://differentOrigin.com/',
47+
pathname: 'differentApp',
48+
},
49+
});
50+
expect(() => getRedirectUrl(mockNonHttpRedirectUrls)).toThrow(
51+
invalidRedirectException,
52+
);
53+
});
54+
55+
it('should return the redirectUrl if it is provided and matches one of the redirectUrls from config', () => {
56+
expect(getRedirectUrl(mockRedirectUrls, mockRedirectUrls[0])).toStrictEqual(
57+
mockRedirectUrls[0],
58+
);
59+
});
60+
61+
it('should throw an exception if redirectUrl is given but does not match any of the redirectUrls from config', () => {
62+
expect(() =>
63+
getRedirectUrl(mockRedirectUrls, 'https://unknownOrigin.com'),
64+
).toThrow(invalidPreferredRedirectUrlException);
65+
});
66+
});

packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.native.test.ts

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

4+
import { tokenOrchestrator } from '../../../../../src/providers/cognito/tokenProvider';
45
import { completeOAuthSignOut } from '../../../../../src/providers/cognito/utils/oauth/completeOAuthSignOut';
56
import { handleOAuthSignOut } from '../../../../../src/providers/cognito/utils/oauth/handleOAuthSignOut.native';
67
import { oAuthSignOutRedirect } from '../../../../../src/providers/cognito/utils/oauth/oAuthSignOutRedirect';
@@ -23,6 +24,9 @@ describe('handleOAuthSignOut (native)', () => {
2324
// assert mocks
2425
const mockCompleteOAuthSignOut = completeOAuthSignOut as jest.Mock;
2526
const mockOAuthSignOutRedirect = oAuthSignOutRedirect as jest.Mock;
27+
const mockTokenOrchestrator = tokenOrchestrator as jest.Mocked<
28+
typeof tokenOrchestrator
29+
>;
2630
// create mocks
2731
const mockStore = {
2832
loadOAuthSignIn: jest.fn(),
@@ -43,33 +47,51 @@ describe('handleOAuthSignOut (native)', () => {
4347
});
4448
it('should complete OAuth sign out and redirect', async () => {
4549
mockOAuthSignOutRedirect.mockResolvedValue({ type: 'success' });
46-
await handleOAuthSignOut(cognitoConfig, mockStore);
50+
await handleOAuthSignOut(
51+
cognitoConfig,
52+
mockStore,
53+
mockTokenOrchestrator,
54+
undefined,
55+
);
4756

4857
expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(
4958
cognitoConfig,
5059
false,
60+
undefined,
5161
);
5262
expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore);
5363
});
5464

5565
it('should not complete OAuth sign out if redirect is canceled', async () => {
5666
mockOAuthSignOutRedirect.mockResolvedValue({ type: 'canceled' });
57-
await handleOAuthSignOut(cognitoConfig, mockStore);
67+
await handleOAuthSignOut(
68+
cognitoConfig,
69+
mockStore,
70+
mockTokenOrchestrator,
71+
undefined,
72+
);
5873

5974
expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(
6075
cognitoConfig,
6176
false,
77+
undefined,
6278
);
6379
expect(mockCompleteOAuthSignOut).not.toHaveBeenCalled();
6480
});
6581

6682
it('should not complete OAuth sign out if redirect failed', async () => {
6783
mockOAuthSignOutRedirect.mockResolvedValue({ type: 'error' });
68-
await handleOAuthSignOut(cognitoConfig, mockStore);
84+
await handleOAuthSignOut(
85+
cognitoConfig,
86+
mockStore,
87+
mockTokenOrchestrator,
88+
undefined,
89+
);
6990

7091
expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(
7192
cognitoConfig,
7293
false,
94+
undefined,
7395
);
7496
expect(mockCompleteOAuthSignOut).not.toHaveBeenCalled();
7597
});
@@ -81,9 +103,18 @@ describe('handleOAuthSignOut (native)', () => {
81103
preferPrivateSession: true,
82104
});
83105
mockOAuthSignOutRedirect.mockResolvedValue({ type: 'error' });
84-
await handleOAuthSignOut(cognitoConfig, mockStore);
106+
await handleOAuthSignOut(
107+
cognitoConfig,
108+
mockStore,
109+
mockTokenOrchestrator,
110+
undefined,
111+
);
85112

86-
expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(cognitoConfig, true);
113+
expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(
114+
cognitoConfig,
115+
true,
116+
undefined,
117+
);
87118
expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore);
88119
});
89120

@@ -92,7 +123,12 @@ describe('handleOAuthSignOut (native)', () => {
92123
isOAuthSignIn: false,
93124
preferPrivateSession: false,
94125
});
95-
await handleOAuthSignOut(cognitoConfig, mockStore);
126+
await handleOAuthSignOut(
127+
cognitoConfig,
128+
mockStore,
129+
mockTokenOrchestrator,
130+
undefined,
131+
);
96132

97133
expect(mockOAuthSignOutRedirect).not.toHaveBeenCalled();
98134
expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore);

packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,19 @@ describe('handleOAuthSignOut', () => {
4545
isOAuthSignIn: true,
4646
preferPrivateSession: false,
4747
});
48-
await handleOAuthSignOut(cognitoConfig, mockStore, mockTokenOrchestrator);
48+
await handleOAuthSignOut(
49+
cognitoConfig,
50+
mockStore,
51+
mockTokenOrchestrator,
52+
undefined,
53+
);
4954

5055
expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore);
51-
expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(cognitoConfig);
56+
expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(
57+
cognitoConfig,
58+
false,
59+
undefined,
60+
);
5261
});
5362

5463
it('should complete OAuth sign out and redirect when there oauth metadata in tokenOrchestrator', async () => {
@@ -59,18 +68,32 @@ describe('handleOAuthSignOut', () => {
5968
isOAuthSignIn: false,
6069
preferPrivateSession: false,
6170
});
62-
await handleOAuthSignOut(cognitoConfig, mockStore, mockTokenOrchestrator);
71+
await handleOAuthSignOut(
72+
cognitoConfig,
73+
mockStore,
74+
mockTokenOrchestrator,
75+
undefined,
76+
);
6377

6478
expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore);
65-
expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(cognitoConfig);
79+
expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(
80+
cognitoConfig,
81+
false,
82+
undefined,
83+
);
6684
});
6785

6886
it('should complete OAuth sign out but not redirect', async () => {
6987
mockStore.loadOAuthSignIn.mockResolvedValue({
7088
isOAuthSignIn: false,
7189
preferPrivateSession: false,
7290
});
73-
await handleOAuthSignOut(cognitoConfig, mockStore, mockTokenOrchestrator);
91+
await handleOAuthSignOut(
92+
cognitoConfig,
93+
mockStore,
94+
mockTokenOrchestrator,
95+
undefined,
96+
);
7497

7598
expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore);
7699
expect(mockOAuthSignOutRedirect).not.toHaveBeenCalled();

0 commit comments

Comments
 (0)