@aws-amplify/auth
Version:
Auth category of aws-amplify
1,320 lines (1,227 loc) • 62.2 kB
text/typescript
/*
* Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0/
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/
import {
AuthOptions,
FederatedResponse,
SignUpParams,
FederatedUser,
ConfirmSignUpOptions,
SignOutOpts,
CurrentUserOpts,
GetPreferredMFAOpts,
SignInOpts,
isUsernamePasswordOpts,
isCognitoHostedOpts,
isFederatedSignInOptions,
isFederatedSignInOptionsCustom,
FederatedSignInOptionsCustom,
LegacyProvider,
FederatedSignInOptions,
AwsCognitoOAuthOpts
} from './types';
import {
AWS,
ConsoleLogger as Logger,
Constants,
Hub,
JS,
Parser,
Credentials,
StorageHelper,
ICredentials,
Platform
} from '@aws-amplify/core';
import {
CookieStorage,
CognitoUserPool,
AuthenticationDetails,
ICognitoUserPoolData,
ICognitoUserData,
ISignUpResult,
CognitoUser,
MFAOption,
CognitoUserSession,
IAuthenticationCallback,
ICognitoUserAttributeData,
CognitoUserAttribute,
CognitoIdToken,
CognitoRefreshToken,
CognitoAccessToken
} from 'amazon-cognito-identity-js';
import { parse } from 'url';
import OAuth from './OAuth/OAuth';
import { default as urlListener } from './urlListener';
const logger = new Logger('AuthClass');
const USER_ADMIN_SCOPE = 'aws.cognito.signin.user.admin';
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);
};
export enum CognitoHostedUIIdentityProvider {
Cognito = 'COGNITO',
Google = 'Google',
Facebook = 'Facebook',
Amazon = 'LoginWithAmazon',
}
/**
* Provide authentication steps
*/
export default class AuthClass {
private _config: AuthOptions;
private userPool = null;
private user: any = null;
private _oAuthHandler: OAuth;
private _storage;
private _storageSync;
/**
* Initialize Auth with AWS configurations
* @param {Object} config - Configuration of the Auth
*/
constructor(config: AuthOptions) {
this.configure(config);
this.currentUserCredentials = this.currentUserCredentials.bind(this);
if (AWS.config) {
AWS.config.update({ customUserAgent: Constants.userAgent });
} else {
logger.warn('No AWS.config');
}
Hub.listen('auth', ({ payload }) => {
const { event } = payload;
switch (event) {
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, Parser.parseMobilehubConfig(config).Auth, config);
this._config = conf;
const {
userPoolId,
userPoolWebClientId,
cookieStorage,
oauth,
region,
identityPoolId,
mandatorySignIn,
refreshHandlers,
identityPoolRegion
} = this._config;
if (!this._config.storage) {
// backward compatbility
if (cookieStorage) this._storage = new CookieStorage(cookieStorage);
else {
this._storage = new StorageHelper().getStorage();
}
} else {
if (JS.isEmpty(this._config.storage)) {
logger.error('The storage in the Auth config can not be empty!');
throw new Error('Empty storage object');
}
this._storage = this._config.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,
};
userPoolData.Storage = this._storage;
this.userPool = new CognitoUserPool(userPoolData);
}
Credentials.configure({
mandatorySignIn,
region: identityPoolRegion || region,
userPoolId,
identityPoolId,
refreshHandlers,
storage: this._storage
});
// initiailize 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']
},
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
urlListener(({ url }) => {
this._handleAuthResponse(url);
});
}
dispatchAuthEvent(
'configured',
null,
`The Auth category has been configured successfully`
);
return this._config;
}
/**
* Sign up with username, password and other attrbutes like phone, email
* @param {String | object} params - The user attirbutes 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 Promise.reject('No userPool'); }
let username: string = null;
let password: string = null;
const attributes: object[] = [];
let validationData: object[] = null;
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({ Name: 'email', Value: email });
if (phone_number) attributes.push({ Name: 'phone_number', Value: phone_number });
} else if (params && typeof params === 'object') {
username = params['username'];
password = params['password'];
const attrs = params['attributes'];
if (attrs) {
Object.keys(attrs).map(key => {
const ele: object = { Name: key, Value: attrs[key] };
attributes.push(ele);
});
}
validationData = params['validationData'] || null;
} else {
return Promise.reject('The first parameter should either be non-null string or object');
}
if (!username) { return Promise.reject('Username cannot be empty'); }
if (!password) { return Promise.reject('Password cannot be empty'); }
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`
);
resolve(data);
}
});
});
}
/**
* Send the verfication 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 Promise.reject('No userPool'); }
if (!username) { return Promise.reject('Username cannot be empty'); }
if (!code) { return Promise.reject('Code cannot be empty'); }
const user = this.createCognitoUser(username);
const forceAliasCreation = options && typeof options.forceAliasCreation === 'boolean'
? options.forceAliasCreation : true;
return new Promise((resolve, reject) => {
user.confirmRegistration(code, forceAliasCreation, (err, data) => {
if (err) { reject(err); } else { resolve(data); }
});
});
}
/**
* Resend the verification code
* @param {String} username - The username to be confirmed
* @return - A promise resolves data if success
*/
public resendSignUp(username: string): Promise<string> {
if (!this.userPool) { return Promise.reject('No userPool'); }
if (!username) { return Promise.reject('Username cannot be empty'); }
const user = this.createCognitoUser(username);
return new Promise((resolve, reject) => {
user.resendConfirmationCode((err, data) => {
if (err) { reject(err); } else { resolve(data); }
});
});
}
/**
* Sign in
* @param {String | SignInOpts} usernameOrSignInOpts - The username to be signed in or the sign in options
* @param {String} password - The password of the username
* @return - A promise resolves the CognitoUser
*/
public signIn(usernameOrSignInOpts: string | SignInOpts, pw?: string): Promise<CognitoUser | any> {
if (!this.userPool) { return Promise.reject('No userPool'); }
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 Promise.reject(new Error('The username should either be a string or one of the sign in types'));
}
if (!username) { return Promise.reject('Username cannot be empty'); }
const authDetails = new AuthenticationDetails({
Username: username,
Password: password,
ValidationData: validationData
});
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 Credentials.clear();
const cred = await 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> {
const user = this.createCognitoUser(authDetails.getUsername());
return new Promise((resolve, reject) => {
user.authenticateUser(authDetails, this.authCallbacks(user, resolve, reject));
});
}
/**
* 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));
});
}
/**
* get user current preferred mfa option
* this method doesn't work with totp, we need to deprecate it.
* @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 bypassCache = params? params.bypassCache: false;
user.getUserData(
(err, data) => {
if (err) {
logger.debug('getting preferred mfa failed', err);
rej(err);
return;
}
const mfaType = that._getMfaTypeFromUserData(data);
if (!mfaType) {
rej('invalid MFA Type');
return;
} else {
res(mfaType);
return;
}
},
{ bypassCache }
);
});
}
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(
(err, data) => {
if (err) {
logger.debug('getting user data failed', err);
rej(err);
return;
} else {
res(data);
return;
}
},
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'): Promise<string> {
const userData = await this._getUserData(user, { bypassCache: true });
let smsMfaSettings = null;
let totpMfaSettings = null;
switch (mfaMethod) {
case 'TOTP' || 'SOFTWARE_TOKEN_MFA':
totpMfaSettings = {
PreferredMfa: true,
Enabled: true
};
break;
case 'SMS' || '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 Promise.reject('invalid MFA type');
}
// 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 Promise.reject('no validmfa method provided');
}
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(
(err, data) => {
if (err) {
logger.debug('getting user data failed', err);
return rej(err);
} else {
return res(result);
}
},
{bypassCache: true}
);
});
});
}
/**
* diable 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('verfication totp token', user, challengeAnswer);
return new Promise((res, rej) => {
user.verifySoftwareToken(challengeAnswer, 'My TOTP device', {
onFailure: (err) => {
logger.debug('verifyTotpToken failed', err);
rej(err);
return;
},
onSuccess: (data) => {
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
): Promise<CognitoUser | any> {
if (!code) { return Promise.reject('Code cannot be empty'); }
const that = this;
return new Promise((resolve, reject) => {
user.sendMFACode(
code, {
onSuccess: async (session) => {
logger.debug(session);
try {
await Credentials.clear();
const cred = await 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,
`${user} has signed in`
);
resolve(user);
}
},
onFailure: (err) => {
logger.debug('confirm signIn failure', err);
reject(err);
}
},
mfaType);
});
}
public completeNewPassword(
user: CognitoUser | any,
password: string,
requiredAttributes: any
): Promise<CognitoUser | any> {
if (!password) { return Promise.reject('Password cannot be empty'); }
const that = this;
return new Promise((resolve, reject) => {
user.completeNewPasswordChallenge(password, requiredAttributes, {
onSuccess: async (session) => {
logger.debug(session);
try {
await Credentials.clear();
const cred = await 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, `${user} has 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);
}
});
});
}
/**
* 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): Promise<CognitoUser | any> {
if (!this.userPool) { return Promise.reject('No userPool'); }
if (!challengeResponses) { return Promise.reject('Challenge response cannot be empty'); }
const that = this;
return new Promise((resolve, reject) => {
user.sendCustomChallengeAnswer(challengeResponses, this.authCallbacks(user, resolve, reject));
});
}
/**
* Update an authenticated users' attributes
* @param {CognitoUser} - The currently logged in user object
* @return {Promise}
**/
public updateUserAttributes(user: CognitoUser | any, attributes: object): 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) => {
if (err) { return reject(err); } else { return resolve(result); }
});
});
});
}
/**
* 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
};
});
}
/**
* 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 Promise.reject('No userPool'); }
const that = this;
return new Promise((res, rej) => {
this._storageSync.then(() => {
const user = that.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.
user.getSession((err, session) => {
if (err) {
logger.debug('Failed to get the user session', err);
rej(err);
return;
}
// get user data from Cognito
const bypassCache = params ? params.bypassCache : false;
// validate the token's scope fisrt before calling this function
const { scope = '' } = session.getAccessToken().decodePayload();
if (scope.split(' ').includes(USER_ADMIN_SCOPE)) {
user.getUserData(
(err, data) => {
if (err) {
logger.debug('getting user data failed', err);
// Make sure the user is still valid
if (err.message === 'User is disabled' || err.message === 'User does not exist.') {
rej(err);
} else {
// the error may also be thrown when lack of permissions to get user info etc
// in that case we just bypass the error
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 = that.attributesToObject(attributeList);
Object.assign(user, { attributes, preferredMFA });
return res(user);
},
{ bypassCache }
);
} 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(e => {
logger.debug('Failed to sync cache info into memory', e);
return rej(e);
});
});
}
/**
* 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 authenticted user');
let federatedUser = null;
try {
await this._storageSync;
} catch (e) {
logger.debug('Failed to sync cache info into memory', e);
throw e;
}
try {
federatedUser = JSON.parse(this._storage.getItem('aws-amplify-federatedInfo')).user;
} 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);
throw ('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');
if (!this.userPool) { return Promise.reject('No userPool'); }
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;
});
});
}
/**
* Get the corresponding user session
* @param {Object} user - The CognitoUser object
* @return - A promise resolves to the session
*/
public userSession(user): Promise<CognitoUserSession> {
if (!user) {
logger.debug('the user is null');
return Promise.reject('Failed to get the session because the user is empty');
}
return new Promise((resolve, reject) => {
logger.debug('Getting the session from this user:', user);
user.getSession((err, session) => {
if (err) {
logger.debug('Failed to get the session from user', user);
reject(err);
return;
} else {
logger.debug('Succeed to get the user session', session);
resolve(session);
return;
}
});
});
}
/**
* Get authenticated credentials of current user.
* @return - A promise resolves to be current user's credentials
*/
public async currentUserCredentials(): Promise<ICredentials> {
const that = this;
logger.debug('Getting current user credentials');
try {
await this._storageSync;
} catch (e) {
logger.debug('Failed to sync cache info into memory', e);
throw e;
}
// first to check whether there is federation info in the auth storage
let federatedInfo = null;
try {
federatedInfo = JSON.parse(this._storage.getItem('aws-amplify-federatedInfo'));
} catch (e) {
logger.debug('failed to get or parse item aws-amplify-federatedInfo', e);
}
if (federatedInfo) {
// refresh the jwt token here if necessary
return Credentials.refreshFederatedToken(federatedInfo);
} else {
return this.currentSession()
.then(session => {
logger.debug('getting session success', session);
return Credentials.set(session, 'session');
}).catch((error) => {
logger.debug('getting session failed', error);
return Credentials.set(null, 'guest');
});
}
}
public currentCredentials(): Promise<ICredentials> {
logger.debug('getting current credntials');
return Credentials.get();
}
/**
* Initiate an attribute confirmation request
* @param {Object} user - The CognitoUser
* @param {Object} attr - The attributes to be verified
* @return - A promise resolves to callback data if success
*/
public verifyUserAttribute(user: CognitoUser | any, attr: string): Promise<void> {
return new Promise((resolve, reject) => {
user.getAttributeVerificationCode(attr, {
onSuccess() { return resolve(); },
onFailure(err) { return reject(err); }
});
});
}
/**
* Confirm an attribute using a confirmation code
* @param {Object} user - The CognitoUser
* @param {Object} attr - The attribute to be verified
* @param {String} code - The confirmation code
* @return - A promise resolves to callback data if success
*/
public verifyUserAttributeSubmit(user: CognitoUser | any, attr: string, code: string): Promise<string> {
if (!code) { return Promise.reject('Code cannot be empty'); }
return new Promise((resolve, reject) => {
user.verifyAttribute(attr, code, {
onSuccess(data) {
resolve(data);
return;
},
onFailure(err) {
reject(err);
return;
}
});
});
}
public verifyCurrentUserAttribute(attr: string): Promise<void> {
const that = this;
return that.currentUserPoolUser()
.then(user => that.verifyUserAttribute(user, attr));
}
/**
* Confirm current user's attribute using a confirmation code
* @param {Object} attr - The attribute to be verified
* @param {String} code - The confirmation code
* @return - A promise resolves to callback data if success
*/
verifyCurrentUserAttributeSubmit(attr: string, code: string): Promise<string> {
const that = this;
return that.currentUserPoolUser()
.then(user => that.verifyUserAttributeSubmit(user, attr, code));
}
private async cognitoIdentitySignOut(opts: SignOutOpts, user: CognitoUser | any) {
try {
await this._storageSync;
} catch (e) {
logger.debug('Failed to sync cache info into memory', e);
throw e;
}
const isSignedInHostedUI = this._oAuthHandler
&& this._storage.getItem('amplify-signin-with-hostedUI') === 'true';
return new Promise((res, rej) => {
if (opts && opts.global) {
logger.debug('user global sign out', user);
// in order to use global signout
// we must validate the user as an authenticated user by using getSession
user.getSession((err, result) => {
if (err) {
logger.debug('failed to get the user session', err);
return rej(err);
}
user.globalSignOut({
onSuccess: (data) => {
logger.debug('global sign out success');
if (isSignedInHostedUI) {
this._oAuthHandler.signOut();
}
return res();
},
onFailure: (err) => {
logger.debug('global sign out failed', err);
return rej(err);
}
});
});
} else {
logger.debug('user sign out', user);
user.signOut();
if (isSignedInHostedUI) {
this._oAuthHandler.signOut();
}
return res();
}
});
}
/**
* Sign out method
* @
* @return - A promise resolved if success
*/
public async signOut(opts?: SignOutOpts): Promise<any> {
try {
await this.cleanCachedItems();
} catch (e) {
logger.debug('failed to clear cached items');
}
if (this.userPool) {
const user = this.userPool.getCurrentUser();
if (user) {
await this.cognitoIdentitySignOut(opts, user);
} else {
logger.debug('no current Cognito user');
}
} else {
logger.debug('no Congito User pool');
}
/**
* Note for future refactor - no reliable way to get username with
* Cognito User Pools vs Identity when federating with Social Providers
* This is why we need a well structured session object that can be inspected
* and information passed back in the message below for Hub dispatch
*/
dispatchAuthEvent(
'signOut',
this.user,
`A user has been signed out`
);
this.user = null;
}
private async cleanCachedItems() {
// clear cognito cached item
await Credentials.clear();
}
/**
* Change a password for an authenticated user
* @param {Object} user - The CognitoUser object
* @param {String} oldPassword - the current password
* @param {String} newPassword - the requested new password
* @return - A promise resolves if success
*/
public changePassword(user: CognitoUser | any, oldPassword: string, newPassword: string): Promise<"SUCCESS"> {
return new Promise((resolve, reject) => {
this.userSession(user).then(session => {
user.changePassword(oldPassword, newPassword