From e2d577fc0c8211930b03e3f8908c0e0681bcd9d3 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 29 May 2024 13:34:26 -0700 Subject: [PATCH] feat(auth): add option to disable idp oauth flow --- packages/auth/__tests__/auth-unit-test.ts | 123 ++++++++++++++++++++++ packages/auth/src/Auth.ts | 34 ++++-- packages/auth/src/types/Auth.ts | 1 + 3 files changed, 147 insertions(+), 11 deletions(-) diff --git a/packages/auth/__tests__/auth-unit-test.ts b/packages/auth/__tests__/auth-unit-test.ts index bc717723ecf..28bd82982b1 100644 --- a/packages/auth/__tests__/auth-unit-test.ts +++ b/packages/auth/__tests__/auth-unit-test.ts @@ -281,6 +281,7 @@ const createMockLocalStorage = () => import { AuthOptions, SignUpParams, AwsCognitoOAuthOpts } from '../src/types'; import { AuthClass as Auth } from '../src/Auth'; +import * as urlListener from '../src/urlListener'; import { Credentials, StorageHelper, Hub } from '@aws-amplify/core'; import { AuthError, NoUserPoolError } from '../src/Errors'; import { AuthErrorTypes } from '../src/types/Auth'; @@ -3544,6 +3545,53 @@ describe('auth unit test', () => { spyon3.mockClear(); expect(urlOpener).not.toBeCalled(); }); + + test('should add SP initiated inflight flag to local storage', async () => { + const localStorageSetItemMock = jest.fn(); + jest + .spyOn(StorageHelper.prototype, 'getStorage') + .mockImplementation(() => { + return { + setItem: localStorageSetItemMock, + getItem: jest.fn(), + removeItem: jest.fn(), + }; + }); + let user; + jest.spyOn(Credentials, 'set').mockImplementationOnce(() => { + user = { name: 'username', email: 'xxx@email.com' }; + return Promise.resolve('cred' as any); + }); + jest + .spyOn(Auth.prototype, 'currentAuthenticatedUser') + .mockImplementation(() => { + if (!user) return Promise.reject('error'); + else return Promise.resolve(user); + }); + + const options: AuthOptions = { + region: 'region', + identityPoolId: 'awsCognitoIdentityPoolId', + userPoolId: 'userPoolId', + oauth: { + domain: 'mydomain.auth.us-east-1.amazoncognito.com', + scope: ['aws.cognito.signin.user.admin'], + redirectSignIn: 'http://localhost:3000/', + redirectSignOut: 'http://localhost:3000/', + responseType: 'code', + urlOpener: jest.fn(), + }, + }; + + const auth = new Auth(options); + await auth.federatedSignIn(); + + expect(localStorageSetItemMock).toBeCalled(); + expect(localStorageSetItemMock).toHaveBeenCalledWith( + 'amplify-sp-initiated-oauth-inFlight', + 'true' + ); + }); }); describe('handleAuthResponse test', () => { @@ -3561,6 +3609,12 @@ describe('auth unit test', () => { setItem() { return null; }, + getItem() { + return null; + }, + removeItem() { + return null; + }, }; }); }); @@ -3759,6 +3813,75 @@ describe('auth unit test', () => { }); }); + describe('OAuth flow', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should call urlListener by default', async () => { + const urlListenerSpy = jest.spyOn(urlListener, 'default'); + const options: AuthOptions = { + region: 'region', + userPoolId: 'userPoolId', + oauth: { + domain: 'mydomain.auth.us-east-1.amazoncognito.com', + scope: ['aws.cognito.signin.user.admin'], + redirectSignIn: 'http://localhost:3000/', + redirectSignOut: 'http://localhost:3000/', + responseType: 'code', + }, + identityPoolId: 'awsCognitoIdentityPoolId', + }; + new Auth(options); + expect(urlListenerSpy).toHaveBeenCalledTimes(1); + }); + + test('should not call urlListener when IDP initiated oauth is disabled', async () => { + const urlListenerSpy = jest.spyOn(urlListener, 'default'); + const options: AuthOptions = { + region: 'region', + userPoolId: 'userPoolId', + oauth: { + domain: 'mydomain.auth.us-east-1.amazoncognito.com', + scope: ['aws.cognito.signin.user.admin'], + redirectSignIn: 'http://localhost:3000/', + redirectSignOut: 'http://localhost:3000/', + responseType: 'code', + idpEnabled: false, + }, + identityPoolId: 'awsCognitoIdentityPoolId', + }; + new Auth(options); + expect(urlListenerSpy).not.toHaveBeenCalled(); + }); + + test('should call urlListener when SP initiated oauth is in flight and IDP is disabled', async () => { + const mockLocalStorage = createMockLocalStorage(); + jest + .spyOn(StorageHelper.prototype, 'getStorage') + .mockImplementation(() => mockLocalStorage); + mockLocalStorage.setItem('amplify-sp-initiated-oauth-inFlight', 'true'); + + const urlListenerSpy = jest.spyOn(urlListener, 'default'); + + const options: AuthOptions = { + region: 'region', + userPoolId: 'userPoolId', + oauth: { + domain: 'mydomain.auth.us-east-1.amazoncognito.com', + scope: ['aws.cognito.signin.user.admin'], + redirectSignIn: 'http://localhost:3000/', + redirectSignOut: 'http://localhost:3000/', + responseType: 'code', + idpEnabled: false, + }, + identityPoolId: 'awsCognitoIdentityPoolId', + }; + new Auth(options); + expect(urlListenerSpy).toHaveBeenCalledTimes(1); + }); + }); + describe('verifiedContact test', () => { test('happy case with unverified', async () => { const spyon = jest diff --git a/packages/auth/src/Auth.ts b/packages/auth/src/Auth.ts index bfd5dd0afbc..3099d76d886 100644 --- a/packages/auth/src/Auth.ts +++ b/packages/auth/src/Auth.ts @@ -226,6 +226,10 @@ export class AuthClass { : (oauth).awsCognito : undefined; + const isIdpInitiatedOAuthEnabled = + cognitoHostedUIConfig?.idpEnabled ?? true; // default true, avoid breaking change + const isSpInitiatedOAuthInFlight = + this._storage.getItem('amplify-sp-initiated-oauth-inFlight') === 'true'; if (cognitoHostedUIConfig) { const cognitoAuthParams = Object.assign( { @@ -249,18 +253,20 @@ export class AuthClass { cognitoClientId: cognitoAuthParams.cognitoClientId, }); - // **NOTE** - Remove this in a future major release as it is a breaking change - // Prevents _handleAuthResponse from being called multiple times in Expo - // See https://github.com/aws-amplify/amplify-js/issues/4388 - const usedResponseUrls = {}; - urlListener(({ url }) => { - if (usedResponseUrls[url]) { - return; - } + if (isIdpInitiatedOAuthEnabled || isSpInitiatedOAuthInFlight) { + // **NOTE** - Remove this in a future major release as it is a breaking change + // Prevents _handleAuthResponse from being called multiple times in Expo + // See https://github.com/aws-amplify/amplify-js/issues/4388 + const usedResponseUrls = {}; + urlListener(({ url }) => { + if (usedResponseUrls[url]) { + return; + } - usedResponseUrls[url] = true; - this._handleAuthResponse(url); - }); + usedResponseUrls[url] = true; + this._handleAuthResponse(url); + }); + } } dispatchAuthEvent( @@ -2437,6 +2443,7 @@ export class AuthClass { ? this._config.oauth.redirectSignIn : this._config.oauth.redirectUri; + this._storage.setItem('amplify-sp-initiated-oauth-inFlight', 'true'); this._oAuthHandler.oauthSignIn( this._config.oauth.responseType, this._config.oauth.domain, @@ -2517,6 +2524,11 @@ export class AuthClass { if (hasCodeOrError || hasTokenOrError) { this._storage.setItem('amplify-redirected-from-hosted-ui', 'true'); + // clear temp value + if (this._storage.getItem('amplify-sp-initiated-oauth-inFlight')) { + this._storage.removeItem('amplify-sp-initiated-oauth-inFlight'); + } + try { const { accessToken, idToken, refreshToken, state } = await this._oAuthHandler.handleAuthResponse(currentUrl); diff --git a/packages/auth/src/types/Auth.ts b/packages/auth/src/types/Auth.ts index 61996e767ec..b532ae53ab2 100644 --- a/packages/auth/src/types/Auth.ts +++ b/packages/auth/src/types/Auth.ts @@ -127,6 +127,7 @@ export interface AwsCognitoOAuthOpts { redirectSignOut: string; responseType: string; options?: object; + idpEnabled?: boolean; urlOpener?: (url: string, redirectUrl: string) => Promise; }