UNPKG

amazon-cognito-identity-js

Version:

Amazon Cognito Identity Provider JavaScript SDK

1,655 lines (1,475 loc) • 65.6 kB
/*! * 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 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; } /** * 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. * @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); }); } /** * 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. * @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; } /** * 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.<attribute_name>. * @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 * @returns {void} */ completeNewPasswordChallenge( newPassword, requiredAttributeData, 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 ); } ); 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 * @returns {void} * @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 }); } /** * 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<string>} callback Called on success or error. * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger * @returns {void} */ confirmRegistration( confirmationCode, forceAliasCreation, 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'); }); } /** * 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 * @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 ); }); } /** * 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 * @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; } ); } /** * 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<string>} callback Called on success or error. * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger * @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'); } ); return undefined; } /** * This is used by an authenticated user to enable MFA for itself * @deprecated * @param {nodeCallback<string>} callback Called on success or error. * @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; } /** * 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<string>} callback Called on success or error. * @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'); } ); return undefined; } /** * This is used by an authenticated user to disable MFA for itself * @deprecated * @param {nodeCallback<string>} callback Called on success or error. * @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; } /** * This is used by an authenticated user to delete itself * @param {nodeCallback<string>} callback Called on success or error. * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger * @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; } /** * @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<string>} callback Called on success or error. * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger * @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; } /** * This is used by an authenticated user to get a list of attributes * @param {nodeCallback<CognitoUserAttribute[]>} callback Called on success or error. * @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; } /** * 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<MFAOptions>} callback Called on success or error. * @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(); } }); } /** * @typedef {Object} GetUserDataOptions * @property {boolean} bypassCache - force getting data from Cognito service * @property {Record<string, string>} clientMetadata - clientMetadata for getSession */ /** * This is used by an authenticated users to get the userData * @param {nodeCallback<UserData>} callback Called on success or error. * @param {GetUserDataOptions} params * @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; }); } /** * This is used by an authenticated user to delete a list of attributes * @param {string[]} attributeList Names of the attributes to delete. * @param {nodeCallback<string>} callback Called on success or error. * @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; } /** * This is used by a user to resend a confirmation code * @param {nodeCallback<string>} callback Called on success or error. * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger * @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); }); } /** * @typedef {Object} GetSessionOptions * @property {Record<string, string>} 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<CognitoUserSession>} callback Called on success or error. * @param {GetSessionOptions} options * @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; } /** * This uses the refreshToken to retrieve a new session * @param {CognitoRefreshToken} refreshToken A previous session's refresh token. * @param {nodeCallback<CognitoUserSession>} callback Called on success or error. * @param {ClientMetadata} clientMetadata object which is passed from client to Cognito Lambda trigger * @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); } /** * 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 succe