diff --git a/packages/amazon-cognito-identity-js/__tests__/CognitoUser.test.js b/packages/amazon-cognito-identity-js/__tests__/CognitoUser.test.js index 23a015b5c4c..c9c86c439d4 100644 --- a/packages/amazon-cognito-identity-js/__tests__/CognitoUser.test.js +++ b/packages/amazon-cognito-identity-js/__tests__/CognitoUser.test.js @@ -203,7 +203,7 @@ describe('authenticateUser()', () => { user.setAuthenticationFlowType('USER_PASSWORD_AUTH'); user.authenticateUser(authDetails, callback); - expect(spyon).toHaveBeenCalledWith(authDetails, callback); + expect(spyon).toHaveBeenCalledWith(authDetails, callback, undefined); }); test('USER_SRP_AUTH and CUSTOM_AUTH flow types', () => { @@ -212,12 +212,12 @@ describe('authenticateUser()', () => { user.setAuthenticationFlowType('USER_SRP_AUTH'); user.authenticateUser(authDetails, callback); - expect(spyon).toHaveBeenCalledWith(authDetails, callback); + expect(spyon).toHaveBeenCalledWith(authDetails, callback, undefined); user.setAuthenticationFlowType('CUSTOM_AUTH'); user.authenticateUser(authDetails, callback); - expect(spyon).toHaveBeenCalledWith(authDetails, callback); + expect(spyon).toHaveBeenCalledWith(authDetails, callback, undefined); }); test('throws error for invalid Authentication flow type', () => { @@ -310,7 +310,8 @@ describe('authenticateUserPlainUsernamePassword()', () => { expect(userSpy3).toBeCalledWith( 'test auth result', userSpy3.mock.calls[0][1], - callback + callback, + undefined ); expect(userSpy3.mock.results[0].value).toBe('test return value'); }); @@ -755,7 +756,8 @@ describe('sendCustomChallengeAnswer()', () => { expect(spyon3).toBeCalledWith( vCognitoUserSession, expect.any(AuthenticationHelper), - callback + callback, + undefined ); }); @@ -1176,33 +1178,33 @@ describe('confirmPassword() and forgotPassword()', () => { jest.clearAllMocks(); }); - test('happy path should callback onSuccess', () => { + test('confirmPassword happy path should callback onSuccess', () => { netRequestMockSuccess(true); cognitoUser.confirmPassword(...confirmPasswordDefaults); expect(callback.onSuccess).toHaveBeenCalledWith('SUCCESS'); }); - test('client request throws an error', () => { + test('confirmPassword client request throws an error', () => { netRequestMockSuccess(false); cognitoUser.confirmPassword(...confirmPasswordDefaults); expect(callback.onFailure.mock.calls.length).toEqual(1); }); - test('happy path should callback onSuccess', () => { + test('forgotPassword happy path should callback onSuccess', () => { callback.inputVerificationCode = null; netRequestMockSuccess(true); cognitoUser.forgotPassword(...forgotPasswordDefaults); expect(callback.onSuccess.mock.calls.length).toEqual(1); }); - test('inputVerification code is a function should callback inputVerificationCode', () => { + test('forgotPassword inputVerification code is a function should callback inputVerificationCode', () => { callback.inputVerificationCode = jest.fn(); netRequestMockSuccess(true); cognitoUser.forgotPassword(...forgotPasswordDefaults); expect(callback.inputVerificationCode.mock.calls.length).toEqual(1); }); - test('client returning an error should call onFailure', () => { + test('forgotPassword client returning an error should call onFailure', () => { netRequestMockSuccess(false); cognitoUser.forgotPassword(...forgotPasswordDefaults); expect(callback.onFailure.mock.calls.length).toEqual(1); diff --git a/packages/amazon-cognito-identity-js/__tests__/internalsIndex.test.js b/packages/amazon-cognito-identity-js/__tests__/internalsIndex.test.js index 4b448a3e8a1..12b4ffa9676 100644 --- a/packages/amazon-cognito-identity-js/__tests__/internalsIndex.test.js +++ b/packages/amazon-cognito-identity-js/__tests__/internalsIndex.test.js @@ -6,6 +6,7 @@ describe('import * keys', () => { Array [ "addAuthCategoryToCognitoUserAgent", "addFrameworkToCognitoUserAgent", + "InternalCognitoUser", ] `); }); diff --git a/packages/amazon-cognito-identity-js/index.d.ts b/packages/amazon-cognito-identity-js/index.d.ts index 0a32f2c9fc5..2c526568121 100644 --- a/packages/amazon-cognito-identity-js/index.d.ts +++ b/packages/amazon-cognito-identity-js/index.d.ts @@ -1,3 +1,4 @@ +import { InternalCognitoUser } from './internals'; declare module 'amazon-cognito-identity-js' { //import * as AWS from "aws-sdk"; @@ -89,19 +90,7 @@ declare module 'amazon-cognito-identity-js' { | 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA'; - export class CognitoUser { - constructor(data: ICognitoUserData); - - challengeName?: ChallengeName; - - public setSignInUserSession(signInUserSession: CognitoUserSession): void; - public getSignInUserSession(): CognitoUserSession | null; - public getUsername(): string; - - public getAuthenticationFlowType(): string; - public setAuthenticationFlowType(authenticationFlowType: string): string; - public getCachedDeviceKeyAndPassword(): void; - + export class CognitoUser extends InternalCognitoUser { public getSession( callback: | ((error: Error, session: null) => void) diff --git a/packages/amazon-cognito-identity-js/internals/index.d.ts b/packages/amazon-cognito-identity-js/internals/index.d.ts index c7ecb6928c7..f1160c9563d 100644 --- a/packages/amazon-cognito-identity-js/internals/index.d.ts +++ b/packages/amazon-cognito-identity-js/internals/index.d.ts @@ -1,2 +1,268 @@ +import { + NodeCallback, + UpdateAttributesNodeCallback, + ClientMetadata, + IAuthenticationCallback, + IMfaSettings, + AuthenticationDetails, + ICognitoUserData, + GetSessionOptions, + ChallengeName, + CognitoUserSession, + CognitoRefreshToken, + CognitoUserAttribute, + ICognitoUserAttributeData, + MFAOption, + UserData, +} from 'amazon-cognito-identity-js'; + export const addAuthCategoryToCognitoUserAgent: () => void; export const addFrameworkToCognitoUserAgent: (content: string) => void; + +export class InternalCognitoUser { + constructor(data: ICognitoUserData); + + challengeName?: ChallengeName; + + public setSignInUserSession(signInUserSession: CognitoUserSession): void; + public getSignInUserSession(): CognitoUserSession | null; + public getUsername(): string; + + public getAuthenticationFlowType(): string; + public setAuthenticationFlowType(authenticationFlowType: string): string; + public getCachedDeviceKeyAndPassword(): void; + + public getSession( + callback: + | ((error: Error, session: null) => void) + | ((error: null, session: CognitoUserSession) => void), + options?: GetSessionOptions, + userAgentValue?: string + ): void; + public refreshSession( + refreshToken: CognitoRefreshToken, + callback: NodeCallback, + clientMetadata?: ClientMetadata, + userAgentValue?: string + ): void; + public authenticateUser( + authenticationDetails: AuthenticationDetails, + callbacks: IAuthenticationCallback, + userAgentValue?: string + ): void; + public initiateAuth( + authenticationDetails: AuthenticationDetails, + callbacks: IAuthenticationCallback, + userAgentValue?: string + ): void; + public confirmRegistration( + code: string, + forceAliasCreation: boolean, + callback: NodeCallback, + clientMetadata?: ClientMetadata, + userAgentValue?: string + ): void; + public sendCustomChallengeAnswer( + answerChallenge: any, + callback: IAuthenticationCallback, + clientMetaData?: ClientMetadata, + userAgentValue?: string + ): void; + public resendConfirmationCode( + callback: NodeCallback, + clientMetaData?: ClientMetadata, + userAgentValue?: string + ): void; + public changePassword( + oldPassword: string, + newPassword: string, + callback: NodeCallback, + clientMetadata?: ClientMetadata, + userAgentValue?: string + ): void; + public forgotPassword( + callbacks: { + onSuccess: (data: any) => void; + onFailure: (err: Error) => void; + inputVerificationCode?: (data: any) => void; + }, + clientMetaData?: ClientMetadata, + userAgentValue?: string + ): void; + public confirmPassword( + verificationCode: string, + newPassword: string, + callbacks: { + onSuccess: (success: string) => void; + onFailure: (err: Error) => void; + }, + clientMetaData?: ClientMetadata, + userAgentValue?: string + ): void; + public setDeviceStatusRemembered( + callbacks: { + onSuccess: (success: string) => void; + onFailure: (err: any) => void; + }, + userAgentValue?: string + ): void; + public setDeviceStatusNotRemembered( + callbacks: { + onSuccess: (success: string) => void; + onFailure: (err: any) => void; + }, + userAgentValue?: string + ): void; + public getDevice( + callbacks: { + onSuccess: (success: string) => void; + onFailure: (err: Error) => void; + }, + userAgentValue?: string + ): any; + public forgetDevice( + callbacks: { + onSuccess: (success: string) => void; + onFailure: (err: Error) => void; + }, + userAgentValue?: string + ): void; + public forgetSpecificDevice( + deviceKey: string, + callbacks: { + onSuccess: (success: string) => void; + onFailure: (err: Error) => void; + }, + userAgentValue?: string + ): void; + public sendMFACode( + confirmationCode: string, + callbacks: { + onSuccess: ( + session: CognitoUserSession, + userConfirmationNecessary?: boolean + ) => void; + onFailure: (err: any) => void; + }, + mfaType?: string, + clientMetadata?: ClientMetadata, + userAgentValue?: string + ): void; + public listDevices( + limit: number, + paginationToken: string | null, + callbacks: { + onSuccess: (data: any) => void; + onFailure: (err: Error) => void; + }, + userAgentValue?: string + ): void; + public completeNewPasswordChallenge( + newPassword: string, + requiredAttributeData: any, + callbacks: IAuthenticationCallback, + clientMetadata?: ClientMetadata, + userAgentValue?: string + ): void; + public signOut(callback?: () => void, userAgentValue?: string): void; + public globalSignOut( + callbacks: { + onSuccess: (msg: string) => void; + onFailure: (err: Error) => void; + }, + userAgentValue?: string + ): void; + public verifyAttribute( + attributeName: string, + confirmationCode: string, + callbacks: { + onSuccess: (success: string) => void; + onFailure: (err: Error) => void; + }, + userAgentValue?: string + ): void; + public getUserAttributes( + callback: NodeCallback, + userAgentValue?: string + ): void; + public updateAttributes( + attributes: (CognitoUserAttribute | ICognitoUserAttributeData)[], + callback: UpdateAttributesNodeCallback, + clientMetadata?: ClientMetadata, + userAgentValue?: string + ): void; + public deleteAttributes( + attributeList: string[], + callback: NodeCallback, + userAgentValue?: string + ): void; + public getAttributeVerificationCode( + name: string, + callback: { + onSuccess: (success: string) => void; + onFailure: (err: Error) => void; + inputVerificationCode?: (data: string) => void | null; + }, + clientMetadata?: ClientMetadata, + userAgentValue?: string + ): void; + public deleteUser( + callback: NodeCallback, + userAgentValue?: string + ): void; + public enableMFA( + callback: NodeCallback, + userAgentValue?: string + ): void; + public disableMFA( + callback: NodeCallback, + userAgentValue?: string + ): void; + public getMFAOptions( + callback: NodeCallback, + userAgentValue?: string + ): void; + public getUserData( + callback: NodeCallback, + params?: any, + userAgentValue?: string + ): void; + public associateSoftwareToken( + callbacks: { + associateSecretCode: (secretCode: string) => void; + onFailure: (err: any) => void; + }, + userAgentValue?: string + ): void; + public verifySoftwareToken( + totpCode: string, + friendlyDeviceName: string, + callbacks: { + onSuccess: (session: CognitoUserSession) => void; + onFailure: (err: Error) => void; + }, + userAgentValue?: string + ): void; + public setUserMfaPreference( + smsMfaSettings: IMfaSettings | null, + softwareTokenMfaSettings: IMfaSettings | null, + callback: NodeCallback, + userAgentValue?: string + ): void; + public sendMFASelectionAnswer( + answerChallenge: string, + callbacks: { + onSuccess: (session: CognitoUserSession) => void; + onFailure: (err: any) => void; + mfaRequired?: ( + challengeName: ChallengeName, + challengeParameters: any + ) => void; + totpRequired?: ( + challengeName: ChallengeName, + challengeParameters: any + ) => void; + }, + userAgentValue?: string + ): void; +} diff --git a/packages/amazon-cognito-identity-js/src/Client.js b/packages/amazon-cognito-identity-js/src/Client.js index 1ee3f8b4dbc..199432eb44a 100644 --- a/packages/amazon-cognito-identity-js/src/Client.js +++ b/packages/amazon-cognito-identity-js/src/Client.js @@ -32,33 +32,43 @@ export default class Client { * @param {object} params Input parameters * @returns Promise */ - promisifyRequest(operation, params) { + promisifyRequest(operation, params, userAgentValue) { return new Promise((resolve, reject) => { - this.request(operation, params, (err, data) => { - if (err) { - reject( - new CognitoError(err.message, err.code, err.name, err.statusCode) - ); - } else { - resolve(data); - } - }); + this.request( + operation, + params, + (err, data) => { + if (err) { + reject( + new CognitoError(err.message, err.code, err.name, err.statusCode) + ); + } else { + resolve(data); + } + }, + userAgentValue + ); }); } - requestWithRetry(operation, params, callback) { + requestWithRetry(operation, params, callback, userAgentValue) { const MAX_DELAY_IN_MILLIS = 5 * 1000; jitteredExponentialRetry( p => new Promise((res, rej) => { - this.request(operation, p, (error, result) => { - if (error) { - rej(error); - } else { - res(result); - } - }); + this.request( + operation, + p, + (error, result) => { + if (error) { + rej(error); + } else { + res(result); + } + }, + userAgentValue + ); }), [params], MAX_DELAY_IN_MILLIS @@ -75,11 +85,11 @@ export default class Client { * @param {function} callback Callback called when a response is returned * @returns {void} */ - request(operation, params, callback) { + request(operation, params, callback, userAgentValue) { const headers = { 'Content-Type': 'application/x-amz-json-1.1', 'X-Amz-Target': `AWSCognitoIdentityProviderService.${operation}`, - 'X-Amz-User-Agent': getAmplifyUserAgent(), + 'X-Amz-User-Agent': userAgentValue || getAmplifyUserAgent(), 'Cache-Control': 'no-store', }; diff --git a/packages/amazon-cognito-identity-js/src/CognitoUser.js b/packages/amazon-cognito-identity-js/src/CognitoUser.js index 9dc61d9e228..345e8bf88c0 100644 --- a/packages/amazon-cognito-identity-js/src/CognitoUser.js +++ b/packages/amazon-cognito-identity-js/src/CognitoUser.js @@ -3,19 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Buffer } from 'buffer'; -import { Sha256 } from '@aws-crypto/sha256-js'; -import { Platform } from './Platform'; - -import BigInteger from './BigInteger'; -import AuthenticationHelper from './AuthenticationHelper'; -import CognitoAccessToken from './CognitoAccessToken'; -import CognitoIdToken from './CognitoIdToken'; import CognitoRefreshToken from './CognitoRefreshToken'; import CognitoUserSession from './CognitoUserSession'; -import DateHelper from './DateHelper'; import CognitoUserAttribute from './CognitoUserAttribute'; -import StorageHelper from './StorageHelper'; +import { InternalCognitoUser } from './internals'; /** * @callback nodeCallback @@ -56,83 +47,8 @@ import StorageHelper from './StorageHelper'; * @param {bool=} userConfirmationNecessary User must be confirmed. */ -const isNavigatorAvailable = typeof navigator !== 'undefined'; -const userAgent = isNavigatorAvailable - ? Platform.isReactNative - ? 'react-native' - : navigator.userAgent - : 'nodejs'; - /** @class */ -export default class CognitoUser { - /** - * Constructs a new CognitoUser object - * @param {object} data Creation options - * @param {string} data.Username The user's username. - * @param {CognitoUserPool} data.Pool Pool containing the user. - * @param {object} data.Storage Optional storage object. - */ - constructor(data) { - if (data == null || data.Username == null || data.Pool == null) { - throw new Error('Username and Pool information are required.'); - } - - this.username = data.Username || ''; - this.pool = data.Pool; - this.Session = null; - - this.client = data.Pool.client; - - this.signInUserSession = null; - this.authenticationFlowType = 'USER_SRP_AUTH'; - - this.storage = data.Storage || new StorageHelper().getStorage(); - - this.keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}`; - this.userDataKey = `${this.keyPrefix}.${this.username}.userData`; - } - - /** - * Sets the session for this user - * @param {CognitoUserSession} signInUserSession the session - * @returns {void} - */ - setSignInUserSession(signInUserSession) { - this.clearCachedUserData(); - this.signInUserSession = signInUserSession; - this.cacheTokens(); - } - - /** - * @returns {CognitoUserSession} the current session for this user - */ - getSignInUserSession() { - return this.signInUserSession; - } - - /** - * @returns {string} the user's username - */ - getUsername() { - return this.username; - } - - /** - * @returns {String} the authentication flow type - */ - getAuthenticationFlowType() { - return this.authenticationFlowType; - } - - /** - * sets authentication flow type - * @param {string} authenticationFlowType New value. - * @returns {void} - */ - setAuthenticationFlowType(authenticationFlowType) { - this.authenticationFlowType = authenticationFlowType; - } - +export default class CognitoUser extends InternalCognitoUser { /** * This is used for authenticating the user through the custom authentication flow. * @param {AuthenticationDetails} authDetails Contains the authentication data @@ -144,41 +60,7 @@ export default class CognitoUser { * @returns {void} */ initiateAuth(authDetails, callback) { - const authParameters = authDetails.getAuthParameters(); - authParameters.USERNAME = this.username; - - const clientMetaData = - Object.keys(authDetails.getValidationData()).length !== 0 - ? authDetails.getValidationData() - : authDetails.getClientMetadata(); - - const jsonReq = { - AuthFlow: 'CUSTOM_AUTH', - ClientId: this.pool.getClientId(), - AuthParameters: authParameters, - ClientMetadata: clientMetaData, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - - this.client.request('InitiateAuth', jsonReq, (err, data) => { - if (err) { - return callback.onFailure(err); - } - const challengeName = data.ChallengeName; - const challengeParameters = data.ChallengeParameters; - - if (challengeName === 'CUSTOM_CHALLENGE') { - this.Session = data.Session; - return callback.customChallenge(challengeParameters); - } - this.signInUserSession = this.getCognitoUserSession( - data.AuthenticationResult - ); - this.cacheTokens(); - return callback.onSuccess(this.signInUserSession); - }); + super.initiateAuth(authDetails, callback); } /** @@ -197,375 +79,7 @@ export default class CognitoUser { * @returns {void} */ authenticateUser(authDetails, callback) { - if (this.authenticationFlowType === 'USER_PASSWORD_AUTH') { - return this.authenticateUserPlainUsernamePassword(authDetails, callback); - } else if ( - this.authenticationFlowType === 'USER_SRP_AUTH' || - this.authenticationFlowType === 'CUSTOM_AUTH' - ) { - return this.authenticateUserDefaultAuth(authDetails, callback); - } - return callback.onFailure( - new Error('Authentication flow type is invalid.') - ); - } - - /** - * PRIVATE ONLY: This is an internal only method and should not - * be directly called by the consumers. - * It calls the AuthenticationHelper for SRP related - * stuff - * @param {AuthenticationDetails} authDetails Contains the authentication data - * @param {object} callback Result callback map. - * @param {onFailure} callback.onFailure Called on any error. - * @param {newPasswordRequired} callback.newPasswordRequired new - * password and any required attributes are required to continue - * @param {mfaRequired} callback.mfaRequired MFA code - * required to continue. - * @param {customChallenge} callback.customChallenge Custom challenge - * response required to continue. - * @param {authSuccess} callback.onSuccess Called on success with the new session. - * @returns {void} - */ - authenticateUserDefaultAuth(authDetails, callback) { - const authenticationHelper = new AuthenticationHelper( - this.pool.getUserPoolName() - ); - const dateHelper = new DateHelper(); - - let serverBValue; - let salt; - const authParameters = {}; - - if (this.deviceKey != null) { - authParameters.DEVICE_KEY = this.deviceKey; - } - - authParameters.USERNAME = this.username; - authenticationHelper.getLargeAValue((errOnAValue, aValue) => { - // getLargeAValue callback start - if (errOnAValue) { - callback.onFailure(errOnAValue); - } - - authParameters.SRP_A = aValue.toString(16); - - if (this.authenticationFlowType === 'CUSTOM_AUTH') { - authParameters.CHALLENGE_NAME = 'SRP_A'; - } - - const clientMetaData = - Object.keys(authDetails.getValidationData()).length !== 0 - ? authDetails.getValidationData() - : authDetails.getClientMetadata(); - - const jsonReq = { - AuthFlow: this.authenticationFlowType, - ClientId: this.pool.getClientId(), - AuthParameters: authParameters, - ClientMetadata: clientMetaData, - }; - if (this.getUserContextData(this.username)) { - jsonReq.UserContextData = this.getUserContextData(this.username); - } - - this.client.request('InitiateAuth', jsonReq, (err, data) => { - if (err) { - return callback.onFailure(err); - } - - const challengeParameters = data.ChallengeParameters; - - this.username = challengeParameters.USER_ID_FOR_SRP; - this.userDataKey = `${this.keyPrefix}.${this.username}.userData`; - serverBValue = new BigInteger(challengeParameters.SRP_B, 16); - salt = new BigInteger(challengeParameters.SALT, 16); - this.getCachedDeviceKeyAndPassword(); - - authenticationHelper.getPasswordAuthenticationKey( - this.username, - authDetails.getPassword(), - serverBValue, - salt, - (errOnHkdf, hkdf) => { - // getPasswordAuthenticationKey callback start - if (errOnHkdf) { - callback.onFailure(errOnHkdf); - } - - const dateNow = dateHelper.getNowString(); - - const concatBuffer = Buffer.concat([ - Buffer.from(this.pool.getUserPoolName(), 'utf8'), - Buffer.from(this.username, 'utf8'), - Buffer.from(challengeParameters.SECRET_BLOCK, 'base64'), - Buffer.from(dateNow, 'utf8'), - ]); - - const awsCryptoHash = new Sha256(hkdf); - awsCryptoHash.update(concatBuffer); - - const resultFromAWSCrypto = awsCryptoHash.digestSync(); - const signatureString = - Buffer.from(resultFromAWSCrypto).toString('base64'); - - const challengeResponses = {}; - - challengeResponses.USERNAME = this.username; - challengeResponses.PASSWORD_CLAIM_SECRET_BLOCK = - challengeParameters.SECRET_BLOCK; - challengeResponses.TIMESTAMP = dateNow; - challengeResponses.PASSWORD_CLAIM_SIGNATURE = signatureString; - - if (this.deviceKey != null) { - challengeResponses.DEVICE_KEY = this.deviceKey; - } - - const respondToAuthChallenge = (challenge, challengeCallback) => - this.client.request( - 'RespondToAuthChallenge', - challenge, - (errChallenge, dataChallenge) => { - if ( - errChallenge && - errChallenge.code === 'ResourceNotFoundException' && - errChallenge.message.toLowerCase().indexOf('device') !== -1 - ) { - challengeResponses.DEVICE_KEY = null; - this.deviceKey = null; - this.randomPassword = null; - this.deviceGroupKey = null; - this.clearCachedDeviceKeyAndPassword(); - return respondToAuthChallenge(challenge, challengeCallback); - } - return challengeCallback(errChallenge, dataChallenge); - } - ); - - const jsonReqResp = { - ChallengeName: 'PASSWORD_VERIFIER', - ClientId: this.pool.getClientId(), - ChallengeResponses: challengeResponses, - Session: data.Session, - ClientMetadata: clientMetaData, - }; - if (this.getUserContextData()) { - jsonReqResp.UserContextData = this.getUserContextData(); - } - respondToAuthChallenge( - jsonReqResp, - (errAuthenticate, dataAuthenticate) => { - if (errAuthenticate) { - return callback.onFailure(errAuthenticate); - } - - return this.authenticateUserInternal( - dataAuthenticate, - authenticationHelper, - callback - ); - } - ); - return undefined; - // getPasswordAuthenticationKey callback end - } - ); - return undefined; - }); - // getLargeAValue callback end - }); - } - - /** - * PRIVATE ONLY: This is an internal only method and should not - * be directly called by the consumers. - * @param {AuthenticationDetails} authDetails Contains the authentication data. - * @param {object} callback Result callback map. - * @param {onFailure} callback.onFailure Called on any error. - * @param {mfaRequired} callback.mfaRequired MFA code - * required to continue. - * @param {authSuccess} callback.onSuccess Called on success with the new session. - * @returns {void} - */ - authenticateUserPlainUsernamePassword(authDetails, callback) { - const authParameters = {}; - authParameters.USERNAME = this.username; - authParameters.PASSWORD = authDetails.getPassword(); - if (!authParameters.PASSWORD) { - callback.onFailure(new Error('PASSWORD parameter is required')); - return; - } - const authenticationHelper = new AuthenticationHelper( - this.pool.getUserPoolName() - ); - this.getCachedDeviceKeyAndPassword(); - if (this.deviceKey != null) { - authParameters.DEVICE_KEY = this.deviceKey; - } - - const clientMetaData = - Object.keys(authDetails.getValidationData()).length !== 0 - ? authDetails.getValidationData() - : authDetails.getClientMetadata(); - - const jsonReq = { - AuthFlow: 'USER_PASSWORD_AUTH', - ClientId: this.pool.getClientId(), - AuthParameters: authParameters, - ClientMetadata: clientMetaData, - }; - if (this.getUserContextData(this.username)) { - jsonReq.UserContextData = this.getUserContextData(this.username); - } - // USER_PASSWORD_AUTH happens in a single round-trip: client sends userName and password, - // Cognito UserPools verifies password and returns tokens. - this.client.request('InitiateAuth', jsonReq, (err, authResult) => { - if (err) { - return callback.onFailure(err); - } - return this.authenticateUserInternal( - authResult, - authenticationHelper, - callback - ); - }); - } - - /** - * PRIVATE ONLY: This is an internal only method and should not - * be directly called by the consumers. - * @param {object} dataAuthenticate authentication data - * @param {object} authenticationHelper helper created - * @param {callback} callback passed on from caller - * @returns {void} - */ - authenticateUserInternal(dataAuthenticate, authenticationHelper, callback) { - const challengeName = dataAuthenticate.ChallengeName; - const challengeParameters = dataAuthenticate.ChallengeParameters; - - if (challengeName === 'SMS_MFA') { - this.Session = dataAuthenticate.Session; - return callback.mfaRequired(challengeName, challengeParameters); - } - - if (challengeName === 'SELECT_MFA_TYPE') { - this.Session = dataAuthenticate.Session; - return callback.selectMFAType(challengeName, challengeParameters); - } - - if (challengeName === 'MFA_SETUP') { - this.Session = dataAuthenticate.Session; - return callback.mfaSetup(challengeName, challengeParameters); - } - - if (challengeName === 'SOFTWARE_TOKEN_MFA') { - this.Session = dataAuthenticate.Session; - return callback.totpRequired(challengeName, challengeParameters); - } - - if (challengeName === 'CUSTOM_CHALLENGE') { - this.Session = dataAuthenticate.Session; - return callback.customChallenge(challengeParameters); - } - - if (challengeName === 'NEW_PASSWORD_REQUIRED') { - this.Session = dataAuthenticate.Session; - - let userAttributes = null; - let rawRequiredAttributes = null; - const requiredAttributes = []; - const userAttributesPrefix = - authenticationHelper.getNewPasswordRequiredChallengeUserAttributePrefix(); - - if (challengeParameters) { - userAttributes = JSON.parse( - dataAuthenticate.ChallengeParameters.userAttributes - ); - rawRequiredAttributes = JSON.parse( - dataAuthenticate.ChallengeParameters.requiredAttributes - ); - } - - if (rawRequiredAttributes) { - for (let i = 0; i < rawRequiredAttributes.length; i++) { - requiredAttributes[i] = rawRequiredAttributes[i].substr( - userAttributesPrefix.length - ); - } - } - return callback.newPasswordRequired(userAttributes, requiredAttributes); - } - - if (challengeName === 'DEVICE_SRP_AUTH') { - this.Session = dataAuthenticate.Session; - this.getDeviceResponse(callback); - return undefined; - } - - this.signInUserSession = this.getCognitoUserSession( - dataAuthenticate.AuthenticationResult - ); - this.challengeName = challengeName; - this.cacheTokens(); - - const newDeviceMetadata = - dataAuthenticate.AuthenticationResult.NewDeviceMetadata; - if (newDeviceMetadata == null) { - return callback.onSuccess(this.signInUserSession); - } - - authenticationHelper.generateHashDevice( - dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceGroupKey, - dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceKey, - errGenHash => { - if (errGenHash) { - return callback.onFailure(errGenHash); - } - - const deviceSecretVerifierConfig = { - Salt: Buffer.from( - authenticationHelper.getSaltDevices(), - 'hex' - ).toString('base64'), - PasswordVerifier: Buffer.from( - authenticationHelper.getVerifierDevices(), - 'hex' - ).toString('base64'), - }; - - this.verifierDevices = deviceSecretVerifierConfig.PasswordVerifier; - this.deviceGroupKey = newDeviceMetadata.DeviceGroupKey; - this.randomPassword = authenticationHelper.getRandomPassword(); - - this.client.request( - 'ConfirmDevice', - { - DeviceKey: newDeviceMetadata.DeviceKey, - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - DeviceSecretVerifierConfig: deviceSecretVerifierConfig, - DeviceName: userAgent, - }, - (errConfirm, dataConfirm) => { - if (errConfirm) { - return callback.onFailure(errConfirm); - } - - this.deviceKey = - dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceKey; - this.cacheDeviceKeyAndPassword(); - if (dataConfirm.UserConfirmationNecessary === true) { - return callback.onSuccess( - this.signInUserSession, - dataConfirm.UserConfirmationNecessary - ); - } - return callback.onSuccess(this.signInUserSession); - } - ); - return undefined; - } - ); - return undefined; + super.authenticateUser(authDetails, callback); } /** @@ -589,51 +103,12 @@ export default class CognitoUser { callback, clientMetadata ) { - if (!newPassword) { - return callback.onFailure(new Error('New password is required.')); - } - const authenticationHelper = new AuthenticationHelper( - this.pool.getUserPoolName() - ); - const userAttributesPrefix = - authenticationHelper.getNewPasswordRequiredChallengeUserAttributePrefix(); - - const finalUserAttributes = {}; - if (requiredAttributeData) { - Object.keys(requiredAttributeData).forEach(key => { - finalUserAttributes[userAttributesPrefix + key] = - requiredAttributeData[key]; - }); - } - - finalUserAttributes.NEW_PASSWORD = newPassword; - finalUserAttributes.USERNAME = this.username; - const jsonReq = { - ChallengeName: 'NEW_PASSWORD_REQUIRED', - ClientId: this.pool.getClientId(), - ChallengeResponses: finalUserAttributes, - Session: this.Session, - ClientMetadata: clientMetadata, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - - this.client.request( - 'RespondToAuthChallenge', - jsonReq, - (errAuthenticate, dataAuthenticate) => { - if (errAuthenticate) { - return callback.onFailure(errAuthenticate); - } - return this.authenticateUserInternal( - dataAuthenticate, - authenticationHelper, - callback - ); - } + super.completeNewPasswordChallenge( + newPassword, + requiredAttributeData, + callback, + clientMetadata ); - return undefined; } /** @@ -648,111 +123,7 @@ export default class CognitoUser { * @private */ getDeviceResponse(callback, clientMetadata) { - const authenticationHelper = new AuthenticationHelper(this.deviceGroupKey); - const dateHelper = new DateHelper(); - - const authParameters = {}; - - authParameters.USERNAME = this.username; - authParameters.DEVICE_KEY = this.deviceKey; - authenticationHelper.getLargeAValue((errAValue, aValue) => { - // getLargeAValue callback start - if (errAValue) { - callback.onFailure(errAValue); - } - - authParameters.SRP_A = aValue.toString(16); - - const jsonReq = { - ChallengeName: 'DEVICE_SRP_AUTH', - ClientId: this.pool.getClientId(), - ChallengeResponses: authParameters, - ClientMetadata: clientMetadata, - Session: this.Session, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - this.client.request('RespondToAuthChallenge', jsonReq, (err, data) => { - if (err) { - return callback.onFailure(err); - } - - const challengeParameters = data.ChallengeParameters; - - const serverBValue = new BigInteger(challengeParameters.SRP_B, 16); - const salt = new BigInteger(challengeParameters.SALT, 16); - - authenticationHelper.getPasswordAuthenticationKey( - this.deviceKey, - this.randomPassword, - serverBValue, - salt, - (errHkdf, hkdf) => { - // getPasswordAuthenticationKey callback start - if (errHkdf) { - return callback.onFailure(errHkdf); - } - - const dateNow = dateHelper.getNowString(); - - const concatBuffer = Buffer.concat([ - Buffer.from(this.deviceGroupKey, 'utf8'), - Buffer.from(this.deviceKey, 'utf8'), - Buffer.from(challengeParameters.SECRET_BLOCK, 'base64'), - Buffer.from(dateNow, 'utf8'), - ]); - - const awsCryptoHash = new Sha256(hkdf); - awsCryptoHash.update(concatBuffer); - - const resultFromAWSCrypto = awsCryptoHash.digestSync(); - const signatureString = - Buffer.from(resultFromAWSCrypto).toString('base64'); - - const challengeResponses = {}; - - challengeResponses.USERNAME = this.username; - challengeResponses.PASSWORD_CLAIM_SECRET_BLOCK = - challengeParameters.SECRET_BLOCK; - challengeResponses.TIMESTAMP = dateNow; - challengeResponses.PASSWORD_CLAIM_SIGNATURE = signatureString; - challengeResponses.DEVICE_KEY = this.deviceKey; - - const jsonReqResp = { - ChallengeName: 'DEVICE_PASSWORD_VERIFIER', - ClientId: this.pool.getClientId(), - ChallengeResponses: challengeResponses, - Session: data.Session, - }; - if (this.getUserContextData()) { - jsonReqResp.UserContextData = this.getUserContextData(); - } - - this.client.request( - 'RespondToAuthChallenge', - jsonReqResp, - (errAuthenticate, dataAuthenticate) => { - if (errAuthenticate) { - return callback.onFailure(errAuthenticate); - } - - this.signInUserSession = this.getCognitoUserSession( - dataAuthenticate.AuthenticationResult - ); - this.cacheTokens(); - - return callback.onSuccess(this.signInUserSession); - } - ); - return undefined; - // getPasswordAuthenticationKey callback end - } - ); - return undefined; - }); - // getLargeAValue callback end - }); + super.getDeviceResponse(callback, clientMetadata); } /** @@ -769,22 +140,12 @@ export default class CognitoUser { callback, clientMetadata ) { - const jsonReq = { - ClientId: this.pool.getClientId(), - ConfirmationCode: confirmationCode, - Username: this.username, - ForceAliasCreation: forceAliasCreation, - ClientMetadata: clientMetadata, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - this.client.request('ConfirmSignUp', jsonReq, err => { - if (err) { - return callback(err, null); - } - return callback(null, 'SUCCESS'); - }); + super.confirmRegistration( + confirmationCode, + forceAliasCreation, + callback, + clientMetadata + ); } /** @@ -799,39 +160,7 @@ export default class CognitoUser { * @returns {void} */ sendCustomChallengeAnswer(answerChallenge, callback, clientMetadata) { - const challengeResponses = {}; - challengeResponses.USERNAME = this.username; - challengeResponses.ANSWER = answerChallenge; - - const authenticationHelper = new AuthenticationHelper( - this.pool.getUserPoolName() - ); - this.getCachedDeviceKeyAndPassword(); - if (this.deviceKey != null) { - challengeResponses.DEVICE_KEY = this.deviceKey; - } - - const jsonReq = { - ChallengeName: 'CUSTOM_CHALLENGE', - ChallengeResponses: challengeResponses, - ClientId: this.pool.getClientId(), - Session: this.Session, - ClientMetadata: clientMetadata, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - this.client.request('RespondToAuthChallenge', jsonReq, (err, data) => { - if (err) { - return callback.onFailure(err); - } - - return this.authenticateUserInternal( - data, - authenticationHelper, - callback - ); - }); + super.sendCustomChallengeAnswer(answerChallenge, callback, clientMetadata); } /** @@ -845,116 +174,7 @@ export default class CognitoUser { * @returns {void} */ sendMFACode(confirmationCode, callback, mfaType, clientMetadata) { - const challengeResponses = {}; - challengeResponses.USERNAME = this.username; - challengeResponses.SMS_MFA_CODE = confirmationCode; - const mfaTypeSelection = mfaType || 'SMS_MFA'; - if (mfaTypeSelection === 'SOFTWARE_TOKEN_MFA') { - challengeResponses.SOFTWARE_TOKEN_MFA_CODE = confirmationCode; - } - - if (this.deviceKey != null) { - challengeResponses.DEVICE_KEY = this.deviceKey; - } - - const jsonReq = { - ChallengeName: mfaTypeSelection, - ChallengeResponses: challengeResponses, - ClientId: this.pool.getClientId(), - Session: this.Session, - ClientMetadata: clientMetadata, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - - this.client.request( - 'RespondToAuthChallenge', - jsonReq, - (err, dataAuthenticate) => { - if (err) { - return callback.onFailure(err); - } - - const challengeName = dataAuthenticate.ChallengeName; - - if (challengeName === 'DEVICE_SRP_AUTH') { - this.getDeviceResponse(callback); - return undefined; - } - - this.signInUserSession = this.getCognitoUserSession( - dataAuthenticate.AuthenticationResult - ); - this.cacheTokens(); - - if (dataAuthenticate.AuthenticationResult.NewDeviceMetadata == null) { - return callback.onSuccess(this.signInUserSession); - } - - const authenticationHelper = new AuthenticationHelper( - this.pool.getUserPoolName() - ); - authenticationHelper.generateHashDevice( - dataAuthenticate.AuthenticationResult.NewDeviceMetadata - .DeviceGroupKey, - dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceKey, - errGenHash => { - if (errGenHash) { - return callback.onFailure(errGenHash); - } - - const deviceSecretVerifierConfig = { - Salt: Buffer.from( - authenticationHelper.getSaltDevices(), - 'hex' - ).toString('base64'), - PasswordVerifier: Buffer.from( - authenticationHelper.getVerifierDevices(), - 'hex' - ).toString('base64'), - }; - - this.verifierDevices = deviceSecretVerifierConfig.PasswordVerifier; - this.deviceGroupKey = - dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceGroupKey; - this.randomPassword = authenticationHelper.getRandomPassword(); - - this.client.request( - 'ConfirmDevice', - { - DeviceKey: - dataAuthenticate.AuthenticationResult.NewDeviceMetadata - .DeviceKey, - AccessToken: this.signInUserSession - .getAccessToken() - .getJwtToken(), - DeviceSecretVerifierConfig: deviceSecretVerifierConfig, - DeviceName: userAgent, - }, - (errConfirm, dataConfirm) => { - if (errConfirm) { - return callback.onFailure(errConfirm); - } - - this.deviceKey = - dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceKey; - this.cacheDeviceKeyAndPassword(); - if (dataConfirm.UserConfirmationNecessary === true) { - return callback.onSuccess( - this.signInUserSession, - dataConfirm.UserConfirmationNecessary - ); - } - return callback.onSuccess(this.signInUserSession); - } - ); - return undefined; - } - ); - return undefined; - } - ); + super.sendMFACode(confirmationCode, callback, mfaType, clientMetadata); } /** @@ -966,26 +186,12 @@ export default class CognitoUser { * @returns {void} */ changePassword(oldUserPassword, newUserPassword, callback, clientMetadata) { - if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { - return callback(new Error('User is not authenticated'), null); - } - - this.client.request( - 'ChangePassword', - { - PreviousPassword: oldUserPassword, - ProposedPassword: newUserPassword, - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - ClientMetadata: clientMetadata, - }, - err => { - if (err) { - return callback(err, null); - } - return callback(null, 'SUCCESS'); - } + super.changePassword( + oldUserPassword, + newUserPassword, + callback, + clientMetadata ); - return undefined; } /** @@ -995,31 +201,7 @@ export default class CognitoUser { * @returns {void} */ enableMFA(callback) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback(new Error('User is not authenticated'), null); - } - - const mfaOptions = []; - const mfaEnabled = { - DeliveryMedium: 'SMS', - AttributeName: 'phone_number', - }; - mfaOptions.push(mfaEnabled); - - this.client.request( - 'SetUserSettings', - { - MFAOptions: mfaOptions, - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - }, - err => { - if (err) { - return callback(err, null); - } - return callback(null, 'SUCCESS'); - } - ); - return undefined; + super.enableMFA(callback); } /** @@ -1030,25 +212,11 @@ export default class CognitoUser { * @returns {void} */ setUserMfaPreference(smsMfaSettings, softwareTokenMfaSettings, callback) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback(new Error('User is not authenticated'), null); - } - - this.client.request( - 'SetUserMFAPreference', - { - SMSMfaSettings: smsMfaSettings, - SoftwareTokenMfaSettings: softwareTokenMfaSettings, - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - }, - err => { - if (err) { - return callback(err, null); - } - return callback(null, 'SUCCESS'); - } + super.setUserMfaPreference( + smsMfaSettings, + softwareTokenMfaSettings, + callback ); - return undefined; } /** @@ -1058,26 +226,7 @@ export default class CognitoUser { * @returns {void} */ disableMFA(callback) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback(new Error('User is not authenticated'), null); - } - - const mfaOptions = []; - - this.client.request( - 'SetUserSettings', - { - MFAOptions: mfaOptions, - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - }, - err => { - if (err) { - return callback(err, null); - } - return callback(null, 'SUCCESS'); - } - ); - return undefined; + super.disableMFA(callback); } /** @@ -1087,25 +236,7 @@ export default class CognitoUser { * @returns {void} */ deleteUser(callback, clientMetadata) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback(new Error('User is not authenticated'), null); - } - - this.client.request( - 'DeleteUser', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - ClientMetadata: clientMetadata, - }, - err => { - if (err) { - return callback(err, null); - } - this.clearCachedUser(); - return callback(null, 'SUCCESS'); - } - ); - return undefined; + super.deleteUser(callback, clientMetadata); } /** @@ -1119,29 +250,7 @@ export default class CognitoUser { * @returns {void} */ updateAttributes(attributes, callback, clientMetadata) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback(new Error('User is not authenticated'), null); - } - - this.client.request( - 'UpdateUserAttributes', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - UserAttributes: attributes, - ClientMetadata: clientMetadata, - }, - (err,result) => { - if (err) { - return callback(err, null); - } - - // update cached user - return this.getUserData(() => callback(null, 'SUCCESS', result), { - bypassCache: true, - }); - } - ); - return undefined; + super.updateAttributes(attributes, callback, clientMetadata); } /** @@ -1150,35 +259,7 @@ export default class CognitoUser { * @returns {void} */ getUserAttributes(callback) { - if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { - return callback(new Error('User is not authenticated'), null); - } - - this.client.request( - 'GetUser', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - }, - (err, userData) => { - if (err) { - return callback(err, null); - } - - const attributeList = []; - - for (let i = 0; i < userData.UserAttributes.length; i++) { - const attribute = { - Name: userData.UserAttributes[i].Name, - Value: userData.UserAttributes[i].Value, - }; - const userAttribute = new CognitoUserAttribute(attribute); - attributeList.push(userAttribute); - } - - return callback(null, attributeList); - } - ); - return undefined; + super.getUserAttributes(callback); } /** @@ -1190,50 +271,7 @@ export default class CognitoUser { * @returns {void} */ getMFAOptions(callback) { - if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { - return callback(new Error('User is not authenticated'), null); - } - - this.client.request( - 'GetUser', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - }, - (err, userData) => { - if (err) { - return callback(err, null); - } - - return callback(null, userData.MFAOptions); - } - ); - return undefined; - } - - /** - * PRIVATE ONLY: This is an internal only method and should not - * be directly called by the consumers. - */ - createGetUserRequest() { - return this.client.promisifyRequest('GetUser', { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - }); - } - - /** - * PRIVATE ONLY: This is an internal only method and should not - * be directly called by the consumers. - */ - refreshSessionIfPossible(options = {}) { - // best effort, if not possible - return new Promise(resolve => { - const refresh = this.signInUserSession.getRefreshToken(); - if (refresh && refresh.getToken()) { - this.refreshSession(refresh, resolve, options.clientMetadata); - } else { - resolve(); - } - }); + super.getMFAOptions(callback); } /** @@ -1249,73 +287,7 @@ export default class CognitoUser { * @returns {void} */ getUserData(callback, params) { - if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { - this.clearCachedUserData(); - return callback(new Error('User is not authenticated'), null); - } - - const userData = this.getUserDataFromCache(); - - if (!userData) { - this.fetchUserData() - .then(data => { - callback(null, data); - }) - .catch(callback); - return; - } - - if (this.isFetchUserDataAndTokenRequired(params)) { - this.fetchUserData() - .then(data => { - return this.refreshSessionIfPossible(params).then(() => data); - }) - .then(data => callback(null, data)) - .catch(callback); - return; - } - - try { - callback(null, JSON.parse(userData)); - return; - } catch (err) { - this.clearCachedUserData(); - callback(err, null); - return; - } - } - - /** - * - * PRIVATE ONLY: This is an internal only method and should not - * be directly called by the consumers. - */ - getUserDataFromCache() { - const userData = this.storage.getItem(this.userDataKey); - - return userData; - } - - /** - * - * PRIVATE ONLY: This is an internal only method and should not - * be directly called by the consumers. - */ - isFetchUserDataAndTokenRequired(params) { - const { bypassCache = false } = params || {}; - - return bypassCache; - } - /** - * - * PRIVATE ONLY: This is an internal only method and should not - * be directly called by the consumers. - */ - fetchUserData() { - return this.createGetUserRequest().then(data => { - this.cacheUserData(data); - return data; - }); + super.getUserData(callback, params); } /** @@ -1325,28 +297,7 @@ export default class CognitoUser { * @returns {void} */ deleteAttributes(attributeList, callback) { - if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { - return callback(new Error('User is not authenticated'), null); - } - - this.client.request( - 'DeleteUserAttributes', - { - UserAttributeNames: attributeList, - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - }, - err => { - if (err) { - return callback(err, null); - } - - // update cached user - return this.getUserData(() => callback(null, 'SUCCESS'), { - bypassCache: true, - }); - } - ); - return undefined; + super.deleteAttributes(attributeList, callback); } /** @@ -1356,18 +307,7 @@ export default class CognitoUser { * @returns {void} */ resendConfirmationCode(callback, clientMetadata) { - const jsonReq = { - ClientId: this.pool.getClientId(), - Username: this.username, - ClientMetadata: clientMetadata, - }; - - this.client.request('ResendConfirmationCode', jsonReq, (err, result) => { - if (err) { - return callback(err, null); - } - return callback(null, result); - }); + super.resendConfirmationCode(callback, clientMetadata); } /** @@ -1384,66 +324,7 @@ export default class CognitoUser { * @returns {void} */ getSession(callback, options = {}) { - if (this.username == null) { - return callback( - new Error('Username is null. Cannot retrieve a new session'), - null - ); - } - - if (this.signInUserSession != null && this.signInUserSession.isValid()) { - return callback(null, this.signInUserSession); - } - - const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}.${ - this.username - }`; - const idTokenKey = `${keyPrefix}.idToken`; - const accessTokenKey = `${keyPrefix}.accessToken`; - const refreshTokenKey = `${keyPrefix}.refreshToken`; - const clockDriftKey = `${keyPrefix}.clockDrift`; - - if (this.storage.getItem(idTokenKey)) { - const idToken = new CognitoIdToken({ - IdToken: this.storage.getItem(idTokenKey), - }); - const accessToken = new CognitoAccessToken({ - AccessToken: this.storage.getItem(accessTokenKey), - }); - const refreshToken = new CognitoRefreshToken({ - RefreshToken: this.storage.getItem(refreshTokenKey), - }); - const clockDrift = parseInt(this.storage.getItem(clockDriftKey), 0) || 0; - - const sessionData = { - IdToken: idToken, - AccessToken: accessToken, - RefreshToken: refreshToken, - ClockDrift: clockDrift, - }; - const cachedSession = new CognitoUserSession(sessionData); - - if (cachedSession.isValid()) { - this.signInUserSession = cachedSession; - return callback(null, this.signInUserSession); - } - - if (!refreshToken.getToken()) { - return callback( - new Error('Cannot retrieve a new session. Please authenticate.'), - null - ); - } - - this.refreshSession(refreshToken, callback, options.clientMetadata); - } else { - callback( - new Error('Local storage is missing an ID Token, Please authenticate'), - null - ); - } - - return undefined; + super.getSession(callback, options); } /** @@ -1454,196 +335,7 @@ export default class CognitoUser { * @returns {void} */ refreshSession(refreshToken, callback, clientMetadata) { - const wrappedCallback = this.pool.wrapRefreshSessionCallback - ? this.pool.wrapRefreshSessionCallback(callback) - : callback; - const authParameters = {}; - authParameters.REFRESH_TOKEN = refreshToken.getToken(); - const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}`; - const lastUserKey = `${keyPrefix}.LastAuthUser`; - - if (this.storage.getItem(lastUserKey)) { - this.username = this.storage.getItem(lastUserKey); - const deviceKeyKey = `${keyPrefix}.${this.username}.deviceKey`; - this.deviceKey = this.storage.getItem(deviceKeyKey); - authParameters.DEVICE_KEY = this.deviceKey; - } - - const jsonReq = { - ClientId: this.pool.getClientId(), - AuthFlow: 'REFRESH_TOKEN_AUTH', - AuthParameters: authParameters, - ClientMetadata: clientMetadata, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - this.client.request('InitiateAuth', jsonReq, (err, authResult) => { - if (err) { - if (err.code === 'NotAuthorizedException') { - this.clearCachedUser(); - } - return wrappedCallback(err, null); - } - if (authResult) { - const authenticationResult = authResult.AuthenticationResult; - if ( - !Object.prototype.hasOwnProperty.call( - authenticationResult, - 'RefreshToken' - ) - ) { - authenticationResult.RefreshToken = refreshToken.getToken(); - } - this.signInUserSession = - this.getCognitoUserSession(authenticationResult); - this.cacheTokens(); - return wrappedCallback(null, this.signInUserSession); - } - return undefined; - }); - } - - /** - * This is used to save the session tokens to local storage - * @returns {void} - */ - cacheTokens() { - const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}`; - const idTokenKey = `${keyPrefix}.${this.username}.idToken`; - const accessTokenKey = `${keyPrefix}.${this.username}.accessToken`; - const refreshTokenKey = `${keyPrefix}.${this.username}.refreshToken`; - const clockDriftKey = `${keyPrefix}.${this.username}.clockDrift`; - const lastUserKey = `${keyPrefix}.LastAuthUser`; - - this.storage.setItem( - idTokenKey, - this.signInUserSession.getIdToken().getJwtToken() - ); - this.storage.setItem( - accessTokenKey, - this.signInUserSession.getAccessToken().getJwtToken() - ); - this.storage.setItem( - refreshTokenKey, - this.signInUserSession.getRefreshToken().getToken() - ); - this.storage.setItem( - clockDriftKey, - `${this.signInUserSession.getClockDrift()}` - ); - this.storage.setItem(lastUserKey, this.username); - } - - /** - * This is to cache user data - */ - cacheUserData(userData) { - this.storage.setItem(this.userDataKey, JSON.stringify(userData)); - } - - /** - * This is to remove cached user data - */ - clearCachedUserData() { - this.storage.removeItem(this.userDataKey); - } - - clearCachedUser() { - this.clearCachedTokens(); - this.clearCachedUserData(); - } - - /** - * This is used to cache the device key and device group and device password - * @returns {void} - */ - cacheDeviceKeyAndPassword() { - const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}.${ - this.username - }`; - const deviceKeyKey = `${keyPrefix}.deviceKey`; - const randomPasswordKey = `${keyPrefix}.randomPasswordKey`; - const deviceGroupKeyKey = `${keyPrefix}.deviceGroupKey`; - - this.storage.setItem(deviceKeyKey, this.deviceKey); - this.storage.setItem(randomPasswordKey, this.randomPassword); - this.storage.setItem(deviceGroupKeyKey, this.deviceGroupKey); - } - - /** - * This is used to get current device key and device group and device password - * @returns {void} - */ - getCachedDeviceKeyAndPassword() { - const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}.${ - this.username - }`; - const deviceKeyKey = `${keyPrefix}.deviceKey`; - const randomPasswordKey = `${keyPrefix}.randomPasswordKey`; - const deviceGroupKeyKey = `${keyPrefix}.deviceGroupKey`; - - if (this.storage.getItem(deviceKeyKey)) { - this.deviceKey = this.storage.getItem(deviceKeyKey); - this.randomPassword = this.storage.getItem(randomPasswordKey); - this.deviceGroupKey = this.storage.getItem(deviceGroupKeyKey); - } - } - - /** - * This is used to clear the device key info from local storage - * @returns {void} - */ - clearCachedDeviceKeyAndPassword() { - const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}.${ - this.username - }`; - const deviceKeyKey = `${keyPrefix}.deviceKey`; - const randomPasswordKey = `${keyPrefix}.randomPasswordKey`; - const deviceGroupKeyKey = `${keyPrefix}.deviceGroupKey`; - - this.storage.removeItem(deviceKeyKey); - this.storage.removeItem(randomPasswordKey); - this.storage.removeItem(deviceGroupKeyKey); - } - - /** - * This is used to clear the session tokens from local storage - * @returns {void} - */ - clearCachedTokens() { - const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}`; - const idTokenKey = `${keyPrefix}.${this.username}.idToken`; - const accessTokenKey = `${keyPrefix}.${this.username}.accessToken`; - const refreshTokenKey = `${keyPrefix}.${this.username}.refreshToken`; - const lastUserKey = `${keyPrefix}.LastAuthUser`; - const clockDriftKey = `${keyPrefix}.${this.username}.clockDrift`; - - this.storage.removeItem(idTokenKey); - this.storage.removeItem(accessTokenKey); - this.storage.removeItem(refreshTokenKey); - this.storage.removeItem(lastUserKey); - this.storage.removeItem(clockDriftKey); - } - - /** - * This is used to build a user session from tokens retrieved in the authentication result - * @param {object} authResult Successful auth response from server. - * @returns {CognitoUserSession} The new user session. - * @private - */ - getCognitoUserSession(authResult) { - const idToken = new CognitoIdToken(authResult); - const accessToken = new CognitoAccessToken(authResult); - const refreshToken = new CognitoRefreshToken(authResult); - - const sessionData = { - IdToken: idToken, - AccessToken: accessToken, - RefreshToken: refreshToken, - }; - - return new CognitoUserSession(sessionData); + super.refreshSession(refreshToken, callback, clientMetadata); } /** @@ -1657,23 +349,7 @@ export default class CognitoUser { * @returns {void} */ forgotPassword(callback, clientMetadata) { - const jsonReq = { - ClientId: this.pool.getClientId(), - Username: this.username, - ClientMetadata: clientMetadata, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - this.client.request('ForgotPassword', jsonReq, (err, data) => { - if (err) { - return callback.onFailure(err); - } - if (typeof callback.inputVerificationCode === 'function') { - return callback.inputVerificationCode(data); - } - return callback.onSuccess(data); - }); + super.forgotPassword(callback, clientMetadata); } /** @@ -1687,22 +363,12 @@ export default class CognitoUser { * @returns {void} */ confirmPassword(confirmationCode, newPassword, callback, clientMetadata) { - const jsonReq = { - ClientId: this.pool.getClientId(), - Username: this.username, - ConfirmationCode: confirmationCode, - Password: newPassword, - ClientMetadata: clientMetadata, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - this.client.request('ConfirmForgotPassword', jsonReq, err => { - if (err) { - return callback.onFailure(err); - } - return callback.onSuccess('SUCCESS'); - }); + super.confirmPassword( + confirmationCode, + newPassword, + callback, + clientMetadata + ); } /** @@ -1715,28 +381,7 @@ export default class CognitoUser { * @returns {void} */ getAttributeVerificationCode(attributeName, callback, clientMetadata) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback.onFailure(new Error('User is not authenticated')); - } - - this.client.request( - 'GetUserAttributeVerificationCode', - { - AttributeName: attributeName, - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - ClientMetadata: clientMetadata, - }, - (err, data) => { - if (err) { - return callback.onFailure(err); - } - if (typeof callback.inputVerificationCode === 'function') { - return callback.inputVerificationCode(data); - } - return callback.onSuccess('SUCCESS'); - } - ); - return undefined; + super.getAttributeVerificationCode(attributeName, callback, clientMetadata); } /** @@ -1749,25 +394,7 @@ export default class CognitoUser { * @returns {void} */ verifyAttribute(attributeName, confirmationCode, callback) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback.onFailure(new Error('User is not authenticated')); - } - - this.client.request( - 'VerifyUserAttribute', - { - AttributeName: attributeName, - Code: confirmationCode, - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - }, - err => { - if (err) { - return callback.onFailure(err); - } - return callback.onSuccess('SUCCESS'); - } - ); - return undefined; + super.verifyAttribute(attributeName, confirmationCode, callback); } /** @@ -1778,24 +405,7 @@ export default class CognitoUser { * @returns {void} */ getDevice(callback) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback.onFailure(new Error('User is not authenticated')); - } - - this.client.request( - 'GetDevice', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - DeviceKey: this.deviceKey, - }, - (err, data) => { - if (err) { - return callback.onFailure(err); - } - return callback.onSuccess(data); - } - ); - return undefined; + super.getDevice(callback); } /** @@ -1807,24 +417,7 @@ export default class CognitoUser { * @returns {void} */ forgetSpecificDevice(deviceKey, callback) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback.onFailure(new Error('User is not authenticated')); - } - - this.client.request( - 'ForgetDevice', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - DeviceKey: deviceKey, - }, - err => { - if (err) { - return callback.onFailure(err); - } - return callback.onSuccess('SUCCESS'); - } - ); - return undefined; + super.forgetSpecificDevice(deviceKey, callback); } /** @@ -1835,16 +428,7 @@ export default class CognitoUser { * @returns {void} */ forgetDevice(callback) { - this.forgetSpecificDevice(this.deviceKey, { - onFailure: callback.onFailure, - onSuccess: result => { - this.deviceKey = null; - this.deviceGroupKey = null; - this.randomPassword = null; - this.clearCachedDeviceKeyAndPassword(); - return callback.onSuccess(result); - }, - }); + super.forgetDevice(callback); } /** @@ -1855,25 +439,7 @@ export default class CognitoUser { * @returns {void} */ setDeviceStatusRemembered(callback) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback.onFailure(new Error('User is not authenticated')); - } - - this.client.request( - 'UpdateDeviceStatus', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - DeviceKey: this.deviceKey, - DeviceRememberedStatus: 'remembered', - }, - err => { - if (err) { - return callback.onFailure(err); - } - return callback.onSuccess('SUCCESS'); - } - ); - return undefined; + super.setDeviceStatusRemembered(callback); } /** @@ -1884,25 +450,7 @@ export default class CognitoUser { * @returns {void} */ setDeviceStatusNotRemembered(callback) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback.onFailure(new Error('User is not authenticated')); - } - - this.client.request( - 'UpdateDeviceStatus', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - DeviceKey: this.deviceKey, - DeviceRememberedStatus: 'not_remembered', - }, - err => { - if (err) { - return callback.onFailure(err); - } - return callback.onSuccess('SUCCESS'); - } - ); - return undefined; + super.setDeviceStatusNotRemembered(callback); } /** @@ -1916,25 +464,7 @@ export default class CognitoUser { * @returns {void} */ listDevices(limit, paginationToken, callback) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback.onFailure(new Error('User is not authenticated')); - } - const requestParams = { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - Limit: limit, - }; - - if (paginationToken) { - requestParams.PaginationToken = paginationToken; - } - - this.client.request('ListDevices', requestParams, (err, data) => { - if (err) { - return callback.onFailure(err); - } - return callback.onSuccess(data); - }); - return undefined; + super.listDevices(limit, paginationToken, callback); } /** @@ -1945,24 +475,7 @@ export default class CognitoUser { * @returns {void} */ globalSignOut(callback) { - if (this.signInUserSession == null || !this.signInUserSession.isValid()) { - return callback.onFailure(new Error('User is not authenticated')); - } - - this.client.request( - 'GlobalSignOut', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - }, - err => { - if (err) { - return callback.onFailure(err); - } - this.clearCachedUser(); - return callback.onSuccess('SUCCESS'); - } - ); - return undefined; + super.globalSignOut(callback); } /** @@ -1970,92 +483,15 @@ export default class CognitoUser { * @returns {void} */ signOut(revokeTokenCallback) { - // If tokens won't be revoked, we just clean the client data. - if (!revokeTokenCallback || typeof revokeTokenCallback !== 'function') { - this.cleanClientData(); - - return; - } - - this.getSession((error, _session) => { - if (error) { - return revokeTokenCallback(error); - } - - this.revokeTokens(err => { - this.cleanClientData(); - - revokeTokenCallback(err); - }); - }); + super.signOut(revokeTokenCallback); } - revokeTokens(revokeTokenCallback = () => {}) { - if (typeof revokeTokenCallback !== 'function') { - throw new Error('Invalid revokeTokenCallback. It should be a function.'); - } - - const tokensToBeRevoked = []; - - if (!this.signInUserSession) { - const error = new Error('User is not authenticated'); - - return revokeTokenCallback(error); - } - - if (!this.signInUserSession.getAccessToken()) { - const error = new Error('No Access token available'); - - return revokeTokenCallback(error); - } - - const refreshToken = this.signInUserSession.getRefreshToken().getToken(); - const accessToken = this.signInUserSession.getAccessToken(); - - if (this.isSessionRevocable(accessToken)) { - if (refreshToken) { - return this.revokeToken({ - token: refreshToken, - callback: revokeTokenCallback, - }); - } - } - revokeTokenCallback(); - } - - isSessionRevocable(token) { - if (token && typeof token.decodePayload === 'function') { - try { - const { origin_jti } = token.decodePayload(); - return !!origin_jti; - } catch (err) { - // Nothing to do, token doesnt have origin_jti claim - } - } - - return false; - } - - cleanClientData() { - this.signInUserSession = null; - this.clearCachedUser(); + revokeTokens(revokeTokenCallback = () => { }) { + super.revokeTokens(revokeTokenCallback); } revokeToken({ token, callback }) { - this.client.requestWithRetry( - 'RevokeToken', - { - Token: token, - ClientId: this.pool.getClientId(), - }, - err => { - if (err) { - return callback(err); - } - - callback(); - } - ); + super.revokeToken({ token, callback }); } /** @@ -2065,47 +501,7 @@ export default class CognitoUser { * @returns {void} */ sendMFASelectionAnswer(answerChallenge, callback) { - const challengeResponses = {}; - challengeResponses.USERNAME = this.username; - challengeResponses.ANSWER = answerChallenge; - - const jsonReq = { - ChallengeName: 'SELECT_MFA_TYPE', - ChallengeResponses: challengeResponses, - ClientId: this.pool.getClientId(), - Session: this.Session, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - this.client.request('RespondToAuthChallenge', jsonReq, (err, data) => { - if (err) { - return callback.onFailure(err); - } - this.Session = data.Session; - if (answerChallenge === 'SMS_MFA') { - return callback.mfaRequired( - data.ChallengeName, - data.ChallengeParameters - ); - } - if (answerChallenge === 'SOFTWARE_TOKEN_MFA') { - return callback.totpRequired( - data.ChallengeName, - data.ChallengeParameters - ); - } - return undefined; - }); - } - - /** - * This returns the user context data for advanced security feature. - * @returns {string} the user context data from CognitoUserPool - */ - getUserContextData() { - const pool = this.pool; - return pool.getUserContextData(this.username); + super.sendMFASelectionAnswer(answerChallenge, callback); } /** @@ -2114,34 +510,7 @@ export default class CognitoUser { * @returns {void} */ associateSoftwareToken(callback) { - if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { - this.client.request( - 'AssociateSoftwareToken', - { - Session: this.Session, - }, - (err, data) => { - if (err) { - return callback.onFailure(err); - } - this.Session = data.Session; - return callback.associateSecretCode(data.SecretCode); - } - ); - } else { - this.client.request( - 'AssociateSoftwareToken', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - }, - (err, data) => { - if (err) { - return callback.onFailure(err); - } - return callback.associateSecretCode(data.SecretCode); - } - ); - } + super.associateSoftwareToken(callback); } /** @@ -2152,62 +521,6 @@ export default class CognitoUser { * @returns {void} */ verifySoftwareToken(totpCode, friendlyDeviceName, callback) { - if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { - this.client.request( - 'VerifySoftwareToken', - { - Session: this.Session, - UserCode: totpCode, - FriendlyDeviceName: friendlyDeviceName, - }, - (err, data) => { - if (err) { - return callback.onFailure(err); - } - this.Session = data.Session; - const challengeResponses = {}; - challengeResponses.USERNAME = this.username; - const jsonReq = { - ChallengeName: 'MFA_SETUP', - ClientId: this.pool.getClientId(), - ChallengeResponses: challengeResponses, - Session: this.Session, - }; - if (this.getUserContextData()) { - jsonReq.UserContextData = this.getUserContextData(); - } - this.client.request( - 'RespondToAuthChallenge', - jsonReq, - (errRespond, dataRespond) => { - if (errRespond) { - return callback.onFailure(errRespond); - } - this.signInUserSession = this.getCognitoUserSession( - dataRespond.AuthenticationResult - ); - this.cacheTokens(); - return callback.onSuccess(this.signInUserSession); - } - ); - return undefined; - } - ); - } else { - this.client.request( - 'VerifySoftwareToken', - { - AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), - UserCode: totpCode, - FriendlyDeviceName: friendlyDeviceName, - }, - (err, data) => { - if (err) { - return callback.onFailure(err); - } - return callback.onSuccess(data); - } - ); - } + super.verifySoftwareToken(totpCode, friendlyDeviceName, callback); } } diff --git a/packages/amazon-cognito-identity-js/src/internals/InternalCognitoUser.js b/packages/amazon-cognito-identity-js/src/internals/InternalCognitoUser.js new file mode 100644 index 00000000000..19f0e87b79e --- /dev/null +++ b/packages/amazon-cognito-identity-js/src/internals/InternalCognitoUser.js @@ -0,0 +1,2428 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Buffer } from 'buffer'; +import { Sha256 } from '@aws-crypto/sha256-js'; +import { Platform } from '../Platform'; + +import BigInteger from '../BigInteger'; +import AuthenticationHelper from '../AuthenticationHelper'; +import CognitoAccessToken from '../CognitoAccessToken'; +import CognitoIdToken from '../CognitoIdToken'; +import CognitoRefreshToken from '../CognitoRefreshToken'; +import CognitoUserSession from '../CognitoUserSession'; +import DateHelper from '../DateHelper'; +import CognitoUserAttribute from '../CognitoUserAttribute'; +import StorageHelper from '../StorageHelper'; + +/** + * @callback nodeCallback + * @template T result + * @param {*} err The operation failure reason, or null. + * @param {T} result The operation result. + */ + +/** + * @callback onFailure + * @param {*} err Failure reason. + */ + +/** + * @callback onSuccess + * @template T result + * @param {T} result The operation result. + */ + +/** + * @callback mfaRequired + * @param {*} details MFA challenge details. + */ + +/** + * @callback customChallenge + * @param {*} details Custom challenge details. + */ + +/** + * @callback inputVerificationCode + * @param {*} data Server response. + */ + +/** + * @callback authSuccess + * @param {CognitoUserSession} session The new session. + * @param {bool=} userConfirmationNecessary User must be confirmed. + */ + +const isNavigatorAvailable = typeof navigator !== 'undefined'; +const userAgent = isNavigatorAvailable + ? Platform.isReactNative + ? 'react-native' + : navigator.userAgent + : 'nodejs'; + +/** @class */ +export class InternalCognitoUser { + /** + * Constructs a new CognitoUser object + * @param {object} data Creation options + * @param {string} data.Username The user's username. + * @param {CognitoUserPool} data.Pool Pool containing the user. + * @param {object} data.Storage Optional storage object. + */ + constructor(data) { + if (data == null || data.Username == null || data.Pool == null) { + throw new Error('Username and Pool information are required.'); + } + + this.username = data.Username || ''; + this.pool = data.Pool; + this.Session = null; + + this.client = data.Pool.client; + + this.signInUserSession = null; + this.authenticationFlowType = 'USER_SRP_AUTH'; + + this.storage = data.Storage || new StorageHelper().getStorage(); + + this.keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}`; + this.userDataKey = `${this.keyPrefix}.${this.username}.userData`; + } + + /** + * Sets the session for this user + * @param {CognitoUserSession} signInUserSession the session + * @returns {void} + */ + setSignInUserSession(signInUserSession) { + this.clearCachedUserData(); + this.signInUserSession = signInUserSession; + this.cacheTokens(); + } + + /** + * @returns {CognitoUserSession} the current session for this user + */ + getSignInUserSession() { + return this.signInUserSession; + } + + /** + * @returns {string} the user's username + */ + getUsername() { + return this.username; + } + + /** + * @returns {String} the authentication flow type + */ + getAuthenticationFlowType() { + return this.authenticationFlowType; + } + + /** + * sets authentication flow type + * @param {string} authenticationFlowType New value. + * @returns {void} + */ + setAuthenticationFlowType(authenticationFlowType) { + this.authenticationFlowType = authenticationFlowType; + } + + /** + * This is used for authenticating the user through the custom authentication flow. + * @param {AuthenticationDetails} authDetails Contains the authentication data + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {customChallenge} callback.customChallenge Custom challenge + * response required to continue. + * @param {authSuccess} callback.onSuccess Called on success with the new session. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + initiateAuth(authDetails, callback, userAgentValue) { + const authParameters = authDetails.getAuthParameters(); + authParameters.USERNAME = this.username; + + const clientMetaData = + Object.keys(authDetails.getValidationData()).length !== 0 + ? authDetails.getValidationData() + : authDetails.getClientMetadata(); + + const jsonReq = { + AuthFlow: 'CUSTOM_AUTH', + ClientId: this.pool.getClientId(), + AuthParameters: authParameters, + ClientMetadata: clientMetaData, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + + this.client.request( + 'InitiateAuth', + jsonReq, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + const challengeName = data.ChallengeName; + const challengeParameters = data.ChallengeParameters; + + if (challengeName === 'CUSTOM_CHALLENGE') { + this.Session = data.Session; + return callback.customChallenge(challengeParameters); + } + this.signInUserSession = this.getCognitoUserSession( + data.AuthenticationResult + ); + this.cacheTokens(); + return callback.onSuccess(this.signInUserSession); + }, + userAgentValue + ); + } + + /** + * This is used for authenticating the user. + * stuff + * @param {AuthenticationDetails} authDetails Contains the authentication data + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {newPasswordRequired} callback.newPasswordRequired new + * password and any required attributes are required to continue + * @param {mfaRequired} callback.mfaRequired MFA code + * required to continue. + * @param {customChallenge} callback.customChallenge Custom challenge + * response required to continue. + * @param {authSuccess} callback.onSuccess Called on success with the new session. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + authenticateUser(authDetails, callback, userAgentValue) { + if (this.authenticationFlowType === 'USER_PASSWORD_AUTH') { + return this.authenticateUserPlainUsernamePassword( + authDetails, + callback, + userAgentValue + ); + } else if ( + this.authenticationFlowType === 'USER_SRP_AUTH' || + this.authenticationFlowType === 'CUSTOM_AUTH' + ) { + return this.authenticateUserDefaultAuth( + authDetails, + callback, + userAgentValue + ); + } + return callback.onFailure( + new Error('Authentication flow type is invalid.') + ); + } + + /** + * PRIVATE ONLY: This is an internal only method and should not + * be directly called by the consumers. + * It calls the AuthenticationHelper for SRP related + * stuff + * @param {AuthenticationDetails} authDetails Contains the authentication data + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {newPasswordRequired} callback.newPasswordRequired new + * password and any required attributes are required to continue + * @param {mfaRequired} callback.mfaRequired MFA code + * required to continue. + * @param {customChallenge} callback.customChallenge Custom challenge + * response required to continue. + * @param {authSuccess} callback.onSuccess Called on success with the new session. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + authenticateUserDefaultAuth(authDetails, callback, userAgentValue) { + const authenticationHelper = new AuthenticationHelper( + this.pool.getUserPoolName() + ); + const dateHelper = new DateHelper(); + + let serverBValue; + let salt; + const authParameters = {}; + + if (this.deviceKey != null) { + authParameters.DEVICE_KEY = this.deviceKey; + } + + authParameters.USERNAME = this.username; + authenticationHelper.getLargeAValue((errOnAValue, aValue) => { + // getLargeAValue callback start + if (errOnAValue) { + callback.onFailure(errOnAValue); + } + + authParameters.SRP_A = aValue.toString(16); + + if (this.authenticationFlowType === 'CUSTOM_AUTH') { + authParameters.CHALLENGE_NAME = 'SRP_A'; + } + + const clientMetaData = + Object.keys(authDetails.getValidationData()).length !== 0 + ? authDetails.getValidationData() + : authDetails.getClientMetadata(); + + const jsonReq = { + AuthFlow: this.authenticationFlowType, + ClientId: this.pool.getClientId(), + AuthParameters: authParameters, + ClientMetadata: clientMetaData, + }; + if (this.getUserContextData(this.username)) { + jsonReq.UserContextData = this.getUserContextData(this.username); + } + + this.client.request( + 'InitiateAuth', + jsonReq, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + + const challengeParameters = data.ChallengeParameters; + + this.username = challengeParameters.USER_ID_FOR_SRP; + this.userDataKey = `${this.keyPrefix}.${this.username}.userData`; + serverBValue = new BigInteger(challengeParameters.SRP_B, 16); + salt = new BigInteger(challengeParameters.SALT, 16); + this.getCachedDeviceKeyAndPassword(); + + authenticationHelper.getPasswordAuthenticationKey( + this.username, + authDetails.getPassword(), + serverBValue, + salt, + (errOnHkdf, hkdf) => { + // getPasswordAuthenticationKey callback start + if (errOnHkdf) { + callback.onFailure(errOnHkdf); + } + + const dateNow = dateHelper.getNowString(); + + const concatBuffer = Buffer.concat([ + Buffer.from(this.pool.getUserPoolName(), 'utf8'), + Buffer.from(this.username, 'utf8'), + Buffer.from(challengeParameters.SECRET_BLOCK, 'base64'), + Buffer.from(dateNow, 'utf8'), + ]); + + const awsCryptoHash = new Sha256(hkdf); + awsCryptoHash.update(concatBuffer); + + const resultFromAWSCrypto = awsCryptoHash.digestSync(); + const signatureString = + Buffer.from(resultFromAWSCrypto).toString('base64'); + + const challengeResponses = {}; + + challengeResponses.USERNAME = this.username; + challengeResponses.PASSWORD_CLAIM_SECRET_BLOCK = + challengeParameters.SECRET_BLOCK; + challengeResponses.TIMESTAMP = dateNow; + challengeResponses.PASSWORD_CLAIM_SIGNATURE = signatureString; + + if (this.deviceKey != null) { + challengeResponses.DEVICE_KEY = this.deviceKey; + } + + const respondToAuthChallenge = (challenge, challengeCallback) => + this.client.request( + 'RespondToAuthChallenge', + challenge, + (errChallenge, dataChallenge) => { + if ( + errChallenge && + errChallenge.code === 'ResourceNotFoundException' && + errChallenge.message.toLowerCase().indexOf('device') !== + -1 + ) { + challengeResponses.DEVICE_KEY = null; + this.deviceKey = null; + this.randomPassword = null; + this.deviceGroupKey = null; + this.clearCachedDeviceKeyAndPassword(); + return respondToAuthChallenge( + challenge, + challengeCallback + ); + } + return challengeCallback(errChallenge, dataChallenge); + }, + userAgentValue + ); + + const jsonReqResp = { + ChallengeName: 'PASSWORD_VERIFIER', + ClientId: this.pool.getClientId(), + ChallengeResponses: challengeResponses, + Session: data.Session, + ClientMetadata: clientMetaData, + }; + if (this.getUserContextData()) { + jsonReqResp.UserContextData = this.getUserContextData(); + } + respondToAuthChallenge( + jsonReqResp, + (errAuthenticate, dataAuthenticate) => { + if (errAuthenticate) { + return callback.onFailure(errAuthenticate); + } + + return this.authenticateUserInternal( + dataAuthenticate, + authenticationHelper, + callback, + userAgentValue + ); + } + ); + return undefined; + // getPasswordAuthenticationKey callback end + } + ); + return undefined; + }, + userAgentValue + ); + // getLargeAValue callback end + }); + } + + /** + * PRIVATE ONLY: This is an internal only method and should not + * be directly called by the consumers. + * @param {AuthenticationDetails} authDetails Contains the authentication data. + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {mfaRequired} callback.mfaRequired MFA code + * required to continue. + * @param {authSuccess} callback.onSuccess Called on success with the new session. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + authenticateUserPlainUsernamePassword(authDetails, callback, userAgentValue) { + const authParameters = {}; + authParameters.USERNAME = this.username; + authParameters.PASSWORD = authDetails.getPassword(); + if (!authParameters.PASSWORD) { + callback.onFailure(new Error('PASSWORD parameter is required')); + return; + } + const authenticationHelper = new AuthenticationHelper( + this.pool.getUserPoolName() + ); + this.getCachedDeviceKeyAndPassword(); + if (this.deviceKey != null) { + authParameters.DEVICE_KEY = this.deviceKey; + } + + const clientMetaData = + Object.keys(authDetails.getValidationData()).length !== 0 + ? authDetails.getValidationData() + : authDetails.getClientMetadata(); + + const jsonReq = { + AuthFlow: 'USER_PASSWORD_AUTH', + ClientId: this.pool.getClientId(), + AuthParameters: authParameters, + ClientMetadata: clientMetaData, + }; + if (this.getUserContextData(this.username)) { + jsonReq.UserContextData = this.getUserContextData(this.username); + } + // USER_PASSWORD_AUTH happens in a single round-trip: client sends userName and password, + // Cognito UserPools verifies password and returns tokens. + this.client.request( + 'InitiateAuth', + jsonReq, + (err, authResult) => { + if (err) { + return callback.onFailure(err); + } + return this.authenticateUserInternal( + authResult, + authenticationHelper, + callback, + userAgentValue + ); + }, + userAgentValue + ); + } + + /** + * PRIVATE ONLY: This is an internal only method and should not + * be directly called by the consumers. + * @param {object} dataAuthenticate authentication data + * @param {object} authenticationHelper helper created + * @param {callback} callback passed on from caller + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + authenticateUserInternal( + dataAuthenticate, + authenticationHelper, + callback, + userAgentValue + ) { + const challengeName = dataAuthenticate.ChallengeName; + const challengeParameters = dataAuthenticate.ChallengeParameters; + + if (challengeName === 'SMS_MFA') { + this.Session = dataAuthenticate.Session; + return callback.mfaRequired(challengeName, challengeParameters); + } + + if (challengeName === 'SELECT_MFA_TYPE') { + this.Session = dataAuthenticate.Session; + return callback.selectMFAType(challengeName, challengeParameters); + } + + if (challengeName === 'MFA_SETUP') { + this.Session = dataAuthenticate.Session; + return callback.mfaSetup(challengeName, challengeParameters); + } + + if (challengeName === 'SOFTWARE_TOKEN_MFA') { + this.Session = dataAuthenticate.Session; + return callback.totpRequired(challengeName, challengeParameters); + } + + if (challengeName === 'CUSTOM_CHALLENGE') { + this.Session = dataAuthenticate.Session; + return callback.customChallenge(challengeParameters); + } + + if (challengeName === 'NEW_PASSWORD_REQUIRED') { + this.Session = dataAuthenticate.Session; + + let userAttributes = null; + let rawRequiredAttributes = null; + const requiredAttributes = []; + const userAttributesPrefix = + authenticationHelper.getNewPasswordRequiredChallengeUserAttributePrefix(); + + if (challengeParameters) { + userAttributes = JSON.parse( + dataAuthenticate.ChallengeParameters.userAttributes + ); + rawRequiredAttributes = JSON.parse( + dataAuthenticate.ChallengeParameters.requiredAttributes + ); + } + + if (rawRequiredAttributes) { + for (let i = 0; i < rawRequiredAttributes.length; i++) { + requiredAttributes[i] = rawRequiredAttributes[i].substr( + userAttributesPrefix.length + ); + } + } + return callback.newPasswordRequired(userAttributes, requiredAttributes); + } + + if (challengeName === 'DEVICE_SRP_AUTH') { + this.Session = dataAuthenticate.Session; + this.getDeviceResponse(callback, undefined, userAgentValue); + return undefined; + } + + this.signInUserSession = this.getCognitoUserSession( + dataAuthenticate.AuthenticationResult + ); + this.challengeName = challengeName; + this.cacheTokens(); + + const newDeviceMetadata = + dataAuthenticate.AuthenticationResult.NewDeviceMetadata; + if (newDeviceMetadata == null) { + return callback.onSuccess(this.signInUserSession); + } + + authenticationHelper.generateHashDevice( + dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceGroupKey, + dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceKey, + errGenHash => { + if (errGenHash) { + return callback.onFailure(errGenHash); + } + + const deviceSecretVerifierConfig = { + Salt: Buffer.from( + authenticationHelper.getSaltDevices(), + 'hex' + ).toString('base64'), + PasswordVerifier: Buffer.from( + authenticationHelper.getVerifierDevices(), + 'hex' + ).toString('base64'), + }; + + this.verifierDevices = deviceSecretVerifierConfig.PasswordVerifier; + this.deviceGroupKey = newDeviceMetadata.DeviceGroupKey; + this.randomPassword = authenticationHelper.getRandomPassword(); + + this.client.request( + 'ConfirmDevice', + { + DeviceKey: newDeviceMetadata.DeviceKey, + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + DeviceSecretVerifierConfig: deviceSecretVerifierConfig, + DeviceName: userAgent, + }, + (errConfirm, dataConfirm) => { + if (errConfirm) { + return callback.onFailure(errConfirm); + } + + this.deviceKey = + dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceKey; + this.cacheDeviceKeyAndPassword(); + if (dataConfirm.UserConfirmationNecessary === true) { + return callback.onSuccess( + this.signInUserSession, + dataConfirm.UserConfirmationNecessary + ); + } + return callback.onSuccess(this.signInUserSession); + }, + userAgentValue + ); + return undefined; + } + ); + return undefined; + } + + /** + * This method is user to complete the NEW_PASSWORD_REQUIRED challenge. + * Pass the new password with any new user attributes to be updated. + * User attribute keys must be of format userAttributes.. + * @param {string} newPassword new password for this user + * @param {object} requiredAttributeData map with values for all required attributes + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {mfaRequired} callback.mfaRequired MFA code required to continue. + * @param {customChallenge} callback.customChallenge Custom challenge + * response required to continue. + * @param {authSuccess} callback.onSuccess Called on success with the new session. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + completeNewPasswordChallenge( + newPassword, + requiredAttributeData, + callback, + clientMetadata, + userAgentValue + ) { + if (!newPassword) { + return callback.onFailure(new Error('New password is required.')); + } + const authenticationHelper = new AuthenticationHelper( + this.pool.getUserPoolName() + ); + const userAttributesPrefix = + authenticationHelper.getNewPasswordRequiredChallengeUserAttributePrefix(); + + const finalUserAttributes = {}; + if (requiredAttributeData) { + Object.keys(requiredAttributeData).forEach(key => { + finalUserAttributes[userAttributesPrefix + key] = + requiredAttributeData[key]; + }); + } + + finalUserAttributes.NEW_PASSWORD = newPassword; + finalUserAttributes.USERNAME = this.username; + const jsonReq = { + ChallengeName: 'NEW_PASSWORD_REQUIRED', + ClientId: this.pool.getClientId(), + ChallengeResponses: finalUserAttributes, + Session: this.Session, + ClientMetadata: clientMetadata, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + + this.client.request( + 'RespondToAuthChallenge', + jsonReq, + (errAuthenticate, dataAuthenticate) => { + if (errAuthenticate) { + return callback.onFailure(errAuthenticate); + } + return this.authenticateUserInternal( + dataAuthenticate, + authenticationHelper, + callback, + userAgentValue + ); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used to get a session using device authentication. It is called at the end of user + * authentication + * + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {authSuccess} callback.onSuccess Called on success with the new session. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + * @private + */ + getDeviceResponse(callback, clientMetadata, userAgentValue) { + const authenticationHelper = new AuthenticationHelper(this.deviceGroupKey); + const dateHelper = new DateHelper(); + + const authParameters = {}; + + authParameters.USERNAME = this.username; + authParameters.DEVICE_KEY = this.deviceKey; + authenticationHelper.getLargeAValue((errAValue, aValue) => { + // getLargeAValue callback start + if (errAValue) { + callback.onFailure(errAValue); + } + + authParameters.SRP_A = aValue.toString(16); + + const jsonReq = { + ChallengeName: 'DEVICE_SRP_AUTH', + ClientId: this.pool.getClientId(), + ChallengeResponses: authParameters, + ClientMetadata: clientMetadata, + Session: this.Session, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + this.client.request( + 'RespondToAuthChallenge', + jsonReq, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + + const challengeParameters = data.ChallengeParameters; + + const serverBValue = new BigInteger(challengeParameters.SRP_B, 16); + const salt = new BigInteger(challengeParameters.SALT, 16); + + authenticationHelper.getPasswordAuthenticationKey( + this.deviceKey, + this.randomPassword, + serverBValue, + salt, + (errHkdf, hkdf) => { + // getPasswordAuthenticationKey callback start + if (errHkdf) { + return callback.onFailure(errHkdf); + } + + const dateNow = dateHelper.getNowString(); + + const concatBuffer = Buffer.concat([ + Buffer.from(this.deviceGroupKey, 'utf8'), + Buffer.from(this.deviceKey, 'utf8'), + Buffer.from(challengeParameters.SECRET_BLOCK, 'base64'), + Buffer.from(dateNow, 'utf8'), + ]); + + const awsCryptoHash = new Sha256(hkdf); + awsCryptoHash.update(concatBuffer); + + const resultFromAWSCrypto = awsCryptoHash.digestSync(); + const signatureString = + Buffer.from(resultFromAWSCrypto).toString('base64'); + + const challengeResponses = {}; + + challengeResponses.USERNAME = this.username; + challengeResponses.PASSWORD_CLAIM_SECRET_BLOCK = + challengeParameters.SECRET_BLOCK; + challengeResponses.TIMESTAMP = dateNow; + challengeResponses.PASSWORD_CLAIM_SIGNATURE = signatureString; + challengeResponses.DEVICE_KEY = this.deviceKey; + + const jsonReqResp = { + ChallengeName: 'DEVICE_PASSWORD_VERIFIER', + ClientId: this.pool.getClientId(), + ChallengeResponses: challengeResponses, + Session: data.Session, + }; + if (this.getUserContextData()) { + jsonReqResp.UserContextData = this.getUserContextData(); + } + + this.client.request( + 'RespondToAuthChallenge', + jsonReqResp, + (errAuthenticate, dataAuthenticate) => { + if (errAuthenticate) { + return callback.onFailure(errAuthenticate); + } + + this.signInUserSession = this.getCognitoUserSession( + dataAuthenticate.AuthenticationResult + ); + this.cacheTokens(); + + return callback.onSuccess(this.signInUserSession); + }, + userAgentValue + ); + return undefined; + // getPasswordAuthenticationKey callback end + } + ); + return undefined; + }, + userAgentValue + ); + // getLargeAValue callback end + }); + } + + /** + * This is used for a certain user to confirm the registration by using a confirmation code + * @param {string} confirmationCode Code entered by user. + * @param {bool} forceAliasCreation Allow migrating from an existing email / phone number. + * @param {nodeCallback} callback Called on success or error. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + confirmRegistration( + confirmationCode, + forceAliasCreation, + callback, + clientMetadata, + userAgentValue + ) { + const jsonReq = { + ClientId: this.pool.getClientId(), + ConfirmationCode: confirmationCode, + Username: this.username, + ForceAliasCreation: forceAliasCreation, + ClientMetadata: clientMetadata, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + this.client.request( + 'ConfirmSignUp', + jsonReq, + err => { + if (err) { + return callback(err, null); + } + return callback(null, 'SUCCESS'); + }, + userAgentValue + ); + } + + /** + * This is used by the user once he has the responses to a custom challenge + * @param {string} answerChallenge The custom challenge answer. + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {customChallenge} callback.customChallenge + * Custom challenge response required to continue. + * @param {authSuccess} callback.onSuccess Called on success with the new session. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + sendCustomChallengeAnswer( + answerChallenge, + callback, + clientMetadata, + userAgentValue + ) { + const challengeResponses = {}; + challengeResponses.USERNAME = this.username; + challengeResponses.ANSWER = answerChallenge; + + const authenticationHelper = new AuthenticationHelper( + this.pool.getUserPoolName() + ); + this.getCachedDeviceKeyAndPassword(); + if (this.deviceKey != null) { + challengeResponses.DEVICE_KEY = this.deviceKey; + } + + const jsonReq = { + ChallengeName: 'CUSTOM_CHALLENGE', + ChallengeResponses: challengeResponses, + ClientId: this.pool.getClientId(), + Session: this.Session, + ClientMetadata: clientMetadata, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + this.client.request('RespondToAuthChallenge', jsonReq, (err, data) => { + if (err) { + return callback.onFailure(err); + } + + return this.authenticateUserInternal( + data, + authenticationHelper, + callback, + userAgentValue + ); + }); + } + + /** + * This is used by the user once he has an MFA code + * @param {string} confirmationCode The MFA code entered by the user. + * @param {object} callback Result callback map. + * @param {string} mfaType The mfa we are replying to. + * @param {onFailure} callback.onFailure Called on any error. + * @param {authSuccess} callback.onSuccess Called on success with the new session. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + sendMFACode( + confirmationCode, + callback, + mfaType, + clientMetadata, + userAgentValue + ) { + const challengeResponses = {}; + challengeResponses.USERNAME = this.username; + challengeResponses.SMS_MFA_CODE = confirmationCode; + const mfaTypeSelection = mfaType || 'SMS_MFA'; + if (mfaTypeSelection === 'SOFTWARE_TOKEN_MFA') { + challengeResponses.SOFTWARE_TOKEN_MFA_CODE = confirmationCode; + } + + if (this.deviceKey != null) { + challengeResponses.DEVICE_KEY = this.deviceKey; + } + + const jsonReq = { + ChallengeName: mfaTypeSelection, + ChallengeResponses: challengeResponses, + ClientId: this.pool.getClientId(), + Session: this.Session, + ClientMetadata: clientMetadata, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + + this.client.request( + 'RespondToAuthChallenge', + jsonReq, + (err, dataAuthenticate) => { + if (err) { + return callback.onFailure(err); + } + + const challengeName = dataAuthenticate.ChallengeName; + + if (challengeName === 'DEVICE_SRP_AUTH') { + this.getDeviceResponse(callback, undefined, userAgentValue); + return undefined; + } + + this.signInUserSession = this.getCognitoUserSession( + dataAuthenticate.AuthenticationResult + ); + this.cacheTokens(); + + if (dataAuthenticate.AuthenticationResult.NewDeviceMetadata == null) { + return callback.onSuccess(this.signInUserSession); + } + + const authenticationHelper = new AuthenticationHelper( + this.pool.getUserPoolName() + ); + authenticationHelper.generateHashDevice( + dataAuthenticate.AuthenticationResult.NewDeviceMetadata + .DeviceGroupKey, + dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceKey, + errGenHash => { + if (errGenHash) { + return callback.onFailure(errGenHash); + } + + const deviceSecretVerifierConfig = { + Salt: Buffer.from( + authenticationHelper.getSaltDevices(), + 'hex' + ).toString('base64'), + PasswordVerifier: Buffer.from( + authenticationHelper.getVerifierDevices(), + 'hex' + ).toString('base64'), + }; + + this.verifierDevices = deviceSecretVerifierConfig.PasswordVerifier; + this.deviceGroupKey = + dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceGroupKey; + this.randomPassword = authenticationHelper.getRandomPassword(); + + this.client.request( + 'ConfirmDevice', + { + DeviceKey: + dataAuthenticate.AuthenticationResult.NewDeviceMetadata + .DeviceKey, + AccessToken: this.signInUserSession + .getAccessToken() + .getJwtToken(), + DeviceSecretVerifierConfig: deviceSecretVerifierConfig, + DeviceName: userAgent, + }, + (errConfirm, dataConfirm) => { + if (errConfirm) { + return callback.onFailure(errConfirm); + } + + this.deviceKey = + dataAuthenticate.AuthenticationResult.NewDeviceMetadata.DeviceKey; + this.cacheDeviceKeyAndPassword(); + if (dataConfirm.UserConfirmationNecessary === true) { + return callback.onSuccess( + this.signInUserSession, + dataConfirm.UserConfirmationNecessary + ); + } + return callback.onSuccess(this.signInUserSession); + }, + userAgentValue + ); + return undefined; + } + ); + return undefined; + }, + userAgentValue + ); + } + + /** + * This is used by an authenticated user to change the current password + * @param {string} oldUserPassword The current password. + * @param {string} newUserPassword The requested new password. + * @param {nodeCallback} callback Called on success or error. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + changePassword( + oldUserPassword, + newUserPassword, + callback, + clientMetadata, + userAgentValue + ) { + if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { + return callback(new Error('User is not authenticated'), null); + } + + this.client.request( + 'ChangePassword', + { + PreviousPassword: oldUserPassword, + ProposedPassword: newUserPassword, + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + ClientMetadata: clientMetadata, + }, + err => { + if (err) { + return callback(err, null); + } + return callback(null, 'SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used by an authenticated user to enable MFA for itself + * @deprecated + * @param {nodeCallback} callback Called on success or error. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + enableMFA(callback, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback(new Error('User is not authenticated'), null); + } + + const mfaOptions = []; + const mfaEnabled = { + DeliveryMedium: 'SMS', + AttributeName: 'phone_number', + }; + mfaOptions.push(mfaEnabled); + + this.client.request( + 'SetUserSettings', + { + MFAOptions: mfaOptions, + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + }, + err => { + if (err) { + return callback(err, null); + } + return callback(null, 'SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used by an authenticated user to enable MFA for itself + * @param {IMfaSettings} smsMfaSettings the sms mfa settings + * @param {IMFASettings} softwareTokenMfaSettings the software token mfa settings + * @param {nodeCallback} callback Called on success or error. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + setUserMfaPreference( + smsMfaSettings, + softwareTokenMfaSettings, + callback, + userAgentValue + ) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback(new Error('User is not authenticated'), null); + } + + this.client.request( + 'SetUserMFAPreference', + { + SMSMfaSettings: smsMfaSettings, + SoftwareTokenMfaSettings: softwareTokenMfaSettings, + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + }, + err => { + if (err) { + return callback(err, null); + } + return callback(null, 'SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used by an authenticated user to disable MFA for itself + * @deprecated + * @param {nodeCallback} callback Called on success or error. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + disableMFA(callback, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback(new Error('User is not authenticated'), null); + } + + const mfaOptions = []; + + this.client.request( + 'SetUserSettings', + { + MFAOptions: mfaOptions, + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + }, + err => { + if (err) { + return callback(err, null); + } + return callback(null, 'SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used by an authenticated user to delete itself + * @param {nodeCallback} callback Called on success or error. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + deleteUser(callback, clientMetadata, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback(new Error('User is not authenticated'), null); + } + + this.client.request( + 'DeleteUser', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + ClientMetadata: clientMetadata, + }, + err => { + if (err) { + return callback(err, null); + } + this.clearCachedUser(); + return callback(null, 'SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * @typedef {CognitoUserAttribute | { Name:string, Value:string }} AttributeArg + */ + /** + * This is used by an authenticated user to change a list of attributes + * @param {AttributeArg[]} attributes A list of the new user attributes. + * @param {nodeCallback} callback Called on success or error. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + updateAttributes(attributes, callback, clientMetadata, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback(new Error('User is not authenticated'), null); + } + + this.client.request( + 'UpdateUserAttributes', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + UserAttributes: attributes, + ClientMetadata: clientMetadata, + }, + (err, result) => { + if (err) { + return callback(err, null); + } + + // update cached user + return this.getUserData( + () => callback(null, 'SUCCESS', result), + { + bypassCache: true, + }, + userAgentValue + ); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used by an authenticated user to get a list of attributes + * @param {nodeCallback} callback Called on success or error. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + getUserAttributes(callback, userAgentValue) { + if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { + return callback(new Error('User is not authenticated'), null); + } + + this.client.request( + 'GetUser', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + }, + (err, userData) => { + if (err) { + return callback(err, null); + } + + const attributeList = []; + + for (let i = 0; i < userData.UserAttributes.length; i++) { + const attribute = { + Name: userData.UserAttributes[i].Name, + Value: userData.UserAttributes[i].Value, + }; + const userAttribute = new CognitoUserAttribute(attribute); + attributeList.push(userAttribute); + } + + return callback(null, attributeList); + }, + userAgentValue + ); + return undefined; + } + + /** + * This was previously used by an authenticated user to get MFAOptions, + * but no longer returns a meaningful response. Refer to the documentation for + * how to setup and use MFA: https://docs.amplify.aws/lib/auth/mfa/q/platform/js + * @deprecated + * @param {nodeCallback} callback Called on success or error. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + getMFAOptions(callback, userAgentValue) { + if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { + return callback(new Error('User is not authenticated'), null); + } + + this.client.request( + 'GetUser', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + }, + (err, userData) => { + if (err) { + return callback(err, null); + } + + return callback(null, userData.MFAOptions); + }, + userAgentValue + ); + return undefined; + } + + /** + * PRIVATE ONLY: This is an internal only method and should not + * be directly called by the consumers. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {Promise} + */ + createGetUserRequest(userAgentValue) { + return this.client.promisifyRequest( + 'GetUser', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + }, + userAgentValue + ); + } + + /** + * PRIVATE ONLY: This is an internal only method and should not + * be directly called by the consumers. + * @param {object} options + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {Promise} + */ + refreshSessionIfPossible(options = {}, userAgentValue) { + // best effort, if not possible + return new Promise(resolve => { + const refresh = this.signInUserSession.getRefreshToken(); + if (refresh && refresh.getToken()) { + this.refreshSession( + refresh, + resolve, + options.clientMetadata, + userAgentValue + ); + } else { + resolve(); + } + }); + } + + /** + * @typedef {Object} GetUserDataOptions + * @property {boolean} bypassCache - force getting data from Cognito service + * @property {Record} clientMetadata - clientMetadata for getSession + */ + + /** + * This is used by an authenticated users to get the userData + * @param {nodeCallback} callback Called on success or error. + * @param {GetUserDataOptions} params + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + getUserData(callback, params, userAgentValue) { + if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { + this.clearCachedUserData(); + return callback(new Error('User is not authenticated'), null); + } + + const userData = this.getUserDataFromCache(); + + if (!userData) { + this.fetchUserData() + .then(data => { + callback(null, data); + }) + .catch(callback); + return; + } + + if (this.isFetchUserDataAndTokenRequired(params)) { + this.fetchUserData() + .then(data => { + return this.refreshSessionIfPossible(params, userAgentValue).then( + () => data + ); + }) + .then(data => callback(null, data)) + .catch(callback); + return; + } + + try { + callback(null, JSON.parse(userData)); + return; + } catch (err) { + this.clearCachedUserData(); + callback(err, null); + return; + } + } + + /** + * + * PRIVATE ONLY: This is an internal only method and should not + * be directly called by the consumers. + */ + getUserDataFromCache() { + const userData = this.storage.getItem(this.userDataKey); + + return userData; + } + + /** + * + * PRIVATE ONLY: This is an internal only method and should not + * be directly called by the consumers. + */ + isFetchUserDataAndTokenRequired(params) { + const { bypassCache = false } = params || {}; + + return bypassCache; + } + /** + * + * PRIVATE ONLY: This is an internal only method and should not + * be directly called by the consumers. + * @param {string} userAgentValue Optional string containing custom user agent value + */ + fetchUserData(userAgentValue) { + return this.createGetUserRequest().then(data => { + this.cacheUserData(data); + return data; + }, userAgentValue); + } + + /** + * This is used by an authenticated user to delete a list of attributes + * @param {string[]} attributeList Names of the attributes to delete. + * @param {nodeCallback} callback Called on success or error. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + deleteAttributes(attributeList, callback, userAgentValue) { + if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { + return callback(new Error('User is not authenticated'), null); + } + + this.client.request( + 'DeleteUserAttributes', + { + UserAttributeNames: attributeList, + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + }, + err => { + if (err) { + return callback(err, null); + } + + // update cached user + return this.getUserData( + () => callback(null, 'SUCCESS'), + { + bypassCache: true, + }, + userAgentValue + ); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used by a user to resend a confirmation code + * @param {nodeCallback} callback Called on success or error. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + resendConfirmationCode(callback, clientMetadata, userAgentValue) { + const jsonReq = { + ClientId: this.pool.getClientId(), + Username: this.username, + ClientMetadata: clientMetadata, + }; + + this.client.request( + 'ResendConfirmationCode', + jsonReq, + (err, result) => { + if (err) { + return callback(err, null); + } + return callback(null, result); + }, + userAgentValue + ); + } + + /** + * @typedef {Object} GetSessionOptions + * @property {Record} clientMetadata - clientMetadata for getSession + */ + + /** + * This is used to get a session, either from the session object + * or from the local storage, or by using a refresh token + * + * @param {nodeCallback} callback Called on success or error. + * @param {GetSessionOptions} options + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + getSession(callback, options = {}, userAgentValue) { + if (this.username == null) { + return callback( + new Error('Username is null. Cannot retrieve a new session'), + null + ); + } + + if (this.signInUserSession != null && this.signInUserSession.isValid()) { + return callback(null, this.signInUserSession); + } + + const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}.${ + this.username + }`; + const idTokenKey = `${keyPrefix}.idToken`; + const accessTokenKey = `${keyPrefix}.accessToken`; + const refreshTokenKey = `${keyPrefix}.refreshToken`; + const clockDriftKey = `${keyPrefix}.clockDrift`; + + if (this.storage.getItem(idTokenKey)) { + const idToken = new CognitoIdToken({ + IdToken: this.storage.getItem(idTokenKey), + }); + const accessToken = new CognitoAccessToken({ + AccessToken: this.storage.getItem(accessTokenKey), + }); + const refreshToken = new CognitoRefreshToken({ + RefreshToken: this.storage.getItem(refreshTokenKey), + }); + const clockDrift = parseInt(this.storage.getItem(clockDriftKey), 0) || 0; + + const sessionData = { + IdToken: idToken, + AccessToken: accessToken, + RefreshToken: refreshToken, + ClockDrift: clockDrift, + }; + const cachedSession = new CognitoUserSession(sessionData); + + if (cachedSession.isValid()) { + this.signInUserSession = cachedSession; + return callback(null, this.signInUserSession); + } + + if (!refreshToken.getToken()) { + return callback( + new Error('Cannot retrieve a new session. Please authenticate.'), + null + ); + } + + this.refreshSession( + refreshToken, + callback, + options.clientMetadata, + userAgentValue + ); + } else { + callback( + new Error('Local storage is missing an ID Token, Please authenticate'), + null + ); + } + + return undefined; + } + + /** + * This uses the refreshToken to retrieve a new session + * @param {CognitoRefreshToken} refreshToken A previous session's refresh token. + * @param {nodeCallback} callback Called on success or error. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + refreshSession(refreshToken, callback, clientMetadata, userAgentValue) { + const wrappedCallback = this.pool.wrapRefreshSessionCallback + ? this.pool.wrapRefreshSessionCallback(callback) + : callback; + const authParameters = {}; + authParameters.REFRESH_TOKEN = refreshToken.getToken(); + const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}`; + const lastUserKey = `${keyPrefix}.LastAuthUser`; + + if (this.storage.getItem(lastUserKey)) { + this.username = this.storage.getItem(lastUserKey); + const deviceKeyKey = `${keyPrefix}.${this.username}.deviceKey`; + this.deviceKey = this.storage.getItem(deviceKeyKey); + authParameters.DEVICE_KEY = this.deviceKey; + } + + const jsonReq = { + ClientId: this.pool.getClientId(), + AuthFlow: 'REFRESH_TOKEN_AUTH', + AuthParameters: authParameters, + ClientMetadata: clientMetadata, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + this.client.request( + 'InitiateAuth', + jsonReq, + (err, authResult) => { + if (err) { + if (err.code === 'NotAuthorizedException') { + this.clearCachedUser(); + } + return wrappedCallback(err, null); + } + if (authResult) { + const authenticationResult = authResult.AuthenticationResult; + if ( + !Object.prototype.hasOwnProperty.call( + authenticationResult, + 'RefreshToken' + ) + ) { + authenticationResult.RefreshToken = refreshToken.getToken(); + } + this.signInUserSession = + this.getCognitoUserSession(authenticationResult); + this.cacheTokens(); + return wrappedCallback(null, this.signInUserSession); + } + return undefined; + }, + userAgentValue + ); + } + + /** + * This is used to save the session tokens to local storage + * @returns {void} + */ + cacheTokens() { + const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}`; + const idTokenKey = `${keyPrefix}.${this.username}.idToken`; + const accessTokenKey = `${keyPrefix}.${this.username}.accessToken`; + const refreshTokenKey = `${keyPrefix}.${this.username}.refreshToken`; + const clockDriftKey = `${keyPrefix}.${this.username}.clockDrift`; + const lastUserKey = `${keyPrefix}.LastAuthUser`; + + this.storage.setItem( + idTokenKey, + this.signInUserSession.getIdToken().getJwtToken() + ); + this.storage.setItem( + accessTokenKey, + this.signInUserSession.getAccessToken().getJwtToken() + ); + this.storage.setItem( + refreshTokenKey, + this.signInUserSession.getRefreshToken().getToken() + ); + this.storage.setItem( + clockDriftKey, + `${this.signInUserSession.getClockDrift()}` + ); + this.storage.setItem(lastUserKey, this.username); + } + + /** + * This is to cache user data + */ + cacheUserData(userData) { + this.storage.setItem(this.userDataKey, JSON.stringify(userData)); + } + + /** + * This is to remove cached user data + */ + clearCachedUserData() { + this.storage.removeItem(this.userDataKey); + } + + clearCachedUser() { + this.clearCachedTokens(); + this.clearCachedUserData(); + } + + /** + * This is used to cache the device key and device group and device password + * @returns {void} + */ + cacheDeviceKeyAndPassword() { + const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}.${ + this.username + }`; + const deviceKeyKey = `${keyPrefix}.deviceKey`; + const randomPasswordKey = `${keyPrefix}.randomPasswordKey`; + const deviceGroupKeyKey = `${keyPrefix}.deviceGroupKey`; + + this.storage.setItem(deviceKeyKey, this.deviceKey); + this.storage.setItem(randomPasswordKey, this.randomPassword); + this.storage.setItem(deviceGroupKeyKey, this.deviceGroupKey); + } + + /** + * This is used to get current device key and device group and device password + * @returns {void} + */ + getCachedDeviceKeyAndPassword() { + const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}.${ + this.username + }`; + const deviceKeyKey = `${keyPrefix}.deviceKey`; + const randomPasswordKey = `${keyPrefix}.randomPasswordKey`; + const deviceGroupKeyKey = `${keyPrefix}.deviceGroupKey`; + + if (this.storage.getItem(deviceKeyKey)) { + this.deviceKey = this.storage.getItem(deviceKeyKey); + this.randomPassword = this.storage.getItem(randomPasswordKey); + this.deviceGroupKey = this.storage.getItem(deviceGroupKeyKey); + } + } + + /** + * This is used to clear the device key info from local storage + * @returns {void} + */ + clearCachedDeviceKeyAndPassword() { + const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}.${ + this.username + }`; + const deviceKeyKey = `${keyPrefix}.deviceKey`; + const randomPasswordKey = `${keyPrefix}.randomPasswordKey`; + const deviceGroupKeyKey = `${keyPrefix}.deviceGroupKey`; + + this.storage.removeItem(deviceKeyKey); + this.storage.removeItem(randomPasswordKey); + this.storage.removeItem(deviceGroupKeyKey); + } + + /** + * This is used to clear the session tokens from local storage + * @returns {void} + */ + clearCachedTokens() { + const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}`; + const idTokenKey = `${keyPrefix}.${this.username}.idToken`; + const accessTokenKey = `${keyPrefix}.${this.username}.accessToken`; + const refreshTokenKey = `${keyPrefix}.${this.username}.refreshToken`; + const lastUserKey = `${keyPrefix}.LastAuthUser`; + const clockDriftKey = `${keyPrefix}.${this.username}.clockDrift`; + + this.storage.removeItem(idTokenKey); + this.storage.removeItem(accessTokenKey); + this.storage.removeItem(refreshTokenKey); + this.storage.removeItem(lastUserKey); + this.storage.removeItem(clockDriftKey); + } + + /** + * This is used to build a user session from tokens retrieved in the authentication result + * @param {object} authResult Successful auth response from server. + * @returns {CognitoUserSession} The new user session. + * @private + */ + getCognitoUserSession(authResult) { + const idToken = new CognitoIdToken(authResult); + const accessToken = new CognitoAccessToken(authResult); + const refreshToken = new CognitoRefreshToken(authResult); + + const sessionData = { + IdToken: idToken, + AccessToken: accessToken, + RefreshToken: refreshToken, + }; + + return new CognitoUserSession(sessionData); + } + + /** + * This is used to initiate a forgot password request + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {inputVerificationCode?} callback.inputVerificationCode + * Optional callback raised instead of onSuccess with response data. + * @param {onSuccess} callback.onSuccess Called on success. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + forgotPassword(callback, clientMetadata, userAgentValue) { + const jsonReq = { + ClientId: this.pool.getClientId(), + Username: this.username, + ClientMetadata: clientMetadata, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + this.client.request( + 'ForgotPassword', + jsonReq, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + if (typeof callback.inputVerificationCode === 'function') { + return callback.inputVerificationCode(data); + } + return callback.onSuccess(data); + }, + userAgentValue + ); + } + + /** + * This is used to confirm a new password using a confirmationCode + * @param {string} confirmationCode Code entered by user. + * @param {string} newPassword Confirm new password. + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {onSuccess} callback.onSuccess Called on success. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + confirmPassword( + confirmationCode, + newPassword, + callback, + clientMetadata, + userAgentValue + ) { + const jsonReq = { + ClientId: this.pool.getClientId(), + Username: this.username, + ConfirmationCode: confirmationCode, + Password: newPassword, + ClientMetadata: clientMetadata, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + this.client.request( + 'ConfirmForgotPassword', + jsonReq, + err => { + if (err) { + return callback.onFailure(err); + } + return callback.onSuccess('SUCCESS'); + }, + userAgentValue + ); + } + + /** + * This is used to initiate an attribute confirmation request + * @param {string} attributeName User attribute that needs confirmation. + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {inputVerificationCode} callback.inputVerificationCode Called on success. + * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + getAttributeVerificationCode( + attributeName, + callback, + clientMetadata, + userAgentValue + ) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback.onFailure(new Error('User is not authenticated')); + } + + this.client.request( + 'GetUserAttributeVerificationCode', + { + AttributeName: attributeName, + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + ClientMetadata: clientMetadata, + }, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + if (typeof callback.inputVerificationCode === 'function') { + return callback.inputVerificationCode(data); + } + return callback.onSuccess('SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used to confirm an attribute using a confirmation code + * @param {string} attributeName Attribute being confirmed. + * @param {string} confirmationCode Code entered by user. + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {onSuccess} callback.onSuccess Called on success. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + verifyAttribute(attributeName, confirmationCode, callback, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback.onFailure(new Error('User is not authenticated')); + } + + this.client.request( + 'VerifyUserAttribute', + { + AttributeName: attributeName, + Code: confirmationCode, + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + }, + err => { + if (err) { + return callback.onFailure(err); + } + return callback.onSuccess('SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used to get the device information using the current device key + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {onSuccess<*>} callback.onSuccess Called on success with device data. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + getDevice(callback, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback.onFailure(new Error('User is not authenticated')); + } + + this.client.request( + 'GetDevice', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + DeviceKey: this.deviceKey, + }, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + return callback.onSuccess(data); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used to forget a specific device + * @param {string} deviceKey Device key. + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {onSuccess} callback.onSuccess Called on success. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + forgetSpecificDevice(deviceKey, callback, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback.onFailure(new Error('User is not authenticated')); + } + + this.client.request( + 'ForgetDevice', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + DeviceKey: deviceKey, + }, + err => { + if (err) { + return callback.onFailure(err); + } + return callback.onSuccess('SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used to forget the current device + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {onSuccess} callback.onSuccess Called on success. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + forgetDevice(callback, userAgentValue) { + this.forgetSpecificDevice( + this.deviceKey, + { + onFailure: callback.onFailure, + onSuccess: result => { + this.deviceKey = null; + this.deviceGroupKey = null; + this.randomPassword = null; + this.clearCachedDeviceKeyAndPassword(); + return callback.onSuccess(result); + }, + }, + userAgentValue + ); + } + + /** + * This is used to set the device status as remembered + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {onSuccess} callback.onSuccess Called on success. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + setDeviceStatusRemembered(callback, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback.onFailure(new Error('User is not authenticated')); + } + + this.client.request( + 'UpdateDeviceStatus', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + DeviceKey: this.deviceKey, + DeviceRememberedStatus: 'remembered', + }, + err => { + if (err) { + return callback.onFailure(err); + } + return callback.onSuccess('SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used to set the device status as not remembered + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {onSuccess} callback.onSuccess Called on success. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + setDeviceStatusNotRemembered(callback, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback.onFailure(new Error('User is not authenticated')); + } + + this.client.request( + 'UpdateDeviceStatus', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + DeviceKey: this.deviceKey, + DeviceRememberedStatus: 'not_remembered', + }, + err => { + if (err) { + return callback.onFailure(err); + } + return callback.onSuccess('SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used to list all devices for a user + * + * @param {int} limit the number of devices returned in a call + * @param {string | null} paginationToken the pagination token in case any was returned before + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {onSuccess<*>} callback.onSuccess Called on success with device list. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + listDevices(limit, paginationToken, callback, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback.onFailure(new Error('User is not authenticated')); + } + const requestParams = { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + Limit: limit, + }; + + if (paginationToken) { + requestParams.PaginationToken = paginationToken; + } + + this.client.request( + 'ListDevices', + requestParams, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + return callback.onSuccess(data); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used to globally revoke all tokens issued to a user + * @param {object} callback Result callback map. + * @param {onFailure} callback.onFailure Called on any error. + * @param {onSuccess} callback.onSuccess Called on success. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + globalSignOut(callback, userAgentValue) { + if (this.signInUserSession == null || !this.signInUserSession.isValid()) { + return callback.onFailure(new Error('User is not authenticated')); + } + + this.client.request( + 'GlobalSignOut', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + }, + err => { + if (err) { + return callback.onFailure(err); + } + this.clearCachedUser(); + return callback.onSuccess('SUCCESS'); + }, + userAgentValue + ); + return undefined; + } + + /** + * This is used for the user to signOut of the application and clear the cached tokens. + * @param revokeTokenCallback + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + signOut(revokeTokenCallback, userAgentValue) { + // If tokens won't be revoked, we just clean the client data. + if (!revokeTokenCallback || typeof revokeTokenCallback !== 'function') { + this.cleanClientData(); + + return; + } + + this.getSession((error, _session) => { + if (error) { + return revokeTokenCallback(error); + } + + this.revokeTokens(err => { + this.cleanClientData(); + + revokeTokenCallback(err); + }, userAgentValue); + }); + } + + revokeTokens(revokeTokenCallback = () => {}, userAgentValue) { + if (typeof revokeTokenCallback !== 'function') { + throw new Error('Invalid revokeTokenCallback. It should be a function.'); + } + + const tokensToBeRevoked = []; + + if (!this.signInUserSession) { + const error = new Error('User is not authenticated'); + + return revokeTokenCallback(error); + } + + if (!this.signInUserSession.getAccessToken()) { + const error = new Error('No Access token available'); + + return revokeTokenCallback(error); + } + + const refreshToken = this.signInUserSession.getRefreshToken().getToken(); + const accessToken = this.signInUserSession.getAccessToken(); + + if (this.isSessionRevocable(accessToken)) { + if (refreshToken) { + return this.revokeToken( + { + token: refreshToken, + callback: revokeTokenCallback, + }, + userAgentValue + ); + } + } + revokeTokenCallback(); + } + + isSessionRevocable(token) { + if (token && typeof token.decodePayload === 'function') { + try { + const { origin_jti } = token.decodePayload(); + return !!origin_jti; + } catch (err) { + // Nothing to do, token doesnt have origin_jti claim + } + } + + return false; + } + + cleanClientData() { + this.signInUserSession = null; + this.clearCachedUser(); + } + + revokeToken({ token, callback }, userAgentValue) { + this.client.requestWithRetry( + 'RevokeToken', + { + Token: token, + ClientId: this.pool.getClientId(), + }, + err => { + if (err) { + return callback(err); + } + + callback(); + }, + userAgentValue + ); + } + + /** + * This is used by a user trying to select a given MFA + * @param {string} answerChallenge the mfa the user wants + * @param {nodeCallback} callback Called on success or error. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + sendMFASelectionAnswer(answerChallenge, callback, userAgentValue) { + const challengeResponses = {}; + challengeResponses.USERNAME = this.username; + challengeResponses.ANSWER = answerChallenge; + + const jsonReq = { + ChallengeName: 'SELECT_MFA_TYPE', + ChallengeResponses: challengeResponses, + ClientId: this.pool.getClientId(), + Session: this.Session, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + this.client.request( + 'RespondToAuthChallenge', + jsonReq, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + this.Session = data.Session; + if (answerChallenge === 'SMS_MFA') { + return callback.mfaRequired( + data.ChallengeName, + data.ChallengeParameters + ); + } + if (answerChallenge === 'SOFTWARE_TOKEN_MFA') { + return callback.totpRequired( + data.ChallengeName, + data.ChallengeParameters + ); + } + return undefined; + }, + userAgentValue + ); + } + + /** + * This returns the user context data for advanced security feature. + * @returns {string} the user context data from CognitoUserPool + */ + getUserContextData() { + const pool = this.pool; + return pool.getUserContextData(this.username); + } + + /** + * This is used by an authenticated or a user trying to authenticate to associate a TOTP MFA + * @param {nodeCallback} callback Called on success or error. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + associateSoftwareToken(callback, userAgentValue) { + if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { + this.client.request( + 'AssociateSoftwareToken', + { + Session: this.Session, + }, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + this.Session = data.Session; + return callback.associateSecretCode(data.SecretCode); + }, + userAgentValue + ); + } else { + this.client.request( + 'AssociateSoftwareToken', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + }, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + return callback.associateSecretCode(data.SecretCode); + }, + userAgentValue + ); + } + } + + /** + * This is used by an authenticated or a user trying to authenticate to verify a TOTP MFA + * @param {string} totpCode The MFA code entered by the user. + * @param {string} friendlyDeviceName The device name we are assigning to the device. + * @param {nodeCallback} callback Called on success or error. + * @param {string} userAgentValue Optional string containing custom user agent value + * @returns {void} + */ + verifySoftwareToken(totpCode, friendlyDeviceName, callback, userAgentValue) { + if (!(this.signInUserSession != null && this.signInUserSession.isValid())) { + this.client.request( + 'VerifySoftwareToken', + { + Session: this.Session, + UserCode: totpCode, + FriendlyDeviceName: friendlyDeviceName, + }, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + this.Session = data.Session; + const challengeResponses = {}; + challengeResponses.USERNAME = this.username; + const jsonReq = { + ChallengeName: 'MFA_SETUP', + ClientId: this.pool.getClientId(), + ChallengeResponses: challengeResponses, + Session: this.Session, + }; + if (this.getUserContextData()) { + jsonReq.UserContextData = this.getUserContextData(); + } + this.client.request( + 'RespondToAuthChallenge', + jsonReq, + (errRespond, dataRespond) => { + if (errRespond) { + return callback.onFailure(errRespond); + } + this.signInUserSession = this.getCognitoUserSession( + dataRespond.AuthenticationResult + ); + this.cacheTokens(); + return callback.onSuccess(this.signInUserSession); + }, + userAgentValue + ); + return undefined; + }, + userAgentValue + ); + } else { + this.client.request( + 'VerifySoftwareToken', + { + AccessToken: this.signInUserSession.getAccessToken().getJwtToken(), + UserCode: totpCode, + FriendlyDeviceName: friendlyDeviceName, + }, + (err, data) => { + if (err) { + return callback.onFailure(err); + } + return callback.onSuccess(data); + }, + userAgentValue + ); + } + } +} diff --git a/packages/amazon-cognito-identity-js/src/internals/index.js b/packages/amazon-cognito-identity-js/src/internals/index.js index 42084ebe8da..5de97fc3cc3 100644 --- a/packages/amazon-cognito-identity-js/src/internals/index.js +++ b/packages/amazon-cognito-identity-js/src/internals/index.js @@ -2,3 +2,5 @@ export { addAuthCategoryToCognitoUserAgent, addFrameworkToCognitoUserAgent, } from '../UserAgent'; + +export { InternalCognitoUser } from './InternalCognitoUser'; diff --git a/packages/api-graphql/package.json b/packages/api-graphql/package.json index b8c8ad8b43b..19702c757e3 100644 --- a/packages/api-graphql/package.json +++ b/packages/api-graphql/package.json @@ -65,7 +65,7 @@ "name": "API (GraphQL client)", "path": "./lib-esm/index.js", "import": "{ Amplify, GraphQLAPI }", - "limit": "88.35 kB" + "limit": "88.92 kB" } ], "jest": { diff --git a/packages/api/package.json b/packages/api/package.json index 7a7ada7de39..72958f27512 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -67,7 +67,7 @@ "name": "API (top-level class)", "path": "./lib-esm/index.js", "import": "{ Amplify, API }", - "limit": "89.07 kB" + "limit": "89.68 kB" } ], "jest": { diff --git a/packages/auth/__tests__/auth-unit-test.ts b/packages/auth/__tests__/auth-unit-test.ts index ecafed17013..3a03f0f98a3 100644 --- a/packages/auth/__tests__/auth-unit-test.ts +++ b/packages/auth/__tests__/auth-unit-test.ts @@ -1407,38 +1407,38 @@ describe('auth unit test', () => { spyon.mockClear(); }); - test('currentUserPoolUser fails but hub event still dispatches', async () => { - const auth = new Auth(authOptions); - const spyon = jest - .spyOn(CognitoUser.prototype, 'sendMFACode') - .mockImplementationOnce((code, callback) => { - callback.onSuccess(session); - }); - - const spyon2 = jest - .spyOn(auth, 'currentUserPoolUser') - .mockImplementationOnce(() => { - return Promise.reject('Could not get current user.'); - }); - const hubSpy = jest.spyOn(Hub, 'dispatch'); - const user = new CognitoUser({ - Username: 'username', - Pool: userPool, - }); - const result = await auth.confirmSignIn(user, 'code', null); - expect(result).toEqual(user); - expect(hubSpy).toHaveBeenCalledWith( - 'auth', - { - data: user, - event: 'signIn', - message: 'A user username has been signed in', - }, - 'Auth', - Symbol.for('amplify_default') - ); - spyon.mockClear(); - }); + test('currentUserPoolUser fails but hub event still dispatches', async () => { + const auth = new Auth(authOptions); + const spyon = jest + .spyOn(CognitoUser.prototype, 'sendMFACode') + .mockImplementationOnce((code, callback) => { + callback.onSuccess(session); + }); + + const spyon2 = jest + .spyOn(auth, 'currentUserPoolUser') + .mockImplementationOnce(() => { + return Promise.reject('Could not get current user.'); + }); + const hubSpy = jest.spyOn(Hub, 'dispatch'); + const user = new CognitoUser({ + Username: 'username', + Pool: userPool, + }); + const result = await auth.confirmSignIn(user, 'code', null); + expect(result).toEqual(user); + expect(hubSpy).toHaveBeenCalledWith( + 'auth', + { + data: user, + event: 'signIn', + message: 'A user username has been signed in', + }, + 'Auth', + Symbol.for('amplify_default') + ); + spyon.mockClear(); + }); test('onFailure', async () => { const spyon = jest @@ -3058,12 +3058,13 @@ describe('auth unit test', () => { spyon.mockClear(); }); - test('error hub event', async (done) => { + test('error hub event', async done => { expect.assertions(3); - const spyon = jest.spyOn(CognitoUser.prototype, 'updateAttributes') + const spyon = jest + .spyOn(CognitoUser.prototype, 'updateAttributes') .mockImplementationOnce((attrs, callback: any) => { callback(new Error('Error'), null, null); - }); + }); const auth = new Auth(authOptions); @@ -3097,19 +3098,20 @@ describe('auth unit test', () => { spyon.mockClear(); }); - test('happy case code delivery details hub event', async (done) => { + test('happy case code delivery details hub event', async done => { expect.assertions(2); - + const codeDeliverDetailsResult: any = { - 'CodeDeliveryDetailsList': [ - { - 'AttributeName': 'email', - 'DeliveryMedium': 'EMAIL', - 'Destination': 'e***@e***' - } - ] + CodeDeliveryDetailsList: [ + { + AttributeName: 'email', + DeliveryMedium: 'EMAIL', + Destination: 'e***@e***', + }, + ], }; - const spyon = jest.spyOn(CognitoUser.prototype, 'updateAttributes') + const spyon = jest + .spyOn(CognitoUser.prototype, 'updateAttributes') .mockImplementationOnce((attrs, callback: any) => { callback(null, 'SUCCESS', codeDeliverDetailsResult); }); @@ -3126,20 +3128,20 @@ describe('auth unit test', () => { sub: 'sub', }; const payloadData = { - 'email': { + email: { isUpdated: false, codeDeliveryDetails: { AttributeName: 'email', DeliveryMedium: 'EMAIL', - Destination: 'e***@e***' - } + Destination: 'e***@e***', + }, }, - 'phone_number': { - isUpdated: true + phone_number: { + isUpdated: true, + }, + sub: { + isUpdated: true, }, - 'sub': { - isUpdated: true - } }; const listenToHub = Hub.listen('auth', ({ payload }) => { const { event } = payload; diff --git a/packages/auth/package.json b/packages/auth/package.json index dc445c5e39c..3d4d401cff6 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -59,7 +59,7 @@ "name": "Auth (top-level class)", "path": "./lib-esm/index.js", "import": "{ Amplify, Auth }", - "limit": "55.15 kB" + "limit": "55.73 kB" } ], "jest": { diff --git a/packages/datastore/package.json b/packages/datastore/package.json index 38f08cd9ef8..4c79866400c 100644 --- a/packages/datastore/package.json +++ b/packages/datastore/package.json @@ -72,7 +72,7 @@ "name": "DataStore (top-level class)", "path": "./lib-esm/index.js", "import": "{ Amplify, DataStore }", - "limit": "137.1 kB" + "limit": "137.68 kB" } ], "jest": { diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 112b1312972..0b66e569f97 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -65,13 +65,13 @@ "name": "PubSub (IoT provider)", "path": "./lib-esm/index.js", "import": "{ Amplify, PubSub, AWSIoTProvider }", - "limit": "80.2 kB" + "limit": "80.81 kB" }, { "name": "PubSub (Mqtt provider)", "path": "./lib-esm/index.js", "import": "{ Amplify, PubSub, MqttOverWSProvider }", - "limit": "80.1 kB" + "limit": "80.68 kB" } ], "jest": {