@aws-amplify/auth
Version:
Auth category of aws-amplify
1,868 lines (1,741 loc) • 74.4 kB
text/typescript
// 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) {