UNPKG

@aws-amplify/auth

Version:
1,868 lines (1,741 loc) • 74.4 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { AuthOptions, FederatedResponse, SignUpParams, FederatedUser, ConfirmSignUpOptions, SignOutOpts, CurrentUserOpts, GetPreferredMFAOpts, SignInOpts, isUsernamePasswordOpts, isCognitoHostedOpts, isFederatedSignInOptions, isFederatedSignInOptionsCustom, hasCustomState, FederatedSignInOptionsCustom, LegacyProvider, FederatedSignInOptions, AwsCognitoOAuthOpts, ClientMetaData, } from './types'; import { Amplify, ConsoleLogger as Logger, Credentials, Hub, StorageHelper, ICredentials, browserOrNode, parseAWSExports, UniversalStorage, urlSafeDecode, HubCallback, } from '@aws-amplify/core'; import { CookieStorage, CognitoUserPool, AuthenticationDetails, ICognitoUserPoolData, ICognitoUserData, ISignUpResult, CognitoUser, MFAOption, CognitoUserSession, IAuthenticationCallback, ICognitoUserAttributeData, CognitoUserAttribute, CognitoIdToken, CognitoRefreshToken, CognitoAccessToken, NodeCallback, CodeDeliveryDetails, } from 'amazon-cognito-identity-js'; import { parse } from 'url'; import OAuth from './OAuth/OAuth'; import { default as urlListener } from './urlListener'; import { AuthError, NoUserPoolError } from './Errors'; import { AuthErrorTypes, AutoSignInOptions, CognitoHostedUIIdentityProvider, IAuthDevice, } from './types/Auth'; const logger = new Logger('AuthClass'); const USER_ADMIN_SCOPE = 'aws.cognito.signin.user.admin'; // 10 sec, following this guide https://www.nngroup.com/articles/response-times-3-important-limits/ const OAUTH_FLOW_MS_TIMEOUT = 10 * 1000; const AMPLIFY_SYMBOL = ( typeof Symbol !== 'undefined' && typeof Symbol.for === 'function' ? Symbol.for('amplify_default') : '@@amplify_default' ) as Symbol; const dispatchAuthEvent = (event: string, data: any, message: string) => { Hub.dispatch('auth', { event, data, message }, 'Auth', AMPLIFY_SYMBOL); }; // Cognito Documentation for max device // tslint:disable-next-line:max-line-length // https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListDevices.html#API_ListDevices_RequestSyntax const MAX_DEVICES = 60; const MAX_AUTOSIGNIN_POLLING_MS = 3 * 60 * 1000; /** * Provide authentication steps */ export class AuthClass { private _config: AuthOptions; private userPool: CognitoUserPool = null; private user: any = null; private _oAuthHandler: OAuth; private _storage; private _storageSync; private oAuthFlowInProgress: boolean = false; private pendingSignIn: ReturnType<AuthClass['signInWithPassword']> | null; private autoSignInInitiated: boolean = false; private inflightSessionPromise: Promise<CognitoUserSession> | null = null; private inflightSessionPromiseCounter: number = 0; Credentials = Credentials; /** * Initialize Auth with AWS configurations * @param {Object} config - Configuration of the Auth */ constructor(config: AuthOptions) { this.configure(config); this.currentCredentials = this.currentCredentials.bind(this); this.currentUserCredentials = this.currentUserCredentials.bind(this); Hub.listen('auth', ({ payload }) => { const { event } = payload; switch (event) { case 'verify': case 'signIn': this._storage.setItem('amplify-signin-with-hostedUI', 'false'); break; case 'signOut': this._storage.removeItem('amplify-signin-with-hostedUI'); break; case 'cognitoHostedUI': this._storage.setItem('amplify-signin-with-hostedUI', 'true'); break; } }); } public getModuleName() { return 'Auth'; } configure(config?) { if (!config) return this._config || {}; logger.debug('configure Auth'); const conf = Object.assign( {}, this._config, parseAWSExports(config).Auth, config ); this._config = conf; const { userPoolId, userPoolWebClientId, cookieStorage, oauth, region, identityPoolId, mandatorySignIn, refreshHandlers, identityPoolRegion, clientMetadata, endpoint, storage, } = this._config; if (!storage) { // backward compatability if (cookieStorage) this._storage = new CookieStorage(cookieStorage); else { this._storage = config.ssr ? new UniversalStorage() : new StorageHelper().getStorage(); } } else { if (!this._isValidAuthStorage(storage)) { logger.error('The storage in the Auth config is not valid!'); throw new Error('Empty storage object'); } this._storage = storage; } this._storageSync = Promise.resolve(); if (typeof this._storage['sync'] === 'function') { this._storageSync = this._storage['sync'](); } if (userPoolId) { const userPoolData: ICognitoUserPoolData = { UserPoolId: userPoolId, ClientId: userPoolWebClientId, endpoint, }; userPoolData.Storage = this._storage; this.userPool = new CognitoUserPool( userPoolData, this.wrapRefreshSessionCallback ); } this.Credentials.configure({ mandatorySignIn, region, userPoolId, identityPoolId, refreshHandlers, storage: this._storage, identityPoolRegion, }); // initialize cognitoauth client if hosted ui options provided // to keep backward compatibility: const cognitoHostedUIConfig = oauth ? isCognitoHostedOpts(this._config.oauth) ? oauth : (<any>oauth).awsCognito : undefined; if (cognitoHostedUIConfig) { const cognitoAuthParams = Object.assign( { cognitoClientId: userPoolWebClientId, UserPoolId: userPoolId, domain: cognitoHostedUIConfig['domain'], scopes: cognitoHostedUIConfig['scope'], redirectSignIn: cognitoHostedUIConfig['redirectSignIn'], redirectSignOut: cognitoHostedUIConfig['redirectSignOut'], responseType: cognitoHostedUIConfig['responseType'], Storage: this._storage, urlOpener: cognitoHostedUIConfig['urlOpener'], clientMetadata, }, cognitoHostedUIConfig['options'] ); this._oAuthHandler = new OAuth({ scopes: cognitoAuthParams.scopes, config: cognitoAuthParams, cognitoClientId: cognitoAuthParams.cognitoClientId, }); // **NOTE** - Remove this in a future major release as it is a breaking change // Prevents _handleAuthResponse from being called multiple times in Expo // See https://github.com/aws-amplify/amplify-js/issues/4388 const usedResponseUrls = {}; urlListener(({ url }) => { if (usedResponseUrls[url]) { return; } usedResponseUrls[url] = true; this._handleAuthResponse(url); }); } dispatchAuthEvent( 'configured', null, `The Auth category has been configured successfully` ); if ( !this.autoSignInInitiated && typeof this._storage['getItem'] === 'function' ) { const pollingInitiated = this.isTrueStorageValue( 'amplify-polling-started' ); if (pollingInitiated) { dispatchAuthEvent( 'autoSignIn_failure', null, AuthErrorTypes.AutoSignInError ); this._storage.removeItem('amplify-auto-sign-in'); } this._storage.removeItem('amplify-polling-started'); } return this._config; } wrapRefreshSessionCallback = (callback: NodeCallback.Any) => { const wrapped: NodeCallback.Any = (error, data) => { if (data) { dispatchAuthEvent('tokenRefresh', undefined, `New token retrieved`); } else { dispatchAuthEvent( 'tokenRefresh_failure', error, `Failed to retrieve new token` ); } return callback(error, data); }; return wrapped; } // prettier-ignore /** * Sign up with username, password and other attributes like phone, email * @param {String | object} params - The user attributes used for signin * @param {String[]} restOfAttrs - for the backward compatability * @return - A promise resolves callback data if success */ public signUp( params: string | SignUpParams, ...restOfAttrs: string[] ): Promise<ISignUpResult> { if (!this.userPool) { return this.rejectNoUserPool(); } let username: string = null; let password: string = null; const attributes: CognitoUserAttribute[] = []; let validationData: CognitoUserAttribute[] = null; let clientMetadata; let autoSignIn: AutoSignInOptions = { enabled: false }; let autoSignInValidationData = {}; let autoSignInClientMetaData: ClientMetaData = {}; if (params && typeof params === 'string') { username = params; password = restOfAttrs ? restOfAttrs[0] : null; const email: string = restOfAttrs ? restOfAttrs[1] : null; const phone_number: string = restOfAttrs ? restOfAttrs[2] : null; if (email) attributes.push( new CognitoUserAttribute({ Name: 'email', Value: email }) ); if (phone_number) attributes.push( new CognitoUserAttribute({ Name: 'phone_number', Value: phone_number, }) ); } else if (params && typeof params === 'object') { username = params['username']; password = params['password']; if (params && params.clientMetadata) { clientMetadata = params.clientMetadata; } else if (this._config.clientMetadata) { clientMetadata = this._config.clientMetadata; } const attrs = params['attributes']; if (attrs) { Object.keys(attrs).map(key => { attributes.push( new CognitoUserAttribute({ Name: key, Value: attrs[key] }) ); }); } const validationDataObject = params['validationData']; if (validationDataObject) { validationData = []; Object.keys(validationDataObject).map(key => { validationData.push( new CognitoUserAttribute({ Name: key, Value: validationDataObject[key], }) ); }); } autoSignIn = params.autoSignIn ?? { enabled: false }; if (autoSignIn.enabled) { this._storage.setItem('amplify-auto-sign-in', 'true'); autoSignInValidationData = autoSignIn.validationData ?? {}; autoSignInClientMetaData = autoSignIn.clientMetaData ?? {}; } } else { return this.rejectAuthError(AuthErrorTypes.SignUpError); } if (!username) { return this.rejectAuthError(AuthErrorTypes.EmptyUsername); } if (!password) { return this.rejectAuthError(AuthErrorTypes.EmptyPassword); } logger.debug('signUp attrs:', attributes); logger.debug('signUp validation data:', validationData); return new Promise((resolve, reject) => { this.userPool.signUp( username, password, attributes, validationData, (err, data) => { if (err) { dispatchAuthEvent( 'signUp_failure', err, `${username} failed to signup` ); reject(err); } else { dispatchAuthEvent( 'signUp', data, `${username} has signed up successfully` ); if (autoSignIn.enabled) { this.handleAutoSignIn( username, password, autoSignInValidationData, autoSignInClientMetaData, data ); } resolve(data); } }, clientMetadata ); }); } private handleAutoSignIn( username: string, password: string, validationData: {}, clientMetadata: any, data: any ) { this.autoSignInInitiated = true; const authDetails = new AuthenticationDetails({ Username: username, Password: password, ValidationData: validationData, ClientMetadata: clientMetadata, }); if (data.userConfirmed) { this.signInAfterUserConfirmed(authDetails); } else if (this._config.signUpVerificationMethod === 'link') { this.handleLinkAutoSignIn(authDetails); } else { this.handleCodeAutoSignIn(authDetails); } } private handleCodeAutoSignIn(authDetails: AuthenticationDetails) { const listenEvent = ({ payload }) => { if (payload.event === 'confirmSignUp') { this.signInAfterUserConfirmed(authDetails, listenEvent); } }; Hub.listen('auth', listenEvent); } private handleLinkAutoSignIn(authDetails: AuthenticationDetails) { this._storage.setItem('amplify-polling-started', 'true'); const start = Date.now(); const autoSignInPollingIntervalId = setInterval(() => { if (Date.now() - start > MAX_AUTOSIGNIN_POLLING_MS) { clearInterval(autoSignInPollingIntervalId); dispatchAuthEvent( 'autoSignIn_failure', null, 'Please confirm your account and use your credentials to sign in.' ); this._storage.removeItem('amplify-auto-sign-in'); } else { this.signInAfterUserConfirmed( authDetails, null, autoSignInPollingIntervalId ); } }, 5000); } private async signInAfterUserConfirmed( authDetails: AuthenticationDetails, listenEvent?: HubCallback, autoSignInPollingIntervalId?: ReturnType<typeof setInterval> ) { const user = this.createCognitoUser(authDetails.getUsername()); try { await user.authenticateUser( authDetails, this.authCallbacks( user, value => { dispatchAuthEvent( 'autoSignIn', value, `${authDetails.getUsername()} has signed in successfully` ); if (listenEvent) { Hub.remove('auth', listenEvent); } if (autoSignInPollingIntervalId) { clearInterval(autoSignInPollingIntervalId); this._storage.removeItem('amplify-polling-started'); } this._storage.removeItem('amplify-auto-sign-in'); }, error => { logger.error(error); this._storage.removeItem('amplify-auto-sign-in'); } ) ); } catch (error) { logger.error(error); } } /** * Send the verification code to confirm sign up * @param {String} username - The username to be confirmed * @param {String} code - The verification code * @param {ConfirmSignUpOptions} options - other options for confirm signup * @return - A promise resolves callback data if success */ public confirmSignUp( username: string, code: string, options?: ConfirmSignUpOptions ): Promise<any> { if (!this.userPool) { return this.rejectNoUserPool(); } if (!username) { return this.rejectAuthError(AuthErrorTypes.EmptyUsername); } if (!code) { return this.rejectAuthError(AuthErrorTypes.EmptyCode); } const user = this.createCognitoUser(username); const forceAliasCreation = options && typeof options.forceAliasCreation === 'boolean' ? options.forceAliasCreation : true; let clientMetadata; if (options && options.clientMetadata) { clientMetadata = options.clientMetadata; } else if (this._config.clientMetadata) { clientMetadata = this._config.clientMetadata; } return new Promise((resolve, reject) => { user.confirmRegistration( code, forceAliasCreation, (err, data) => { if (err) { reject(err); } else { dispatchAuthEvent( 'confirmSignUp', data, `${username} has been confirmed successfully` ); const autoSignIn = this.isTrueStorageValue('amplify-auto-sign-in'); if (autoSignIn && !this.autoSignInInitiated) { dispatchAuthEvent( 'autoSignIn_failure', null, AuthErrorTypes.AutoSignInError ); this._storage.removeItem('amplify-auto-sign-in'); } resolve(data); } }, clientMetadata ); }); } private isTrueStorageValue(value: string) { const item = this._storage.getItem(value); return item ? item === 'true' : false; } /** * Resend the verification code * @param {String} username - The username to be confirmed * @param {ClientMetadata} clientMetadata - Metadata to be passed to Cognito Lambda triggers * @return - A promise resolves code delivery details if successful */ public resendSignUp( username: string, clientMetadata: ClientMetaData = this._config.clientMetadata ): Promise<any> { if (!this.userPool) { return this.rejectNoUserPool(); } if (!username) { return this.rejectAuthError(AuthErrorTypes.EmptyUsername); } const user = this.createCognitoUser(username); return new Promise((resolve, reject) => { user.resendConfirmationCode((err, data) => { if (err) { reject(err); } else { resolve(data); } }, clientMetadata); }); } /** * Sign in * @param {String | SignInOpts} usernameOrSignInOpts - The username to be signed in or the sign in options * @param {String} pw - The password of the username * @param {ClientMetaData} clientMetadata - Client metadata for custom workflows * @return - A promise resolves the CognitoUser */ public signIn( usernameOrSignInOpts: string | SignInOpts, pw?: string, clientMetadata: ClientMetaData = this._config.clientMetadata ): Promise<CognitoUser | any> { if (!this.userPool) { return this.rejectNoUserPool(); } let username = null; let password = null; let validationData = {}; // for backward compatibility if (typeof usernameOrSignInOpts === 'string') { username = usernameOrSignInOpts; password = pw; } else if (isUsernamePasswordOpts(usernameOrSignInOpts)) { if (typeof pw !== 'undefined') { logger.warn( 'The password should be defined under the first parameter object!' ); } username = usernameOrSignInOpts.username; password = usernameOrSignInOpts.password; validationData = usernameOrSignInOpts.validationData; } else { return this.rejectAuthError(AuthErrorTypes.InvalidUsername); } if (!username) { return this.rejectAuthError(AuthErrorTypes.EmptyUsername); } const authDetails = new AuthenticationDetails({ Username: username, Password: password, ValidationData: validationData, ClientMetadata: clientMetadata, }); if (password) { return this.signInWithPassword(authDetails); } else { return this.signInWithoutPassword(authDetails); } } /** * Return an object with the authentication callbacks * @param {CognitoUser} user - the cognito user object * @param {} resolve - function called when resolving the current step * @param {} reject - function called when rejecting the current step * @return - an object with the callback methods for user authentication */ private authCallbacks( user: CognitoUser, resolve: (value?: CognitoUser | any) => void, reject: (value?: any) => void ): IAuthenticationCallback { const that = this; return { onSuccess: async session => { logger.debug(session); delete user['challengeName']; delete user['challengeParam']; try { await this.Credentials.clear(); const cred = await this.Credentials.set(session, 'session'); logger.debug('succeed to get cognito credentials', cred); } catch (e) { logger.debug('cannot get cognito credentials', e); } finally { try { // In order to get user attributes and MFA methods // We need to trigger currentUserPoolUser again const currentUser = await this.currentUserPoolUser(); that.user = currentUser; dispatchAuthEvent( 'signIn', currentUser, `A user ${user.getUsername()} has been signed in` ); resolve(currentUser); } catch (e) { logger.error('Failed to get the signed in user', e); reject(e); } } }, onFailure: err => { logger.debug('signIn failure', err); dispatchAuthEvent( 'signIn_failure', err, `${user.getUsername()} failed to signin` ); reject(err); }, customChallenge: challengeParam => { logger.debug('signIn custom challenge answer required'); user['challengeName'] = 'CUSTOM_CHALLENGE'; user['challengeParam'] = challengeParam; resolve(user); }, mfaRequired: (challengeName, challengeParam) => { logger.debug('signIn MFA required'); user['challengeName'] = challengeName; user['challengeParam'] = challengeParam; resolve(user); }, mfaSetup: (challengeName, challengeParam) => { logger.debug('signIn mfa setup', challengeName); user['challengeName'] = challengeName; user['challengeParam'] = challengeParam; resolve(user); }, newPasswordRequired: (userAttributes, requiredAttributes) => { logger.debug('signIn new password'); user['challengeName'] = 'NEW_PASSWORD_REQUIRED'; user['challengeParam'] = { userAttributes, requiredAttributes, }; resolve(user); }, totpRequired: (challengeName, challengeParam) => { logger.debug('signIn totpRequired'); user['challengeName'] = challengeName; user['challengeParam'] = challengeParam; resolve(user); }, selectMFAType: (challengeName, challengeParam) => { logger.debug('signIn selectMFAType', challengeName); user['challengeName'] = challengeName; user['challengeParam'] = challengeParam; resolve(user); }, }; } /** * Sign in with a password * @private * @param {AuthenticationDetails} authDetails - the user sign in data * @return - A promise resolves the CognitoUser object if success or mfa required */ private signInWithPassword( authDetails: AuthenticationDetails ): Promise<CognitoUser | any> { if (this.pendingSignIn) { throw new Error('Pending sign-in attempt already in progress'); } const user = this.createCognitoUser(authDetails.getUsername()); this.pendingSignIn = new Promise((resolve, reject) => { user.authenticateUser( authDetails, this.authCallbacks( user, value => { this.pendingSignIn = null; resolve(value); }, error => { this.pendingSignIn = null; reject(error); } ) ); }); return this.pendingSignIn; } /** * Sign in without a password * @private * @param {AuthenticationDetails} authDetails - the user sign in data * @return - A promise resolves the CognitoUser object if success or mfa required */ private signInWithoutPassword( authDetails: AuthenticationDetails ): Promise<CognitoUser | any> { const user = this.createCognitoUser(authDetails.getUsername()); user.setAuthenticationFlowType('CUSTOM_AUTH'); return new Promise((resolve, reject) => { user.initiateAuth(authDetails, this.authCallbacks(user, resolve, reject)); }); } /** * 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 {CognitoUser} user - the current user * @return - A promise resolves the current preferred mfa option if success */ public getMFAOptions(user: CognitoUser | any): Promise<MFAOption[]> { return new Promise((res, rej) => { user.getMFAOptions((err, mfaOptions) => { if (err) { logger.debug('get MFA Options failed', err); rej(err); return; } logger.debug('get MFA options success', mfaOptions); res(mfaOptions); return; }); }); } /** * get preferred mfa method * @param {CognitoUser} user - the current cognito user * @param {GetPreferredMFAOpts} params - options for getting the current user preferred MFA */ public getPreferredMFA( user: CognitoUser | any, params?: GetPreferredMFAOpts ): Promise<string> { const that = this; return new Promise((res, rej) => { const clientMetadata = this._config.clientMetadata; // TODO: verify behavior if this is override during signIn const bypassCache = params ? params.bypassCache : false; user.getUserData( async (err, data) => { if (err) { logger.debug('getting preferred mfa failed', err); if (this.isSessionInvalid(err)) { try { await this.cleanUpInvalidSession(user); } catch (cleanUpError) { rej( new Error( `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}` ) ); return; } } rej(err); return; } const mfaType = that._getMfaTypeFromUserData(data); if (!mfaType) { rej('invalid MFA Type'); return; } else { res(mfaType); return; } }, { bypassCache, clientMetadata } ); }); } private _getMfaTypeFromUserData(data) { let ret = null; const preferredMFA = data.PreferredMfaSetting; // if the user has used Auth.setPreferredMFA() to setup the mfa type // then the "PreferredMfaSetting" would exist in the response if (preferredMFA) { ret = preferredMFA; } else { // if mfaList exists but empty, then its noMFA const mfaList = data.UserMFASettingList; if (!mfaList) { // if SMS was enabled by using Auth.enableSMS(), // the response would contain MFAOptions // as for now Cognito only supports for SMS, so we will say it is 'SMS_MFA' // if it does not exist, then it should be NOMFA const MFAOptions = data.MFAOptions; if (MFAOptions) { ret = 'SMS_MFA'; } else { ret = 'NOMFA'; } } else if (mfaList.length === 0) { ret = 'NOMFA'; } else { logger.debug('invalid case for getPreferredMFA', data); } } return ret; } private _getUserData(user, params) { return new Promise((res, rej) => { user.getUserData(async (err, data) => { if (err) { logger.debug('getting user data failed', err); if (this.isSessionInvalid(err)) { try { await this.cleanUpInvalidSession(user); } catch (cleanUpError) { rej( new Error( `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}` ) ); return; } } rej(err); return; } else { res(data); } }, params); }); } /** * set preferred MFA method * @param {CognitoUser} user - the current Cognito user * @param {string} mfaMethod - preferred mfa method * @return - A promise resolve if success */ public async setPreferredMFA( user: CognitoUser | any, mfaMethod: 'TOTP' | 'SMS' | 'NOMFA' | 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA' ): Promise<string> { const clientMetadata = this._config.clientMetadata; // TODO: verify behavior if this is override during signIn const userData = await this._getUserData(user, { bypassCache: true, clientMetadata, }); let smsMfaSettings = null; let totpMfaSettings = null; switch (mfaMethod) { case 'TOTP': case 'SOFTWARE_TOKEN_MFA': totpMfaSettings = { PreferredMfa: true, Enabled: true, }; break; case 'SMS': case 'SMS_MFA': smsMfaSettings = { PreferredMfa: true, Enabled: true, }; break; case 'NOMFA': const mfaList = userData['UserMFASettingList']; const currentMFAType = await this._getMfaTypeFromUserData(userData); if (currentMFAType === 'NOMFA') { return Promise.resolve('No change for mfa type'); } else if (currentMFAType === 'SMS_MFA') { smsMfaSettings = { PreferredMfa: false, Enabled: false, }; } else if (currentMFAType === 'SOFTWARE_TOKEN_MFA') { totpMfaSettings = { PreferredMfa: false, Enabled: false, }; } else { return this.rejectAuthError(AuthErrorTypes.InvalidMFA); } // if there is a UserMFASettingList in the response // we need to disable every mfa type in that list if (mfaList && mfaList.length !== 0) { // to disable SMS or TOTP if exists in that list mfaList.forEach(mfaType => { if (mfaType === 'SMS_MFA') { smsMfaSettings = { PreferredMfa: false, Enabled: false, }; } else if (mfaType === 'SOFTWARE_TOKEN_MFA') { totpMfaSettings = { PreferredMfa: false, Enabled: false, }; } }); } break; default: logger.debug('no validmfa method provided'); return this.rejectAuthError(AuthErrorTypes.NoMFA); } const that = this; return new Promise<string>((res, rej) => { user.setUserMfaPreference( smsMfaSettings, totpMfaSettings, (err, result) => { if (err) { logger.debug('Set user mfa preference error', err); return rej(err); } logger.debug('Set user mfa success', result); logger.debug('Caching the latest user data into local'); // cache the latest result into user data user.getUserData( async (err, data) => { if (err) { logger.debug('getting user data failed', err); if (this.isSessionInvalid(err)) { try { await this.cleanUpInvalidSession(user); } catch (cleanUpError) { rej( new Error( `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}` ) ); return; } } return rej(err); } else { return res(result); } }, { bypassCache: true, clientMetadata, } ); } ); }); } /** * disable SMS * @deprecated * @param {CognitoUser} user - the current user * @return - A promise resolves is success */ public disableSMS(user: CognitoUser): Promise<string> { return new Promise((res, rej) => { user.disableMFA((err, data) => { if (err) { logger.debug('disable mfa failed', err); rej(err); return; } logger.debug('disable mfa succeed', data); res(data); return; }); }); } /** * enable SMS * @deprecated * @param {CognitoUser} user - the current user * @return - A promise resolves is success */ public enableSMS(user: CognitoUser): Promise<string> { return new Promise((res, rej) => { user.enableMFA((err, data) => { if (err) { logger.debug('enable mfa failed', err); rej(err); return; } logger.debug('enable mfa succeed', data); res(data); return; }); }); } /** * Setup TOTP * @param {CognitoUser} user - the current user * @return - A promise resolves with the secret code if success */ public setupTOTP(user: CognitoUser | any): Promise<string> { return new Promise((res, rej) => { user.associateSoftwareToken({ onFailure: err => { logger.debug('associateSoftwareToken failed', err); rej(err); return; }, associateSecretCode: secretCode => { logger.debug('associateSoftwareToken sucess', secretCode); res(secretCode); return; }, }); }); } /** * verify TOTP setup * @param {CognitoUser} user - the current user * @param {string} challengeAnswer - challenge answer * @return - A promise resolves is success */ public verifyTotpToken( user: CognitoUser | any, challengeAnswer: string ): Promise<CognitoUserSession> { logger.debug('verification totp token', user, challengeAnswer); let signInUserSession; if (user && typeof user.getSignInUserSession === 'function') { signInUserSession = (user as CognitoUser).getSignInUserSession(); } const isLoggedIn = signInUserSession?.isValid(); return new Promise((res, rej) => { user.verifySoftwareToken(challengeAnswer, 'My TOTP device', { onFailure: err => { logger.debug('verifyTotpToken failed', err); rej(err); return; }, onSuccess: data => { if (!isLoggedIn) { dispatchAuthEvent( 'signIn', user, `A user ${user.getUsername()} has been signed in` ); } dispatchAuthEvent( 'verify', user, `A user ${user.getUsername()} has been verified` ); logger.debug('verifyTotpToken success', data); res(data); return; }, }); }); } /** * Send MFA code to confirm sign in * @param {Object} user - The CognitoUser object * @param {String} code - The confirmation code */ public confirmSignIn( user: CognitoUser | any, code: string, mfaType?: 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA' | null, clientMetadata: ClientMetaData = this._config.clientMetadata ): Promise<CognitoUser | any> { if (!code) { return this.rejectAuthError(AuthErrorTypes.EmptyCode); } const that = this; return new Promise((resolve, reject) => { user.sendMFACode( code, { onSuccess: async session => { logger.debug(session); try { await this.Credentials.clear(); const cred = await this.Credentials.set(session, 'session'); logger.debug('succeed to get cognito credentials', cred); } catch (e) { logger.debug('cannot get cognito credentials', e); } finally { that.user = user; try { const currentUser = await this.currentUserPoolUser(); user.attributes = currentUser.attributes; } catch (e) { logger.debug('cannot get updated Cognito User', e); } dispatchAuthEvent( 'signIn', user, `A user ${user.getUsername()} has been signed in` ); resolve(user); } }, onFailure: err => { logger.debug('confirm signIn failure', err); reject(err); }, }, mfaType, clientMetadata ); }); } public completeNewPassword( user: CognitoUser | any, password: string, requiredAttributes: any = {}, clientMetadata: ClientMetaData = this._config.clientMetadata ): Promise<CognitoUser | any> { if (!password) { return this.rejectAuthError(AuthErrorTypes.EmptyPassword); } const that = this; return new Promise((resolve, reject) => { user.completeNewPasswordChallenge( password, requiredAttributes, { onSuccess: async session => { logger.debug(session); try { await this.Credentials.clear(); const cred = await this.Credentials.set(session, 'session'); logger.debug('succeed to get cognito credentials', cred); } catch (e) { logger.debug('cannot get cognito credentials', e); } finally { that.user = user; dispatchAuthEvent( 'signIn', user, `A user ${user.getUsername()} has been signed in` ); resolve(user); } }, onFailure: err => { logger.debug('completeNewPassword failure', err); dispatchAuthEvent( 'completeNewPassword_failure', err, `${this.user} failed to complete the new password flow` ); reject(err); }, mfaRequired: (challengeName, challengeParam) => { logger.debug('signIn MFA required'); user['challengeName'] = challengeName; user['challengeParam'] = challengeParam; resolve(user); }, mfaSetup: (challengeName, challengeParam) => { logger.debug('signIn mfa setup', challengeName); user['challengeName'] = challengeName; user['challengeParam'] = challengeParam; resolve(user); }, totpRequired: (challengeName, challengeParam) => { logger.debug('signIn mfa setup', challengeName); user['challengeName'] = challengeName; user['challengeParam'] = challengeParam; resolve(user); }, }, clientMetadata ); }); } /** * Send the answer to a custom challenge * @param {CognitoUser} user - The CognitoUser object * @param {String} challengeResponses - The confirmation code */ public sendCustomChallengeAnswer( user: CognitoUser | any, challengeResponses: string, clientMetadata: ClientMetaData = this._config.clientMetadata ): Promise<CognitoUser | any> { if (!this.userPool) { return this.rejectNoUserPool(); } if (!challengeResponses) { return this.rejectAuthError(AuthErrorTypes.EmptyChallengeResponse); } const that = this; return new Promise((resolve, reject) => { user.sendCustomChallengeAnswer( challengeResponses, this.authCallbacks(user, resolve, reject), clientMetadata ); }); } /** * Delete an authenticated users' attributes * @param {CognitoUser} - The currently logged in user object * @return {Promise} **/ public deleteUserAttributes( user: CognitoUser | any, attributeNames: string[] ) { const that = this; return new Promise((resolve, reject) => { that.userSession(user).then(session => { user.deleteAttributes(attributeNames, (err, result) => { if (err) { return reject(err); } else { return resolve(result); } }); }); }); } /** * Delete the current authenticated user * @return {Promise} **/ // TODO: Check return type void public async deleteUser(): Promise<string | void> { try { await this._storageSync; } catch (e) { logger.debug('Failed to sync cache info into memory', e); throw new Error(e); } const isSignedInHostedUI = this._oAuthHandler && this._storage.getItem('amplify-signin-with-hostedUI') === 'true'; return new Promise(async (res, rej) => { if (this.userPool) { const user = this.userPool.getCurrentUser(); if (!user) { logger.debug('Failed to get user from user pool'); return rej(new Error('No current user.')); } else { user.getSession(async (err, session) => { if (err) { logger.debug('Failed to get the user session', err); if (this.isSessionInvalid(err)) { try { await this.cleanUpInvalidSession(user); } catch (cleanUpError) { rej( new Error( `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}` ) ); return; } } return rej(err); } else { user.deleteUser((err, result: string) => { if (err) { rej(err); } else { dispatchAuthEvent( 'userDeleted', result, 'The authenticated user has been deleted.' ); user.signOut(); this.user = null; try { this.cleanCachedItems(); // clean aws credentials } catch (e) { // TODO: change to rejects in refactor logger.debug('failed to clear cached items'); } if (isSignedInHostedUI) { this.oAuthSignOutRedirect(res, rej); } else { dispatchAuthEvent( 'signOut', this.user, `A user has been signed out` ); res(result); } } }); } }); } } else { logger.debug('no Congito User pool'); rej(new Error('Cognito User pool does not exist')); } }); } /** * Update an authenticated users' attributes * @param {CognitoUser} - The currently logged in user object * @return {Promise} **/ public updateUserAttributes( user: CognitoUser | any, attributes: object, clientMetadata: ClientMetaData = this._config.clientMetadata ): Promise<string> { const attributeList: ICognitoUserAttributeData[] = []; const that = this; return new Promise((resolve, reject) => { that.userSession(user).then(session => { for (const key in attributes) { if (key !== 'sub' && key.indexOf('_verified') < 0) { const attr: ICognitoUserAttributeData = { Name: key, Value: attributes[key], }; attributeList.push(attr); } } user.updateAttributes( attributeList, (err, result, details) => { if (err) { dispatchAuthEvent( 'updateUserAttributes_failure', err, 'Failed to update attributes' ); return reject(err); } else { const attrs = this.createUpdateAttributesResultList( attributes as Record<string, string>, details?.CodeDeliveryDetailsList ); dispatchAuthEvent( 'updateUserAttributes', attrs, 'Attributes successfully updated' ); return resolve(result); } }, clientMetadata ); }); }); } private createUpdateAttributesResultList( attributes: Record<string, string>, codeDeliveryDetailsList?: CodeDeliveryDetails[] ): Record<string, string> { const attrs = {}; Object.keys(attributes).forEach(key => { attrs[key] = { isUpdated: true, }; const codeDeliveryDetails = codeDeliveryDetailsList?.find( value => value.AttributeName === key ); if (codeDeliveryDetails) { attrs[key].isUpdated = false; attrs[key].codeDeliveryDetails = codeDeliveryDetails; } }); return attrs; } /** * Return user attributes * @param {Object} user - The CognitoUser object * @return - A promise resolves to user attributes if success */ public userAttributes( user: CognitoUser | any ): Promise<CognitoUserAttribute[]> { return new Promise((resolve, reject) => { this.userSession(user).then(session => { user.getUserAttributes((err, attributes) => { if (err) { reject(err); } else { resolve(attributes); } }); }); }); } public verifiedContact(user: CognitoUser | any) { const that = this; return this.userAttributes(user).then(attributes => { const attrs = that.attributesToObject(attributes); const unverified = {}; const verified = {}; if (attrs['email']) { if (attrs['email_verified']) { verified['email'] = attrs['email']; } else { unverified['email'] = attrs['email']; } } if (attrs['phone_number']) { if (attrs['phone_number_verified']) { verified['phone_number'] = attrs['phone_number']; } else { unverified['phone_number'] = attrs['phone_number']; } } return { verified, unverified, }; }); } private isErrorWithMessage(err: any): err is { message: string } { return ( typeof err === 'object' && Object.prototype.hasOwnProperty.call(err, 'message') ); } // Session revoked by another app private isTokenRevokedError( err: any ): err is { message: 'Access Token has been revoked' } { return ( this.isErrorWithMessage(err) && err.message === 'Access Token has been revoked' ); } private isRefreshTokenRevokedError( err: any ): err is { message: 'Refresh Token has been revoked' } { return ( this.isErrorWithMessage(err) && err.message === 'Refresh Token has been revoked' ); } private isUserDisabledError( err: any ): err is { message: 'User is disabled.' } { return this.isErrorWithMessage(err) && err.message === 'User is disabled.'; } private isUserDoesNotExistError( err: any ): err is { message: 'User does not exist.' } { return ( this.isErrorWithMessage(err) && err.message === 'User does not exist.' ); } private isRefreshTokenExpiredError( err: any ): err is { message: 'Refresh Token has expired' } { return ( this.isErrorWithMessage(err) && err.message === 'Refresh Token has expired' ); } private isSignedInHostedUI() { return ( this._oAuthHandler && this._storage.getItem('amplify-signin-with-hostedUI') === 'true' ); } private isSessionInvalid(err: any) { return ( this.isUserDisabledError(err) || this.isUserDoesNotExistError(err) || this.isTokenRevokedError(err) || this.isRefreshTokenRevokedError(err) || this.isRefreshTokenExpiredError(err) ); } private async cleanUpInvalidSession(user: CognitoUser) { user.signOut(); this.user = null; try { await this.cleanCachedItems(); // clean aws credentials } catch (e) { logger.debug('failed to clear cached items'); } if (this.isSignedInHostedUI()) { return new Promise((res, rej) => { this.oAuthSignOutRedirect(res, rej); }); } else { dispatchAuthEvent('signOut', this.user, `A user has been signed out`); } } /** * Get current authenticated user * @return - A promise resolves to current authenticated CognitoUser if success */ public currentUserPoolUser( params?: CurrentUserOpts ): Promise<CognitoUser | any> { if (!this.userPool) { return this.rejectNoUserPool(); } return new Promise((res, rej) => { this._storageSync .then(async () => { if (this.isOAuthInProgress()) { logger.debug('OAuth signIn in progress, waiting for resolution...'); await new Promise(res => { const timeoutId = setTimeout(() => { logger.debug('OAuth signIn in progress timeout'); Hub.remove('auth', hostedUISignCallback); res(); }, OAUTH_FLOW_MS_TIMEOUT); Hub.listen('auth', hostedUISignCallback); function hostedUISignCallback({ payload }) { const { event } = payload; if ( event === 'cognitoHostedUI' || event === 'cognitoHostedUI_failure' ) { logger.debug(`OAuth signIn resolved: ${event}`); clearTimeout(timeoutId); Hub.remove('auth', hostedUISignCallback); res(); } } }); } const user = this.userPool.getCurrentUser(); if (!user) { logger.debug('Failed to get user from user pool'); rej('No current user'); return; } // refresh the session if the session expired. try { const session = await this._userSession(user); // get user data from Cognito const bypassCache = params ? params.bypassCache : false; if (bypassCache) { await this.Credentials.clear(); } const clientMetadata = this._config.clientMetadata; // validate the token's scope first before calling this function const { scope = '' } = session.getAccessToken().decodePayload(); if (scope.split(' ').includes(USER_ADMIN_SCOPE)) { user.getUserData( async (err, data) => { if (err) { logger.debug('getting user data failed', err); if (this.isSessionInvalid(err)) { try { await this.cleanUpInvalidSession(user); } catch (cleanUpError) { rej( new Error( `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}` ) ); return; } rej(err); } else { res(user); } return; } const preferredMFA = data.PreferredMfaSetting || 'NOMFA'; const attributeList = []; for (let i = 0; i < data.UserAttributes.length; i++) { const attribute = { Name: data.UserAttributes[i].Name, Value: data.UserAttributes[i].Value, }; const userAttribute = new CognitoUserAttribute(attribute); attributeList.push(userAttribute); } const attributes = this.attributesToObject(attributeList); Object.assign(user, { attributes, preferredMFA }); return res(user); }, { bypassCache, clientMetadata } ); } else { logger.debug( `Unable to get the user data because the ${USER_ADMIN_SCOPE} ` + `is not in the scopes of the access token` ); return res(user); } } catch (err) { rej(err); } }) .catch(e => { logger.debug('Failed to sync cache info into memory', e); return rej(e); }); }); } private isOAuthInProgress(): boolean { return this.oAuthFlowInProgress; } /** * Get current authenticated user * @param {CurrentUserOpts} - options for getting the current user * @return - A promise resolves to current authenticated CognitoUser if success */ public async currentAuthenticatedUser( params?: CurrentUserOpts ): Promise<CognitoUser | any> { logger.debug('getting current authenticated user'); let federatedUser = null; try { await this._storageSync; } catch (e) { logger.debug('Failed to sync cache info into memory', e); throw e; } try { const federatedInfo = JSON.parse( this._storage.getItem('aws-amplify-federatedInfo') ); if (federatedInfo) { federatedUser = { ...federatedInfo.user, token: federatedInfo.token, }; } } catch (e) { logger.debug('cannot load federated user from auth storage'); } if (federatedUser) { this.user = federatedUser; logger.debug('get current authenticated federated user', this.user); return this.user; } else { logger.debug('get current authenticated userpool user'); let user = null; try { user = await this.currentUserPoolUser(params); } catch (e) { if (e === 'No userPool') { logger.error( 'Cannot get the current user because the user pool is missing. ' + 'Please make sure the Auth module is configured with a valid Cognito User Pool ID' ); } logger.debug('The user is not authenticated by the error', e); return Promise.reject('The user is not authenticated'); } this.user = user; return this.user; } } /** * Get current user's session * @return - A promise resolves to session object if success */ public currentSession(): Promise<CognitoUserSession> { const that = this; logger.debug('Getting current session'); // Purposely not calling the reject method here because we don't need a console error if (!this.userPool) { return Promise.reject(new Error('No User Pool in the configuration.')); } return new Promise((res, rej) => { that .currentUserPoolUser() .then(user => { that .userSession(user) .then(session => { res(session); return; }) .catch(e => { logger.debug('Failed to get the current session', e); rej(e); return; }); }) .catch(e => { logger.debug('Failed to get the current user', e); rej(e); return; }); }); } private async _userSession(user?: CognitoUser): Promise<CognitoUserSession> { if (!user) { logger.debug('the user is null'); return this.rejectAuthError(AuthErrorTypes.NoUserSession); } const clientMetadata = this._config.clientMetadata; // Debouncing the concurrent userSession calls by caching the promise. // This solution assumes users will always call this function with the same CognitoUser instance. if (this.inflightSessionPromiseCounter === 0) { this.inflightSessionPromise = new Promise<CognitoUserSession>( (res, rej) => { user.getSession( async (err, session) => { if (err) { logger.debug('Failed to get the session from user', user); if (this.isSessionInvalid(err)) { try { await this.cleanUpInvalidSession(user); } catch (cleanUpError) {