From 8b489d1667c67741cbc1111b0a52d10996aa857d Mon Sep 17 00:00:00 2001 From: "Aaron S." <94858815+stocaaro@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:51:39 -0600 Subject: [PATCH 1/8] fix(api-graphql): Data messages should maintain the keep alive status (#14164) --- .../AWSAppSyncRealTimeProvider.test.ts | 72 +++++++++++++--- .../Providers/AWSWebSocketProvider/index.ts | 82 +++++++++++++------ .../api-graphql/src/Providers/constants.ts | 5 ++ 3 files changed, 122 insertions(+), 37 deletions(-) diff --git a/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts b/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts index 041b9624898..e4947386178 100644 --- a/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts +++ b/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts @@ -147,6 +147,10 @@ describe('AWSAppSyncRealTimeProvider', () => { Object.defineProperty(constants, 'RECONNECT_DELAY', { value: 100, }); + // Reduce the keep alive heartbeat to 10ms + Object.defineProperty(constants, 'DEFAULT_KEEP_ALIVE_HEARTBEAT_TIMEOUT', { + value: 10, + }); }); afterEach(async () => { @@ -765,7 +769,7 @@ describe('AWSAppSyncRealTimeProvider', () => { // Resolve the message delivery actions await replaceConstant( 'DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT', - 5, + 10, async () => { await fakeWebSocketInterface?.readyForUse; await fakeWebSocketInterface?.triggerOpen(); @@ -776,17 +780,17 @@ describe('AWSAppSyncRealTimeProvider', () => { await fakeWebSocketInterface?.startAckMessage(); await fakeWebSocketInterface?.keepAlive(); - }, - ); - await fakeWebSocketInterface?.waitUntilConnectionStateIn([ - CS.Connected, - ]); + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.Connected, + ]); - // Wait until the socket is automatically disconnected - await fakeWebSocketInterface?.waitUntilConnectionStateIn([ - CS.ConnectionDisrupted, - ]); + // Wait until the socket is automatically disconnected + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.ConnectionDisrupted, + ]); + }, + ); expect(fakeWebSocketInterface?.observedConnectionStates).toContain( CS.ConnectedPendingKeepAlive, @@ -798,6 +802,54 @@ describe('AWSAppSyncRealTimeProvider', () => { ); }); + test('subscription observer ka is cleared if data is received', async () => { + expect.assertions(1); + + const observer = provider.subscribe({ + appSyncGraphqlEndpoint: 'ws://localhost:8080', + }); + + observer.subscribe({ error: () => {} }); + // Resolve the message delivery actions + await replaceConstant( + 'DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT', + 5, + async () => { + await fakeWebSocketInterface?.readyForUse; + await fakeWebSocketInterface?.triggerOpen(); + await fakeWebSocketInterface?.handShakeMessage({ + connectionTimeoutMs: 100, + }); + + await fakeWebSocketInterface?.startAckMessage(); + + await fakeWebSocketInterface?.keepAlive(); + + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.ConnectedPendingKeepAlive, + ]); + }, + ); + + // Send message + await fakeWebSocketInterface?.sendDataMessage({ + type: MESSAGE_TYPES.DATA, + payload: { data: {} }, + }); + + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.Connected, + ]); + + expect(fakeWebSocketInterface?.observedConnectionStates).toEqual([ + CS.Disconnected, + CS.Connecting, + CS.Connected, + CS.ConnectedPendingKeepAlive, + CS.Connected, + ]); + }); + test('subscription connection disruption triggers automatic reconnection', async () => { expect.assertions(1); diff --git a/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts b/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts index 5d710e478bc..f62f0518f1a 100644 --- a/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts +++ b/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts @@ -23,7 +23,7 @@ import { CONNECTION_INIT_TIMEOUT, CONNECTION_STATE_CHANGE, DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT, - DEFAULT_KEEP_ALIVE_TIMEOUT, + DEFAULT_KEEP_ALIVE_HEARTBEAT_TIMEOUT, MAX_DELAY_MS, MESSAGE_TYPES, NON_RETRYABLE_CODES, @@ -83,9 +83,8 @@ export abstract class AWSWebSocketProvider { protected awsRealTimeSocket?: WebSocket; private socketStatus: SOCKET_STATUS = SOCKET_STATUS.CLOSED; - private keepAliveTimeoutId?: ReturnType; - private keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT; - private keepAliveAlertTimeoutId?: ReturnType; + private keepAliveTimestamp: number = Date.now(); + private keepAliveHeartbeatIntervalId?: ReturnType; private promiseArray: { res(): void; rej(reason?: any): void }[] = []; private connectionState: ConnectionState | undefined; private readonly connectionStateMonitor = new ConnectionStateMonitor(); @@ -119,6 +118,7 @@ export abstract class AWSWebSocketProvider { return new Promise((resolve, reject) => { if (this.awsRealTimeSocket) { this.awsRealTimeSocket.onclose = (_: CloseEvent) => { + this._closeSocket(); this.subscriptionObserverMap = new Map(); this.awsRealTimeSocket = undefined; resolve(); @@ -171,7 +171,7 @@ export abstract class AWSWebSocketProvider { this.logger.debug( `${CONTROL_MSG.REALTIME_SUBSCRIPTION_INIT_ERROR}: ${err}`, ); - this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); + this._closeSocket(); }) .finally(() => { subscriptionStartInProgress = false; @@ -435,7 +435,7 @@ export abstract class AWSWebSocketProvider { this.logger.debug({ err }); const message = String(err.message ?? ''); // Resolving to give the state observer time to propogate the update - this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); + this._closeSocket(); // Capture the error only when the network didn't cause disruption if ( @@ -544,12 +544,7 @@ export abstract class AWSWebSocketProvider { setTimeout(this._closeSocketIfRequired.bind(this), 1000); } else { this.logger.debug('closing WebSocket...'); - if (this.keepAliveTimeoutId) { - clearTimeout(this.keepAliveTimeoutId); - } - if (this.keepAliveAlertTimeoutId) { - clearTimeout(this.keepAliveAlertTimeoutId); - } + const tempSocket = this.awsRealTimeSocket; // Cleaning callbacks to avoid race condition, socket still exists tempSocket.onclose = null; @@ -557,7 +552,7 @@ export abstract class AWSWebSocketProvider { tempSocket.close(1000); this.awsRealTimeSocket = undefined; this.socketStatus = SOCKET_STATUS.CLOSED; - this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); + this._closeSocket(); } } @@ -577,13 +572,40 @@ export abstract class AWSWebSocketProvider { errorType: string; }; + private maintainKeepAlive() { + this.keepAliveTimestamp = Date.now(); + } + + private keepAliveHeartbeat(connectionTimeoutMs: number) { + const currentTime = Date.now(); + + // Check for missed KA message + if ( + currentTime - this.keepAliveTimestamp > + DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT + ) { + this.connectionStateMonitor.record(CONNECTION_CHANGE.KEEP_ALIVE_MISSED); + } else { + this.connectionStateMonitor.record(CONNECTION_CHANGE.KEEP_ALIVE); + } + + // Recognize we are disconnected if we haven't seen messages in the keep alive timeout period + if (currentTime - this.keepAliveTimestamp > connectionTimeoutMs) { + this._errorDisconnect(CONTROL_MSG.TIMEOUT_DISCONNECT); + } + } + private _handleIncomingSubscriptionMessage(message: MessageEvent) { if (typeof message.data !== 'string') { return; } const [isData, data] = this._handleSubscriptionData(message); - if (isData) return; + if (isData) { + this.maintainKeepAlive(); + + return; + } const { type, id, payload } = data; @@ -632,16 +654,7 @@ export abstract class AWSWebSocketProvider { } if (type === MESSAGE_TYPES.GQL_CONNECTION_KEEP_ALIVE) { - if (this.keepAliveTimeoutId) clearTimeout(this.keepAliveTimeoutId); - if (this.keepAliveAlertTimeoutId) - clearTimeout(this.keepAliveAlertTimeoutId); - this.keepAliveTimeoutId = setTimeout(() => { - this._errorDisconnect(CONTROL_MSG.TIMEOUT_DISCONNECT); - }, this.keepAliveTimeout); - this.keepAliveAlertTimeoutId = setTimeout(() => { - this.connectionStateMonitor.record(CONNECTION_CHANGE.KEEP_ALIVE_MISSED); - }, DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT); - this.connectionStateMonitor.record(CONNECTION_CHANGE.KEEP_ALIVE); + this.maintainKeepAlive(); return; } @@ -686,13 +699,21 @@ export abstract class AWSWebSocketProvider { this.logger.debug(`Disconnect error: ${msg}`); if (this.awsRealTimeSocket) { - this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); + this._closeSocket(); this.awsRealTimeSocket.close(); } this.socketStatus = SOCKET_STATUS.CLOSED; } + private _closeSocket() { + if (this.keepAliveHeartbeatIntervalId) { + clearInterval(this.keepAliveHeartbeatIntervalId); + this.keepAliveHeartbeatIntervalId = undefined; + } + this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); + } + private _timeoutStartSubscriptionAck(subscriptionId: string) { const subscriptionObserver = this.subscriptionObserverMap.get(subscriptionId); @@ -708,7 +729,7 @@ export abstract class AWSWebSocketProvider { subscriptionState: SUBSCRIPTION_STATUS.FAILED, }); - this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); + this._closeSocket(); this.logger.debug( 'timeoutStartSubscription', JSON.stringify({ query, variables }), @@ -820,6 +841,7 @@ export abstract class AWSWebSocketProvider { this.logger.debug(`WebSocket connection error`); }; newSocket.onclose = () => { + this._closeSocket(); reject(new Error('Connection handshake error')); }; newSocket.onopen = () => { @@ -849,6 +871,7 @@ export abstract class AWSWebSocketProvider { this.awsRealTimeSocket.onclose = event => { this.logger.debug(`WebSocket closed ${event.reason}`); + this._closeSocket(); reject(new Error(JSON.stringify(event))); }; @@ -912,7 +935,11 @@ export abstract class AWSWebSocketProvider { return; } - this.keepAliveTimeout = connectionTimeoutMs; + // Set up a keep alive heartbeat for this connection + this.keepAliveHeartbeatIntervalId = setInterval(() => { + this.keepAliveHeartbeat(connectionTimeoutMs); + }, DEFAULT_KEEP_ALIVE_HEARTBEAT_TIMEOUT); + this.awsRealTimeSocket.onmessage = this._handleIncomingSubscriptionMessage.bind(this); @@ -923,6 +950,7 @@ export abstract class AWSWebSocketProvider { this.awsRealTimeSocket.onclose = event => { this.logger.debug(`WebSocket closed ${event.reason}`); + this._closeSocket(); this._errorDisconnect(CONTROL_MSG.CONNECTION_CLOSED); }; } diff --git a/packages/api-graphql/src/Providers/constants.ts b/packages/api-graphql/src/Providers/constants.ts index 5e82f672081..5aef1a60130 100644 --- a/packages/api-graphql/src/Providers/constants.ts +++ b/packages/api-graphql/src/Providers/constants.ts @@ -128,6 +128,11 @@ export const START_ACK_TIMEOUT = 15000; */ export const DEFAULT_KEEP_ALIVE_TIMEOUT = 5 * 60 * 1000; +/** + * Default Time in milleseconds between monitoring checks of keep alive status + */ +export const DEFAULT_KEEP_ALIVE_HEARTBEAT_TIMEOUT = 5 * 1000; + /** * Default Time in milleseconds to alert for missed GQL_CONNECTION_KEEP_ALIVE message */ From 22ca811743f6729d3a00dd71726ff6b5afb44b53 Mon Sep 17 00:00:00 2001 From: Pranav Malewadkar Date: Tue, 28 Jan 2025 08:37:14 -0800 Subject: [PATCH 2/8] fix(deps): fix more implicit deps and add linting (#14137) Co-authored-by: ashika112 <155593080+ashika112@users.noreply.github.com> Co-authored-by: ashika112 --- eslint.config.mjs | 13 ++++++++++++- packages/api-rest/package.json | 1 + packages/api/package.json | 6 ++++++ packages/core/package.json | 1 + packages/datastore/package.json | 1 + packages/geo/package.json | 1 + packages/notifications/package.json | 1 + packages/rtn-push-notification/package.json | 7 +++++++ .../providers/s3/apis/internal/testUtils.ts | 1 - 9 files changed, 30 insertions(+), 2 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 4d4f9c7e3ac..4e079dc7d75 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -24,7 +24,7 @@ const compat = new FlatCompat({ const customClientDtsFiles = customClientDtsBundlerConfig.entries .map(clientBundlerConfig => clientBundlerConfig.outFile) .filter(outFile => outFile?.length > 0) - .map(outFile => outFile.replace(__dirname + path.sep, '')) // Convert absolute path to relative path + .map(outFile => outFile.replace(__dirname + path.sep, '')); // Convert absolute path to relative path export default [ { @@ -294,4 +294,15 @@ export default [ 'jsdoc/no-undefined-types': 1, }, }, + { + ignores: [ + '**/**.{native,android,ios}.**', + '**/__tests__/**', + '**/packages/adapter-nextjs/**', + '**/packages/react-native/example/**', + ], + rules: { + 'import/no-extraneous-dependencies': 'error', + }, + }, ]; diff --git a/packages/api-rest/package.json b/packages/api-rest/package.json index 782b5b082f3..0a3a8d7d460 100644 --- a/packages/api-rest/package.json +++ b/packages/api-rest/package.json @@ -89,6 +89,7 @@ "devDependencies": { "@aws-amplify/core": "6.9.2", "@aws-amplify/react-native": "1.1.6", + "@aws-sdk/types": "3.387.0", "typescript": "5.0.2" }, "size-limit": [ diff --git a/packages/api/package.json b/packages/api/package.json index cececda6512..4527f853c15 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -68,6 +68,7 @@ }, "homepage": "https://aws-amplify.github.io/", "devDependencies": { + "@aws-amplify/core": "6.9.2", "jest-fetch-mock": "3.0.3", "typescript": "5.0.2" }, @@ -82,6 +83,11 @@ "dependencies": { "@aws-amplify/api-graphql": "4.7.2", "@aws-amplify/api-rest": "4.0.67", + "@aws-amplify/data-schema": "^1.7.0", + "rxjs": "^7.8.1", "tslib": "^2.5.0" + }, + "peerDependencies": { + "@aws-amplify/core": "^6.1.0" } } diff --git a/packages/core/package.json b/packages/core/package.json index 7e8c0d13a5d..0732012fc50 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -54,6 +54,7 @@ "@aws-sdk/types": "3.398.0", "@smithy/util-hex-encoding": "2.0.0", "@types/uuid": "^9.0.0", + "cookie": "^0.7.0", "js-cookie": "^3.0.5", "rxjs": "^7.8.1", "tslib": "^2.5.0", diff --git a/packages/datastore/package.json b/packages/datastore/package.json index c0c03841550..f6b8878e800 100644 --- a/packages/datastore/package.json +++ b/packages/datastore/package.json @@ -45,6 +45,7 @@ ], "dependencies": { "@aws-amplify/api": "6.2.2", + "@aws-amplify/api-graphql": "4.7.2", "buffer": "4.9.2", "idb": "5.0.6", "immer": "9.0.6", diff --git a/packages/geo/package.json b/packages/geo/package.json index 5d73b70b4eb..4fab3548d38 100644 --- a/packages/geo/package.json +++ b/packages/geo/package.json @@ -69,6 +69,7 @@ "dependencies": { "@aws-sdk/client-location": "3.621.0", "@turf/boolean-clockwise": "6.5.0", + "@aws-sdk/types": "3.398.0", "camelcase-keys": "6.2.2", "tslib": "^2.5.0" }, diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 72891784a8b..53da78f306c 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -91,6 +91,7 @@ "push-notifications" ], "dependencies": { + "@aws-sdk/types": "3.398.0", "lodash": "^4.17.21", "tslib": "^2.5.0" }, diff --git a/packages/rtn-push-notification/package.json b/packages/rtn-push-notification/package.json index 7a24a35a039..b20ef6e0aef 100644 --- a/packages/rtn-push-notification/package.json +++ b/packages/rtn-push-notification/package.json @@ -23,10 +23,17 @@ "lint:fix": "eslint '**/*.{ts,tsx}' --fix", "ts-coverage": "typescript-coverage-report -p ./tsconfig.build.json -t 99" }, + "dependencies": { + "lodash": "^4.17.21" + }, "devDependencies": { "@types/react-native": "0.70.0", + "react-native": "0.71.0", "typescript": "5.0.2" }, + "peerDependencies": { + "react-native": ">=0.70" + }, "repository": { "type": "git", "url": "https://github.com/aws-amplify/amplify-js.git" diff --git a/packages/storage/__tests__/providers/s3/apis/internal/testUtils.ts b/packages/storage/__tests__/providers/s3/apis/internal/testUtils.ts index 75f5dd823b2..3c7ee88f42f 100644 --- a/packages/storage/__tests__/providers/s3/apis/internal/testUtils.ts +++ b/packages/storage/__tests__/providers/s3/apis/internal/testUtils.ts @@ -1,5 +1,4 @@ import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { expect } from '@jest/globals'; import { type MatcherFunction } from 'expect'; const toBeLastCalledWithConfigAndInput: MatcherFunction< From 619f47c4296a5f9d160e0fbae4155d0ab7254170 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Tue, 28 Jan 2025 14:01:30 -0800 Subject: [PATCH 3/8] chore(e2e): remove legacy interactions e2e tests (#14163) Co-authored-by: Ashwin Kumar --- .github/integ-config/integ-all.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index 8dce2c9a9ea..6f467fcd84a 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -758,13 +758,6 @@ tests: # amplifyjs_dir: true # INTERACTIONS - # - test_name: integ_react_interactions_react_interactions - # desc: 'React Interactions' - # framework: react - # category: interactions - # sample_name: [chatbot-component] - # spec: chatbot-component - # browser: *minimal_browser_list - test_name: integ_react_interactions_chatbot_v1 desc: 'Chatbot V1' framework: react @@ -779,27 +772,6 @@ tests: sample_name: [lex-test-component] spec: chatbot-v2 browser: *minimal_browser_list - # - test_name: integ_angular_interactions - # desc: 'Angular Interactions' - # framework: angular - # category: interactions - # sample_name: [chatbot-component] - # spec: chatbot-component - # browser: *minimal_browser_list - # - test_name: integ_vue_interactions_vue_2_interactions - # desc: 'Vue 2 Interactions' - # framework: vue - # category: interactions - # sample_name: [chatbot-component] - # spec: chatbot-component - # browser: [chrome] - # - test_name: integ_vue_interactionsvue_3_interactions - # desc: 'Vue 3 Interactions' - # framework: vue - # category: interactions - # sample_name: [chatbot-component-vue3] - # spec: chatbot-component - # browser: [chrome] # PREDICTIONS - test_name: integ_react_predictions From e888e7c0ce264871def577a01fd804919f7b79d5 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Tue, 28 Jan 2025 14:43:24 -0800 Subject: [PATCH 4/8] chore(ci): remove storage-browser integ test auth0 deps (#14080) Co-authored-by: Ashwin Kumar --- .github/workflows/callable-e2e-test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/callable-e2e-test.yml b/.github/workflows/callable-e2e-test.yml index 9eb0eca702e..0a5002fa211 100644 --- a/.github/workflows/callable-e2e-test.yml +++ b/.github/workflows/callable-e2e-test.yml @@ -49,10 +49,6 @@ env: CYPRESS_GOOGLE_CLIENTID: ${{ secrets.CYPRESS_GOOGLE_CLIENTID }} CYPRESS_GOOGLE_CLIENT_SECRET: ${{ secrets.CYPRESS_GOOGLE_CLIENT_SECRET }} CYPRESS_GOOGLE_REFRESH_TOKEN: ${{ secrets.CYPRESS_GOOGLE_REFRESH_TOKEN }} - CYPRESS_AUTH0_CLIENTID: ${{ secrets.CYPRESS_AUTH0_CLIENTID }} - CYPRESS_AUTH0_SECRET: ${{ secrets.CYPRESS_AUTH0_SECRET }} - CYPRESS_AUTH0_AUDIENCE: ${{ secrets.CYPRESS_AUTH0_AUDIENCE }} - CYPRESS_AUTH0_DOMAIN: ${{ secrets.CYPRESS_AUTH0_DOMAIN }} jobs: e2e-test: From ba025e52522ab5a0b292fc0c0c6b47c8e4c53cf9 Mon Sep 17 00:00:00 2001 From: Chris F <5827964+cshfang@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:32:36 -0800 Subject: [PATCH 5/8] chore(storage): Improve error messaging for MP upload missing ETag (#14170) * chore(storage): Improve error messaging for MP upload missing ETag * Bump size limit --- packages/aws-amplify/package.json | 2 +- .../s3Data/completeMultipartUpload.test.ts | 15 ++++++++++++++- .../client/s3data/completeMultipartUpload.ts | 17 +++++++++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 159c7da99d5..c399a5309e6 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -515,7 +515,7 @@ "name": "[Storage] uploadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ uploadData }", - "limit": "22.95 kB" + "limit": "23.00 kB" } ] } diff --git a/packages/storage/__tests__/providers/s3/utils/client/s3Data/completeMultipartUpload.test.ts b/packages/storage/__tests__/providers/s3/utils/client/s3Data/completeMultipartUpload.test.ts index 5036a9de6fb..16e65d11b21 100644 --- a/packages/storage/__tests__/providers/s3/utils/client/s3Data/completeMultipartUpload.test.ts +++ b/packages/storage/__tests__/providers/s3/utils/client/s3Data/completeMultipartUpload.test.ts @@ -83,7 +83,6 @@ describe('completeMultipartUploadSerializer', () => { ], }, }); - console.log(output); expect(output).toEqual({ $metadata: expect.objectContaining(expectedMetadata), }); @@ -140,4 +139,18 @@ describe('completeMultipartUploadSerializer', () => { }), ).rejects.toThrow(integrityError); }); + + it('should fail with specific error messaging when ETag is missing from response', () => { + mockS3TransferHandler.mockResolvedValue( + mockBinaryResponse(completeMultipartUploadSuccessResponse), + ); + expect( + completeMultipartUpload(defaultConfig, { + Bucket: 'bucket', + Key: 'key', + UploadId: 'uploadId', + MultipartUpload: { Parts: [{ PartNumber: 1 }] }, + }), + ).rejects.toThrow('ETag missing'); + }); }); diff --git a/packages/storage/src/providers/s3/utils/client/s3data/completeMultipartUpload.ts b/packages/storage/src/providers/s3/utils/client/s3data/completeMultipartUpload.ts index e7c4c516157..6ce44756b76 100644 --- a/packages/storage/src/providers/s3/utils/client/s3data/completeMultipartUpload.ts +++ b/packages/storage/src/providers/s3/utils/client/s3data/completeMultipartUpload.ts @@ -36,7 +36,11 @@ import type { } from './types'; const INVALID_PARAMETER_ERROR_MSG = - 'Invalid parameter for ComplteMultipartUpload API'; + 'Invalid parameter for CompleteMultipartUpload API'; + +const MISSING_ETAG_ERROR_MSG = 'ETag missing from multipart upload'; +const MISSING_ETAG_ERROR_SUGGESTION = + 'Please ensure S3 bucket CORS configuration includes ETag as part of its `ExposeHeaders` element'; export type CompleteMultipartUploadInput = Pick< CompleteMultipartUploadCommandInput, @@ -95,7 +99,7 @@ const serializeCompletedMultipartUpload = ( input: CompletedMultipartUpload, ): string => { if (!input.Parts?.length) { - throw new Error(`${INVALID_PARAMETER_ERROR_MSG}: ${input}`); + throw new Error(`${INVALID_PARAMETER_ERROR_MSG}: ${JSON.stringify(input)}`); } return `${input.Parts.map( @@ -104,8 +108,13 @@ const serializeCompletedMultipartUpload = ( }; const serializeCompletedPartList = (input: CompletedPart): string => { - if (!input.ETag || input.PartNumber == null) { - throw new Error(`${INVALID_PARAMETER_ERROR_MSG}: ${input}`); + if (input.PartNumber == null) { + throw new Error(`${INVALID_PARAMETER_ERROR_MSG}: ${JSON.stringify(input)}`); + } + if (!input.ETag) { + throw new Error( + `${MISSING_ETAG_ERROR_MSG}: ${JSON.stringify(input)}. ${MISSING_ETAG_ERROR_SUGGESTION}`, + ); } const eTag = `${input.ETag}`; From 6211707178ccc9aab6f1cc20dc3a54f9fb9ca6bf Mon Sep 17 00:00:00 2001 From: Tiffany Yeung Date: Wed, 29 Jan 2025 10:48:27 -0800 Subject: [PATCH 6/8] chore(gh-workflow): apply maintainer response label only to open issues --- .github/workflows/issue-comment.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-comment.yml b/.github/workflows/issue-comment.yml index cfde2f7d331..eb34e501b89 100644 --- a/.github/workflows/issue-comment.yml +++ b/.github/workflows/issue-comment.yml @@ -19,8 +19,8 @@ jobs: shell: bash run: | gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --remove-label "pending-community-response" - - name: Add pending-maintainer-response when new community comment received - if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }} + - name: Add pending-maintainer-response when new community comment received on open issues + if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) && github.event.issue.state == 'open' }} shell: bash run: | gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --add-label "pending-maintainer-response" From 25dbd610febc655772468f620c7570e696bbee2a Mon Sep 17 00:00:00 2001 From: Chris F <5827964+cshfang@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:27:49 -0800 Subject: [PATCH 7/8] chore(auth): Refactor to eliminate circular dependencies (#14173) * chore(auth): Refactor to eliminate circular dependencies * Bump size limit --- .../flows/shared/handlePasswordSRP.test.ts | 52 +-- .../handleSelectChallengeWithPassword.test.ts | 18 +- ...ndleSelectChallengeWithPasswordSRP.test.ts | 49 +-- .../cognito/getNewDeviceMetadata.test.ts | 2 +- .../handleWebAuthnSignInResult.test.ts | 50 ++- .../client/flows/shared/handlePasswordSRP.ts | 8 +- .../handleSelectChallengeWithPassword.ts | 2 +- .../handleSelectChallengeWithPasswordSRP.ts | 8 +- .../flows/userAuth/handleUserAuthFlow.ts | 2 +- .../userAuth/handleWebAuthnSignInResult.ts | 14 +- .../auth/src/client/flows/userAuth/types.ts | 15 + .../providers/cognito/apis/confirmSignIn.ts | 2 +- .../cognito/apis/signInWithCustomAuth.ts | 4 +- .../cognito/apis/signInWithCustomSRPAuth.ts | 2 +- .../providers/cognito/apis/signInWithSRP.ts | 2 +- .../cognito/apis/signInWithUserAuth.ts | 2 +- .../cognito/apis/signInWithUserPassword.ts | 4 +- .../cognito/utils/getNewDeviceMetadata.ts | 88 +++++ .../cognito/utils/handleDeviceSRPAuth.ts | 146 ++++++++ .../utils/handlePasswordVerifierChallenge.ts | 106 ++++++ .../utils/retryOnResourceNotFoundException.ts | 34 ++ .../cognito/utils/setActiveSignInUsername.ts | 10 + .../providers/cognito/utils/signInHelpers.ts | 346 +----------------- packages/aws-amplify/package.json | 4 +- 24 files changed, 550 insertions(+), 420 deletions(-) create mode 100644 packages/auth/src/client/flows/userAuth/types.ts create mode 100644 packages/auth/src/providers/cognito/utils/getNewDeviceMetadata.ts create mode 100644 packages/auth/src/providers/cognito/utils/handleDeviceSRPAuth.ts create mode 100644 packages/auth/src/providers/cognito/utils/handlePasswordVerifierChallenge.ts create mode 100644 packages/auth/src/providers/cognito/utils/retryOnResourceNotFoundException.ts create mode 100644 packages/auth/src/providers/cognito/utils/setActiveSignInUsername.ts diff --git a/packages/auth/__tests__/client/flows/shared/handlePasswordSRP.test.ts b/packages/auth/__tests__/client/flows/shared/handlePasswordSRP.test.ts index de71d7a071b..6c66929065a 100644 --- a/packages/auth/__tests__/client/flows/shared/handlePasswordSRP.test.ts +++ b/packages/auth/__tests__/client/flows/shared/handlePasswordSRP.test.ts @@ -6,7 +6,9 @@ import { createCognitoUserPoolEndpointResolver } from '../../../../src/providers import { getAuthenticationHelper } from '../../../../src/providers/cognito/utils/srp'; import { getUserContextData } from '../../../../src/providers/cognito/utils/userContextData'; import { handlePasswordSRP } from '../../../../src/client/flows/shared/handlePasswordSRP'; -import * as signInHelpers from '../../../../src/providers/cognito/utils/signInHelpers'; +import { handlePasswordVerifierChallenge } from '../../../../src/providers/cognito/utils/handlePasswordVerifierChallenge'; +import { retryOnResourceNotFoundException } from '../../../../src/providers/cognito/utils/retryOnResourceNotFoundException'; +import { setActiveSignInUsername } from '../../../../src/providers/cognito/utils/setActiveSignInUsername'; // Mock dependencies jest.mock( @@ -15,14 +17,13 @@ jest.mock( jest.mock('../../../../src/providers/cognito/factories'); jest.mock('../../../../src/providers/cognito/utils/srp'); jest.mock('../../../../src/providers/cognito/utils/userContextData'); -jest.mock('../../../../src/providers/cognito/utils/signInHelpers', () => ({ - ...jest.requireActual( - '../../../../src/providers/cognito/utils/signInHelpers', - ), - setActiveSignInUsername: jest.fn(), - handlePasswordVerifierChallenge: jest.fn(), - retryOnResourceNotFoundException: jest.fn(), -})); +jest.mock( + '../../../../src/providers/cognito/utils/handlePasswordVerifierChallenge', +); +jest.mock( + '../../../../src/providers/cognito/utils/retryOnResourceNotFoundException', +); +jest.mock('../../../../src/providers/cognito/utils/setActiveSignInUsername'); describe('handlePasswordSRP', () => { const mockConfig = { @@ -31,6 +32,13 @@ describe('handlePasswordSRP', () => { userPoolEndpoint: 'test-endpoint', }; + const mockHandlePasswordVerifierChallenge = jest.mocked( + handlePasswordVerifierChallenge, + ); + const mockRetryOnResourceNotFoundException = jest.mocked( + retryOnResourceNotFoundException, + ); + const mockSetActiveSignInUsername = jest.mocked(setActiveSignInUsername); const mockInitiateAuth = jest.fn(); const mockCreateEndpointResolver = jest.fn(); const mockAuthenticationHelper = { @@ -53,9 +61,9 @@ describe('handlePasswordSRP', () => { (getUserContextData as jest.Mock).mockReturnValue({ UserContextData: 'test', }); - ( - signInHelpers.retryOnResourceNotFoundException as jest.Mock - ).mockImplementation((fn, args) => fn(...args)); + mockRetryOnResourceNotFoundException.mockImplementation((fn, args) => + fn(...args), + ); mockInitiateAuth.mockResolvedValue({ ChallengeParameters: { USERNAME: 'testuser' }, Session: 'test-session', @@ -173,8 +181,8 @@ describe('handlePasswordSRP', () => { authFlow: 'USER_AUTH', }); - expect(signInHelpers.retryOnResourceNotFoundException).toHaveBeenCalledWith( - signInHelpers.handlePasswordVerifierChallenge, + expect(mockRetryOnResourceNotFoundException).toHaveBeenCalledWith( + mockHandlePasswordVerifierChallenge, [ password, challengeParameters, @@ -208,9 +216,7 @@ describe('handlePasswordSRP', () => { }); expect(result).toEqual(mockResponse); - expect( - signInHelpers.retryOnResourceNotFoundException, - ).not.toHaveBeenCalled(); + expect(mockRetryOnResourceNotFoundException).not.toHaveBeenCalled(); }); test('should handle client metadata when provided', async () => { @@ -254,9 +260,7 @@ describe('handlePasswordSRP', () => { authFlow: 'USER_SRP_AUTH', }); - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - challengeUsername, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(challengeUsername); }); test('should call handlePasswordVerifierChallenge with correct parameters', async () => { @@ -285,8 +289,8 @@ describe('handlePasswordSRP', () => { authFlow: 'USER_SRP_AUTH', }); - expect(signInHelpers.retryOnResourceNotFoundException).toHaveBeenCalledWith( - signInHelpers.handlePasswordVerifierChallenge, + expect(mockRetryOnResourceNotFoundException).toHaveBeenCalledWith( + mockHandlePasswordVerifierChallenge, [ password, challengeParameters, @@ -341,9 +345,7 @@ describe('handlePasswordSRP', () => { authFlow: 'USER_AUTH', }); - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - username, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(username); }); test('should not add PREFERRED_CHALLENGE for USER_AUTH when preferredChallenge is undefined', async () => { diff --git a/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPassword.test.ts b/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPassword.test.ts index 78322b59536..9d304e03871 100644 --- a/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPassword.test.ts +++ b/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPassword.test.ts @@ -5,7 +5,7 @@ import { createRespondToAuthChallengeClient } from '../../../../src/foundation/f import { createCognitoUserPoolEndpointResolver } from '../../../../src/providers/cognito/factories'; import { getUserContextData } from '../../../../src/providers/cognito/utils/userContextData'; import { handleSelectChallengeWithPassword } from '../../../../src/client/flows/userAuth/handleSelectChallengeWithPassword'; -import * as signInHelpers from '../../../../src/providers/cognito/utils/signInHelpers'; +import { setActiveSignInUsername } from '../../../../src/providers/cognito/utils/setActiveSignInUsername'; // Mock dependencies jest.mock( @@ -13,12 +13,7 @@ jest.mock( ); jest.mock('../../../../src/providers/cognito/factories'); jest.mock('../../../../src/providers/cognito/utils/userContextData'); -jest.mock('../../../../src/providers/cognito/utils/signInHelpers', () => ({ - ...jest.requireActual( - '../../../../src/providers/cognito/utils/signInHelpers', - ), - setActiveSignInUsername: jest.fn(), -})); +jest.mock('../../../../src/providers/cognito/utils/setActiveSignInUsername'); describe('handlePasswordChallenge', () => { const mockConfig = { @@ -27,6 +22,7 @@ describe('handlePasswordChallenge', () => { userPoolEndpoint: 'test-endpoint', }; + const mockSetActiveSignInUsername = jest.mocked(setActiveSignInUsername); const mockRespondToAuthChallenge = jest.fn(); const mockCreateEndpointResolver = jest.fn(); @@ -124,9 +120,7 @@ describe('handlePasswordChallenge', () => { session, ); - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - challengeUsername, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(challengeUsername); }); test('should set active username as original username when challenge parameters are missing', async () => { @@ -148,9 +142,7 @@ describe('handlePasswordChallenge', () => { session, ); - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - username, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(username); }); test('should throw error when respondToAuthChallenge fails', async () => { diff --git a/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.test.ts b/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.test.ts index b89414c3ae1..ba44e6e293e 100644 --- a/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.test.ts +++ b/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.test.ts @@ -5,7 +5,9 @@ import { createRespondToAuthChallengeClient } from '../../../../src/foundation/f import { getAuthenticationHelper } from '../../../../src/providers/cognito/utils/srp'; import { getUserContextData } from '../../../../src/providers/cognito/utils/userContextData'; import { handleSelectChallengeWithPasswordSRP } from '../../../../src/client/flows/userAuth/handleSelectChallengeWithPasswordSRP'; -import * as signInHelpers from '../../../../src/providers/cognito/utils/signInHelpers'; +import { handlePasswordVerifierChallenge } from '../../../../src/providers/cognito/utils/handlePasswordVerifierChallenge'; +import { retryOnResourceNotFoundException } from '../../../../src/providers/cognito/utils/retryOnResourceNotFoundException'; +import { setActiveSignInUsername } from '../../../../src/providers/cognito/utils/setActiveSignInUsername'; // Mock dependencies jest.mock( @@ -14,14 +16,13 @@ jest.mock( jest.mock('../../../../src/providers/cognito/factories'); jest.mock('../../../../src/providers/cognito/utils/srp'); jest.mock('../../../../src/providers/cognito/utils/userContextData'); -jest.mock('../../../../src/providers/cognito/utils/signInHelpers', () => ({ - ...jest.requireActual( - '../../../../src/providers/cognito/utils/signInHelpers', - ), - setActiveSignInUsername: jest.fn(), - handlePasswordVerifierChallenge: jest.fn(), - retryOnResourceNotFoundException: jest.fn(), -})); +jest.mock( + '../../../../src/providers/cognito/utils/handlePasswordVerifierChallenge', +); +jest.mock( + '../../../../src/providers/cognito/utils/retryOnResourceNotFoundException', +); +jest.mock('../../../../src/providers/cognito/utils/setActiveSignInUsername'); describe('handleSelectChallengeWithPasswordSRP', () => { const mockConfig = { @@ -35,6 +36,13 @@ describe('handleSelectChallengeWithPasswordSRP', () => { clearDeviceMetadata: jest.fn(), } as any; + const mockHandlePasswordVerifierChallenge = jest.mocked( + handlePasswordVerifierChallenge, + ); + const mockRetryOnResourceNotFoundException = jest.mocked( + retryOnResourceNotFoundException, + ); + const mockSetActiveSignInUsername = jest.mocked(setActiveSignInUsername); const mockRespondToAuthChallenge = jest.fn(); const mockAuthenticationHelper = { A: { toString: () => '123456' }, @@ -108,13 +116,12 @@ describe('handleSelectChallengeWithPasswordSRP', () => { }; mockRespondToAuthChallenge.mockResolvedValueOnce(verifierResponse); - ( - signInHelpers.retryOnResourceNotFoundException as jest.Mock - ).mockImplementation((fn, args) => fn(...args)); - ( - signInHelpers.handlePasswordVerifierChallenge as jest.Mock - ).mockResolvedValue({ + mockRetryOnResourceNotFoundException.mockImplementation((fn, args) => + fn(...args), + ); + mockHandlePasswordVerifierChallenge.mockResolvedValue({ AuthenticationResult: { AccessToken: 'token' }, + $metadata: {}, }); await handleSelectChallengeWithPasswordSRP( @@ -126,8 +133,8 @@ describe('handleSelectChallengeWithPasswordSRP', () => { mockTokenOrchestrator, ); - expect(signInHelpers.retryOnResourceNotFoundException).toHaveBeenCalledWith( - signInHelpers.handlePasswordVerifierChallenge, + expect(mockRetryOnResourceNotFoundException).toHaveBeenCalledWith( + mockHandlePasswordVerifierChallenge, [ password, verifierResponse.ChallengeParameters, @@ -188,9 +195,7 @@ describe('handleSelectChallengeWithPasswordSRP', () => { mockTokenOrchestrator, ); - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - challengeUsername, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(challengeUsername); }); test('should use original username when ChallengeParameters is undefined', async () => { @@ -215,9 +220,7 @@ describe('handleSelectChallengeWithPasswordSRP', () => { ); // Verify it falls back to the original username - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - username, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(username); }); test('should handle userPoolId without second part after underscore', async () => { diff --git a/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts b/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts index 6058fc363b7..f05c4f8c603 100644 --- a/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts +++ b/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts @@ -5,7 +5,7 @@ import { Amplify } from '@aws-amplify/core'; import { AuthError } from '../../../src/errors/AuthError'; import { ConfirmDeviceException } from '../../../src/providers/cognito/types/errors'; -import { getNewDeviceMetadata } from '../../../src/providers/cognito/utils/signInHelpers'; +import { getNewDeviceMetadata } from '../../../src/providers/cognito/utils/getNewDeviceMetadata'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; import { createConfirmDeviceClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; diff --git a/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts b/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts index dc9a7c2296a..bf5773ac0bd 100644 --- a/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts @@ -19,6 +19,11 @@ import { assertCredentialIsPkcWithAuthenticatorAssertionResponse, assertCredentialIsPkcWithAuthenticatorAttestationResponse, } from '../../../../../src/client/utils/passkey/types'; +import { AuthSignInOutput } from '../../../../../src/types'; +import { + ChallengeName, + ChallengeParameters, +} from '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; jest.mock('@aws-amplify/core', () => ({ ...(jest.createMockFromModule('@aws-amplify/core') as object), @@ -101,6 +106,7 @@ describe('handleWebAuthnSignInResult', () => { expect(error.name).toBe(AuthErrorCodes.SignInException); } }); + it('should throw an error when CREDENTIAL_REQUEST_OPTIONS is empty', async () => { expect.assertions(2); try { @@ -166,9 +172,51 @@ describe('handleWebAuthnSignInResult', () => { mockCacheCognitoTokens.mockResolvedValue(undefined); mockDispatchSignedInHubEvent.mockResolvedValue(undefined); - const result = await handleWebAuthnSignInResult(challengeParameters); + const result = (await handleWebAuthnSignInResult( + challengeParameters, + )) as AuthSignInOutput; expect(result.isSignedIn).toBe(true); expect(result.nextStep.signInStep).toBe('DONE'); }); + + it('should return the next challenge', async () => { + mockStoreGetState.mockReturnValue({ + username, + challengeName, + signInSession, + }); + mockRespondToAuthChallenge.mockResolvedValue( + authAPITestParams.CustomChallengeResponse, + ); + mockCacheCognitoTokens.mockResolvedValue(undefined); + mockDispatchSignedInHubEvent.mockResolvedValue(undefined); + + const result = (await handleWebAuthnSignInResult(challengeParameters)) as { + challengeName: ChallengeName; + challengeParameters: ChallengeParameters; + }; + + expect(result.challengeName).toBe( + authAPITestParams.CustomChallengeResponse.ChallengeName, + ); + }); + + it('should throw an error if next challenge is WEB_AUTHN', async () => { + mockStoreGetState.mockReturnValue({ + username, + challengeName, + signInSession, + }); + mockRespondToAuthChallenge.mockResolvedValue({ + ChallengeName: 'WEB_AUTHN', + Session: 'Session', + }); + mockCacheCognitoTokens.mockResolvedValue(undefined); + mockDispatchSignedInHubEvent.mockResolvedValue(undefined); + + await expect( + handleWebAuthnSignInResult(challengeParameters), + ).rejects.toThrow('Sequential WEB_AUTHN challenges returned'); + }); }); diff --git a/packages/auth/src/client/flows/shared/handlePasswordSRP.ts b/packages/auth/src/client/flows/shared/handlePasswordSRP.ts index 77e298867df..6afbb873229 100644 --- a/packages/auth/src/client/flows/shared/handlePasswordSRP.ts +++ b/packages/auth/src/client/flows/shared/handlePasswordSRP.ts @@ -13,16 +13,14 @@ import { RespondToAuthChallengeCommandOutput, } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { getAuthenticationHelper } from '../../../providers/cognito/utils/srp'; -import { - handlePasswordVerifierChallenge, - retryOnResourceNotFoundException, - setActiveSignInUsername, -} from '../../../providers/cognito/utils/signInHelpers'; import { createInitiateAuthClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../providers/cognito/factories'; import { getRegionFromUserPoolId } from '../../../foundation/parsers'; import { getAuthUserAgentValue } from '../../../utils'; import { AuthFactorType } from '../../../providers/cognito/types/models'; +import { handlePasswordVerifierChallenge } from '../../../providers/cognito/utils/handlePasswordVerifierChallenge'; +import { retryOnResourceNotFoundException } from '../../../providers/cognito/utils/retryOnResourceNotFoundException'; +import { setActiveSignInUsername } from '../../../providers/cognito/utils/setActiveSignInUsername'; interface HandlePasswordSRPInput { username: string; diff --git a/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPassword.ts b/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPassword.ts index 50858764c79..3369f755148 100644 --- a/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPassword.ts +++ b/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPassword.ts @@ -11,7 +11,7 @@ import { getRegionFromUserPoolId } from '../../../foundation/parsers'; import { getAuthUserAgentValue } from '../../../utils'; import { getUserContextData } from '../../../providers/cognito/utils/userContextData'; import { RespondToAuthChallengeCommandOutput } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; -import { setActiveSignInUsername } from '../../../providers/cognito/utils/signInHelpers'; +import { setActiveSignInUsername } from '../../../providers/cognito/utils/setActiveSignInUsername'; /** * Handles the SELECT_CHALLENGE response specifically for Password authentication. diff --git a/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.ts b/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.ts index 1a463e60a68..2613b0abbbe 100644 --- a/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.ts +++ b/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.ts @@ -16,11 +16,9 @@ import { getRegionFromUserPoolId } from '../../../foundation/parsers'; import { getAuthUserAgentValue } from '../../../utils'; import { getAuthenticationHelper } from '../../../providers/cognito/utils/srp'; import { getUserContextData } from '../../../providers/cognito/utils/userContextData'; -import { - handlePasswordVerifierChallenge, - retryOnResourceNotFoundException, - setActiveSignInUsername, -} from '../../../providers/cognito/utils/signInHelpers'; +import { setActiveSignInUsername } from '../../../providers/cognito/utils/setActiveSignInUsername'; +import { retryOnResourceNotFoundException } from '../../../providers/cognito/utils/retryOnResourceNotFoundException'; +import { handlePasswordVerifierChallenge } from '../../../providers/cognito/utils/handlePasswordVerifierChallenge'; /** * Handles the SELECT_CHALLENGE response specifically for Password SRP authentication. diff --git a/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts b/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts index 753ac66db04..f45530b7aa8 100644 --- a/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts +++ b/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts @@ -18,7 +18,7 @@ import { getAuthUserAgentValue } from '../../../utils'; import { handlePasswordSRP } from '../shared/handlePasswordSRP'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; -import { setActiveSignInUsername } from '../../../providers/cognito/utils/signInHelpers'; +import { setActiveSignInUsername } from '../../../providers/cognito/utils/setActiveSignInUsername'; export interface HandleUserAuthFlowInput { username: string; diff --git a/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts b/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts index ee3bc6e6b6c..b0105694047 100644 --- a/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts +++ b/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts @@ -17,12 +17,7 @@ import { getRegionFromUserPoolId } from '../../../foundation/parsers'; import { createCognitoUserPoolEndpointResolver } from '../../../providers/cognito/factories'; import { cacheCognitoTokens } from '../../../providers/cognito/tokenProvider/cacheTokens'; import { dispatchSignedInHubEvent } from '../../../providers/cognito/utils/dispatchSignedInHubEvent'; -import { - getNewDeviceMetadata, - getSignInResult, -} from '../../../providers/cognito/utils/signInHelpers'; import { setActiveSignInState, signInStore } from '../../../client/utils/store'; -import { AuthSignInOutput } from '../../../types'; import { getAuthUserAgentValue } from '../../../utils'; import { getPasskey } from '../../utils/passkey'; import { @@ -30,10 +25,13 @@ import { assertPasskeyError, } from '../../utils/passkey/errors'; import { AuthError } from '../../../errors/AuthError'; +import { getNewDeviceMetadata } from '../../../providers/cognito/utils/getNewDeviceMetadata'; + +import { WebAuthnSignInResult } from './types'; export async function handleWebAuthnSignInResult( challengeParameters: ChallengeParameters, -): Promise { +): Promise { const authConfig = Amplify.getConfig().Auth?.Cognito; assertTokenProviderConfig(authConfig); const { username, signInSession, signInDetails, challengeName } = @@ -119,8 +117,8 @@ export async function handleWebAuthnSignInResult( }); } - return getSignInResult({ + return { challengeName: nextChallengeName as ChallengeName, challengeParameters: nextChallengeParameters as ChallengeParameters, - }); + }; } diff --git a/packages/auth/src/client/flows/userAuth/types.ts b/packages/auth/src/client/flows/userAuth/types.ts new file mode 100644 index 00000000000..d5e644f489b --- /dev/null +++ b/packages/auth/src/client/flows/userAuth/types.ts @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + ChallengeName, + ChallengeParameters, +} from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { AuthSignInOutput } from '../../../types'; + +export type WebAuthnSignInResult = + | AuthSignInOutput + | { + challengeName: ChallengeName; + challengeParameters: ChallengeParameters; + }; diff --git a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts index 3edb0e9eab0..48c904d5897 100644 --- a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts +++ b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts @@ -17,7 +17,6 @@ import { } from '../../../client/utils/store'; import { AuthError } from '../../../errors/AuthError'; import { - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, handleChallengeName, @@ -33,6 +32,7 @@ import { ChallengeName, ChallengeParameters, } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; /** * Continues or completes the sign in process when required by the initial call to `signIn`. diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts index 3ee2de1302c..a1260538d17 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts @@ -9,11 +9,9 @@ import { assertValidationError } from '../../../errors/utils/assertValidationErr import { assertServiceError } from '../../../errors/utils/assertServiceError'; import { getActiveSignInUsername, - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, handleCustomAuthFlowWithoutSRP, - retryOnResourceNotFoundException, } from '../utils/signInHelpers'; import { InitiateAuthException } from '../types/errors'; import { @@ -32,6 +30,8 @@ import { } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; +import { retryOnResourceNotFoundException } from '../utils/retryOnResourceNotFoundException'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; /** * Signs a user in using a custom authentication flow without password diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts index 35eb7f29419..3827699f476 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts @@ -9,7 +9,6 @@ import { assertValidationError } from '../../../errors/utils/assertValidationErr import { assertServiceError } from '../../../errors/utils/assertServiceError'; import { getActiveSignInUsername, - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, handleCustomSRPAuthFlow, @@ -34,6 +33,7 @@ import { } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; /** * Signs a user in using a custom authentication flow with SRP diff --git a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts index 05c79cf35a0..d2d9588f6bc 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts @@ -17,7 +17,6 @@ import { } from '../types/errors'; import { getActiveSignInUsername, - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, handleUserSRPAuthFlow, @@ -34,6 +33,7 @@ import { import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; import { resetAutoSignIn } from './autoSignIn'; diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts index 4f653f46c94..1b2957c9480 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts @@ -17,7 +17,6 @@ import { } from '../types/errors'; import { getActiveSignInUsername, - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, } from '../utils/signInHelpers'; @@ -38,6 +37,7 @@ import { HandleUserAuthFlowInput, handleUserAuthFlow, } from '../../../client/flows/userAuth/handleUserAuthFlow'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; import { resetAutoSignIn } from './autoSignIn'; diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts index 56e1c1af9f5..e9280227a37 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts @@ -13,11 +13,9 @@ import { } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { getActiveSignInUsername, - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, handleUserPasswordAuthFlow, - retryOnResourceNotFoundException, } from '../utils/signInHelpers'; import { InitiateAuthException } from '../types/errors'; import { @@ -32,6 +30,8 @@ import { import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; +import { retryOnResourceNotFoundException } from '../utils/retryOnResourceNotFoundException'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; import { resetAutoSignIn } from './autoSignIn'; diff --git a/packages/auth/src/providers/cognito/utils/getNewDeviceMetadata.ts b/packages/auth/src/providers/cognito/utils/getNewDeviceMetadata.ts new file mode 100644 index 00000000000..ea687b4b3d1 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/getNewDeviceMetadata.ts @@ -0,0 +1,88 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + base64Encoder, + getDeviceName, +} from '@aws-amplify/core/internals/utils'; + +import { DeviceMetadata } from '../tokenProvider/types'; +import { createConfirmDeviceClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider'; +import { createCognitoUserPoolEndpointResolver } from '../factories'; +import { NewDeviceMetadataType } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { getRegionFromUserPoolId } from '../../../foundation/parsers'; + +import { getAuthenticationHelper, getBytesFromHex } from './srp'; + +/** + * This function is used to kick off the device management flow. + * + * If an error is thrown while generating a hash device or calling the `ConfirmDevice` + * client, then this API will ignore the error and return undefined. Otherwise the authentication + * flow will not complete and the user won't be able to be signed in. + * + * @returns DeviceMetadata | undefined + */ +export async function getNewDeviceMetadata({ + userPoolId, + userPoolEndpoint, + newDeviceMetadata, + accessToken, +}: { + userPoolId: string; + userPoolEndpoint: string | undefined; + newDeviceMetadata?: NewDeviceMetadataType; + accessToken?: string; +}): Promise { + if (!newDeviceMetadata) return undefined; + const userPoolName = userPoolId.split('_')[1] || ''; + const authenticationHelper = await getAuthenticationHelper(userPoolName); + const deviceKey = newDeviceMetadata?.DeviceKey; + const deviceGroupKey = newDeviceMetadata?.DeviceGroupKey; + + try { + await authenticationHelper.generateHashDevice( + deviceGroupKey ?? '', + deviceKey ?? '', + ); + } catch (errGenHash) { + // TODO: log error here + return undefined; + } + + const deviceSecretVerifierConfig = { + Salt: base64Encoder.convert( + getBytesFromHex(authenticationHelper.getSaltToHashDevices()), + ), + PasswordVerifier: base64Encoder.convert( + getBytesFromHex(authenticationHelper.getVerifierDevices()), + ), + }; + const randomPassword = authenticationHelper.getRandomPassword(); + + try { + const confirmDevice = createConfirmDeviceClient({ + endpointResolver: createCognitoUserPoolEndpointResolver({ + endpointOverride: userPoolEndpoint, + }), + }); + await confirmDevice( + { region: getRegionFromUserPoolId(userPoolId) }, + { + AccessToken: accessToken, + DeviceName: await getDeviceName(), + DeviceKey: newDeviceMetadata?.DeviceKey, + DeviceSecretVerifierConfig: deviceSecretVerifierConfig, + }, + ); + + return { + deviceKey, + deviceGroupKey, + randomPassword, + }; + } catch (error) { + // TODO: log error here + return undefined; + } +} diff --git a/packages/auth/src/providers/cognito/utils/handleDeviceSRPAuth.ts b/packages/auth/src/providers/cognito/utils/handleDeviceSRPAuth.ts new file mode 100644 index 00000000000..27f5b1c1e5c --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/handleDeviceSRPAuth.ts @@ -0,0 +1,146 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CognitoUserPoolConfig } from '@aws-amplify/core'; + +import { ClientMetadata } from '../types'; +import { AuthTokenOrchestrator } from '../tokenProvider/types'; +import { createRespondToAuthChallengeClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider'; +import { createCognitoUserPoolEndpointResolver } from '../factories'; +import { + ChallengeParameters, + RespondToAuthChallengeCommandInput, + RespondToAuthChallengeCommandOutput, +} from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { getRegionFromUserPoolId } from '../../../foundation/parsers'; + +import { assertDeviceMetadata } from './types'; +import { + getAuthenticationHelper, + getNowString, + getSignatureString, +} from './srp'; +import { BigInteger } from './srp/BigInteger'; +import { AuthenticationHelper } from './srp/AuthenticationHelper'; +import { getUserContextData } from './userContextData'; + +interface HandleDeviceSRPInput { + username: string; + config: CognitoUserPoolConfig; + clientMetadata: ClientMetadata | undefined; + session: string | undefined; + tokenOrchestrator?: AuthTokenOrchestrator; +} + +export async function handleDeviceSRPAuth({ + username, + config, + clientMetadata, + session, + tokenOrchestrator, +}: HandleDeviceSRPInput): Promise { + const { userPoolId, userPoolEndpoint } = config; + const clientId = config.userPoolClientId; + const deviceMetadata = await tokenOrchestrator?.getDeviceMetadata(username); + assertDeviceMetadata(deviceMetadata); + const authenticationHelper = await getAuthenticationHelper( + deviceMetadata.deviceGroupKey, + ); + const challengeResponses: Record = { + USERNAME: username, + SRP_A: authenticationHelper.A.toString(16), + DEVICE_KEY: deviceMetadata.deviceKey, + }; + + const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { + ChallengeName: 'DEVICE_SRP_AUTH', + ClientId: clientId, + ChallengeResponses: challengeResponses, + ClientMetadata: clientMetadata, + Session: session, + }; + const respondToAuthChallenge = createRespondToAuthChallengeClient({ + endpointResolver: createCognitoUserPoolEndpointResolver({ + endpointOverride: userPoolEndpoint, + }), + }); + const { ChallengeParameters: respondedChallengeParameters, Session } = + await respondToAuthChallenge( + { region: getRegionFromUserPoolId(userPoolId) }, + jsonReqResponseChallenge, + ); + + return handleDevicePasswordVerifier( + username, + respondedChallengeParameters as ChallengeParameters, + clientMetadata, + Session, + authenticationHelper, + config, + tokenOrchestrator, + ); +} + +async function handleDevicePasswordVerifier( + username: string, + challengeParameters: ChallengeParameters, + clientMetadata: ClientMetadata | undefined, + session: string | undefined, + authenticationHelper: AuthenticationHelper, + { userPoolId, userPoolClientId, userPoolEndpoint }: CognitoUserPoolConfig, + tokenOrchestrator?: AuthTokenOrchestrator, +): Promise { + const deviceMetadata = await tokenOrchestrator?.getDeviceMetadata(username); + assertDeviceMetadata(deviceMetadata); + + const serverBValue = new BigInteger(challengeParameters?.SRP_B, 16); + const salt = new BigInteger(challengeParameters?.SALT, 16); + const { deviceKey } = deviceMetadata; + const { deviceGroupKey } = deviceMetadata; + const hkdf = await authenticationHelper.getPasswordAuthenticationKey({ + username: deviceMetadata.deviceKey, + password: deviceMetadata.randomPassword, + serverBValue, + salt, + }); + + const dateNow = getNowString(); + const challengeResponses = { + USERNAME: (challengeParameters?.USERNAME as string) ?? username, + PASSWORD_CLAIM_SECRET_BLOCK: challengeParameters?.SECRET_BLOCK, + TIMESTAMP: dateNow, + PASSWORD_CLAIM_SIGNATURE: getSignatureString({ + username: deviceKey, + userPoolName: deviceGroupKey, + challengeParameters, + dateNow, + hkdf, + }), + DEVICE_KEY: deviceKey, + } as Record; + + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + + const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { + ChallengeName: 'DEVICE_PASSWORD_VERIFIER', + ClientId: userPoolClientId, + ChallengeResponses: challengeResponses, + Session: session, + ClientMetadata: clientMetadata, + UserContextData, + }; + const respondToAuthChallenge = createRespondToAuthChallengeClient({ + endpointResolver: createCognitoUserPoolEndpointResolver({ + endpointOverride: userPoolEndpoint, + }), + }); + + return respondToAuthChallenge( + { region: getRegionFromUserPoolId(userPoolId) }, + jsonReqResponseChallenge, + ); +} diff --git a/packages/auth/src/providers/cognito/utils/handlePasswordVerifierChallenge.ts b/packages/auth/src/providers/cognito/utils/handlePasswordVerifierChallenge.ts new file mode 100644 index 00000000000..aa35a918d99 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/handlePasswordVerifierChallenge.ts @@ -0,0 +1,106 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CognitoUserPoolConfig } from '@aws-amplify/core'; + +import { ClientMetadata } from '../types'; +import { AuthError } from '../../../errors/AuthError'; +import { AuthTokenOrchestrator } from '../tokenProvider/types'; +import { createRespondToAuthChallengeClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider'; +import { createCognitoUserPoolEndpointResolver } from '../factories'; +import { + ChallengeParameters, + RespondToAuthChallengeCommandInput, + RespondToAuthChallengeCommandOutput, +} from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { getRegionFromUserPoolId } from '../../../foundation/parsers'; + +import { getNowString, getSignatureString } from './srp'; +import { BigInteger } from './srp/BigInteger'; +import { AuthenticationHelper } from './srp/AuthenticationHelper'; +import { getUserContextData } from './userContextData'; +import { handleDeviceSRPAuth } from './handleDeviceSRPAuth'; + +export async function handlePasswordVerifierChallenge( + password: string, + challengeParameters: ChallengeParameters, + clientMetadata: ClientMetadata | undefined, + session: string | undefined, + authenticationHelper: AuthenticationHelper, + config: CognitoUserPoolConfig, + tokenOrchestrator: AuthTokenOrchestrator, +): Promise { + const { userPoolId, userPoolClientId, userPoolEndpoint } = config; + const userPoolName = userPoolId?.split('_')[1] || ''; + const serverBValue = new (BigInteger as any)(challengeParameters?.SRP_B, 16); + const salt = new (BigInteger as any)(challengeParameters?.SALT, 16); + const username = challengeParameters?.USER_ID_FOR_SRP; + if (!username) + throw new AuthError({ + name: 'EmptyUserIdForSRPException', + message: 'USER_ID_FOR_SRP was not found in challengeParameters', + }); + const hkdf = await authenticationHelper.getPasswordAuthenticationKey({ + username, + password, + serverBValue, + salt, + }); + + const dateNow = getNowString(); + + const challengeResponses = { + USERNAME: username, + PASSWORD_CLAIM_SECRET_BLOCK: challengeParameters?.SECRET_BLOCK, + TIMESTAMP: dateNow, + PASSWORD_CLAIM_SIGNATURE: getSignatureString({ + username, + userPoolName, + challengeParameters, + dateNow, + hkdf, + }), + } as Record; + + const deviceMetadata = await tokenOrchestrator.getDeviceMetadata(username); + if (deviceMetadata && deviceMetadata.deviceKey) { + challengeResponses.DEVICE_KEY = deviceMetadata.deviceKey; + } + + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + + const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { + ChallengeName: 'PASSWORD_VERIFIER', + ChallengeResponses: challengeResponses, + ClientMetadata: clientMetadata, + Session: session, + ClientId: userPoolClientId, + UserContextData, + }; + + const respondToAuthChallenge = createRespondToAuthChallengeClient({ + endpointResolver: createCognitoUserPoolEndpointResolver({ + endpointOverride: userPoolEndpoint, + }), + }); + + const response = await respondToAuthChallenge( + { region: getRegionFromUserPoolId(userPoolId) }, + jsonReqResponseChallenge, + ); + + if (response.ChallengeName === 'DEVICE_SRP_AUTH') + return handleDeviceSRPAuth({ + username, + config, + clientMetadata, + session: response.Session, + tokenOrchestrator, + }); + + return response; +} diff --git a/packages/auth/src/providers/cognito/utils/retryOnResourceNotFoundException.ts b/packages/auth/src/providers/cognito/utils/retryOnResourceNotFoundException.ts new file mode 100644 index 00000000000..a8b8095b226 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/retryOnResourceNotFoundException.ts @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AuthError } from '../../../errors/AuthError'; +import { AuthTokenOrchestrator } from '../tokenProvider/types'; + +/** + * It will retry the function if the error is a `ResourceNotFoundException` and + * will clean the device keys stored in the storage mechanism. + * + */ +export async function retryOnResourceNotFoundException< + F extends (...args: any[]) => any, +>( + func: F, + args: Parameters, + username: string, + tokenOrchestrator: AuthTokenOrchestrator, +): Promise> { + try { + return await func(...args); + } catch (error) { + if ( + error instanceof AuthError && + error.name === 'ResourceNotFoundException' && + error.message.includes('Device does not exist.') + ) { + await tokenOrchestrator.clearDeviceMetadata(username); + + return func(...args); + } + throw error; + } +} diff --git a/packages/auth/src/providers/cognito/utils/setActiveSignInUsername.ts b/packages/auth/src/providers/cognito/utils/setActiveSignInUsername.ts new file mode 100644 index 00000000000..8134fd47ff1 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/setActiveSignInUsername.ts @@ -0,0 +1,10 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { signInStore } from '../../../client/utils/store'; + +export function setActiveSignInUsername(username: string) { + const { dispatch } = signInStore; + + dispatch({ type: 'SET_USERNAME', value: username }); +} diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index d4123c475c1..85bb060aa9d 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -6,8 +6,6 @@ import { AmplifyUrl, AuthAction, assertTokenProviderConfig, - base64Encoder, - getDeviceName, } from '@aws-amplify/core/internals/utils'; import { ClientMetadata, ConfirmSignInOptions } from '../types'; @@ -29,11 +27,10 @@ import { AuthValidationErrorCode } from '../../../errors/types/validation'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../errors/constants'; import { getCurrentUser } from '../apis/getCurrentUser'; -import { AuthTokenOrchestrator, DeviceMetadata } from '../tokenProvider/types'; +import { AuthTokenOrchestrator } from '../tokenProvider/types'; import { getAuthUserAgentValue } from '../../../utils'; import { createAssociateSoftwareTokenClient, - createConfirmDeviceClient, createInitiateAuthClient, createRespondToAuthChallengeClient, createVerifySoftwareTokenClient, @@ -45,7 +42,6 @@ import { CognitoMFAType, InitiateAuthCommandInput, InitiateAuthCommandOutput, - NewDeviceMetadataType, RespondToAuthChallengeCommandInput, RespondToAuthChallengeCommandOutput, } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; @@ -56,17 +52,14 @@ import { initiateSelectedChallenge } from '../../../client/flows/userAuth/handle import { handleSelectChallengeWithPassword } from '../../../client/flows/userAuth/handleSelectChallengeWithPassword'; import { handleSelectChallengeWithPasswordSRP } from '../../../client/flows/userAuth/handleSelectChallengeWithPasswordSRP'; import { signInStore } from '../../../client/utils/store'; +import { WebAuthnSignInResult } from '../../../client/flows/userAuth/types'; -import { assertDeviceMetadata } from './types'; -import { - getAuthenticationHelper, - getBytesFromHex, - getNowString, - getSignatureString, -} from './srp'; -import { BigInteger } from './srp/BigInteger'; -import { AuthenticationHelper } from './srp/AuthenticationHelper'; +import { getAuthenticationHelper } from './srp'; import { getUserContextData } from './userContextData'; +import { handlePasswordVerifierChallenge } from './handlePasswordVerifierChallenge'; +import { handleDeviceSRPAuth } from './handleDeviceSRPAuth'; +import { retryOnResourceNotFoundException } from './retryOnResourceNotFoundException'; +import { setActiveSignInUsername } from './setActiveSignInUsername'; const USER_ATTRIBUTES = 'userAttributes.'; @@ -80,12 +73,10 @@ interface HandleAuthChallengeRequest { config: CognitoUserPoolConfig; } -interface HandleDeviceSRPInput { - username: string; - config: CognitoUserPoolConfig; - clientMetadata: ClientMetadata | undefined; - session: string | undefined; - tokenOrchestrator?: AuthTokenOrchestrator; +function isWebAuthnResultAuthSignInOutput( + result: WebAuthnSignInResult, +): result is AuthSignInOutput { + return 'isSignedIn' in result && 'nextStep' in result; } export async function handleCustomChallenge({ @@ -571,203 +562,6 @@ export async function handleCustomSRPAuthFlow( ); } -async function handleDeviceSRPAuth({ - username, - config, - clientMetadata, - session, - tokenOrchestrator, -}: HandleDeviceSRPInput): Promise { - const { userPoolId, userPoolEndpoint } = config; - const clientId = config.userPoolClientId; - const deviceMetadata = await tokenOrchestrator?.getDeviceMetadata(username); - assertDeviceMetadata(deviceMetadata); - const authenticationHelper = await getAuthenticationHelper( - deviceMetadata.deviceGroupKey, - ); - const challengeResponses: Record = { - USERNAME: username, - SRP_A: authenticationHelper.A.toString(16), - DEVICE_KEY: deviceMetadata.deviceKey, - }; - - const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { - ChallengeName: 'DEVICE_SRP_AUTH', - ClientId: clientId, - ChallengeResponses: challengeResponses, - ClientMetadata: clientMetadata, - Session: session, - }; - const respondToAuthChallenge = createRespondToAuthChallengeClient({ - endpointResolver: createCognitoUserPoolEndpointResolver({ - endpointOverride: userPoolEndpoint, - }), - }); - const { ChallengeParameters: respondedChallengeParameters, Session } = - await respondToAuthChallenge( - { region: getRegionFromUserPoolId(userPoolId) }, - jsonReqResponseChallenge, - ); - - return handleDevicePasswordVerifier( - username, - respondedChallengeParameters as ChallengeParameters, - clientMetadata, - Session, - authenticationHelper, - config, - tokenOrchestrator, - ); -} - -async function handleDevicePasswordVerifier( - username: string, - challengeParameters: ChallengeParameters, - clientMetadata: ClientMetadata | undefined, - session: string | undefined, - authenticationHelper: AuthenticationHelper, - { userPoolId, userPoolClientId, userPoolEndpoint }: CognitoUserPoolConfig, - tokenOrchestrator?: AuthTokenOrchestrator, -): Promise { - const deviceMetadata = await tokenOrchestrator?.getDeviceMetadata(username); - assertDeviceMetadata(deviceMetadata); - - const serverBValue = new BigInteger(challengeParameters?.SRP_B, 16); - const salt = new BigInteger(challengeParameters?.SALT, 16); - const { deviceKey } = deviceMetadata; - const { deviceGroupKey } = deviceMetadata; - const hkdf = await authenticationHelper.getPasswordAuthenticationKey({ - username: deviceMetadata.deviceKey, - password: deviceMetadata.randomPassword, - serverBValue, - salt, - }); - - const dateNow = getNowString(); - const challengeResponses = { - USERNAME: (challengeParameters?.USERNAME as string) ?? username, - PASSWORD_CLAIM_SECRET_BLOCK: challengeParameters?.SECRET_BLOCK, - TIMESTAMP: dateNow, - PASSWORD_CLAIM_SIGNATURE: getSignatureString({ - username: deviceKey, - userPoolName: deviceGroupKey, - challengeParameters, - dateNow, - hkdf, - }), - DEVICE_KEY: deviceKey, - } as Record; - - const UserContextData = getUserContextData({ - username, - userPoolId, - userPoolClientId, - }); - - const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { - ChallengeName: 'DEVICE_PASSWORD_VERIFIER', - ClientId: userPoolClientId, - ChallengeResponses: challengeResponses, - Session: session, - ClientMetadata: clientMetadata, - UserContextData, - }; - const respondToAuthChallenge = createRespondToAuthChallengeClient({ - endpointResolver: createCognitoUserPoolEndpointResolver({ - endpointOverride: userPoolEndpoint, - }), - }); - - return respondToAuthChallenge( - { region: getRegionFromUserPoolId(userPoolId) }, - jsonReqResponseChallenge, - ); -} - -export async function handlePasswordVerifierChallenge( - password: string, - challengeParameters: ChallengeParameters, - clientMetadata: ClientMetadata | undefined, - session: string | undefined, - authenticationHelper: AuthenticationHelper, - config: CognitoUserPoolConfig, - tokenOrchestrator: AuthTokenOrchestrator, -): Promise { - const { userPoolId, userPoolClientId, userPoolEndpoint } = config; - const userPoolName = userPoolId?.split('_')[1] || ''; - const serverBValue = new (BigInteger as any)(challengeParameters?.SRP_B, 16); - const salt = new (BigInteger as any)(challengeParameters?.SALT, 16); - const username = challengeParameters?.USER_ID_FOR_SRP; - if (!username) - throw new AuthError({ - name: 'EmptyUserIdForSRPException', - message: 'USER_ID_FOR_SRP was not found in challengeParameters', - }); - const hkdf = await authenticationHelper.getPasswordAuthenticationKey({ - username, - password, - serverBValue, - salt, - }); - - const dateNow = getNowString(); - - const challengeResponses = { - USERNAME: username, - PASSWORD_CLAIM_SECRET_BLOCK: challengeParameters?.SECRET_BLOCK, - TIMESTAMP: dateNow, - PASSWORD_CLAIM_SIGNATURE: getSignatureString({ - username, - userPoolName, - challengeParameters, - dateNow, - hkdf, - }), - } as Record; - - const deviceMetadata = await tokenOrchestrator.getDeviceMetadata(username); - if (deviceMetadata && deviceMetadata.deviceKey) { - challengeResponses.DEVICE_KEY = deviceMetadata.deviceKey; - } - - const UserContextData = getUserContextData({ - username, - userPoolId, - userPoolClientId, - }); - - const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { - ChallengeName: 'PASSWORD_VERIFIER', - ChallengeResponses: challengeResponses, - ClientMetadata: clientMetadata, - Session: session, - ClientId: userPoolClientId, - UserContextData, - }; - - const respondToAuthChallenge = createRespondToAuthChallengeClient({ - endpointResolver: createCognitoUserPoolEndpointResolver({ - endpointOverride: userPoolEndpoint, - }), - }); - - const response = await respondToAuthChallenge( - { region: getRegionFromUserPoolId(userPoolId) }, - jsonReqResponseChallenge, - ); - - if (response.ChallengeName === 'DEVICE_SRP_AUTH') - return handleDeviceSRPAuth({ - username, - config, - clientMetadata, - session: response.Session, - tokenOrchestrator, - }); - - return response; -} - export async function getSignInResult(params: { challengeName: ChallengeName; challengeParameters: ChallengeParameters; @@ -902,8 +696,14 @@ export async function getSignInResult(params: { }, }; - case 'WEB_AUTHN': - return handleWebAuthnSignInResult(challengeParameters); + case 'WEB_AUTHN': { + const result = await handleWebAuthnSignInResult(challengeParameters); + if (isWebAuthnResultAuthSignInOutput(result)) { + return result; + } + + return getSignInResult(result); + } case 'PASSWORD': case 'PASSWORD_SRP': return { @@ -1154,114 +954,6 @@ export async function assertUserNotAuthenticated() { } } -/** - * This function is used to kick off the device management flow. - * - * If an error is thrown while generating a hash device or calling the `ConfirmDevice` - * client, then this API will ignore the error and return undefined. Otherwise the authentication - * flow will not complete and the user won't be able to be signed in. - * - * @returns DeviceMetadata | undefined - */ -export async function getNewDeviceMetadata({ - userPoolId, - userPoolEndpoint, - newDeviceMetadata, - accessToken, -}: { - userPoolId: string; - userPoolEndpoint: string | undefined; - newDeviceMetadata?: NewDeviceMetadataType; - accessToken?: string; -}): Promise { - if (!newDeviceMetadata) return undefined; - const userPoolName = userPoolId.split('_')[1] || ''; - const authenticationHelper = await getAuthenticationHelper(userPoolName); - const deviceKey = newDeviceMetadata?.DeviceKey; - const deviceGroupKey = newDeviceMetadata?.DeviceGroupKey; - - try { - await authenticationHelper.generateHashDevice( - deviceGroupKey ?? '', - deviceKey ?? '', - ); - } catch (errGenHash) { - // TODO: log error here - return undefined; - } - - const deviceSecretVerifierConfig = { - Salt: base64Encoder.convert( - getBytesFromHex(authenticationHelper.getSaltToHashDevices()), - ), - PasswordVerifier: base64Encoder.convert( - getBytesFromHex(authenticationHelper.getVerifierDevices()), - ), - }; - const randomPassword = authenticationHelper.getRandomPassword(); - - try { - const confirmDevice = createConfirmDeviceClient({ - endpointResolver: createCognitoUserPoolEndpointResolver({ - endpointOverride: userPoolEndpoint, - }), - }); - await confirmDevice( - { region: getRegionFromUserPoolId(userPoolId) }, - { - AccessToken: accessToken, - DeviceName: await getDeviceName(), - DeviceKey: newDeviceMetadata?.DeviceKey, - DeviceSecretVerifierConfig: deviceSecretVerifierConfig, - }, - ); - - return { - deviceKey, - deviceGroupKey, - randomPassword, - }; - } catch (error) { - // TODO: log error here - return undefined; - } -} - -/** - * It will retry the function if the error is a `ResourceNotFoundException` and - * will clean the device keys stored in the storage mechanism. - * - */ -export async function retryOnResourceNotFoundException< - F extends (...args: any[]) => any, ->( - func: F, - args: Parameters, - username: string, - tokenOrchestrator: AuthTokenOrchestrator, -): Promise> { - try { - return await func(...args); - } catch (error) { - if ( - error instanceof AuthError && - error.name === 'ResourceNotFoundException' && - error.message.includes('Device does not exist.') - ) { - await tokenOrchestrator.clearDeviceMetadata(username); - - return func(...args); - } - throw error; - } -} - -export function setActiveSignInUsername(username: string) { - const { dispatch } = signInStore; - - dispatch({ type: 'SET_USERNAME', value: username }); -} - export function getActiveSignInUsername(username: string): string { const state = signInStore.getState(); diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index c399a5309e6..dc081bf4ec5 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -383,7 +383,7 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmSignIn }", - "limit": "28.66 kB" + "limit": "28.70 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", @@ -449,7 +449,7 @@ "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "30.89 kB" + "limit": "31.00 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)", From 7b9785548ca0483d619dc0fcb36944dd8593a697 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 30 Jan 2025 13:01:41 -0800 Subject: [PATCH 8/8] test(core): catch type issues when configuring from external json (#14101) * test(core): catch type issues when configuring from external json * update amplify_outputs to reflect user groups in gen2 format * chore: code cleanup --------- Co-authored-by: Ashwin Kumar --- jest.config.js | 3 + .../__tests__/singleton/Singleton.test.ts | 19 ++ .../singleton/utils/amplify_outputs.json | 198 ++++++++++++++++++ .../src/singleton/AmplifyOutputs/types.ts | 2 +- 4 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 packages/core/__tests__/singleton/utils/amplify_outputs.json diff --git a/jest.config.js b/jest.config.js index 99100f706a5..1512d30d8a7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,6 +15,9 @@ module.exports = { noImplicitAny: false, types: ['jest', 'jsdom'], }, + diagnostics: { + warnOnly: false, + }, }, ], }, diff --git a/packages/core/__tests__/singleton/Singleton.test.ts b/packages/core/__tests__/singleton/Singleton.test.ts index 03c0185359d..dfcbec259f9 100644 --- a/packages/core/__tests__/singleton/Singleton.test.ts +++ b/packages/core/__tests__/singleton/Singleton.test.ts @@ -6,6 +6,9 @@ import { AuthClass as Auth } from '../../src/singleton/Auth'; import { decodeJWT } from '../../src/singleton/Auth/utils'; import { CredentialsAndIdentityId } from '../../src/singleton/Auth/types'; import { ResourcesConfig, fetchAuthSession } from '../../src'; +import { AmplifyOutputs } from '../../src/libraryUtils'; + +import mockAmplifyOutputs from './utils/amplify_outputs.json'; Object.assign(global, { TextDecoder, TextEncoder }); @@ -164,6 +167,22 @@ describe('Amplify.configure() and Amplify.getConfig()', () => { expect(result).toEqual(expectedResourceConfig); }); + it(`should successfully configure from a 'amplify_outputs.json' file`, () => { + type DeepRequired = T extends object + ? { [K in keyof T]-?: DeepRequired> } + : T; + + const recreateObjectWithDeepRequired = (obj: T): DeepRequired => + obj as DeepRequired; + + // Ensures that all optional properties in mockAmplifyOutputs are defined + const _amplifyOutputs: DeepRequired = + recreateObjectWithDeepRequired(mockAmplifyOutputs); + + // Configures Amplify with mockAmplifyOutputs, relying on its inferred type + Amplify.configure(mockAmplifyOutputs); + }); + it('dispatches hub event with parsed ResourceConfig from the legacy config', () => { Amplify.configure(mockLegacyConfig); diff --git a/packages/core/__tests__/singleton/utils/amplify_outputs.json b/packages/core/__tests__/singleton/utils/amplify_outputs.json new file mode 100644 index 00000000000..5b1c0697157 --- /dev/null +++ b/packages/core/__tests__/singleton/utils/amplify_outputs.json @@ -0,0 +1,198 @@ +{ + "version": "1", + "auth": { + "aws_region": "us-west-2", + "user_pool_id": "mock-cup-id", + "user_pool_client_id": "mock-cup-client-id", + "identity_pool_id": "mock-idp-id", + "authentication_flow_type": "CUSTOM_AUTH", + "oauth": { + "identity_providers": [ + "FACEBOOK", + "SIGN_IN_WITH_APPLE", + "GOOGLE", + "Auth0" + ], + "domain": "mock-oauth-domain", + "scopes": ["phone"], + "redirect_sign_in_uri": ["mock-sign-in-uri"], + "redirect_sign_out_uri": ["mock-sign-out-uri"], + "response_type": "token" + }, + "standard_required_attributes": ["address", "locale", "email"], + "username_attributes": ["phone_number", "email"], + "user_verification_types": ["email"], + "unauthenticated_identities_enabled": true, + "mfa_configuration": "OPTIONAL", + "mfa_methods": ["TOTP", "SMS"], + "password_policy": { + "require_lowercase": true, + "require_numbers": true, + "require_uppercase": true, + "require_symbols": true, + "min_length": 6 + }, + "groups": [ + { "ADMIN": { "precedence": 0 } }, + { "USER": { "precedence": 1 } }, + { "TEST": { "precedence": 2 } } + ] + }, + "data": { + "aws_region": "us-west-2", + "url": "mock-data-url", + "api_key": "mock-data-api-key", + "default_authorization_type": "API_KEY", + "authorization_types": [], + "conflict_resolution_mode": "AUTOMERGE", + "model_introspection": { + "version": 1, + "models": { + "Todo": { + "name": "Todo", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "content": { + "name": "content", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + } + }, + "syncable": true, + "pluralName": "Todos", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "auth", + "properties": { + "rules": [ + { + "provider": "userPools", + "ownerField": "owner", + "allow": "owner", + "identityClaim": "cognito:username", + "operations": ["create", "update", "delete", "read"] + }, + { + "allow": "private", + "provider": "iam", + "operations": ["create", "update", "delete", "read"] + } + ] + } + } + ], + "primaryKeyInfo": { + "isCustomPrimaryKey": false, + "primaryKeyFieldName": "id", + "sortKeyFieldNames": [] + } + } + }, + "enums": {}, + "nonModels": {} + } + }, + "geo": { + "aws_region": "us-west-2", + "search_indices": { + "items": ["mock-geo-search-item", "mock-geo-search-item-alt"], + "default": "mock-geo-search-item" + }, + "geofence_collections": { + "items": ["mock-geo-fence-item", "mock-geo-fence-item-alt"], + "default": "mock-geo-fence-item" + }, + + "maps": { + "items": { + "map51addb38-dev": { + "style": "VectorEsriStreets" + }, + "map30grxcw31-prod": { + "style": "VectorEsriStreets" + } + }, + "default": "map51addb38-dev" + } + }, + "custom": { + "custom-prop": -51806024, + "custom-prop-alt": 87599986, + "events": { + "url": "mock-events-url", + "aws_region": "us-west-2", + "default_authorization_type": "API_KEY", + "api_key": "mock-events-api-key" + } + }, + "notifications": { + "aws_region": "us-west-2", + "amazon_pinpoint_app_id": "mock-pinpoint-app-id", + "channels": ["IN_APP_MESSAGING", "APNS"] + }, + "analytics": { + "amazon_pinpoint": { + "app_id": "mock-pinpoint-app-id", + "aws_region": "us-west-2" + } + }, + "storage": { + "aws_region": "us-west-2", + "bucket_name": "mock-bucket-1", + "buckets": [ + { + "name": "mock-bucket-1", + "bucket_name": "bucket1", + "aws_region": "us-west-2", + "paths": { + "sub/*": { + "authenticated": ["get", "list"] + }, + "public/*": { + "guest": ["get", "list", "write", "delete"], + "authenticated": ["write", "get", "list", "delete"] + } + } + }, + { + "name": "mock-bucket-2", + "bucket_name": "bucket2", + "aws_region": "us-west-2", + "paths": { + "public/*": { + "guest": ["get", "list", "write", "delete"], + "authenticated": ["write", "get", "list", "delete"] + } + } + } + ] + } +} diff --git a/packages/core/src/singleton/AmplifyOutputs/types.ts b/packages/core/src/singleton/AmplifyOutputs/types.ts index 4ff761a2f08..22768694c62 100644 --- a/packages/core/src/singleton/AmplifyOutputs/types.ts +++ b/packages/core/src/singleton/AmplifyOutputs/types.ts @@ -17,7 +17,7 @@ type UserGroupName = string; type UserGroupPrecedence = Record; export interface AmplifyOutputsAuthProperties { aws_region: string; - authentication_flow_type?: 'USER_SRP_AUTH' | 'CUSTOM_AUTH'; + authentication_flow_type?: string; user_pool_id: string; user_pool_client_id: string; identity_pool_id?: string;