From 606098437021ff03363009094dd79f84fe96f1dc Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Mon, 9 Oct 2023 19:09:47 -0500 Subject: [PATCH 1/6] feat: Added additional Auth config fields & ability to override Cognito endpoint. (#12236) --- .../cognito/apis/resendSignUpCode.ts | 4 +- .../providers/cognito/apis/resetPassword.ts | 4 +- .../src/providers/cognito/types/models.ts | 3 +- .../src/providers/cognito/types/outputs.ts | 2 +- .../clients/CognitoIdentityProvider/base.ts | 13 +++- packages/auth/src/types/index.ts | 2 - packages/auth/src/types/models.ts | 25 +------ .../__tests__/initSingleton.test.ts | 24 +++++++ packages/aws-amplify/package.json | 64 +++++++++--------- .../core/__tests__/parseAWSExports.test.ts | 47 +++++++++++++ .../__tests__/singleton/Singleton.test.ts | 24 +++++++ packages/core/package.json | 2 +- packages/core/src/libraryUtils.ts | 15 +++-- packages/core/src/parseAWSExports.ts | 67 +++++++++++++++---- packages/core/src/singleton/Auth/types.ts | 46 +++++++++++++ 15 files changed, 255 insertions(+), 87 deletions(-) diff --git a/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts b/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts index 874eb9465af..10907b09527 100644 --- a/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts +++ b/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify } from '@aws-amplify/core'; -import { assertTokenProviderConfig, AuthAction } from '@aws-amplify/core/internals/utils'; -import { AuthStandardAttributeKey, AuthDeliveryMedium } from '../../../types'; +import { assertTokenProviderConfig, AuthAction, AuthStandardAttributeKey } from '@aws-amplify/core/internals/utils'; +import { AuthDeliveryMedium } from '../../../types'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; import { ResendSignUpCodeInput, ResendSignUpCodeOutput } from '../types'; diff --git a/packages/auth/src/providers/cognito/apis/resetPassword.ts b/packages/auth/src/providers/cognito/apis/resetPassword.ts index 0f18fc39b0d..5144a723d9d 100644 --- a/packages/auth/src/providers/cognito/apis/resetPassword.ts +++ b/packages/auth/src/providers/cognito/apis/resetPassword.ts @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify } from '@aws-amplify/core'; -import { assertTokenProviderConfig, AuthAction } from '@aws-amplify/core/internals/utils'; +import { assertTokenProviderConfig, AuthAction, AuthStandardAttributeKey } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; -import { AuthDeliveryMedium, AuthStandardAttributeKey } from '../../../types'; +import { AuthDeliveryMedium } from '../../../types'; import { ResetPasswordInput, ResetPasswordOutput } from '../types'; import { forgotPassword } from '../utils/clients/CognitoIdentityProvider'; import { getRegion } from '../utils/clients/CognitoIdentityProvider/utils'; diff --git a/packages/auth/src/providers/cognito/types/models.ts b/packages/auth/src/providers/cognito/types/models.ts index 16fd2eb6d5f..62e0a69386f 100644 --- a/packages/auth/src/providers/cognito/types/models.ts +++ b/packages/auth/src/providers/cognito/types/models.ts @@ -1,9 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AuthStandardAttributeKey, AuthVerifiableAttributeKey } from "@aws-amplify/core/internals/utils"; import { - AuthStandardAttributeKey, - AuthVerifiableAttributeKey, AuthUserAttribute, AuthDevice, } from '../../../types'; diff --git a/packages/auth/src/providers/cognito/types/outputs.ts b/packages/auth/src/providers/cognito/types/outputs.ts index 8f975d9effa..52c03038a35 100644 --- a/packages/auth/src/providers/cognito/types/outputs.ts +++ b/packages/auth/src/providers/cognito/types/outputs.ts @@ -1,11 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AuthStandardAttributeKey } from "@aws-amplify/core/internals/utils"; import { AuthMFAType, AuthUserAttributes, AuthUser, - AuthStandardAttributeKey, AuthCodeDeliveryDetails, AuthTOTPSetupDetails, AuthSignInOutput, diff --git a/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts b/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts index 436e7402dd5..9bdbf611ff4 100644 --- a/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts +++ b/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { Amplify } from '@aws-amplify/core'; import { Endpoint, EndpointResolverOptions, @@ -25,9 +26,15 @@ const SERVICE_NAME = 'cognito-idp'; /** * The endpoint resolver function that returns the endpoint URL for a given region. */ -const endpointResolver = ({ region }: EndpointResolverOptions) => ({ - url: new URL(`https://${SERVICE_NAME}.${region}.${getDnsSuffix(region)}`), -}); +const endpointResolver = ({ region }: EndpointResolverOptions) => { + const authConfig = Amplify.getConfig().Auth?.Cognito; + const customURL = authConfig?.endpoint; + const defaultURL = new URL(`https://${SERVICE_NAME}.${region}.${getDnsSuffix(region)}`); + + return { + url: customURL ? new URL(customURL) : defaultURL, + } +}; /** * A Cognito Identity-specific middleware that disables caching for all requests. diff --git a/packages/auth/src/types/index.ts b/packages/auth/src/types/index.ts index cc42c94ef15..42405ff88a0 100644 --- a/packages/auth/src/types/index.ts +++ b/packages/auth/src/types/index.ts @@ -7,8 +7,6 @@ export { AuthAnyAttribute, AuthCodeDeliveryDetails, AuthNextSignUpStep, - AuthStandardAttributeKey, - AuthVerifiableAttributeKey, AuthUserAttributeKey, AuthUserAttributes, AuthUserAttribute, diff --git a/packages/auth/src/types/models.ts b/packages/auth/src/types/models.ts index ab092909756..9212722495f 100644 --- a/packages/auth/src/types/models.ts +++ b/packages/auth/src/types/models.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AuthStandardAttributeKey } from "@aws-amplify/core/internals/utils"; + /** * Additional data that may be returned from Auth APIs. */ @@ -184,29 +186,6 @@ export type AuthNextSignInStep< | ResetPasswordStep | DoneSignInStep; -export type AuthStandardAttributeKey = - | 'address' - | 'birthdate' - | 'email_verified' - | 'family_name' - | 'gender' - | 'given_name' - | 'locale' - | 'middle_name' - | 'name' - | 'nickname' - | 'phone_number_verified' - | 'picture' - | 'preferred_username' - | 'profile' - | 'sub' - | 'updated_at' - | 'website' - | 'zoneinfo' - | AuthVerifiableAttributeKey; - -export type AuthVerifiableAttributeKey = 'email' | 'phone_number'; - /** * Key/value pairs describing a user attributes. */ diff --git a/packages/aws-amplify/__tests__/initSingleton.test.ts b/packages/aws-amplify/__tests__/initSingleton.test.ts index 619baa9b4f2..d3d9f9ecadf 100644 --- a/packages/aws-amplify/__tests__/initSingleton.test.ts +++ b/packages/aws-amplify/__tests__/initSingleton.test.ts @@ -84,6 +84,30 @@ describe('initSingleton (DefaultAmplify)', () => { Cognito: { allowGuestAccess: true, identityPoolId: 'aws_cognito_identity_pool_id', + loginWith: { + email: false, + phone: false, + username: true, + }, + mfa: { + smsEnabled: true, + status: 'off', + totpEnabled: false, + }, + passwordFormat: { + minLength: 8, + requireLowercase: false, + requireNumbers: false, + requireSpecialCharacters: false, + requireUppercase: false, + }, + userAttributes: [ + { + phone_number: { + required: true, + }, + }, + ], userPoolClientId: 'aws_user_pools_web_client_id', userPoolId: 'aws_user_pools_id', }, diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index edc1f327de6..05a67b331e8 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -252,31 +252,31 @@ "name": "[Analytics] record (Pinpoint)", "path": "./lib-esm/analytics/index.js", "import": "{ record }", - "limit": "21.10 kB" + "limit": "21.62 kB" }, { "name": "[Analytics] record (Kinesis)", "path": "./lib-esm/analytics/kinesis/index.js", "import": "{ record }", - "limit": "46.40 kB" + "limit": "46.89 kB" }, { "name": "[Analytics] record (Kinesis Firehose)", "path": "./lib-esm/analytics/kinesis-firehose/index.js", "import": "{ record }", - "limit": "42.73 kB" + "limit": "43.23 kB" }, { "name": "[Analytics] record (Personalize)", "path": "./lib-esm/analytics/personalize/index.js", "import": "{ record }", - "limit": "47.0 kB" + "limit": "47.50 kB" }, { "name": "[Analytics] identifyUser (Pinpoint)", "path": "./lib-esm/analytics/index.js", "import": "{ identifyUser }", - "limit": "19.30 kB" + "limit": "19.72 kB" }, { "name": "[Analytics] enable", @@ -300,163 +300,163 @@ "name": "[API] REST API handlers", "path": "./lib-esm/api/index.js", "import": "{ get, post, put, del, patch, head, isCancelError }", - "limit": "13.7 kB" + "limit": "14.2 kB" }, { "name": "[Auth] signUp (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signUp }", - "limit": "11.50 kB" + "limit": "11.8 kB" }, { "name": "[Auth] resetPassword (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ resetPassword }", - "limit": "11.30 kB" + "limit": "11.7 kB" }, { "name": "[Auth] confirmResetPassword (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ confirmResetPassword }", - "limit": "11.05 kB" + "limit": "11.64 kB" }, { "name": "[Auth] signIn (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signIn }", - "limit": "29.17 kB" + "limit": "29.7 kB" }, { "name": "[Auth] resendSignUpCode (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ resendSignUpCode }", - "limit": "11.08 kB" + "limit": "11.66 kB" }, { "name": "[Auth] confirmSignUp (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ confirmSignUp }", - "limit": "11.30 kB" + "limit": "11.67 kB" }, { "name": "[Auth] confirmSignIn (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ confirmSignIn }", - "limit": "29.05 kB" + "limit": "29.6 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ updateMFAPreference }", - "limit": "10.22 kB" + "limit": "10.75 kB" }, { "name": "[Auth] fetchMFAPreference (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ fetchMFAPreference }", - "limit": "10.23 kB" + "limit": "10.79 kB" }, { "name": "[Auth] verifyTOTPSetup (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ verifyTOTPSetup }", - "limit": "11.16 kB" + "limit": "11.68 kB" }, { "name": "[Auth] updatePassword (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ updatePassword }", - "limit": "11.19 kB" + "limit": "11.66 kB" }, { "name": "[Auth] setUpTOTP (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ setUpTOTP }", - "limit": "12.08 kB" + "limit": "12.75 kB" }, { "name": "[Auth] updateUserAttributes (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ updateUserAttributes }", - "limit": "10.50 kB" + "limit": "10.93 kB" }, { "name": "[Auth] getCurrentUser (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ getCurrentUser }", - "limit": "5.30 kB" + "limit": "5.68 kB" }, { "name": "[Auth] confirmUserAttribute (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ confirmUserAttribute }", - "limit": "11.10 kB" + "limit": "11.69 kB" }, { "name": "[Auth] signInWithRedirect (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signInWithRedirect }", - "limit": "22.47 kB" + "limit": "22.87 kB" }, { "name": "[Auth] fetchUserAttributes (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ fetchUserAttributes }", - "limit": "10.19 kB" + "limit": "10.78 kB" }, { "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "31.35 kB" + "limit": "31.88 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signInWithRedirect, signOut, fetchAuthSession }", - "limit": "23.04 kB" + "limit": "23.36 kB" }, { "name": "[Storage] copy (S3)", "path": "./lib-esm/storage/index.js", "import": "{ copy }", - "limit": "16.98 kB" + "limit": "17.88 kB" }, { "name": "[Storage] downloadData (S3)", "path": "./lib-esm/storage/index.js", "import": "{ downloadData }", - "limit": "17.73 kB" + "limit": "18.24 kB" }, { "name": "[Storage] getProperties (S3)", "path": "./lib-esm/storage/index.js", "import": "{ getProperties }", - "limit": "17.02 kB" + "limit": "17.52 kB" }, { "name": "[Storage] getUrl (S3)", "path": "./lib-esm/storage/index.js", "import": "{ getUrl }", - "limit": "18.46 kB" + "limit": "18.96 kB" }, { "name": "[Storage] list (S3)", "path": "./lib-esm/storage/index.js", "import": "{ list }", - "limit": "17.53 kB" + "limit": "18.05 kB" }, { "name": "[Storage] remove (S3)", "path": "./lib-esm/storage/index.js", "import": "{ remove }", - "limit": "16.85 kB" + "limit": "17.36 kB" }, { "name": "[Storage] uploadData (S3)", "path": "./lib-esm/storage/index.js", "import": "{ uploadData }", - "limit": "23.66 kB" + "limit": "24.16 kB" } ], "jest": { diff --git a/packages/core/__tests__/parseAWSExports.test.ts b/packages/core/__tests__/parseAWSExports.test.ts index 8305afcf162..cc3d634a2ac 100644 --- a/packages/core/__tests__/parseAWSExports.test.ts +++ b/packages/core/__tests__/parseAWSExports.test.ts @@ -68,6 +68,24 @@ describe('Parser', () => { parseAWSExports({ aws_cognito_identity_pool_id: identityPoolId, aws_cognito_sign_up_verification_method: signUpVerificationMethod, + aws_cognito_username_attributes: ['PHONE_NUMBER'], + aws_cognito_signup_attributes: ['PHONE_NUMBER'], + aws_cognito_mfa_configuration: 'OFF', + aws_cognito_mfa_types: [ + 'SMS', + 'TOTP' + ], + aws_cognito_password_protection_settings: { + passwordPolicyMinLength: 8, + passwordPolicyCharacters: [ + 'REQUIRES_SYMBOLS', + 'REQUIRES_UPPERCASE', + 'REQUIRES_NUMBERS' + ] + }, + aws_cognito_verification_mechanisms: [ + 'EMAIL' + ], aws_mandatory_sign_in: 'enable', aws_mobile_analytics_app_id: appId, aws_mobile_analytics_app_region: region, @@ -103,7 +121,36 @@ describe('Parser', () => { Cognito: { identityPoolId, allowGuestAccess: false, + loginWith: { + email: false, + phone: true, + username: false + }, + mfa: { + smsEnabled: true, + status: 'off', + totpEnabled: true, + }, + passwordFormat: { + minLength: 8, + requireLowercase: false, + requireNumbers: true, + requireSpecialCharacters: true, + requireUppercase: true + }, signUpVerificationMethod, + userAttributes: [ + { + 'email': { + required: true + }, + }, + { + 'phone_number': { + required: true + } + } + ], userPoolId, userPoolClientId, }, diff --git a/packages/core/__tests__/singleton/Singleton.test.ts b/packages/core/__tests__/singleton/Singleton.test.ts index 4877593c152..3a91307688f 100644 --- a/packages/core/__tests__/singleton/Singleton.test.ts +++ b/packages/core/__tests__/singleton/Singleton.test.ts @@ -40,6 +40,30 @@ describe('Amplify.configure() and Amplify.getConfig()', () => { identityPoolId: 'aws_cognito_identity_pool_id', userPoolClientId: 'aws_user_pools_web_client_id', userPoolId: 'aws_user_pools_id', + loginWith: { + email: false, + phone: false, + username: true + }, + mfa: { + smsEnabled: true, + status: 'off', + totpEnabled: false, + }, + passwordFormat: { + minLength: 8, + requireLowercase: false, + requireNumbers: false, + requireSpecialCharacters: false, + requireUppercase: false + }, + userAttributes: [ + { + phone_number: { + required: true + } + } + ] }, }, }); diff --git a/packages/core/package.json b/packages/core/package.json index b216eb278af..f11fb95f781 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -86,7 +86,7 @@ "name": "Core (I18n)", "path": "./lib-esm/index.js", "import": "{ I18n }", - "limit": "5.48 kB" + "limit": "5.60 kB" }, { "name": "Custom clients (fetch handler)", diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index 6e1926178c7..a9861cc6b6a 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -20,12 +20,7 @@ export { } from './utils'; export { parseAWSExports } from './parseAWSExports'; export { LegacyConfig } from './singleton/types'; -export { - JWT, - StrictUnion, - CognitoIdentityPoolConfig, - JwtPayload, -} from './singleton/Auth/types'; + // Auth utilities export { decodeJWT, @@ -36,6 +31,14 @@ export { export { isTokenExpired } from './singleton/Auth'; export { APIAuthMode, DocumentType } from './singleton/API/types'; export { Signer } from './Signer'; +export { + JWT, + StrictUnion, + CognitoIdentityPoolConfig, + JwtPayload, + AuthStandardAttributeKey, + AuthVerifiableAttributeKey +} from './singleton/Auth/types'; // Logging utilities export { ConsoleLogger, ConsoleLogger as Logger } from './Logger'; diff --git a/packages/core/src/parseAWSExports.ts b/packages/core/src/parseAWSExports.ts index e1927b31b59..881b02e89bf 100644 --- a/packages/core/src/parseAWSExports.ts +++ b/packages/core/src/parseAWSExports.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { ConsoleLogger as Logger } from './Logger'; -import { OAuthConfig } from './singleton/Auth/types'; +import { OAuthConfig, AuthStandardAttributeKey, AuthConfigUserAttributes } from './singleton/Auth/types'; import { ResourcesConfig } from './singleton/types'; const logger = new Logger('parseAWSExports'); @@ -34,6 +34,12 @@ export const parseAWSExports = ( aws_appsync_region, aws_cognito_identity_pool_id, aws_cognito_sign_up_verification_method, + aws_cognito_mfa_configuration, + aws_cognito_mfa_types, + aws_cognito_password_protection_settings, + aws_cognito_verification_mechanisms, + aws_cognito_signup_attributes, + aws_cognito_username_attributes, aws_mandatory_sign_in, aws_mobile_analytics_app_id, aws_mobile_analytics_app_region, @@ -93,18 +99,55 @@ export const parseAWSExports = ( } // Auth + const mfaConfig = aws_cognito_mfa_configuration ? { + status: aws_cognito_mfa_configuration && aws_cognito_mfa_configuration.toLowerCase(), + totpEnabled: aws_cognito_mfa_types?.includes('TOTP') ?? false, + smsEnabled: aws_cognito_mfa_types?.includes('SMS') ?? false + } : undefined; + const passwordFormatConfig = aws_cognito_password_protection_settings ? { + minLength: aws_cognito_password_protection_settings.passwordPolicyMinLength, + requireLowercase: + aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes('REQUIRES_LOWERCASE') ?? false, + requireUppercase: + aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes('REQUIRES_UPPERCASE') ?? false, + requireNumbers: + aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes('REQUIRES_NUMBERS') ?? false, + requireSpecialCharacters: + aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes('REQUIRES_SYMBOLS') ?? false, + } : undefined; + const mergedUserAttributes = Array.from( + new Set([ + ...(aws_cognito_verification_mechanisms ?? []), + ...(aws_cognito_signup_attributes ?? []) + ]) + ); + const userAttributesConfig = mergedUserAttributes.map((s: string) => ({ + [s.toLowerCase()]: { + required: true // All user attributes generated by the CLI will be required + } + })) as unknown as AuthConfigUserAttributes; + const loginWithEmailEnabled = aws_cognito_username_attributes?.includes('EMAIL') ?? false; + const loginWithPhoneEnabled = aws_cognito_username_attributes?.includes('PHONE_NUMBER') ?? false; if (aws_cognito_identity_pool_id || aws_user_pools_id) { amplifyConfig.Auth = { Cognito: { identityPoolId: aws_cognito_identity_pool_id, allowGuestAccess: aws_mandatory_sign_in !== 'enable', signUpVerificationMethod: aws_cognito_sign_up_verification_method, + userAttributes: userAttributesConfig, userPoolClientId: aws_user_pools_web_client_id, userPoolId: aws_user_pools_id, - ...(oauth && - Object.keys(oauth).length > 0 && { - loginWith: getOAuthConfig(oauth), - }), + mfa: mfaConfig, + passwordFormat: passwordFormatConfig, + loginWith: { + username: (loginWithEmailEnabled || loginWithPhoneEnabled) ? false : true, + email: loginWithEmailEnabled, + phone: loginWithPhoneEnabled, + ...(oauth && + Object.keys(oauth).length > 0 && { + oauth: getOAuthConfig(oauth), + }), + }, }, }; } @@ -168,12 +211,10 @@ const getOAuthConfig = ({ redirectSignIn, redirectSignOut, responseType, -}: Record): { oauth: OAuthConfig } => ({ - oauth: { - domain, - scopes: scope, - redirectSignIn: getRedirectUrl(redirectSignIn), - redirectSignOut: getRedirectUrl(redirectSignOut), - responseType, - }, +}: Record): OAuthConfig => ({ + domain, + scopes: scope, + redirectSignIn: getRedirectUrl(redirectSignIn), + redirectSignOut: getRedirectUrl(redirectSignOut), + responseType, }); diff --git a/packages/core/src/singleton/Auth/types.ts b/packages/core/src/singleton/Auth/types.ts index 72e98bf8cb3..05f9c3adbd9 100644 --- a/packages/core/src/singleton/Auth/types.ts +++ b/packages/core/src/singleton/Auth/types.ts @@ -70,6 +70,31 @@ export type AuthTokens = { accessToken: JWT; }; +export type AuthStandardAttributeKey = + | 'address' + | 'birthdate' + | 'email_verified' + | 'family_name' + | 'gender' + | 'given_name' + | 'locale' + | 'middle_name' + | 'name' + | 'nickname' + | 'phone_number_verified' + | 'picture' + | 'preferred_username' + | 'profile' + | 'sub' + | 'updated_at' + | 'website' + | 'zoneinfo' + | AuthVerifiableAttributeKey; + +export type AuthVerifiableAttributeKey = 'email' | 'phone_number'; + +export type AuthConfigUserAttributes = Partial>; + export type AuthConfig = StrictUnion< | AuthIdentityPoolConfig | AuthUserPoolConfig @@ -87,6 +112,10 @@ export type AuthIdentityPoolConfig = { userPoolClientId?: never; userPoolId?: never; loginWith?: never; + userAttributes?: never; + mfa?: never; + passwordFormat?: never; + endpoint?: never; }; }; @@ -108,7 +137,24 @@ export type CognitoUserPoolConfig = { signUpVerificationMethod?: 'code' | 'link'; loginWith?: { oauth?: OAuthConfig; + username?: boolean; + email?: boolean; + phone?: boolean; + }; + userAttributes?: AuthConfigUserAttributes; + mfa?: { + status?: 'on' | 'off' | 'optional'; + totpEnabled?: boolean; + smsEnabled?: boolean; + }; + passwordFormat?: { + minLength?: number; + requireLowercase?: boolean; + requireUppercase?: boolean; + requireNumbers?: boolean; + requireSpecialCharacters?: boolean; }; + endpoint?: string; }; export type OAuthConfig = { From 228cd007c236112b95ffd8b617f758e710cba415 Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Tue, 10 Oct 2023 09:46:49 -0500 Subject: [PATCH 2/6] fix: Fixed UA import path (#12248) --- packages/storage/src/providers/s3/apis/internal/getUrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/storage/src/providers/s3/apis/internal/getUrl.ts b/packages/storage/src/providers/s3/apis/internal/getUrl.ts index 47c78e5135a..5f6fb0ed81d 100644 --- a/packages/storage/src/providers/s3/apis/internal/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/internal/getUrl.ts @@ -12,7 +12,7 @@ import { DEFAULT_PRESIGN_EXPIRATION, MAX_URL_EXPIRATION, } from '../../utils/constants'; -import { StorageAction } from '@aws-amplify/core/lib-esm/libraryUtils'; +import { StorageAction } from '@aws-amplify/core/internals/utils'; export const getUrl = async function ( amplify: AmplifyClassV6, From 6c1de3b6face8abe5a067193f604bedc19c97b22 Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Tue, 10 Oct 2023 11:20:17 -0500 Subject: [PATCH 3/6] chore: Disable bundle size tests in CI (#12251) --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a9b133e35a6..945584fedb8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -40,7 +40,7 @@ jobs: name: Unit and Bundle tests have passed needs: - unit-tests - - bundle-size-tests + # - bundle-size-tests - license-test - github-actions-test - tsc-compliance-test From 22c5e12042acfb9706fe06b5050658152206596a Mon Sep 17 00:00:00 2001 From: israx <70438514+israx@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:32:48 -0400 Subject: [PATCH 4/6] feat(auth): add auto sign-in support (#12229) * feat: add autoSignIn support * chore: add default verification signUp method * feat(auth): add auto sign-in (#12153) * feat: add autoSignIn support * chore: add default verification signUp method * autoSignIn refactor * chore: remove unused code * feat: add sign-up helpers for auto-sign-in * feat: add auto sign-in helpers * fix build * chore: fix error during auto-sign-in with code * remove unused types * chore: address feedback * fix autoSignIn enable * chore: add unit tests * fix unit tests * chore: remove unsued type * chore: address feedback * chore: address feedback * chore: import hub-internal from core * fix: bundle size * fix bundle size --- .../providers/cognito/autoSignIn.test.ts | 87 +++++++++ packages/auth/src/errors/constants.ts | 1 + packages/auth/src/index.ts | 1 + .../src/providers/cognito/apis/autoSignIn.ts | 119 ++++++++++++ .../providers/cognito/apis/confirmSignUp.ts | 63 ++++++- .../auth/src/providers/cognito/apis/signUp.ts | 105 ++++++++--- packages/auth/src/providers/cognito/index.ts | 1 + .../src/providers/cognito/types/models.ts | 9 + .../src/providers/cognito/types/options.ts | 2 +- .../providers/cognito/utils/signInHelpers.ts | 54 +++--- .../providers/cognito/utils/signUpHelpers.ts | 170 ++++++++++++++++++ packages/auth/src/types/index.ts | 1 - packages/auth/src/types/models.ts | 47 +++-- packages/auth/src/types/outputs.ts | 2 +- .../aws-amplify/__tests__/exports.test.ts | 2 + packages/aws-amplify/package.json | 6 +- packages/core/src/Hub/index.ts | 8 + packages/core/src/libraryUtils.ts | 4 +- packages/core/src/singleton/Auth/types.ts | 5 +- 19 files changed, 604 insertions(+), 83 deletions(-) create mode 100644 packages/auth/__tests__/providers/cognito/autoSignIn.test.ts create mode 100644 packages/auth/src/providers/cognito/apis/autoSignIn.ts create mode 100644 packages/auth/src/providers/cognito/utils/signUpHelpers.ts diff --git a/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts b/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts new file mode 100644 index 00000000000..6004ffab7d9 --- /dev/null +++ b/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts @@ -0,0 +1,87 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + CognitoUserPoolsTokenProvider, + signUp, +} from '../../../src/providers/cognito'; +import { autoSignIn } from '../../../src/providers/cognito/apis/autoSignIn'; +import * as signUpClient from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; +import { authAPITestParams } from './testUtils/authApiTestParams'; +import { RespondToAuthChallengeCommandOutput } from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider/types'; +import { Amplify } from 'aws-amplify'; +import * as initiateAuthHelpers from '../../../src/providers/cognito/utils/signInHelpers'; +import { AuthError } from '../../../src/errors/AuthError'; +jest.mock('@aws-amplify/core/lib/clients/handlers/fetch'); + +const authConfig = { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + }, +}; +CognitoUserPoolsTokenProvider.setAuthConfig(authConfig); +Amplify.configure({ + Auth: authConfig, +}); +describe('Auto sign-in API Happy Path Cases:', () => { + let signUpSpy; + let handleUserSRPAuthflowSpy; + const { user1 } = authAPITestParams; + beforeEach(async () => { + signUpSpy = jest + .spyOn(signUpClient, 'signUp') + .mockImplementationOnce(async () => { + return { + UserConfirmed: true, + }; + }); + + handleUserSRPAuthflowSpy = jest + .spyOn(initiateAuthHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => + authAPITestParams.RespondToAuthChallengeCommandOutput + ); + }); + afterEach(() => { + signUpSpy.mockClear(); + handleUserSRPAuthflowSpy.mockClear(); + }); + test('signUp should enable autoSignIn and return COMPLETE_AUTO_SIGN_IN step', async () => { + const resp = await signUp({ + username: user1.username, + password: user1.password, + options: { + userAttributes: { email: user1.email }, + serviceOptions: { + autoSignIn: true, + }, + }, + }); + expect(resp).toEqual({ + isSignUpComplete: true, + nextStep: { + signUpStep: 'COMPLETE_AUTO_SIGN_IN', + }, + }); + expect(signUpSpy).toBeCalledTimes(1); + }); + + test('Auto sign-in should resolve to a signIn output', async () => { + const signInOutput = await autoSignIn(); + expect(signInOutput).toEqual(authAPITestParams.signInResult()); + expect(handleUserSRPAuthflowSpy).toBeCalledTimes(1); + }); +}); + +describe('Auto sign-in API Error Path Cases:', () => { + test('autoSignIn should throw an error when autoSignIn is not enabled', async () => { + try { + await autoSignIn(); + } catch (error) { + expect(error).toBeInstanceOf(AuthError); + expect(error.name).toBe('AutoSignInException'); + } + }); +}); diff --git a/packages/auth/src/errors/constants.ts b/packages/auth/src/errors/constants.ts index c0ee65f178f..b8b2340cb15 100644 --- a/packages/auth/src/errors/constants.ts +++ b/packages/auth/src/errors/constants.ts @@ -6,3 +6,4 @@ export const USER_ALREADY_AUTHENTICATED_EXCEPTION = 'UserAlreadyAuthenticatedException'; export const DEVICE_METADATA_NOT_FOUND_EXCEPTION = 'DeviceMetadataNotFoundException'; +export const AUTO_SIGN_IN_EXCEPTION = 'AutoSignInException'; diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 0517c8f5356..bf7bb2f2758 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -28,6 +28,7 @@ export { rememberDevice, forgetDevice, fetchDevices, + autoSignIn, } from './providers/cognito'; export { diff --git a/packages/auth/src/providers/cognito/apis/autoSignIn.ts b/packages/auth/src/providers/cognito/apis/autoSignIn.ts new file mode 100644 index 00000000000..850fd433b91 --- /dev/null +++ b/packages/auth/src/providers/cognito/apis/autoSignIn.ts @@ -0,0 +1,119 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AuthError } from '../../../errors/AuthError'; +import { AUTO_SIGN_IN_EXCEPTION } from '../../../errors/constants'; +import { AutoSignInCallback } from '../../../types/models'; +import { SignInOutput } from '../types'; + +const initialAutoSignIn: AutoSignInCallback = + async (): Promise => { + throw new AuthError({ + name: AUTO_SIGN_IN_EXCEPTION, + message: + 'The autoSignIn flow has not started, or has been cancelled/completed.', + recoverySuggestion: + 'Please try to use the signIn API or log out before starting a new autoSignIn flow.', + }); + }; + +/** + * Signs a user in automatically after finishing the sign-up process. + * + * This API will automatically sign a user in if the autoSignIn flow has been completed in the following cases: + * - User confirmed their account with a verification code sent to their phone or email (default option). + * - User confirmed their account with a verification link sent to their phone or email. In order to + * enable this option you need to go to the Amazon Cognito [console](https://aws.amazon.com/pm/cognito), + * look for your userpool, then go to the `Messaging` tab and enable `link` mode inside the `Verification message` option. + * Finally you need to define the `signUpVerificationMethod` in your `Auth` config. + * + * @example + * ```typescript + * Amplify.configure({ + * Auth: { + * Cognito: { + * ...cognitoConfig, + * signUpVerificationMethod: "link" // the default value is "code" + * } + * }}); + * ``` + * + * @throws AutoSignInException - Thrown when the autoSignIn flow has not started, or has been cancelled/completed. + * @returns The signInOutput. + * + * @example + * ```typescript + * // handleSignUp.ts + * async function handleSignUp( + * username:string, + * password:string + * ){ + * try { + * const { nextStep } = await signUp({ + * username, + * password, + * options: { + * userAttributes:{ email:'email@email.com'}, + * serviceOptions: { + * autoSignIn: true // This enables the auto sign-in flow. + * }, + * }, + * }); + * + * handleSignUpStep(nextStep); + * + * } catch (error) { + * console.log(error); + * } + * } + * + * // handleConfirmSignUp.ts + * async function handleConfirmSignUp(username:string, confirmationCode:string) { + * try { + * const { nextStep } = await confirmSignUp({ + * username, + * confirmationCode, + * }); + * + * handleSignUpStep(nextStep); + * } catch (error) { + * console.log(error); + * } + * } + * + * // signUpUtils.ts + * async function handleSignUpStep( step: SignUpOutput["nextStep"]) { + * switch (step.signUpStep) { + * case "CONFIRM_SIGN_UP": + * + * // Redirect end-user to confirm-sign up screen. + * + * case "COMPLETE_AUTO_SIGN_IN": + * const codeDeliveryDetails = step.codeDeliveryDetails; + * if (codeDeliveryDetails) { + * // Redirect user to confirm-sign-up with link screen. + * } + * const signInOutput = await autoSignIn(); + * // handle sign-in steps + * } + * + * ``` + */ +export let autoSignIn: AutoSignInCallback = initialAutoSignIn; + +/** + * Sets the context of autoSignIn at run time. + * @internal + */ +export function setAutoSignIn(callback: AutoSignInCallback) { + autoSignIn = callback; +} + +/** + * Resets the context + * + * @internal + */ +export function resetAutoSignIn() { + autoSignIn = initialAutoSignIn; +} diff --git a/packages/auth/src/providers/cognito/apis/confirmSignUp.ts b/packages/auth/src/providers/cognito/apis/confirmSignUp.ts index e318b127ade..9d8e340f0e9 100644 --- a/packages/auth/src/providers/cognito/apis/confirmSignUp.ts +++ b/packages/auth/src/providers/cognito/apis/confirmSignUp.ts @@ -2,13 +2,23 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify } from '@aws-amplify/core'; -import { assertTokenProviderConfig, AuthAction } from '@aws-amplify/core/internals/utils'; +import { + assertTokenProviderConfig, + AuthAction, + HubInternal, +} from '@aws-amplify/core/internals/utils'; import { ConfirmSignUpInput, ConfirmSignUpOutput } from '../types'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; import { ConfirmSignUpException } from '../types/errors'; import { confirmSignUp as confirmSignUpClient } from '../utils/clients/CognitoIdentityProvider'; import { getRegion } from '../utils/clients/CognitoIdentityProvider/utils'; +import { AutoSignInEventData } from '../types/models'; +import { + isAutoSignInStarted, + isAutoSignInUserUsingConfirmSignUp, + setAutoSignInStarted, +} from '../utils/signUpHelpers'; import { getAuthUserAgentValue } from '../../../utils'; /** @@ -40,9 +50,9 @@ export async function confirmSignUp( ); await confirmSignUpClient( - { + { region: getRegion(authConfig.userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignUp) + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignUp), }, { Username: username, @@ -54,10 +64,45 @@ export async function confirmSignUp( } ); - return { - isSignUpComplete: true, - nextStep: { - signUpStep: 'DONE', - }, - }; + return new Promise((resolve, reject) => { + try { + const signUpOut: ConfirmSignUpOutput = { + isSignUpComplete: true, + nextStep: { + signUpStep: 'DONE', + }, + }; + + if ( + !isAutoSignInStarted() || + !isAutoSignInUserUsingConfirmSignUp(username) + ) { + return resolve(signUpOut); + } + + const stopListener = HubInternal.listen( + 'auth-internal', + ({ payload }) => { + switch (payload.event) { + case 'autoSignIn': + resolve({ + isSignUpComplete: true, + nextStep: { + signUpStep: 'COMPLETE_AUTO_SIGN_IN', + }, + }); + setAutoSignInStarted(false); + stopListener(); + } + } + ); + + HubInternal.dispatch('auth-internal', { + event: 'confirmSignUp', + data: signUpOut, + }); + } catch (error) { + reject(error); + } + }); } diff --git a/packages/auth/src/providers/cognito/apis/signUp.ts b/packages/auth/src/providers/cognito/apis/signUp.ts index 92e859e81d7..b0ebac05f38 100644 --- a/packages/auth/src/providers/cognito/apis/signUp.ts +++ b/packages/auth/src/providers/cognito/apis/signUp.ts @@ -2,16 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify } from '@aws-amplify/core'; -import { assertTokenProviderConfig, AuthAction } from '@aws-amplify/core/internals/utils'; +import { + assertTokenProviderConfig, + AuthAction, +} from '@aws-amplify/core/internals/utils'; import { AuthDeliveryMedium } from '../../../types'; -import { UserAttributeKey, SignUpInput, SignUpOutput } from '../types'; +import { + UserAttributeKey, + SignUpInput, + SignUpOutput, + SignInInput, +} from '../types'; import { signUp as signUpClient } from '../utils/clients/CognitoIdentityProvider'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; import { SignUpException } from '../types/errors'; -import { AttributeType } from '../utils/clients/CognitoIdentityProvider/types'; import { getRegion } from '../utils/clients/CognitoIdentityProvider/utils'; import { toAttributeType } from '../utils/apiHelpers'; +import { + handleCodeAutoSignIn, + isAutoSignInStarted, + setAutoSignInStarted, + isSignUpComplete, + autoSignInUserConfirmed, + autoSignInWhenUserIsConfirmedWithLink, + setUsernameUsedForAutoSignIn, +} from '../utils/signUpHelpers'; +import { setAutoSignIn } from './autoSignIn'; import { getAuthUserAgentValue } from '../../../utils'; /** @@ -27,7 +44,10 @@ import { getAuthUserAgentValue } from '../../../utils'; export async function signUp(input: SignUpInput): Promise { const { username, password, options } = input; const authConfig = Amplify.getConfig().Auth?.Cognito; - const clientMetadata = input.options?.serviceOptions?.clientMetadata; + const signUpVerificationMethod = + authConfig?.signUpVerificationMethod ?? 'code'; + const { clientMetadata, validationData, autoSignIn } = + input.options?.serviceOptions ?? {}; assertTokenProviderConfig(authConfig); assertValidationError( !!username, @@ -37,46 +57,73 @@ export async function signUp(input: SignUpInput): Promise { !!password, AuthValidationErrorCode.EmptySignUpPassword ); - // TODO: implement autoSignIn - let validationData: AttributeType[] | undefined; - let attributes: AttributeType[] | undefined; - if (options?.serviceOptions?.validationData) { - validationData = toAttributeType(options?.serviceOptions?.validationData); + const signInServiceOptions = + typeof autoSignIn !== 'boolean' ? autoSignIn : undefined; + + const signInInput: SignInInput = { + username, + options: { + serviceOptions: signInServiceOptions, + }, + }; + + // if the authFlowType is 'CUSTOM_WITHOUT_SRP' then we don't include the password + if (signInServiceOptions?.authFlowType !== 'CUSTOM_WITHOUT_SRP') { + signInInput['password'] = password; } - if (options?.userAttributes) { - attributes = toAttributeType(options?.userAttributes); + if (signInServiceOptions || autoSignIn === true) { + setUsernameUsedForAutoSignIn(username); + setAutoSignInStarted(true); } - - const res = await signUpClient( - { + const clientOutput = await signUpClient( + { region: getRegion(authConfig.userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.SignUp) + userAgentValue: getAuthUserAgentValue(AuthAction.SignUp), }, { Username: username, Password: password, - UserAttributes: attributes, + UserAttributes: + options?.userAttributes && toAttributeType(options?.userAttributes), ClientMetadata: clientMetadata, - ValidationData: validationData, + ValidationData: validationData && toAttributeType(validationData), ClientId: authConfig.userPoolClientId, } ); + const { UserSub, CodeDeliveryDetails } = clientOutput; - const { UserConfirmed, CodeDeliveryDetails, UserSub } = res; - - if (UserConfirmed) { + if (isSignUpComplete(clientOutput) && isAutoSignInStarted()) { + setAutoSignIn(autoSignInUserConfirmed(signInInput)); + return { + isSignUpComplete: true, + nextStep: { + signUpStep: 'COMPLETE_AUTO_SIGN_IN', + }, + }; + } else if (isSignUpComplete(clientOutput) && !isAutoSignInStarted()) { return { isSignUpComplete: true, nextStep: { signUpStep: 'DONE', }, }; - } else { + } else if ( + !isSignUpComplete(clientOutput) && + isAutoSignInStarted() && + signUpVerificationMethod === 'code' + ) { + handleCodeAutoSignIn(signInInput); + } else if ( + !isSignUpComplete(clientOutput) && + isAutoSignInStarted() && + signUpVerificationMethod === 'link' + ) { + setAutoSignIn(autoSignInWhenUserIsConfirmedWithLink(signInInput)); return { isSignUpComplete: false, nextStep: { - signUpStep: 'CONFIRM_SIGN_UP', + signUpStep: 'COMPLETE_AUTO_SIGN_IN', codeDeliveryDetails: { deliveryMedium: CodeDeliveryDetails?.DeliveryMedium as AuthDeliveryMedium, @@ -87,4 +134,18 @@ export async function signUp(input: SignUpInput): Promise { userId: UserSub, }; } + + return { + isSignUpComplete: false, + nextStep: { + signUpStep: 'CONFIRM_SIGN_UP', + codeDeliveryDetails: { + deliveryMedium: + CodeDeliveryDetails?.DeliveryMedium as AuthDeliveryMedium, + destination: CodeDeliveryDetails?.Destination as string, + attributeName: CodeDeliveryDetails?.AttributeName as UserAttributeKey, + }, + }, + userId: UserSub, + }; } diff --git a/packages/auth/src/providers/cognito/index.ts b/packages/auth/src/providers/cognito/index.ts index f4a8cf821dd..6879dcd5f11 100644 --- a/packages/auth/src/providers/cognito/index.ts +++ b/packages/auth/src/providers/cognito/index.ts @@ -28,6 +28,7 @@ export { deleteUser } from './apis/deleteUser'; export { rememberDevice } from './apis/rememberDevice'; export { forgetDevice } from './apis/forgetDevice'; export { fetchDevices } from './apis/fetchDevices'; +export { autoSignIn } from './apis/autoSignIn'; export { ConfirmResetPasswordInput, ConfirmSignInInput, diff --git a/packages/auth/src/providers/cognito/types/models.ts b/packages/auth/src/providers/cognito/types/models.ts index 62e0a69386f..d6c0ff24511 100644 --- a/packages/auth/src/providers/cognito/types/models.ts +++ b/packages/auth/src/providers/cognito/types/models.ts @@ -7,6 +7,7 @@ import { AuthDevice, } from '../../../types'; import { AuthProvider } from '../../../types/inputs'; +import { SignInOutput, SignUpOutput } from './outputs'; /** * Cognito supported AuthFlowTypes that may be passed as part of the Sign In request. @@ -62,6 +63,14 @@ export type MFAPreference = | 'PREFERRED' | 'NOT_PREFERRED'; +export type AutoSignInEventData = + | { + event: 'confirmSignUp'; + data: SignUpOutput; + } + | { + event: 'autoSignIn'; + }; /** * Holds the device specific information along with it's id and name. */ diff --git a/packages/auth/src/providers/cognito/types/options.ts b/packages/auth/src/providers/cognito/types/options.ts index 8b536342aad..a92052f403d 100644 --- a/packages/auth/src/providers/cognito/types/options.ts +++ b/packages/auth/src/providers/cognito/types/options.ts @@ -39,7 +39,7 @@ export type SignInOptions = { export type SignUpOptions = { validationData?: ValidationData; clientMetadata?: ClientMetadata; - // autoSignIn?: AutoSignInOptions; + autoSignIn?: SignInOptions | boolean; // default is false; }; /** diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index 48437930e34..67f95c5e068 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -21,6 +21,8 @@ import { AuthAdditionalInfo, AuthSignInOutput, AuthDeliveryMedium, + AuthSignUpOutput, + AuthSignInInput, } from '../../../types'; import { AuthError } from '../../../errors/AuthError'; import { InitiateAuthException } from '../types/errors'; @@ -107,9 +109,9 @@ export async function handleCustomChallenge({ }; const response = await respondToAuthChallenge( - { + { region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn) + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), }, jsonReq ); @@ -139,9 +141,9 @@ export async function handleMFASetupChallenge({ }; const { Session } = await verifySoftwareToken( - { + { region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn) + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), }, { UserCode: challengeResponse, @@ -192,10 +194,10 @@ export async function handleSelectMFATypeChallenge({ }; return respondToAuthChallenge( - { + { region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn) - }, + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), + }, jsonReq ); } @@ -221,10 +223,10 @@ export async function handleSMSMFAChallenge({ }; return respondToAuthChallenge( - { + { region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn) - }, + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), + }, jsonReq ); } @@ -248,10 +250,10 @@ export async function handleSoftwareTokenMFAChallenge({ ClientId: userPoolClientId, }; return respondToAuthChallenge( - { + { region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn) - }, + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), + }, jsonReq ); } @@ -279,10 +281,10 @@ export async function handleCompleteNewPasswordChallenge({ }; return respondToAuthChallenge( - { + { region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn) - }, + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), + }, jsonReq ); } @@ -312,9 +314,9 @@ export async function handleUserPasswordAuthFlow( }; const response = await initiateAuth( - { + { region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.SignIn) + userAgentValue: getAuthUserAgentValue(AuthAction.SignIn), }, jsonReq ); @@ -358,10 +360,10 @@ export async function handleUserSRPAuthFlow( }; const resp = await initiateAuth( - { + { region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.SignIn) - }, + userAgentValue: getAuthUserAgentValue(AuthAction.SignIn), + }, jsonReq ); const { ChallengeParameters: challengeParameters, Session: session } = resp; @@ -400,9 +402,9 @@ export async function handleCustomAuthFlowWithoutSRP( }; const response = await initiateAuth( - { + { region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.SignIn) + userAgentValue: getAuthUserAgentValue(AuthAction.SignIn), }, jsonReq ); @@ -450,10 +452,10 @@ export async function handleCustomSRPAuthFlow( const { ChallengeParameters: challengeParameters, Session: session } = await initiateAuth( - { + { region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.SignIn) - }, + userAgentValue: getAuthUserAgentValue(AuthAction.SignIn), + }, jsonReq ); diff --git a/packages/auth/src/providers/cognito/utils/signUpHelpers.ts b/packages/auth/src/providers/cognito/utils/signUpHelpers.ts new file mode 100644 index 00000000000..55ecd5bb228 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/signUpHelpers.ts @@ -0,0 +1,170 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { HubInternal } from '@aws-amplify/core/internals/utils'; +import { signIn } from '../apis/signIn'; +import { SignInInput, SignInOutput } from '../types'; +import { AutoSignInEventData } from '../types/models'; +import { AutoSignInCallback } from '../../../types/models'; +import { AuthError } from '../../../errors/AuthError'; +import { SignUpCommandOutput } from './clients/CognitoIdentityProvider/types'; +import { resetAutoSignIn, setAutoSignIn } from '../apis/autoSignIn'; +import { AUTO_SIGN_IN_EXCEPTION } from '../../../errors/constants'; + +const MAX_AUTOSIGNIN_POLLING_MS = 3 * 60 * 1000; + +export function handleCodeAutoSignIn(signInInput: SignInInput) { + const stopHubListener = HubInternal.listen( + 'auth-internal', + async ({ payload }) => { + switch (payload.event) { + case 'confirmSignUp': { + const response = payload.data; + if (response?.isSignUpComplete) { + HubInternal.dispatch('auth-internal', { + event: 'autoSignIn', + }); + setAutoSignIn(autoSignInWithCode(signInInput)); + stopHubListener(); + } + } + } + } + ); + + // This will stop the listener if confirmSignUp is not resolved. + const timeOutId = setTimeout(() => { + stopHubListener(); + setAutoSignInStarted(false); + clearTimeout(timeOutId); + resetAutoSignIn(); + }, MAX_AUTOSIGNIN_POLLING_MS); +} + +// Debounces the auto sign-in flow with link +// This approach avoids running the useInterval and signIn API twice in a row. +// This issue would be common as React.18 introduced double rendering of the +// useEffect hook on every mount. +// https://github.com/facebook/react/issues/24502 +// https://legacy.reactjs.org/docs/strict-mode.html#ensuring-reusable-state +type TimeOutOutput = ReturnType; +function debounce any>(fun: F, delay: number) { + let timer: TimeOutOutput | undefined; + return function ( + args: F extends (...args: infer A) => any ? A : never + ): void { + if (!timer) { + fun(...args); + } + clearTimeout(timer as TimeOutOutput); + timer = setTimeout(() => { + timer = undefined; + }, delay); + }; +} + +function handleAutoSignInWithLink( + signInInput: SignInInput, + resolve: Function, + reject: Function +) { + const start = Date.now(); + const autoSignInPollingIntervalId = setInterval(async () => { + const elapsedTime = Date.now() - start; + const maxTime = MAX_AUTOSIGNIN_POLLING_MS; + if (elapsedTime > maxTime) { + clearInterval(autoSignInPollingIntervalId); + setAutoSignInStarted(false); + reject( + new AuthError({ + name: AUTO_SIGN_IN_EXCEPTION, + message: 'The account was not confirmed on time.', + recoverySuggestion: + 'Try to verify your account by clicking the link sent your email or phone and then login manually.', + }) + ); + resetAutoSignIn(); + return; + } else { + try { + const signInOutput = await signIn(signInInput); + if (signInOutput.nextStep.signInStep !== 'CONFIRM_SIGN_UP') { + resolve(signInOutput); + clearInterval(autoSignInPollingIntervalId); + setAutoSignInStarted(false); + resetAutoSignIn(); + return; + } + } catch (error) { + clearInterval(autoSignInPollingIntervalId); + setAutoSignInStarted(false); + reject(error); + resetAutoSignIn(); + } + } + }, 5000); +} +const debouncedAutoSignInWithLink = debounce(handleAutoSignInWithLink, 300); +const debouncedAutoSignWithCodeOrUserConfirmed = debounce( + handleAutoSignInWithCodeOrUserConfirmed, + 300 +); + +let autoSignInStarted: boolean = false; + +let usernameUsedForAutoSignIn: string | undefined; + +export function setUsernameUsedForAutoSignIn(username?: string) { + usernameUsedForAutoSignIn = username; +} +export function isAutoSignInUserUsingConfirmSignUp(username: string) { + return usernameUsedForAutoSignIn === username; +} + +export function isAutoSignInStarted(): boolean { + return autoSignInStarted; +} +export function setAutoSignInStarted(value: boolean) { + if (value === false) { + setUsernameUsedForAutoSignIn(undefined); + } + autoSignInStarted = value; +} + +export function isSignUpComplete(output: SignUpCommandOutput): boolean { + return !!output.UserConfirmed; +} + +export function autoSignInWhenUserIsConfirmedWithLink( + signInInput: SignInInput +): AutoSignInCallback { + return async () => { + return new Promise(async (resolve, reject) => { + debouncedAutoSignInWithLink([signInInput, resolve, reject]); + }); + }; +} +async function handleAutoSignInWithCodeOrUserConfirmed( + signInInput: SignInInput, + resolve: Function, + reject: Function +) { + try { + const output = await signIn(signInInput); + resolve(output); + resetAutoSignIn(); + } catch (error) { + reject(error); + resetAutoSignIn(); + } +} + +function autoSignInWithCode(signInInput: SignInInput): AutoSignInCallback { + return async () => { + return new Promise(async (resolve, reject) => { + debouncedAutoSignWithCodeOrUserConfirmed([signInInput, resolve, reject]); + }); + }; +} + +export const autoSignInUserConfirmed = autoSignInWithCode; diff --git a/packages/auth/src/types/index.ts b/packages/auth/src/types/index.ts index 42405ff88a0..8b89219859e 100644 --- a/packages/auth/src/types/index.ts +++ b/packages/auth/src/types/index.ts @@ -18,7 +18,6 @@ export { AuthUser, AuthTOTPSetupDetails, AuthResetPasswordStep, - AuthSignUpStep, AuthUpdateAttributeStep, AuthDevice, } from './models'; diff --git a/packages/auth/src/types/models.ts b/packages/auth/src/types/models.ts index 9212722495f..4db9c4a85b4 100644 --- a/packages/auth/src/types/models.ts +++ b/packages/auth/src/types/models.ts @@ -1,7 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AuthStandardAttributeKey } from "@aws-amplify/core/internals/utils"; +import { SignInOutput } from '../providers/cognito'; +import { AuthStandardAttributeKey } from '@aws-amplify/core/internals/utils'; /** * Additional data that may be returned from Auth APIs. @@ -210,22 +211,6 @@ export type AuthUserAttribute< */ export type AuthUserAttributeKey = AuthStandardAttributeKey | AuthAnyAttribute; -/** - * Denotes the next step in the Sign Up process. - */ -export type AuthSignUpStep = 'CONFIRM_SIGN_UP' | 'DONE'; - -/** - * Data encapsulating the next step in the Sign Up process - */ -export type AuthNextSignUpStep< - UserAttributeKey extends AuthUserAttributeKey = AuthUserAttributeKey -> = { - signUpStep?: AuthSignUpStep; - additionalInfo?: AuthAdditionalInfo; - codeDeliveryDetails?: AuthCodeDeliveryDetails; -}; - /** * Denotes the next step in the Update User Attribute process. */ @@ -239,6 +224,34 @@ export type AuthUpdateAttributeStep = * Auth update attribute step indicates that the attribute is updated. */ | 'DONE'; +/** + * Data encapsulating the next step in the Sign Up process + */ +export type AuthNextSignUpStep< + UserAttributeKey extends AuthUserAttributeKey = AuthUserAttributeKey +> = + | ConfirmSignUpSignUpStep + | AutoSignInSignUpStep + | DoneSignUpStep; + +export type AutoSignInCallback = () => Promise; +export type DoneSignUpStep = { + signUpStep: 'DONE'; +}; + +export type ConfirmSignUpSignUpStep< + UserAttributeKey extends AuthUserAttributeKey = AuthUserAttributeKey +> = { + signUpStep: 'CONFIRM_SIGN_UP'; + codeDeliveryDetails: AuthCodeDeliveryDetails; +}; + +export type AutoSignInSignUpStep< + UserAttributeKey extends AuthUserAttributeKey = AuthUserAttributeKey +> = { + signUpStep: 'COMPLETE_AUTO_SIGN_IN'; + codeDeliveryDetails?: AuthCodeDeliveryDetails; +}; export type AuthNextUpdateAttributeStep< UserAttributeKey extends AuthUserAttributeKey = AuthUserAttributeKey diff --git a/packages/auth/src/types/outputs.ts b/packages/auth/src/types/outputs.ts index 2d0f3b8a97f..100d422105b 100644 --- a/packages/auth/src/types/outputs.ts +++ b/packages/auth/src/types/outputs.ts @@ -22,8 +22,8 @@ export type AuthSignUpOutput< UserAttributeKey extends AuthUserAttributeKey = AuthUserAttributeKey > = { isSignUpComplete: boolean; - nextStep: AuthNextSignUpStep; userId?: string; + nextStep: AuthNextSignUpStep; }; export type AuthResetPasswordOutput< diff --git a/packages/aws-amplify/__tests__/exports.test.ts b/packages/aws-amplify/__tests__/exports.test.ts index 5530c2c6c9d..982ac31b131 100644 --- a/packages/aws-amplify/__tests__/exports.test.ts +++ b/packages/aws-amplify/__tests__/exports.test.ts @@ -146,6 +146,7 @@ describe('aws-amplify Exports', () => { "rememberDevice", "forgetDevice", "fetchDevices", + "autoSignIn", "AuthError", "fetchAuthSession", ] @@ -180,6 +181,7 @@ describe('aws-amplify Exports', () => { "rememberDevice", "forgetDevice", "fetchDevices", + "autoSignIn", "cognitoCredentialsProvider", "CognitoAWSCredentialsAndIdentityIdProvider", "DefaultIdentityIdStore", diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 05a67b331e8..53389803a5a 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -306,7 +306,7 @@ "name": "[Auth] signUp (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signUp }", - "limit": "11.8 kB" + "limit": "30.51 kB" }, { "name": "[Auth] resetPassword (Cognito)", @@ -324,7 +324,7 @@ "name": "[Auth] signIn (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signIn }", - "limit": "29.7 kB" + "limit": "29.64 kB" }, { "name": "[Auth] resendSignUpCode (Cognito)", @@ -336,7 +336,7 @@ "name": "[Auth] confirmSignUp (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ confirmSignUp }", - "limit": "11.67 kB" + "limit": "30.22 kB" }, { "name": "[Auth] confirmSignIn (Cognito)", diff --git a/packages/core/src/Hub/index.ts b/packages/core/src/Hub/index.ts index cf989243e68..d226a9f1b78 100644 --- a/packages/core/src/Hub/index.ts +++ b/packages/core/src/Hub/index.ts @@ -203,3 +203,11 @@ export class HubClass { pseudo Singleton for the main messaging bus, however you can still create your own instance of HubClass() for a separate "private bus" of events.*/ export const Hub = new HubClass('__default__'); + +/** + * @internal + * + * Internal hub used for core Amplify functionality. Not intended for use outside of Amplify. + * + */ +export const HubInternal = new HubClass('internal-hub'); diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index a9861cc6b6a..21a8f867782 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -37,7 +37,7 @@ export { CognitoIdentityPoolConfig, JwtPayload, AuthStandardAttributeKey, - AuthVerifiableAttributeKey + AuthVerifiableAttributeKey, } from './singleton/Auth/types'; // Logging utilities @@ -96,4 +96,4 @@ export { base64Decoder, base64Encoder } from './utils/convert'; export { getCrypto } from './utils/globalHelpers'; // Hub -export { HubClass } from './Hub'; +export { HubInternal } from './Hub'; diff --git a/packages/core/src/singleton/Auth/types.ts b/packages/core/src/singleton/Auth/types.ts index 05f9c3adbd9..6dba247e925 100644 --- a/packages/core/src/singleton/Auth/types.ts +++ b/packages/core/src/singleton/Auth/types.ts @@ -93,7 +93,9 @@ export type AuthStandardAttributeKey = export type AuthVerifiableAttributeKey = 'email' | 'phone_number'; -export type AuthConfigUserAttributes = Partial>; +export type AuthConfigUserAttributes = Partial< + Record +>; export type AuthConfig = StrictUnion< | AuthIdentityPoolConfig @@ -112,6 +114,7 @@ export type AuthIdentityPoolConfig = { userPoolClientId?: never; userPoolId?: never; loginWith?: never; + signUpVerificationMethod?: never; userAttributes?: never; mfa?: never; passwordFormat?: never; From bd7c8387185ef93c7ad9928a736dd4368d9a9c55 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Tue, 10 Oct 2023 09:52:19 -0700 Subject: [PATCH 5/6] fix(auth): first clear deviceTokens then signout (#12230) fix(auth): deleteUser clear deviceTokens then signout Co-authored-by: Ashwin Kumar --- .../__tests__/providers/cognito/deleteUser.test.ts | 11 ++++++----- .../auth/src/providers/cognito/apis/deleteUser.ts | 11 +++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/deleteUser.test.ts b/packages/auth/__tests__/providers/cognito/deleteUser.test.ts index fcf7e45bdf1..76996d8c7fc 100644 --- a/packages/auth/__tests__/providers/cognito/deleteUser.test.ts +++ b/packages/auth/__tests__/providers/cognito/deleteUser.test.ts @@ -84,12 +84,13 @@ describe('deleteUser API happy path cases', () => { }) ); expect(deleteUserClientSpy).toBeCalledTimes(1); + expect(tokenOrchestratorSpy).toHaveBeenCalledTimes(1); + expect(signOutApiSpy).toHaveBeenCalledTimes(1); - // signout - expect(signOutApiSpy).toBeCalledTimes(1); - - // clear device tokens - expect(tokenOrchestratorSpy).toBeCalled(); + // make sure we clearDeviceToken -> signout, in that order + expect(tokenOrchestratorSpy.mock.invocationCallOrder[0]).toBeLessThan( + signOutApiSpy.mock.invocationCallOrder[0] + ); }); }); diff --git a/packages/auth/src/providers/cognito/apis/deleteUser.ts b/packages/auth/src/providers/cognito/apis/deleteUser.ts index 68c609e765d..3f32627c712 100644 --- a/packages/auth/src/providers/cognito/apis/deleteUser.ts +++ b/packages/auth/src/providers/cognito/apis/deleteUser.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify } from '@aws-amplify/core'; -import { assertTokenProviderConfig, AuthAction } from '@aws-amplify/core/internals/utils'; +import { + assertTokenProviderConfig, + AuthAction, +} from '@aws-amplify/core/internals/utils'; import { fetchAuthSession } from '../../../'; import { getRegion } from '../utils/clients/CognitoIdentityProvider/utils'; import { assertAuthTokens } from '../utils/types'; @@ -26,14 +29,14 @@ export async function deleteUser(): Promise { assertAuthTokens(tokens); await serviceDeleteUser( - { + { region: getRegion(authConfig.userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.DeleteUser) + userAgentValue: getAuthUserAgentValue(AuthAction.DeleteUser), }, { AccessToken: tokens.accessToken.toString(), } ); - await signOut(); await tokenOrchestrator.clearDeviceMetadata(); + await signOut(); } From db28490b9977e61be452863673fb2ca26cb0c7ac Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Tue, 10 Oct 2023 12:16:44 -0500 Subject: [PATCH 6/6] feat: Added internal APIs for setting custom User Agent state (#12249) --- packages/aws-amplify/package.json | 52 ++++++------ .../Platform/customUserAgent.test.ts | 79 +++++++++++++++++++ .../userAgent.test.ts} | 72 ++++++++++++++--- packages/core/src/Platform/customUserAgent.ts | 75 ++++++++++++++++++ packages/core/src/Platform/index.ts | 15 +++- packages/core/src/Platform/types.ts | 45 ++++++++++- packages/core/src/libraryUtils.ts | 4 +- 7 files changed, 301 insertions(+), 41 deletions(-) create mode 100644 packages/core/__tests__/Platform/customUserAgent.test.ts rename packages/core/__tests__/{Platform.test.ts => Platform/userAgent.test.ts} (55%) create mode 100644 packages/core/src/Platform/customUserAgent.ts diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 53389803a5a..28573ac5087 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -252,31 +252,31 @@ "name": "[Analytics] record (Pinpoint)", "path": "./lib-esm/analytics/index.js", "import": "{ record }", - "limit": "21.62 kB" + "limit": "21.69 kB" }, { "name": "[Analytics] record (Kinesis)", "path": "./lib-esm/analytics/kinesis/index.js", "import": "{ record }", - "limit": "46.89 kB" + "limit": "46.96 kB" }, { "name": "[Analytics] record (Kinesis Firehose)", "path": "./lib-esm/analytics/kinesis-firehose/index.js", "import": "{ record }", - "limit": "43.23 kB" + "limit": "43.31 kB" }, { "name": "[Analytics] record (Personalize)", "path": "./lib-esm/analytics/personalize/index.js", "import": "{ record }", - "limit": "47.50 kB" + "limit": "47.59 kB" }, { "name": "[Analytics] identifyUser (Pinpoint)", "path": "./lib-esm/analytics/index.js", "import": "{ identifyUser }", - "limit": "19.72 kB" + "limit": "19.79 kB" }, { "name": "[Analytics] enable", @@ -312,13 +312,13 @@ "name": "[Auth] resetPassword (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ resetPassword }", - "limit": "11.7 kB" + "limit": "11.77 kB" }, { "name": "[Auth] confirmResetPassword (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ confirmResetPassword }", - "limit": "11.64 kB" + "limit": "11.70 kB" }, { "name": "[Auth] signIn (Cognito)", @@ -330,7 +330,7 @@ "name": "[Auth] resendSignUpCode (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ resendSignUpCode }", - "limit": "11.66 kB" + "limit": "11.73 kB" }, { "name": "[Auth] confirmSignUp (Cognito)", @@ -342,31 +342,31 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ confirmSignIn }", - "limit": "29.6 kB" + "limit": "29.65 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ updateMFAPreference }", - "limit": "10.75 kB" + "limit": "10.82 kB" }, { "name": "[Auth] fetchMFAPreference (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ fetchMFAPreference }", - "limit": "10.79 kB" + "limit": "10.85 kB" }, { "name": "[Auth] verifyTOTPSetup (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ verifyTOTPSetup }", - "limit": "11.68 kB" + "limit": "11.74 kB" }, { "name": "[Auth] updatePassword (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ updatePassword }", - "limit": "11.66 kB" + "limit": "11.73 kB" }, { "name": "[Auth] setUpTOTP (Cognito)", @@ -378,7 +378,7 @@ "name": "[Auth] updateUserAttributes (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ updateUserAttributes }", - "limit": "10.93 kB" + "limit": "11.0 kB" }, { "name": "[Auth] getCurrentUser (Cognito)", @@ -390,73 +390,73 @@ "name": "[Auth] confirmUserAttribute (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ confirmUserAttribute }", - "limit": "11.69 kB" + "limit": "11.76 kB" }, { "name": "[Auth] signInWithRedirect (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signInWithRedirect }", - "limit": "22.87 kB" + "limit": "22.94 kB" }, { "name": "[Auth] fetchUserAttributes (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ fetchUserAttributes }", - "limit": "10.78 kB" + "limit": "10.85 kB" }, { "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "31.88 kB" + "limit": "31.92 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signInWithRedirect, signOut, fetchAuthSession }", - "limit": "23.36 kB" + "limit": "23.42 kB" }, { "name": "[Storage] copy (S3)", "path": "./lib-esm/storage/index.js", "import": "{ copy }", - "limit": "17.88 kB" + "limit": "17.95 kB" }, { "name": "[Storage] downloadData (S3)", "path": "./lib-esm/storage/index.js", "import": "{ downloadData }", - "limit": "18.24 kB" + "limit": "18.30 kB" }, { "name": "[Storage] getProperties (S3)", "path": "./lib-esm/storage/index.js", "import": "{ getProperties }", - "limit": "17.52 kB" + "limit": "17.6 kB" }, { "name": "[Storage] getUrl (S3)", "path": "./lib-esm/storage/index.js", "import": "{ getUrl }", - "limit": "18.96 kB" + "limit": "19.03 kB" }, { "name": "[Storage] list (S3)", "path": "./lib-esm/storage/index.js", "import": "{ list }", - "limit": "18.05 kB" + "limit": "18.12 kB" }, { "name": "[Storage] remove (S3)", "path": "./lib-esm/storage/index.js", "import": "{ remove }", - "limit": "17.36 kB" + "limit": "17.43 kB" }, { "name": "[Storage] uploadData (S3)", "path": "./lib-esm/storage/index.js", "import": "{ uploadData }", - "limit": "24.16 kB" + "limit": "24.22 kB" } ], "jest": { diff --git a/packages/core/__tests__/Platform/customUserAgent.test.ts b/packages/core/__tests__/Platform/customUserAgent.test.ts new file mode 100644 index 00000000000..913872eb6db --- /dev/null +++ b/packages/core/__tests__/Platform/customUserAgent.test.ts @@ -0,0 +1,79 @@ +import { + Category, + AuthAction, + StorageAction, + SetCustomUserAgentInput, +} from '../../src/Platform/types'; + +const MOCK_AUTH_UA_STATE: SetCustomUserAgentInput = { + category: Category.Auth, + apis: [AuthAction.ConfirmSignIn, AuthAction.SignIn], + additionalDetails: [['uastate', 'auth']], +}; + +const MOCK_STORAGE_UA_STATE: SetCustomUserAgentInput = { + category: Category.Storage, + apis: [StorageAction.Copy], + additionalDetails: [['uastate', 'storage']], +}; + +describe('Custom user agent utilities', () => { + let getCustomUserAgent; + let setCustomUserAgent; + + beforeEach(() => { + jest.resetModules(); + getCustomUserAgent = + require('../../src/Platform/customUserAgent').getCustomUserAgent; + setCustomUserAgent = + require('../../src/Platform/customUserAgent').setCustomUserAgent; + }); + + it('sets custom user agent state for multiple categories and APIs', () => { + setCustomUserAgent(MOCK_AUTH_UA_STATE); + setCustomUserAgent(MOCK_STORAGE_UA_STATE); + + const confirmSignInState = getCustomUserAgent( + Category.Auth, + AuthAction.ConfirmSignIn + ); + const signInState = getCustomUserAgent(Category.Auth, AuthAction.SignIn); + const copyState = getCustomUserAgent(Category.Storage, StorageAction.Copy); + + expect(copyState).toEqual([['uastate', 'storage']]); + expect(confirmSignInState).toStrictEqual([['uastate', 'auth']]); + expect(signInState).toEqual(confirmSignInState); + }); + + it('returns a callback that will clear user agent state', () => { + const cleanUp = setCustomUserAgent(MOCK_AUTH_UA_STATE); + const cleanUp2 = setCustomUserAgent(MOCK_AUTH_UA_STATE); + const cleanUp3 = setCustomUserAgent(MOCK_STORAGE_UA_STATE); + + // Setting state for the same category & API twice should prevent deletion until all references are cleaned up + cleanUp(); + let confirmSignInState = getCustomUserAgent( + Category.Auth, + AuthAction.ConfirmSignIn + ); + expect(confirmSignInState).toStrictEqual([['uastate', 'auth']]); + + cleanUp2(); + confirmSignInState = getCustomUserAgent( + Category.Auth, + AuthAction.ConfirmSignIn + ); + expect(confirmSignInState).toStrictEqual(undefined); + + // Calling the same cleanup callback twice shouldn't result in errors + cleanUp(); + + // Cleaning up shouldn't impact state set in a different call + let copyState = getCustomUserAgent(Category.Storage, StorageAction.Copy); + expect(copyState).toEqual([['uastate', 'storage']]); + + cleanUp3(); + copyState = getCustomUserAgent(Category.Storage, StorageAction.Copy); + expect(copyState).toStrictEqual(undefined); + }); +}); diff --git a/packages/core/__tests__/Platform.test.ts b/packages/core/__tests__/Platform/userAgent.test.ts similarity index 55% rename from packages/core/__tests__/Platform.test.ts rename to packages/core/__tests__/Platform/userAgent.test.ts index 8198ff1f888..0780e7b9c2c 100644 --- a/packages/core/__tests__/Platform.test.ts +++ b/packages/core/__tests__/Platform/userAgent.test.ts @@ -2,13 +2,26 @@ import { getAmplifyUserAgentObject, getAmplifyUserAgent, Platform, -} from '../src/Platform'; -import { version } from '../src/Platform/version'; -import { ApiAction, Category, Framework } from '../src/Platform/types'; -import { detectFramework, clearCache } from '../src/Platform/detectFramework'; -import * as detection from '../src/Platform/detection'; +} from '../../src/Platform'; +import { version } from '../../src/Platform/version'; +import { + ApiAction, + AuthAction, + Category, + Framework, +} from '../../src/Platform/types'; +import { + detectFramework, + clearCache, +} from '../../src/Platform/detectFramework'; +import * as detection from '../../src/Platform/detection'; +import { getCustomUserAgent } from '../../src/Platform/customUserAgent'; + +jest.mock('../../src/Platform/customUserAgent'); describe('Platform test', () => { + const mockGetCustomUserAgent = getCustomUserAgent as jest.Mock; + beforeAll(() => { jest.useFakeTimers(); }); @@ -18,6 +31,7 @@ describe('Platform test', () => { }); beforeEach(() => { + mockGetCustomUserAgent.mockReset(); clearCache(); }); @@ -39,13 +53,32 @@ describe('Platform test', () => { test('with customUserAgentDetails', () => { expect( getAmplifyUserAgentObject({ - category: Category.API, - action: ApiAction.None, + category: Category.Auth, + action: AuthAction.ConfirmSignIn, + }) + ).toStrictEqual([ + ['aws-amplify', version], + [Category.Auth, AuthAction.ConfirmSignIn], + ['framework', Framework.WebUnknown], + ]); + }); + + it('injects global user agent details when available', () => { + const mockUAState = [['uiversion', '1.0.0'], ['flag']]; + + mockGetCustomUserAgent.mockReturnValue(mockUAState); + + expect( + getAmplifyUserAgentObject({ + category: Category.Auth, + action: AuthAction.ConfirmSignIn, }) ).toStrictEqual([ ['aws-amplify', version], - [Category.API, ApiAction.None], + [Category.Auth, AuthAction.ConfirmSignIn], ['framework', Framework.WebUnknown], + ['uiversion', '1.0.0'], + ['flag'], ]); }); }); @@ -60,11 +93,26 @@ describe('Platform test', () => { test('with customUserAgentDetails', () => { expect( getAmplifyUserAgent({ - category: Category.API, - action: ApiAction.None, + category: Category.Auth, + action: AuthAction.ConfirmSignIn, + }) + ).toBe( + `${Platform.userAgent} ${Category.Auth}/${AuthAction.ConfirmSignIn} framework/${Framework.WebUnknown}` + ); + }); + + it('handles flag UA attributes', () => { + const mockUAState = [['uiversion', '1.0.0'], ['flag']]; + + mockGetCustomUserAgent.mockReturnValue(mockUAState); + + expect( + getAmplifyUserAgent({ + category: Category.Auth, + action: AuthAction.ConfirmSignIn, }) ).toBe( - `${Platform.userAgent} ${Category.API}/${ApiAction.None} framework/${Framework.WebUnknown}` + `${Platform.userAgent} ${Category.Auth}/${AuthAction.ConfirmSignIn} framework/${Framework.WebUnknown} uiversion/1.0.0 flag` ); }); }); @@ -86,7 +134,7 @@ describe('detectFramework observers', () => { beforeAll(() => { jest.resetModules(); - module = require('../src/Platform/detectFramework'); + module = require('../../src/Platform/detectFramework'); jest.useFakeTimers(); }); diff --git a/packages/core/src/Platform/customUserAgent.ts b/packages/core/src/Platform/customUserAgent.ts new file mode 100644 index 00000000000..a62bf6b9fcc --- /dev/null +++ b/packages/core/src/Platform/customUserAgent.ts @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AdditionalDetails, + CategoryUserAgentStateMap, + CustomUserAgentStateMap, + SetCustomUserAgentInput, +} from './types'; + +// Maintains custom user-agent state set by external consumers. +const customUserAgentState: CustomUserAgentStateMap = {}; + +/** + * Sets custom user agent state which will be appended to applicable requests. Returns a function that can be used to + * clean up any custom state set with this API. + * + * @note + * This API operates globally. Calling this API multiple times will result in the most recently set values for a + * particular API being used. + * + * @note + * This utility IS NOT compatible with SSR. + * + * @param input - SetCustomUserAgentInput that defines custom state to apply to the specified APIs. + */ +export const setCustomUserAgent = ( + input: SetCustomUserAgentInput +): (() => void) => { + // Save custom user-agent state & increment reference counter + // TODO Remove `any` when we upgrade to TypeScript 5.2, see: https://github.com/microsoft/TypeScript/issues/44373 + customUserAgentState[input.category] = (input.apis as any[]).reduce( + (acc: CategoryUserAgentStateMap, api: string) => ({ + ...acc, + [api]: { + refCount: acc[api]?.refCount ? acc[api].refCount + 1 : 1, + additionalDetails: input.additionalDetails, + }, + }), + customUserAgentState[input.category] ?? {} + ); + + // Callback that cleans up state for APIs recorded by this call + let cleanUpCallbackCalled = false; + const cleanUpCallback = () => { + // Only allow the cleanup callback to be called once + if (cleanUpCallbackCalled) { + return; + } + cleanUpCallbackCalled = true; + + input.apis.forEach(api => { + const apiRefCount = customUserAgentState[input.category][api].refCount; + + if (apiRefCount > 1) { + customUserAgentState[input.category][api].refCount = apiRefCount - 1; + } else { + delete customUserAgentState[input.category][api]; + + // Clean up category if no more APIs set + if (!Object.keys(customUserAgentState[input.category]).length) { + delete customUserAgentState[input.category]; + } + } + }); + }; + + return cleanUpCallback; +}; + +export const getCustomUserAgent = ( + category: string, + api: string +): AdditionalDetails | undefined => + customUserAgentState[category]?.[api]?.additionalDetails; diff --git a/packages/core/src/Platform/index.ts b/packages/core/src/Platform/index.ts index e9e3ef704be..ec69df9c505 100644 --- a/packages/core/src/Platform/index.ts +++ b/packages/core/src/Platform/index.ts @@ -5,6 +5,7 @@ import { CustomUserAgentDetails, Framework } from './types'; import { version } from './version'; import { detectFramework, observeFrameworkChanges } from './detectFramework'; import { UserAgent as AWSUserAgent } from '@aws-sdk/types'; +import { getCustomUserAgent } from './customUserAgent'; const BASE_USER_AGENT = `aws-amplify`; @@ -39,6 +40,16 @@ export const getAmplifyUserAgentObject = ({ } userAgent.push(['framework', detectFramework()]); + if (category && action) { + const customState = getCustomUserAgent(category, action); + + if (customState) { + customState.forEach(state => { + userAgent.push(state); + }); + } + } + return userAgent; }; @@ -47,7 +58,9 @@ export const getAmplifyUserAgent = ( ): string => { const userAgent = getAmplifyUserAgentObject(customUserAgentDetails); const userAgentString = userAgent - .map(([agentKey, agentValue]) => `${agentKey}/${agentValue}`) + .map(([agentKey, agentValue]) => + agentKey && agentValue ? `${agentKey}/${agentValue}` : agentKey + ) .join(' '); return userAgentString; diff --git a/packages/core/src/Platform/types.ts b/packages/core/src/Platform/types.ts index fe2b293bad0..003c9018838 100644 --- a/packages/core/src/Platform/types.ts +++ b/packages/core/src/Platform/types.ts @@ -110,7 +110,7 @@ export enum StorageAction { Copy = '4', Remove = '5', GetProperties = '6', - GetUrl = '7' + GetUrl = '7', } type ActionMap = { @@ -150,3 +150,46 @@ export type CustomUserAgentDetails = | UserAgentDetailsWithCategory | UserAgentDetailsWithCategory | UserAgentDetailsWithCategory; + +/** + * `refCount` tracks how many consumers have set state for a particular API to avoid it being cleared before all + * consumers are done using it. + * + * Category -> Action -> Custom State + */ +export type CategoryUserAgentStateMap = Record< + string, + { refCount: number; additionalDetails: AdditionalDetails } +>; +export type CustomUserAgentStateMap = Record; + +export type AdditionalDetails = [string, string?][]; + +type StorageUserAgentInput = { + category: Category.Storage; + apis: StorageAction[]; +}; + +type AuthUserAgentInput = { + category: Category.Auth; + apis: AuthAction[]; +}; + +type InAppMessagingUserAgentInput = { + category: Category.InAppMessaging; + apis: InAppMessagingAction[]; +}; + +type GeoUserAgentInput = { + category: Category.Geo; + apis: GeoAction[]; +}; + +export type SetCustomUserAgentInput = ( + | StorageUserAgentInput + | AuthUserAgentInput + | InAppMessagingUserAgentInput + | GeoUserAgentInput +) & { + additionalDetails: AdditionalDetails; +}; diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index 21a8f867782..a332e037b02 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -43,7 +43,7 @@ export { // Logging utilities export { ConsoleLogger, ConsoleLogger as Logger } from './Logger'; -// Platform & device utils +// Platform & user-agent utilities export { ClientDevice } from './ClientDevice'; export { Platform, @@ -65,7 +65,9 @@ export { PubSubAction, PushNotificationAction, StorageAction, + SetCustomUserAgentInput, } from './Platform/types'; +export { setCustomUserAgent } from './Platform/customUserAgent'; // Service worker export { ServiceWorker } from './ServiceWorker';