UNPKG

68.9 kBPlain TextView Raw
1/*
2 * Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
5 * the License. A copy of the License is located at
6 *
7 * http://aws.amazon.com/apache2.0/
8 *
9 * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10 * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
11 * and limitations under the License.
12 */
13
14import {
15 AuthOptions,
16 FederatedResponse,
17 SignUpParams,
18 FederatedUser,
19 ConfirmSignUpOptions,
20 SignOutOpts,
21 CurrentUserOpts,
22 GetPreferredMFAOpts,
23 SignInOpts,
24 isUsernamePasswordOpts,
25 isCognitoHostedOpts,
26 isFederatedSignInOptions,
27 isFederatedSignInOptionsCustom,
28 hasCustomState,
29 FederatedSignInOptionsCustom,
30 LegacyProvider,
31 FederatedSignInOptions,
32 AwsCognitoOAuthOpts,
33 ClientMetaData,
34} from './types';
35
36import {
37 Amplify,
38 ConsoleLogger as Logger,
39 Credentials,
40 Hub,
41 StorageHelper,
42 ICredentials,
43 Parser,
44 JS,
45 UniversalStorage,
46 urlSafeDecode,
47} from '@aws-amplify/core';
48import {
49 CookieStorage,
50 CognitoUserPool,
51 AuthenticationDetails,
52 ICognitoUserPoolData,
53 ICognitoUserData,
54 ISignUpResult,
55 CognitoUser,
56 MFAOption,
57 CognitoUserSession,
58 IAuthenticationCallback,
59 ICognitoUserAttributeData,
60 CognitoUserAttribute,
61 CognitoIdToken,
62 CognitoRefreshToken,
63 CognitoAccessToken,
64 NodeCallback,
65} from 'amazon-cognito-identity-js';
66
67import { parse } from 'url';
68import OAuth from './OAuth/OAuth';
69import { default as urlListener } from './urlListener';
70import { AuthError, NoUserPoolError } from './Errors';
71import {
72 AuthErrorTypes,
73 CognitoHostedUIIdentityProvider,
74 IAuthDevice,
75} from './types/Auth';
76
77const logger = new Logger('AuthClass');
78const USER_ADMIN_SCOPE = 'aws.cognito.signin.user.admin';
79
80// 10 sec, following this guide https://www.nngroup.com/articles/response-times-3-important-limits/
81const OAUTH_FLOW_MS_TIMEOUT = 10 * 1000;
82
83const AMPLIFY_SYMBOL = (
84 typeof Symbol !== 'undefined' && typeof Symbol.for === 'function'
85 ? Symbol.for('amplify_default')
86 : '@@amplify_default'
87) as Symbol;
88
89const dispatchAuthEvent = (event: string, data: any, message: string) => {
90 Hub.dispatch('auth', { event, data, message }, 'Auth', AMPLIFY_SYMBOL);
91};
92
93// Cognito Documentation for max device
94// tslint:disable-next-line:max-line-length
95// https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListDevices.html#API_ListDevices_RequestSyntax
96const MAX_DEVICES = 60;
97
98/**
99 * Provide authentication steps
100 */
101export class AuthClass {
102 private _config: AuthOptions;
103 private userPool: CognitoUserPool = null;
104 private user: any = null;
105 private _oAuthHandler: OAuth;
106 private _storage;
107 private _storageSync;
108 private oAuthFlowInProgress: boolean = false;
109 private pendingSignIn: ReturnType<AuthClass['signInWithPassword']> | null;
110
111 Credentials = Credentials;
112
113 /**
114 * Initialize Auth with AWS configurations
115 * @param {Object} config - Configuration of the Auth
116 */
117 constructor(config: AuthOptions) {
118 this.configure(config);
119 this.currentCredentials = this.currentCredentials.bind(this);
120 this.currentUserCredentials = this.currentUserCredentials.bind(this);
121
122 Hub.listen('auth', ({ payload }) => {
123 const { event } = payload;
124 switch (event) {
125 case 'signIn':
126 this._storage.setItem('amplify-signin-with-hostedUI', 'false');
127 break;
128 case 'signOut':
129 this._storage.removeItem('amplify-signin-with-hostedUI');
130 break;
131 case 'cognitoHostedUI':
132 this._storage.setItem('amplify-signin-with-hostedUI', 'true');
133 break;
134 }
135 });
136 }
137
138 public getModuleName() {
139 return 'Auth';
140 }
141
142 configure(config?) {
143 if (!config) return this._config || {};
144 logger.debug('configure Auth');
145 const conf = Object.assign(
146 {},
147 this._config,
148 Parser.parseMobilehubConfig(config).Auth,
149 config
150 );
151 this._config = conf;
152 const {
153 userPoolId,
154 userPoolWebClientId,
155 cookieStorage,
156 oauth,
157 region,
158 identityPoolId,
159 mandatorySignIn,
160 refreshHandlers,
161 identityPoolRegion,
162 clientMetadata,
163 endpoint,
164 } = this._config;
165
166 if (!this._config.storage) {
167 // backward compatability
168 if (cookieStorage) this._storage = new CookieStorage(cookieStorage);
169 else {
170 this._storage = config.ssr
171 ? new UniversalStorage()
172 : new StorageHelper().getStorage();
173 }
174 } else {
175 if (!this._isValidAuthStorage(this._config.storage)) {
176 logger.error('The storage in the Auth config is not valid!');
177 throw new Error('Empty storage object');
178 }
179 this._storage = this._config.storage;
180 }
181
182 this._storageSync = Promise.resolve();
183 if (typeof this._storage['sync'] === 'function') {
184 this._storageSync = this._storage['sync']();
185 }
186
187 if (userPoolId) {
188 const userPoolData: ICognitoUserPoolData = {
189 UserPoolId: userPoolId,
190 ClientId: userPoolWebClientId,
191 endpoint,
192 };
193 userPoolData.Storage = this._storage;
194
195 this.userPool = new CognitoUserPool(
196 userPoolData,
197 this.wrapRefreshSessionCallback
198 );
199 }
200
201 this.Credentials.configure({
202 mandatorySignIn,
203 region: identityPoolRegion || region,
204 userPoolId,
205 identityPoolId,
206 refreshHandlers,
207 storage: this._storage,
208 });
209
210 // initialize cognitoauth client if hosted ui options provided
211 // to keep backward compatibility:
212 const cognitoHostedUIConfig = oauth
213 ? isCognitoHostedOpts(this._config.oauth)
214 ? oauth
215 : (<any>oauth).awsCognito
216 : undefined;
217
218 if (cognitoHostedUIConfig) {
219 const cognitoAuthParams = Object.assign(
220 {
221 cognitoClientId: userPoolWebClientId,
222 UserPoolId: userPoolId,
223 domain: cognitoHostedUIConfig['domain'],
224 scopes: cognitoHostedUIConfig['scope'],
225 redirectSignIn: cognitoHostedUIConfig['redirectSignIn'],
226 redirectSignOut: cognitoHostedUIConfig['redirectSignOut'],
227 responseType: cognitoHostedUIConfig['responseType'],
228 Storage: this._storage,
229 urlOpener: cognitoHostedUIConfig['urlOpener'],
230 clientMetadata,
231 },
232 cognitoHostedUIConfig['options']
233 );
234
235 this._oAuthHandler = new OAuth({
236 scopes: cognitoAuthParams.scopes,
237 config: cognitoAuthParams,
238 cognitoClientId: cognitoAuthParams.cognitoClientId,
239 });
240
241 // **NOTE** - Remove this in a future major release as it is a breaking change
242 // Prevents _handleAuthResponse from being called multiple times in Expo
243 // See https://github.com/aws-amplify/amplify-js/issues/4388
244 const usedResponseUrls = {};
245 urlListener(({ url }) => {
246 if (usedResponseUrls[url]) {
247 return;
248 }
249
250 usedResponseUrls[url] = true;
251 this._handleAuthResponse(url);
252 });
253 }
254
255 dispatchAuthEvent(
256 'configured',
257 null,
258 `The Auth category has been configured successfully`
259 );
260 return this._config;
261 }
262
263 wrapRefreshSessionCallback = (callback: NodeCallback.Any) => {
264 const wrapped: NodeCallback.Any = (error, data) => {
265 if (data) {
266 dispatchAuthEvent('tokenRefresh', undefined, `New token retrieved`);
267 } else {
268 dispatchAuthEvent(
269 'tokenRefresh_failure',
270 error,
271 `Failed to retrieve new token`
272 );
273 }
274 return callback(error, data);
275 };
276 return wrapped;
277 } // prettier-ignore
278
279 /**
280 * Sign up with username, password and other attributes like phone, email
281 * @param {String | object} params - The user attributes used for signin
282 * @param {String[]} restOfAttrs - for the backward compatability
283 * @return - A promise resolves callback data if success
284 */
285 public signUp(
286 params: string | SignUpParams,
287 ...restOfAttrs: string[]
288 ): Promise<ISignUpResult> {
289 if (!this.userPool) {
290 return this.rejectNoUserPool();
291 }
292
293 let username: string = null;
294 let password: string = null;
295 const attributes: CognitoUserAttribute[] = [];
296 let validationData: CognitoUserAttribute[] = null;
297 let clientMetadata;
298
299 if (params && typeof params === 'string') {
300 username = params;
301 password = restOfAttrs ? restOfAttrs[0] : null;
302 const email: string = restOfAttrs ? restOfAttrs[1] : null;
303 const phone_number: string = restOfAttrs ? restOfAttrs[2] : null;
304
305 if (email)
306 attributes.push(
307 new CognitoUserAttribute({ Name: 'email', Value: email })
308 );
309
310 if (phone_number)
311 attributes.push(
312 new CognitoUserAttribute({
313 Name: 'phone_number',
314 Value: phone_number,
315 })
316 );
317 } else if (params && typeof params === 'object') {
318 username = params['username'];
319 password = params['password'];
320
321 if (params && params.clientMetadata) {
322 clientMetadata = params.clientMetadata;
323 } else if (this._config.clientMetadata) {
324 clientMetadata = this._config.clientMetadata;
325 }
326
327 const attrs = params['attributes'];
328 if (attrs) {
329 Object.keys(attrs).map(key => {
330 attributes.push(
331 new CognitoUserAttribute({ Name: key, Value: attrs[key] })
332 );
333 });
334 }
335
336 const validationDataObject = params['validationData'];
337 if (validationDataObject) {
338 validationData = [];
339 Object.keys(validationDataObject).map(key => {
340 validationData.push(
341 new CognitoUserAttribute({
342 Name: key,
343 Value: validationDataObject[key],
344 })
345 );
346 });
347 }
348 } else {
349 return this.rejectAuthError(AuthErrorTypes.SignUpError);
350 }
351
352 if (!username) {
353 return this.rejectAuthError(AuthErrorTypes.EmptyUsername);
354 }
355 if (!password) {
356 return this.rejectAuthError(AuthErrorTypes.EmptyPassword);
357 }
358
359 logger.debug('signUp attrs:', attributes);
360 logger.debug('signUp validation data:', validationData);
361
362 return new Promise((resolve, reject) => {
363 this.userPool.signUp(
364 username,
365 password,
366 attributes,
367 validationData,
368 (err, data) => {
369 if (err) {
370 dispatchAuthEvent(
371 'signUp_failure',
372 err,
373 `${username} failed to signup`
374 );
375 reject(err);
376 } else {
377 dispatchAuthEvent(
378 'signUp',
379 data,
380 `${username} has signed up successfully`
381 );
382 resolve(data);
383 }
384 },
385 clientMetadata
386 );
387 });
388 }
389
390 /**
391 * Send the verification code to confirm sign up
392 * @param {String} username - The username to be confirmed
393 * @param {String} code - The verification code
394 * @param {ConfirmSignUpOptions} options - other options for confirm signup
395 * @return - A promise resolves callback data if success
396 */
397 public confirmSignUp(
398 username: string,
399 code: string,
400 options?: ConfirmSignUpOptions
401 ): Promise<any> {
402 if (!this.userPool) {
403 return this.rejectNoUserPool();
404 }
405 if (!username) {
406 return this.rejectAuthError(AuthErrorTypes.EmptyUsername);
407 }
408 if (!code) {
409 return this.rejectAuthError(AuthErrorTypes.EmptyCode);
410 }
411
412 const user = this.createCognitoUser(username);
413 const forceAliasCreation =
414 options && typeof options.forceAliasCreation === 'boolean'
415 ? options.forceAliasCreation
416 : true;
417
418 let clientMetadata;
419 if (options && options.clientMetadata) {
420 clientMetadata = options.clientMetadata;
421 } else if (this._config.clientMetadata) {
422 clientMetadata = this._config.clientMetadata;
423 }
424 return new Promise((resolve, reject) => {
425 user.confirmRegistration(
426 code,
427 forceAliasCreation,
428 (err, data) => {
429 if (err) {
430 reject(err);
431 } else {
432 resolve(data);
433 }
434 },
435 clientMetadata
436 );
437 });
438 }
439
440 /**
441 * Resend the verification code
442 * @param {String} username - The username to be confirmed
443 * @param {ClientMetadata} clientMetadata - Metadata to be passed to Cognito Lambda triggers
444 * @return - A promise resolves code delivery details if successful
445 */
446 public resendSignUp(
447 username: string,
448 clientMetadata: ClientMetaData = this._config.clientMetadata
449 ): Promise<any> {
450 if (!this.userPool) {
451 return this.rejectNoUserPool();
452 }
453 if (!username) {
454 return this.rejectAuthError(AuthErrorTypes.EmptyUsername);
455 }
456
457 const user = this.createCognitoUser(username);
458 return new Promise((resolve, reject) => {
459 user.resendConfirmationCode((err, data) => {
460 if (err) {
461 reject(err);
462 } else {
463 resolve(data);
464 }
465 }, clientMetadata);
466 });
467 }
468
469 /**
470 * Sign in
471 * @param {String | SignInOpts} usernameOrSignInOpts - The username to be signed in or the sign in options
472 * @param {String} password - The password of the username
473 * @return - A promise resolves the CognitoUser
474 */
475 public signIn(
476 usernameOrSignInOpts: string | SignInOpts,
477 pw?: string,
478 clientMetadata: ClientMetaData = this._config.clientMetadata
479 ): Promise<CognitoUser | any> {
480 if (!this.userPool) {
481 return this.rejectNoUserPool();
482 }
483
484 let username = null;
485 let password = null;
486 let validationData = {};
487
488 // for backward compatibility
489 if (typeof usernameOrSignInOpts === 'string') {
490 username = usernameOrSignInOpts;
491 password = pw;
492 } else if (isUsernamePasswordOpts(usernameOrSignInOpts)) {
493 if (typeof pw !== 'undefined') {
494 logger.warn(
495 'The password should be defined under the first parameter object!'
496 );
497 }
498 username = usernameOrSignInOpts.username;
499 password = usernameOrSignInOpts.password;
500 validationData = usernameOrSignInOpts.validationData;
501 } else {
502 return this.rejectAuthError(AuthErrorTypes.InvalidUsername);
503 }
504 if (!username) {
505 return this.rejectAuthError(AuthErrorTypes.EmptyUsername);
506 }
507 const authDetails = new AuthenticationDetails({
508 Username: username,
509 Password: password,
510 ValidationData: validationData,
511 ClientMetadata: clientMetadata,
512 });
513 if (password) {
514 return this.signInWithPassword(authDetails);
515 } else {
516 return this.signInWithoutPassword(authDetails);
517 }
518 }
519
520 /**
521 * Return an object with the authentication callbacks
522 * @param {CognitoUser} user - the cognito user object
523 * @param {} resolve - function called when resolving the current step
524 * @param {} reject - function called when rejecting the current step
525 * @return - an object with the callback methods for user authentication
526 */
527 private authCallbacks(
528 user: CognitoUser,
529 resolve: (value?: CognitoUser | any) => void,
530 reject: (value?: any) => void
531 ): IAuthenticationCallback {
532 const that = this;
533 return {
534 onSuccess: async session => {
535 logger.debug(session);
536 delete user['challengeName'];
537 delete user['challengeParam'];
538 try {
539 await this.Credentials.clear();
540 const cred = await this.Credentials.set(session, 'session');
541 logger.debug('succeed to get cognito credentials', cred);
542 } catch (e) {
543 logger.debug('cannot get cognito credentials', e);
544 } finally {
545 try {
546 // In order to get user attributes and MFA methods
547 // We need to trigger currentUserPoolUser again
548 const currentUser = await this.currentUserPoolUser();
549 that.user = currentUser;
550 dispatchAuthEvent(
551 'signIn',
552 currentUser,
553 `A user ${user.getUsername()} has been signed in`
554 );
555 resolve(currentUser);
556 } catch (e) {
557 logger.error('Failed to get the signed in user', e);
558 reject(e);
559 }
560 }
561 },
562 onFailure: err => {
563 logger.debug('signIn failure', err);
564 dispatchAuthEvent(
565 'signIn_failure',
566 err,
567 `${user.getUsername()} failed to signin`
568 );
569 reject(err);
570 },
571 customChallenge: challengeParam => {
572 logger.debug('signIn custom challenge answer required');
573 user['challengeName'] = 'CUSTOM_CHALLENGE';
574 user['challengeParam'] = challengeParam;
575 resolve(user);
576 },
577 mfaRequired: (challengeName, challengeParam) => {
578 logger.debug('signIn MFA required');
579 user['challengeName'] = challengeName;
580 user['challengeParam'] = challengeParam;
581 resolve(user);
582 },
583 mfaSetup: (challengeName, challengeParam) => {
584 logger.debug('signIn mfa setup', challengeName);
585 user['challengeName'] = challengeName;
586 user['challengeParam'] = challengeParam;
587 resolve(user);
588 },
589 newPasswordRequired: (userAttributes, requiredAttributes) => {
590 logger.debug('signIn new password');
591 user['challengeName'] = 'NEW_PASSWORD_REQUIRED';
592 user['challengeParam'] = {
593 userAttributes,
594 requiredAttributes,
595 };
596 resolve(user);
597 },
598 totpRequired: (challengeName, challengeParam) => {
599 logger.debug('signIn totpRequired');
600 user['challengeName'] = challengeName;
601 user['challengeParam'] = challengeParam;
602 resolve(user);
603 },
604 selectMFAType: (challengeName, challengeParam) => {
605 logger.debug('signIn selectMFAType', challengeName);
606 user['challengeName'] = challengeName;
607 user['challengeParam'] = challengeParam;
608 resolve(user);
609 },
610 };
611 }
612
613 /**
614 * Sign in with a password
615 * @private
616 * @param {AuthenticationDetails} authDetails - the user sign in data
617 * @return - A promise resolves the CognitoUser object if success or mfa required
618 */
619 private signInWithPassword(
620 authDetails: AuthenticationDetails
621 ): Promise<CognitoUser | any> {
622 if (this.pendingSignIn) {
623 throw new Error('Pending sign-in attempt already in progress');
624 }
625
626 const user = this.createCognitoUser(authDetails.getUsername());
627
628 this.pendingSignIn = new Promise((resolve, reject) => {
629 user.authenticateUser(
630 authDetails,
631 this.authCallbacks(
632 user,
633 value => {
634 this.pendingSignIn = null;
635 resolve(value);
636 },
637 error => {
638 this.pendingSignIn = null;
639 reject(error);
640 }
641 )
642 );
643 });
644
645 return this.pendingSignIn;
646 }
647
648 /**
649 * Sign in without a password
650 * @private
651 * @param {AuthenticationDetails} authDetails - the user sign in data
652 * @return - A promise resolves the CognitoUser object if success or mfa required
653 */
654 private signInWithoutPassword(
655 authDetails: AuthenticationDetails
656 ): Promise<CognitoUser | any> {
657 const user = this.createCognitoUser(authDetails.getUsername());
658 user.setAuthenticationFlowType('CUSTOM_AUTH');
659
660 return new Promise((resolve, reject) => {
661 user.initiateAuth(authDetails, this.authCallbacks(user, resolve, reject));
662 });
663 }
664
665 /**
666 * This was previously used by an authenticated user to get MFAOptions,
667 * but no longer returns a meaningful response. Refer to the documentation for
668 * how to setup and use MFA: https://docs.amplify.aws/lib/auth/mfa/q/platform/js
669 * @deprecated
670 * @param {CognitoUser} user - the current user
671 * @return - A promise resolves the current preferred mfa option if success
672 */
673 public getMFAOptions(user: CognitoUser | any): Promise<MFAOption[]> {
674 return new Promise((res, rej) => {
675 user.getMFAOptions((err, mfaOptions) => {
676 if (err) {
677 logger.debug('get MFA Options failed', err);
678 rej(err);
679 return;
680 }
681 logger.debug('get MFA options success', mfaOptions);
682 res(mfaOptions);
683 return;
684 });
685 });
686 }
687
688 /**
689 * get preferred mfa method
690 * @param {CognitoUser} user - the current cognito user
691 * @param {GetPreferredMFAOpts} params - options for getting the current user preferred MFA
692 */
693 public getPreferredMFA(
694 user: CognitoUser | any,
695 params?: GetPreferredMFAOpts
696 ): Promise<string> {
697 const that = this;
698 return new Promise((res, rej) => {
699 const clientMetadata = this._config.clientMetadata; // TODO: verify behavior if this is override during signIn
700
701 const bypassCache = params ? params.bypassCache : false;
702 user.getUserData(
703 async (err, data) => {
704 if (err) {
705 logger.debug('getting preferred mfa failed', err);
706 if (this.isSessionInvalid(err)) {
707 try {
708 await this.cleanUpInvalidSession(user);
709 } catch (cleanUpError) {
710 rej(
711 new Error(
712 `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}`
713 )
714 );
715 return;
716 }
717 }
718 rej(err);
719 return;
720 }
721
722 const mfaType = that._getMfaTypeFromUserData(data);
723 if (!mfaType) {
724 rej('invalid MFA Type');
725 return;
726 } else {
727 res(mfaType);
728 return;
729 }
730 },
731 { bypassCache, clientMetadata }
732 );
733 });
734 }
735
736 private _getMfaTypeFromUserData(data) {
737 let ret = null;
738 const preferredMFA = data.PreferredMfaSetting;
739 // if the user has used Auth.setPreferredMFA() to setup the mfa type
740 // then the "PreferredMfaSetting" would exist in the response
741 if (preferredMFA) {
742 ret = preferredMFA;
743 } else {
744 // if mfaList exists but empty, then its noMFA
745 const mfaList = data.UserMFASettingList;
746 if (!mfaList) {
747 // if SMS was enabled by using Auth.enableSMS(),
748 // the response would contain MFAOptions
749 // as for now Cognito only supports for SMS, so we will say it is 'SMS_MFA'
750 // if it does not exist, then it should be NOMFA
751 const MFAOptions = data.MFAOptions;
752 if (MFAOptions) {
753 ret = 'SMS_MFA';
754 } else {
755 ret = 'NOMFA';
756 }
757 } else if (mfaList.length === 0) {
758 ret = 'NOMFA';
759 } else {
760 logger.debug('invalid case for getPreferredMFA', data);
761 }
762 }
763 return ret;
764 }
765
766 private _getUserData(user, params) {
767 return new Promise((res, rej) => {
768 user.getUserData(async (err, data) => {
769 if (err) {
770 logger.debug('getting user data failed', err);
771 if (this.isSessionInvalid(err)) {
772 try {
773 await this.cleanUpInvalidSession(user);
774 } catch (cleanUpError) {
775 rej(
776 new Error(
777 `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}`
778 )
779 );
780 return;
781 }
782 }
783 rej(err);
784 return;
785 } else {
786 res(data);
787 }
788 }, params);
789 });
790 }
791
792 /**
793 * set preferred MFA method
794 * @param {CognitoUser} user - the current Cognito user
795 * @param {string} mfaMethod - preferred mfa method
796 * @return - A promise resolve if success
797 */
798 public async setPreferredMFA(
799 user: CognitoUser | any,
800 mfaMethod: 'TOTP' | 'SMS' | 'NOMFA' | 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA'
801 ): Promise<string> {
802 const clientMetadata = this._config.clientMetadata; // TODO: verify behavior if this is override during signIn
803
804 const userData = await this._getUserData(user, {
805 bypassCache: true,
806 clientMetadata,
807 });
808 let smsMfaSettings = null;
809 let totpMfaSettings = null;
810
811 switch (mfaMethod) {
812 case 'TOTP':
813 case 'SOFTWARE_TOKEN_MFA':
814 totpMfaSettings = {
815 PreferredMfa: true,
816 Enabled: true,
817 };
818 break;
819 case 'SMS':
820 case 'SMS_MFA':
821 smsMfaSettings = {
822 PreferredMfa: true,
823 Enabled: true,
824 };
825 break;
826 case 'NOMFA':
827 const mfaList = userData['UserMFASettingList'];
828 const currentMFAType = await this._getMfaTypeFromUserData(userData);
829 if (currentMFAType === 'NOMFA') {
830 return Promise.resolve('No change for mfa type');
831 } else if (currentMFAType === 'SMS_MFA') {
832 smsMfaSettings = {
833 PreferredMfa: false,
834 Enabled: false,
835 };
836 } else if (currentMFAType === 'SOFTWARE_TOKEN_MFA') {
837 totpMfaSettings = {
838 PreferredMfa: false,
839 Enabled: false,
840 };
841 } else {
842 return this.rejectAuthError(AuthErrorTypes.InvalidMFA);
843 }
844 // if there is a UserMFASettingList in the response
845 // we need to disable every mfa type in that list
846 if (mfaList && mfaList.length !== 0) {
847 // to disable SMS or TOTP if exists in that list
848 mfaList.forEach(mfaType => {
849 if (mfaType === 'SMS_MFA') {
850 smsMfaSettings = {
851 PreferredMfa: false,
852 Enabled: false,
853 };
854 } else if (mfaType === 'SOFTWARE_TOKEN_MFA') {
855 totpMfaSettings = {
856 PreferredMfa: false,
857 Enabled: false,
858 };
859 }
860 });
861 }
862 break;
863 default:
864 logger.debug('no validmfa method provided');
865 return this.rejectAuthError(AuthErrorTypes.NoMFA);
866 }
867
868 const that = this;
869 return new Promise<string>((res, rej) => {
870 user.setUserMfaPreference(
871 smsMfaSettings,
872 totpMfaSettings,
873 (err, result) => {
874 if (err) {
875 logger.debug('Set user mfa preference error', err);
876 return rej(err);
877 }
878 logger.debug('Set user mfa success', result);
879 logger.debug('Caching the latest user data into local');
880 // cache the latest result into user data
881 user.getUserData(
882 async (err, data) => {
883 if (err) {
884 logger.debug('getting user data failed', err);
885 if (this.isSessionInvalid(err)) {
886 try {
887 await this.cleanUpInvalidSession(user);
888 } catch (cleanUpError) {
889 rej(
890 new Error(
891 `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}`
892 )
893 );
894 return;
895 }
896 }
897 return rej(err);
898 } else {
899 return res(result);
900 }
901 },
902 {
903 bypassCache: true,
904 clientMetadata,
905 }
906 );
907 }
908 );
909 });
910 }
911
912 /**
913 * disable SMS
914 * @deprecated
915 * @param {CognitoUser} user - the current user
916 * @return - A promise resolves is success
917 */
918 public disableSMS(user: CognitoUser): Promise<string> {
919 return new Promise((res, rej) => {
920 user.disableMFA((err, data) => {
921 if (err) {
922 logger.debug('disable mfa failed', err);
923 rej(err);
924 return;
925 }
926 logger.debug('disable mfa succeed', data);
927 res(data);
928 return;
929 });
930 });
931 }
932
933 /**
934 * enable SMS
935 * @deprecated
936 * @param {CognitoUser} user - the current user
937 * @return - A promise resolves is success
938 */
939 public enableSMS(user: CognitoUser): Promise<string> {
940 return new Promise((res, rej) => {
941 user.enableMFA((err, data) => {
942 if (err) {
943 logger.debug('enable mfa failed', err);
944 rej(err);
945 return;
946 }
947 logger.debug('enable mfa succeed', data);
948 res(data);
949 return;
950 });
951 });
952 }
953
954 /**
955 * Setup TOTP
956 * @param {CognitoUser} user - the current user
957 * @return - A promise resolves with the secret code if success
958 */
959 public setupTOTP(user: CognitoUser | any): Promise<string> {
960 return new Promise((res, rej) => {
961 user.associateSoftwareToken({
962 onFailure: err => {
963 logger.debug('associateSoftwareToken failed', err);
964 rej(err);
965 return;
966 },
967 associateSecretCode: secretCode => {
968 logger.debug('associateSoftwareToken sucess', secretCode);
969 res(secretCode);
970 return;
971 },
972 });
973 });
974 }
975
976 /**
977 * verify TOTP setup
978 * @param {CognitoUser} user - the current user
979 * @param {string} challengeAnswer - challenge answer
980 * @return - A promise resolves is success
981 */
982 public verifyTotpToken(
983 user: CognitoUser | any,
984 challengeAnswer: string
985 ): Promise<CognitoUserSession> {
986 logger.debug('verification totp token', user, challengeAnswer);
987 return new Promise((res, rej) => {
988 user.verifySoftwareToken(challengeAnswer, 'My TOTP device', {
989 onFailure: err => {
990 logger.debug('verifyTotpToken failed', err);
991 rej(err);
992 return;
993 },
994 onSuccess: data => {
995 dispatchAuthEvent(
996 'signIn',
997 user,
998 `A user ${user.getUsername()} has been signed in`
999 );
1000 logger.debug('verifyTotpToken success', data);
1001 res(data);
1002 return;
1003 },
1004 });
1005 });
1006 }
1007
1008 /**
1009 * Send MFA code to confirm sign in
1010 * @param {Object} user - The CognitoUser object
1011 * @param {String} code - The confirmation code
1012 */
1013 public confirmSignIn(
1014 user: CognitoUser | any,
1015 code: string,
1016 mfaType?: 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA' | null,
1017 clientMetadata: ClientMetaData = this._config.clientMetadata
1018 ): Promise<CognitoUser | any> {
1019 if (!code) {
1020 return this.rejectAuthError(AuthErrorTypes.EmptyCode);
1021 }
1022
1023 const that = this;
1024 return new Promise((resolve, reject) => {
1025 user.sendMFACode(
1026 code,
1027 {
1028 onSuccess: async session => {
1029 logger.debug(session);
1030 try {
1031 await this.Credentials.clear();
1032 const cred = await this.Credentials.set(session, 'session');
1033 logger.debug('succeed to get cognito credentials', cred);
1034 } catch (e) {
1035 logger.debug('cannot get cognito credentials', e);
1036 } finally {
1037 that.user = user;
1038
1039 dispatchAuthEvent(
1040 'signIn',
1041 user,
1042 `A user ${user.getUsername()} has been signed in`
1043 );
1044 resolve(user);
1045 }
1046 },
1047 onFailure: err => {
1048 logger.debug('confirm signIn failure', err);
1049 reject(err);
1050 },
1051 },
1052 mfaType,
1053 clientMetadata
1054 );
1055 });
1056 }
1057
1058 public completeNewPassword(
1059 user: CognitoUser | any,
1060 password: string,
1061 requiredAttributes: any = {},
1062 clientMetadata: ClientMetaData = this._config.clientMetadata
1063 ): Promise<CognitoUser | any> {
1064 if (!password) {
1065 return this.rejectAuthError(AuthErrorTypes.EmptyPassword);
1066 }
1067
1068 const that = this;
1069 return new Promise((resolve, reject) => {
1070 user.completeNewPasswordChallenge(
1071 password,
1072 requiredAttributes,
1073 {
1074 onSuccess: async session => {
1075 logger.debug(session);
1076 try {
1077 await this.Credentials.clear();
1078 const cred = await this.Credentials.set(session, 'session');
1079 logger.debug('succeed to get cognito credentials', cred);
1080 } catch (e) {
1081 logger.debug('cannot get cognito credentials', e);
1082 } finally {
1083 that.user = user;
1084 dispatchAuthEvent(
1085 'signIn',
1086 user,
1087 `A user ${user.getUsername()} has been signed in`
1088 );
1089 resolve(user);
1090 }
1091 },
1092 onFailure: err => {
1093 logger.debug('completeNewPassword failure', err);
1094 dispatchAuthEvent(
1095 'completeNewPassword_failure',
1096 err,
1097 `${this.user} failed to complete the new password flow`
1098 );
1099 reject(err);
1100 },
1101 mfaRequired: (challengeName, challengeParam) => {
1102 logger.debug('signIn MFA required');
1103 user['challengeName'] = challengeName;
1104 user['challengeParam'] = challengeParam;
1105 resolve(user);
1106 },
1107 mfaSetup: (challengeName, challengeParam) => {
1108 logger.debug('signIn mfa setup', challengeName);
1109 user['challengeName'] = challengeName;
1110 user['challengeParam'] = challengeParam;
1111 resolve(user);
1112 },
1113 totpRequired: (challengeName, challengeParam) => {
1114 logger.debug('signIn mfa setup', challengeName);
1115 user['challengeName'] = challengeName;
1116 user['challengeParam'] = challengeParam;
1117 resolve(user);
1118 },
1119 },
1120 clientMetadata
1121 );
1122 });
1123 }
1124
1125 /**
1126 * Send the answer to a custom challenge
1127 * @param {CognitoUser} user - The CognitoUser object
1128 * @param {String} challengeResponses - The confirmation code
1129 */
1130 public sendCustomChallengeAnswer(
1131 user: CognitoUser | any,
1132 challengeResponses: string,
1133 clientMetadata: ClientMetaData = this._config.clientMetadata
1134 ): Promise<CognitoUser | any> {
1135 if (!this.userPool) {
1136 return this.rejectNoUserPool();
1137 }
1138 if (!challengeResponses) {
1139 return this.rejectAuthError(AuthErrorTypes.EmptyChallengeResponse);
1140 }
1141
1142 const that = this;
1143 return new Promise((resolve, reject) => {
1144 user.sendCustomChallengeAnswer(
1145 challengeResponses,
1146 this.authCallbacks(user, resolve, reject),
1147 clientMetadata
1148 );
1149 });
1150 }
1151
1152 /**
1153 * Delete an authenticated users' attributes
1154 * @param {CognitoUser} - The currently logged in user object
1155 * @return {Promise}
1156 **/
1157 public deleteUserAttributes(
1158 user: CognitoUser | any,
1159 attributeNames: string[]
1160 ) {
1161 const that = this;
1162 return new Promise((resolve, reject) => {
1163 that.userSession(user).then(session => {
1164 user.deleteAttributes(attributeNames, (err, result) => {
1165 if (err) {
1166 return reject(err);
1167 } else {
1168 return resolve(result);
1169 }
1170 });
1171 });
1172 });
1173 }
1174
1175 /**
1176 * Delete the current authenticated user
1177 * @return {Promise}
1178 **/
1179 // TODO: Check return type void
1180 public async deleteUser(): Promise<string | void> {
1181 try {
1182 await this._storageSync;
1183 } catch (e) {
1184 logger.debug('Failed to sync cache info into memory', e);
1185 throw new Error(e);
1186 }
1187
1188 const isSignedInHostedUI =
1189 this._oAuthHandler &&
1190 this._storage.getItem('amplify-signin-with-hostedUI') === 'true';
1191
1192 return new Promise(async (res, rej) => {
1193 if (this.userPool) {
1194 const user = this.userPool.getCurrentUser();
1195
1196 if (!user) {
1197 logger.debug('Failed to get user from user pool');
1198 return rej(new Error('No current user.'));
1199 } else {
1200 user.getSession(async (err, session) => {
1201 if (err) {
1202 logger.debug('Failed to get the user session', err);
1203 if (this.isSessionInvalid(err)) {
1204 try {
1205 await this.cleanUpInvalidSession(user);
1206 } catch (cleanUpError) {
1207 rej(
1208 new Error(
1209 `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}`
1210 )
1211 );
1212 return;
1213 }
1214 }
1215 return rej(err);
1216 } else {
1217 user.deleteUser((err, result: string) => {
1218 if (err) {
1219 rej(err);
1220 } else {
1221 dispatchAuthEvent(
1222 'userDeleted',
1223 result,
1224 'The authenticated user has been deleted.'
1225 );
1226 user.signOut();
1227 this.user = null;
1228 try {
1229 this.cleanCachedItems(); // clean aws credentials
1230 } catch (e) {
1231 // TODO: change to rejects in refactor
1232 logger.debug('failed to clear cached items');
1233 }
1234
1235 if (isSignedInHostedUI) {
1236 this.oAuthSignOutRedirect(res, rej);
1237 } else {
1238 dispatchAuthEvent(
1239 'signOut',
1240 this.user,
1241 `A user has been signed out`
1242 );
1243 res(result);
1244 }
1245 }
1246 });
1247 }
1248 });
1249 }
1250 } else {
1251 logger.debug('no Congito User pool');
1252 rej(new Error('Cognito User pool does not exist'));
1253 }
1254 });
1255 }
1256
1257 /**
1258 * Update an authenticated users' attributes
1259 * @param {CognitoUser} - The currently logged in user object
1260 * @return {Promise}
1261 **/
1262 public updateUserAttributes(
1263 user: CognitoUser | any,
1264 attributes: object,
1265 clientMetadata: ClientMetaData = this._config.clientMetadata
1266 ): Promise<string> {
1267 const attributeList: ICognitoUserAttributeData[] = [];
1268 const that = this;
1269 return new Promise((resolve, reject) => {
1270 that.userSession(user).then(session => {
1271 for (const key in attributes) {
1272 if (key !== 'sub' && key.indexOf('_verified') < 0) {
1273 const attr: ICognitoUserAttributeData = {
1274 Name: key,
1275 Value: attributes[key],
1276 };
1277 attributeList.push(attr);
1278 }
1279 }
1280 user.updateAttributes(
1281 attributeList,
1282 (err, result) => {
1283 if (err) {
1284 return reject(err);
1285 } else {
1286 return resolve(result);
1287 }
1288 },
1289 clientMetadata
1290 );
1291 });
1292 });
1293 }
1294 /**
1295 * Return user attributes
1296 * @param {Object} user - The CognitoUser object
1297 * @return - A promise resolves to user attributes if success
1298 */
1299 public userAttributes(
1300 user: CognitoUser | any
1301 ): Promise<CognitoUserAttribute[]> {
1302 return new Promise((resolve, reject) => {
1303 this.userSession(user).then(session => {
1304 user.getUserAttributes((err, attributes) => {
1305 if (err) {
1306 reject(err);
1307 } else {
1308 resolve(attributes);
1309 }
1310 });
1311 });
1312 });
1313 }
1314
1315 public verifiedContact(user: CognitoUser | any) {
1316 const that = this;
1317 return this.userAttributes(user).then(attributes => {
1318 const attrs = that.attributesToObject(attributes);
1319 const unverified = {};
1320 const verified = {};
1321 if (attrs['email']) {
1322 if (attrs['email_verified']) {
1323 verified['email'] = attrs['email'];
1324 } else {
1325 unverified['email'] = attrs['email'];
1326 }
1327 }
1328 if (attrs['phone_number']) {
1329 if (attrs['phone_number_verified']) {
1330 verified['phone_number'] = attrs['phone_number'];
1331 } else {
1332 unverified['phone_number'] = attrs['phone_number'];
1333 }
1334 }
1335 return {
1336 verified,
1337 unverified,
1338 };
1339 });
1340 }
1341
1342 private isErrorWithMessage(err: any): err is { message: string } {
1343 return (
1344 typeof err === 'object' &&
1345 Object.prototype.hasOwnProperty.call(err, 'message')
1346 );
1347 }
1348
1349 // Session revoked by another app
1350 private isTokenRevokedError(
1351 err: any
1352 ): err is { message: 'Access Token has been revoked' } {
1353 return (
1354 this.isErrorWithMessage(err) &&
1355 err.message === 'Access Token has been revoked'
1356 );
1357 }
1358
1359 private isRefreshTokenRevokedError(
1360 err: any
1361 ): err is { message: 'Refresh Token has been revoked' } {
1362 return (
1363 this.isErrorWithMessage(err) &&
1364 err.message === 'Refresh Token has been revoked'
1365 );
1366 }
1367
1368 private isUserDisabledError(
1369 err: any
1370 ): err is { message: 'User is disabled.' } {
1371 return this.isErrorWithMessage(err) && err.message === 'User is disabled.';
1372 }
1373
1374 private isUserDoesNotExistError(
1375 err: any
1376 ): err is { message: 'User does not exist.' } {
1377 return (
1378 this.isErrorWithMessage(err) && err.message === 'User does not exist.'
1379 );
1380 }
1381
1382 private isRefreshTokenExpiredError(
1383 err: any
1384 ): err is { message: 'Refresh Token has expired' } {
1385 return (
1386 this.isErrorWithMessage(err) &&
1387 err.message === 'Refresh Token has expired'
1388 );
1389 }
1390
1391 private isSignedInHostedUI() {
1392 return (
1393 this._oAuthHandler &&
1394 this._storage.getItem('amplify-signin-with-hostedUI') === 'true'
1395 );
1396 }
1397
1398 private isSessionInvalid(err: any) {
1399 return (
1400 this.isUserDisabledError(err) ||
1401 this.isUserDoesNotExistError(err) ||
1402 this.isTokenRevokedError(err) ||
1403 this.isRefreshTokenRevokedError(err) ||
1404 this.isRefreshTokenExpiredError(err)
1405 );
1406 }
1407
1408 private async cleanUpInvalidSession(user: CognitoUser) {
1409 user.signOut();
1410 this.user = null;
1411 try {
1412 await this.cleanCachedItems(); // clean aws credentials
1413 } catch (e) {
1414 logger.debug('failed to clear cached items');
1415 }
1416 if (this.isSignedInHostedUI()) {
1417 return new Promise((res, rej) => {
1418 this.oAuthSignOutRedirect(res, rej);
1419 });
1420 } else {
1421 dispatchAuthEvent('signOut', this.user, `A user has been signed out`);
1422 }
1423 }
1424
1425 /**
1426 * Get current authenticated user
1427 * @return - A promise resolves to current authenticated CognitoUser if success
1428 */
1429 public currentUserPoolUser(
1430 params?: CurrentUserOpts
1431 ): Promise<CognitoUser | any> {
1432 if (!this.userPool) {
1433 return this.rejectNoUserPool();
1434 }
1435
1436 return new Promise((res, rej) => {
1437 this._storageSync
1438 .then(async () => {
1439 if (this.isOAuthInProgress()) {
1440 logger.debug('OAuth signIn in progress, waiting for resolution...');
1441
1442 await new Promise(res => {
1443 const timeoutId = setTimeout(() => {
1444 logger.debug('OAuth signIn in progress timeout');
1445
1446 Hub.remove('auth', hostedUISignCallback);
1447
1448 res();
1449 }, OAUTH_FLOW_MS_TIMEOUT);
1450
1451 Hub.listen('auth', hostedUISignCallback);
1452
1453 function hostedUISignCallback({ payload }) {
1454 const { event } = payload;
1455
1456 if (
1457 event === 'cognitoHostedUI' ||
1458 event === 'cognitoHostedUI_failure'
1459 ) {
1460 logger.debug(`OAuth signIn resolved: ${event}`);
1461 clearTimeout(timeoutId);
1462
1463 Hub.remove('auth', hostedUISignCallback);
1464
1465 res();
1466 }
1467 }
1468 });
1469 }
1470
1471 const user = this.userPool.getCurrentUser();
1472
1473 if (!user) {
1474 logger.debug('Failed to get user from user pool');
1475 rej('No current user');
1476 return;
1477 }
1478
1479 const clientMetadata = this._config.clientMetadata; // TODO: verify behavior if this is override during signIn
1480
1481 // refresh the session if the session expired.
1482 user.getSession(
1483 async (err, session) => {
1484 if (err) {
1485 logger.debug('Failed to get the user session', err);
1486 if (this.isSessionInvalid(err)) {
1487 try {
1488 await this.cleanUpInvalidSession(user);
1489 } catch (cleanUpError) {
1490 rej(
1491 new Error(
1492 `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}`
1493 )
1494 );
1495 return;
1496 }
1497 }
1498 rej(err);
1499 return;
1500 }
1501
1502 // get user data from Cognito
1503 const bypassCache = params ? params.bypassCache : false;
1504
1505 if (bypassCache) {
1506 await this.Credentials.clear();
1507 }
1508
1509 const clientMetadata = this._config.clientMetadata; // TODO: verify behavior if this is override during signIn
1510
1511 // validate the token's scope first before calling this function
1512 const { scope = '' } = session.getAccessToken().decodePayload();
1513 if (scope.split(' ').includes(USER_ADMIN_SCOPE)) {
1514 user.getUserData(
1515 async (err, data) => {
1516 if (err) {
1517 logger.debug('getting user data failed', err);
1518 if (this.isSessionInvalid(err)) {
1519 try {
1520 await this.cleanUpInvalidSession(user);
1521 } catch (cleanUpError) {
1522 rej(
1523 new Error(
1524 `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}`
1525 )
1526 );
1527 return;
1528 }
1529 rej(err);
1530 } else {
1531 res(user);
1532 }
1533 return;
1534 }
1535 const preferredMFA = data.PreferredMfaSetting || 'NOMFA';
1536 const attributeList = [];
1537
1538 for (let i = 0; i < data.UserAttributes.length; i++) {
1539 const attribute = {
1540 Name: data.UserAttributes[i].Name,
1541 Value: data.UserAttributes[i].Value,
1542 };
1543 const userAttribute = new CognitoUserAttribute(attribute);
1544 attributeList.push(userAttribute);
1545 }
1546
1547 const attributes = this.attributesToObject(attributeList);
1548 Object.assign(user, { attributes, preferredMFA });
1549 return res(user);
1550 },
1551 { bypassCache, clientMetadata }
1552 );
1553 } else {
1554 logger.debug(
1555 `Unable to get the user data because the ${USER_ADMIN_SCOPE} ` +
1556 `is not in the scopes of the access token`
1557 );
1558 return res(user);
1559 }
1560 },
1561 { clientMetadata }
1562 );
1563 })
1564 .catch(e => {
1565 logger.debug('Failed to sync cache info into memory', e);
1566 return rej(e);
1567 });
1568 });
1569 }
1570
1571 private isOAuthInProgress(): boolean {
1572 return this.oAuthFlowInProgress;
1573 }
1574
1575 /**
1576 * Get current authenticated user
1577 * @param {CurrentUserOpts} - options for getting the current user
1578 * @return - A promise resolves to current authenticated CognitoUser if success
1579 */
1580 public async currentAuthenticatedUser(
1581 params?: CurrentUserOpts
1582 ): Promise<CognitoUser | any> {
1583 logger.debug('getting current authenticated user');
1584 let federatedUser = null;
1585 try {
1586 await this._storageSync;
1587 } catch (e) {
1588 logger.debug('Failed to sync cache info into memory', e);
1589 throw e;
1590 }
1591
1592 try {
1593 const federatedInfo = JSON.parse(
1594 this._storage.getItem('aws-amplify-federatedInfo')
1595 );
1596 if (federatedInfo) {
1597 federatedUser = {
1598 ...federatedInfo.user,
1599 token: federatedInfo.token,
1600 };
1601 }
1602 } catch (e) {
1603 logger.debug('cannot load federated user from auth storage');
1604 }
1605
1606 if (federatedUser) {
1607 this.user = federatedUser;
1608 logger.debug('get current authenticated federated user', this.user);
1609 return this.user;
1610 } else {
1611 logger.debug('get current authenticated userpool user');
1612 let user = null;
1613 try {
1614 user = await this.currentUserPoolUser(params);
1615 } catch (e) {
1616 if (e === 'No userPool') {
1617 logger.error(
1618 'Cannot get the current user because the user pool is missing. ' +
1619 'Please make sure the Auth module is configured with a valid Cognito User Pool ID'
1620 );
1621 }
1622 logger.debug('The user is not authenticated by the error', e);
1623 return Promise.reject('The user is not authenticated');
1624 }
1625 this.user = user;
1626 return this.user;
1627 }
1628 }
1629
1630 /**
1631 * Get current user's session
1632 * @return - A promise resolves to session object if success
1633 */
1634 public currentSession(): Promise<CognitoUserSession> {
1635 const that = this;
1636 logger.debug('Getting current session');
1637 // Purposely not calling the reject method here because we don't need a console error
1638 if (!this.userPool) {
1639 return this.rejectNoUserPool();
1640 }
1641
1642 return new Promise((res, rej) => {
1643 that
1644 .currentUserPoolUser()
1645 .then(user => {
1646 that
1647 .userSession(user)
1648 .then(session => {
1649 res(session);
1650 return;
1651 })
1652 .catch(e => {
1653 logger.debug('Failed to get the current session', e);
1654 rej(e);
1655 return;
1656 });
1657 })
1658 .catch(e => {
1659 logger.debug('Failed to get the current user', e);
1660 rej(e);
1661 return;
1662 });
1663 });
1664 }
1665
1666 /**
1667 * Get the corresponding user session
1668 * @param {Object} user - The CognitoUser object
1669 * @return - A promise resolves to the session
1670 */
1671 public userSession(user): Promise<CognitoUserSession> {
1672 if (!user) {
1673 logger.debug('the user is null');
1674 return this.rejectAuthError(AuthErrorTypes.NoUserSession);
1675 }
1676 const clientMetadata = this._config.clientMetadata; // TODO: verify behavior if this is override during signIn
1677
1678 return new Promise((res, rej) => {
1679 logger.debug('Getting the session from this user:', user);
1680 user.getSession(
1681 async (err, session) => {
1682 if (err) {
1683 logger.debug('Failed to get the session from user', user);
1684 if (this.isSessionInvalid(err)) {
1685 try {
1686 await this.cleanUpInvalidSession(user);
1687 } catch (cleanUpError) {
1688 rej(
1689 new Error(
1690 `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}`
1691 )
1692 );
1693 return;
1694 }
1695 }
1696 rej(err);
1697 return;
1698 } else {
1699 logger.debug('Succeed to get the user session', session);
1700 res(session);
1701 return;
1702 }
1703 },
1704 { clientMetadata }
1705 );
1706 });
1707 }
1708
1709 /**
1710 * Get authenticated credentials of current user.
1711 * @return - A promise resolves to be current user's credentials
1712 */
1713 public async currentUserCredentials(): Promise<ICredentials> {
1714 logger.debug('Getting current user credentials');
1715
1716 try {
1717 await this._storageSync;
1718 } catch (e) {
1719 logger.debug('Failed to sync cache info into memory', e);
1720 throw e;
1721 }
1722
1723 // first to check whether there is federation info in the auth storage
1724 let federatedInfo = null;
1725 try {
1726 federatedInfo = JSON.parse(
1727 this._storage.getItem('aws-amplify-federatedInfo')
1728 );
1729 } catch (e) {
1730 logger.debug('failed to get or parse item aws-amplify-federatedInfo', e);
1731 }
1732
1733 if (federatedInfo) {
1734 // refresh the jwt token here if necessary
1735 return this.Credentials.refreshFederatedToken(federatedInfo);
1736 } else {
1737 return this.currentSession()
1738 .then(session => {
1739 logger.debug('getting session success', session);
1740 return this.Credentials.set(session, 'session');
1741 })
1742 .catch(() => {
1743 logger.debug('getting guest credentials');
1744 return this.Credentials.set(null, 'guest');
1745 });
1746 }
1747 }
1748
1749 public currentCredentials(): Promise<ICredentials> {
1750 logger.debug('getting current credentials');
1751 return this.Credentials.get();
1752 }
1753
1754 /**
1755 * Initiate an attribute confirmation request
1756 * @param {Object} user - The CognitoUser
1757 * @param {Object} attr - The attributes to be verified
1758 * @return - A promise resolves to callback data if success
1759 */
1760 public verifyUserAttribute(
1761 user: CognitoUser | any,
1762 attr: string,
1763 clientMetadata: ClientMetaData = this._config.clientMetadata
1764 ): Promise<void> {
1765 return new Promise((resolve, reject) => {
1766 user.getAttributeVerificationCode(
1767 attr,
1768 {
1769 onSuccess(success) {
1770 return resolve(success);
1771 },
1772 onFailure(err) {
1773 return reject(err);
1774 },
1775 },
1776 clientMetadata
1777 );
1778 });
1779 }
1780
1781 /**
1782 * Confirm an attribute using a confirmation code
1783 * @param {Object} user - The CognitoUser
1784 * @param {Object} attr - The attribute to be verified
1785 * @param {String} code - The confirmation code
1786 * @return - A promise resolves to callback data if success
1787 */
1788 public verifyUserAttributeSubmit(
1789 user: CognitoUser | any,
1790 attr: string,
1791 code: string
1792 ): Promise<string> {
1793 if (!code) {
1794 return this.rejectAuthError(AuthErrorTypes.EmptyCode);
1795 }
1796
1797 return new Promise((resolve, reject) => {
1798 user.verifyAttribute(attr, code, {
1799 onSuccess(data) {
1800 resolve(data);
1801 return;
1802 },
1803 onFailure(err) {
1804 reject(err);
1805 return;
1806 },
1807 });
1808 });
1809 }
1810
1811 public verifyCurrentUserAttribute(attr: string): Promise<void> {
1812 const that = this;
1813 return that
1814 .currentUserPoolUser()
1815 .then(user => that.verifyUserAttribute(user, attr));
1816 }
1817
1818 /**
1819 * Confirm current user's attribute using a confirmation code
1820 * @param {Object} attr - The attribute to be verified
1821 * @param {String} code - The confirmation code
1822 * @return - A promise resolves to callback data if success
1823 */
1824 verifyCurrentUserAttributeSubmit(
1825 attr: string,
1826 code: string
1827 ): Promise<string> {
1828 const that = this;
1829 return that
1830 .currentUserPoolUser()
1831 .then(user => that.verifyUserAttributeSubmit(user, attr, code));
1832 }
1833
1834 private async cognitoIdentitySignOut(
1835 opts: SignOutOpts,
1836 user: CognitoUser | any
1837 ) {
1838 try {
1839 await this._storageSync;
1840 } catch (e) {
1841 logger.debug('Failed to sync cache info into memory', e);
1842 throw e;
1843 }
1844
1845 const isSignedInHostedUI =
1846 this._oAuthHandler &&
1847 this._storage.getItem('amplify-signin-with-hostedUI') === 'true';
1848
1849 return new Promise((res, rej) => {
1850 if (opts && opts.global) {
1851 logger.debug('user global sign out', user);
1852 // in order to use global signout
1853 // we must validate the user as an authenticated user by using getSession
1854 const clientMetadata = this._config.clientMetadata; // TODO: verify behavior if this is override during signIn
1855
1856 user.getSession(
1857 async (err, result) => {
1858 if (err) {
1859 logger.debug('failed to get the user session', err);
1860 if (this.isSessionInvalid(err)) {
1861 try {
1862 await this.cleanUpInvalidSession(user);
1863 } catch (cleanUpError) {
1864 rej(
1865 new Error(
1866 `Session is invalid due to: ${err.message} and failed to clean up invalid session: ${cleanUpError.message}`
1867 )
1868 );
1869 return;
1870 }
1871 }
1872 return rej(err);
1873 }
1874 user.globalSignOut({
1875 onSuccess: data => {
1876 logger.debug('global sign out success');
1877 if (isSignedInHostedUI) {
1878 this.oAuthSignOutRedirect(res, rej);
1879 } else {
1880 return res();
1881 }
1882 },
1883 onFailure: err => {
1884 logger.debug('global sign out failed', err);
1885 return rej(err);
1886 },
1887 });
1888 },
1889 { clientMetadata }
1890 );
1891 } else {
1892 logger.debug('user sign out', user);
1893 user.signOut(() => {
1894 if (isSignedInHostedUI) {
1895 this.oAuthSignOutRedirect(res, rej);
1896 } else {
1897 return res();
1898 }
1899 });
1900 }
1901 });
1902 }
1903
1904 private oAuthSignOutRedirect(
1905 resolve: () => void,
1906 reject: (reason?: any) => void
1907 ) {
1908 const { isBrowser } = JS.browserOrNode();
1909
1910 if (isBrowser) {
1911 this.oAuthSignOutRedirectOrReject(reject);
1912 } else {
1913 this.oAuthSignOutAndResolve(resolve);
1914 }
1915 }
1916
1917 private oAuthSignOutAndResolve(resolve: () => void) {
1918 this._oAuthHandler.signOut();
1919 resolve();
1920 }
1921
1922 private oAuthSignOutRedirectOrReject(reject: (reason?: any) => void) {
1923 this._oAuthHandler.signOut(); // this method redirects url
1924
1925 // App should be redirected to another url otherwise it will reject
1926 setTimeout(() => reject(Error('Signout timeout fail')), 3000);
1927 }
1928
1929 /**
1930 * Sign out method
1931 * @
1932 * @return - A promise resolved if success
1933 */
1934 public async signOut(opts?: SignOutOpts): Promise<any> {
1935 try {
1936 await this.cleanCachedItems();
1937 } catch (e) {
1938 logger.debug('failed to clear cached items');
1939 }
1940
1941 if (this.userPool) {
1942 const user = this.userPool.getCurrentUser();
1943 if (user) {
1944 await this.cognitoIdentitySignOut(opts, user);
1945 } else {
1946 logger.debug('no current Cognito user');
1947 }
1948 } else {
1949 logger.debug('no Cognito User pool');
1950 }
1951
1952 /**
1953 * Note for future refactor - no reliable way to get username with
1954 * Cognito User Pools vs Identity when federating with Social Providers
1955 * This is why we need a well structured session object that can be inspected
1956 * and information passed back in the message below for Hub dispatch
1957 */
1958 dispatchAuthEvent('signOut', this.user, `A user has been signed out`);
1959 this.user = null;
1960 }
1961
1962 private async cleanCachedItems() {
1963 // clear cognito cached item
1964 await this.Credentials.clear();
1965 }
1966
1967 /**
1968 * Change a password for an authenticated user
1969 * @param {Object} user - The CognitoUser object
1970 * @param {String} oldPassword - the current password
1971 * @param {String} newPassword - the requested new password
1972 * @return - A promise resolves if success
1973 */
1974 public changePassword(
1975 user: CognitoUser | any,
1976 oldPassword: string,
1977 newPassword: string,
1978 clientMetadata: ClientMetaData = this._config.clientMetadata
1979 ): Promise<'SUCCESS'> {
1980 return new Promise((resolve, reject) => {
1981 this.userSession(user).then(session => {
1982 user.changePassword(
1983 oldPassword,
1984 newPassword,
1985 (err, data) => {
1986 if (err) {
1987 logger.debug('change password failure', err);
1988 return reject(err);
1989 } else {
1990 return resolve(data);
1991 }
1992 },
1993 clientMetadata
1994 );
1995 });
1996 });
1997 }
1998
1999 /**
2000 * Initiate a forgot password request
2001 * @param {String} username - the username to change password
2002 * @return - A promise resolves if success
2003 */
2004 public forgotPassword(
2005 username: string,
2006 clientMetadata: ClientMetaData = this._config.clientMetadata
2007 ): Promise<any> {
2008 if (!this.userPool) {
2009 return this.rejectNoUserPool();
2010 }
2011 if (!username) {
2012 return this.rejectAuthError(AuthErrorTypes.EmptyUsername);
2013 }
2014
2015 const user = this.createCognitoUser(username);
2016 return new Promise((resolve, reject) => {
2017 user.forgotPassword(
2018 {
2019 onSuccess: () => {
2020 resolve();
2021 return;
2022 },
2023 onFailure: err => {
2024 logger.debug('forgot password failure', err);
2025 dispatchAuthEvent(
2026 'forgotPassword_failure',
2027 err,
2028 `${username} forgotPassword failed`
2029 );
2030 reject(err);
2031 return;
2032 },
2033 inputVerificationCode: data => {
2034 dispatchAuthEvent(
2035 'forgotPassword',
2036 user,
2037 `${username} has initiated forgot password flow`
2038 );
2039 resolve(data);
2040 return;
2041 },
2042 },
2043 clientMetadata
2044 );
2045 });
2046 }
2047
2048 /**
2049 * Confirm a new password using a confirmation Code
2050 * @param {String} username - The username
2051 * @param {String} code - The confirmation code
2052 * @param {String} password - The new password
2053 * @return - A promise that resolves if success
2054 */
2055 public forgotPasswordSubmit(
2056 username: string,
2057 code: string,
2058 password: string,
2059 clientMetadata: ClientMetaData = this._config.clientMetadata
2060 ): Promise<string> {
2061 if (!this.userPool) {
2062 return this.rejectNoUserPool();
2063 }
2064 if (!username) {
2065 return this.rejectAuthError(AuthErrorTypes.EmptyUsername);
2066 }
2067 if (!code) {
2068 return this.rejectAuthError(AuthErrorTypes.EmptyCode);
2069 }
2070 if (!password) {
2071 return this.rejectAuthError(AuthErrorTypes.EmptyPassword);
2072 }
2073
2074 const user = this.createCognitoUser(username);
2075 return new Promise((resolve, reject) => {
2076 user.confirmPassword(
2077 code,
2078 password,
2079 {
2080 onSuccess: success => {
2081 dispatchAuthEvent(
2082 'forgotPasswordSubmit',
2083 user,
2084 `${username} forgotPasswordSubmit successful`
2085 );
2086 resolve(success);
2087 return;
2088 },
2089 onFailure: err => {
2090 dispatchAuthEvent(
2091 'forgotPasswordSubmit_failure',
2092 err,
2093 `${username} forgotPasswordSubmit failed`
2094 );
2095 reject(err);
2096 return;
2097 },
2098 },
2099 clientMetadata
2100 );
2101 });
2102 }
2103
2104 /**
2105 * Get user information
2106 * @async
2107 * @return {Object }- current User's information
2108 */
2109 public async currentUserInfo() {
2110 const source = this.Credentials.getCredSource();
2111
2112 if (!source || source === 'aws' || source === 'userPool') {
2113 const user = await this.currentUserPoolUser().catch(err =>
2114 logger.error(err)
2115 );
2116 if (!user) {
2117 return null;
2118 }
2119
2120 try {
2121 const attributes = await this.userAttributes(user);
2122 const userAttrs: object = this.attributesToObject(attributes);
2123 let credentials = null;
2124 try {
2125 credentials = await this.currentCredentials();
2126 } catch (e) {
2127 logger.debug(
2128 'Failed to retrieve credentials while getting current user info',
2129 e
2130 );
2131 }
2132
2133 const info = {
2134 id: credentials ? credentials.identityId : undefined,
2135 username: user.getUsername(),
2136 attributes: userAttrs,
2137 };
2138 return info;
2139 } catch (err) {
2140 logger.error('currentUserInfo error', err);
2141 return {};
2142 }
2143 }
2144
2145 if (source === 'federated') {
2146 const user = this.user;
2147 return user ? user : {};
2148 }
2149 }
2150
2151 public async federatedSignIn(
2152 options?: FederatedSignInOptions
2153 ): Promise<ICredentials>;
2154 public async federatedSignIn(
2155 provider: LegacyProvider,
2156 response: FederatedResponse,
2157 user: FederatedUser
2158 ): Promise<ICredentials>;
2159 public async federatedSignIn(
2160 options?: FederatedSignInOptionsCustom
2161 ): Promise<ICredentials>;
2162 public async federatedSignIn(
2163 providerOrOptions:
2164 | LegacyProvider
2165 | FederatedSignInOptions
2166 | FederatedSignInOptionsCustom,
2167 response?: FederatedResponse,
2168 user?: FederatedUser
2169 ): Promise<ICredentials> {
2170 if (!this._config.identityPoolId && !this._config.userPoolId) {
2171 throw new Error(
2172 `Federation requires either a User Pool or Identity Pool in config`
2173 );
2174 }
2175
2176 // Ensure backwards compatability
2177 if (typeof providerOrOptions === 'undefined') {
2178 if (this._config.identityPoolId && !this._config.userPoolId) {
2179 throw new Error(
2180 `Federation with Identity Pools requires tokens passed as arguments`
2181 );
2182 }
2183 }
2184
2185 if (
2186 isFederatedSignInOptions(providerOrOptions) ||
2187 isFederatedSignInOptionsCustom(providerOrOptions) ||
2188 hasCustomState(providerOrOptions) ||
2189 typeof providerOrOptions === 'undefined'
2190 ) {
2191 const options = providerOrOptions || {
2192 provider: CognitoHostedUIIdentityProvider.Cognito,
2193 };
2194 const provider = isFederatedSignInOptions(options)
2195 ? options.provider
2196 : (options as FederatedSignInOptionsCustom).customProvider;
2197
2198 const customState = isFederatedSignInOptions(options)
2199 ? options.customState
2200 : (options as FederatedSignInOptionsCustom).customState;
2201
2202 if (this._config.userPoolId) {
2203 const client_id = isCognitoHostedOpts(this._config.oauth)
2204 ? this._config.userPoolWebClientId
2205 : this._config.oauth.clientID;
2206 /*Note: Invenstigate automatically adding trailing slash */
2207 const redirect_uri = isCognitoHostedOpts(this._config.oauth)
2208 ? this._config.oauth.redirectSignIn
2209 : this._config.oauth.redirectUri;
2210
2211 this._oAuthHandler.oauthSignIn(
2212 this._config.oauth.responseType,
2213 this._config.oauth.domain,
2214 redirect_uri,
2215 client_id,
2216 provider,
2217 customState
2218 );
2219 }
2220 } else {
2221 const provider = providerOrOptions;
2222 // To check if the user is already logged in
2223 try {
2224 const loggedInUser = JSON.stringify(
2225 JSON.parse(this._storage.getItem('aws-amplify-federatedInfo')).user
2226 );
2227 if (loggedInUser) {
2228 logger.warn(`There is already a signed in user: ${loggedInUser} in your app.
2229 You should not call Auth.federatedSignIn method again as it may cause unexpected behavior.`);
2230 }
2231 } catch (e) {}
2232
2233 const { token, identity_id, expires_at } = response;
2234 // Because this.Credentials.set would update the user info with identity id
2235 // So we need to retrieve the user again.
2236 const credentials = await this.Credentials.set(
2237 { provider, token, identity_id, user, expires_at },
2238 'federation'
2239 );
2240 const currentUser = await this.currentAuthenticatedUser();
2241 dispatchAuthEvent(
2242 'signIn',
2243 currentUser,
2244 `A user ${currentUser.username} has been signed in`
2245 );
2246 logger.debug('federated sign in credentials', credentials);
2247 return credentials;
2248 }
2249 }
2250
2251 /**
2252 * Used to complete the OAuth flow with or without the Cognito Hosted UI
2253 * @param {String} URL - optional parameter for customers to pass in the response URL
2254 */
2255 private async _handleAuthResponse(URL?: string) {
2256 if (this.oAuthFlowInProgress) {
2257 logger.debug(`Skipping URL ${URL} current flow in progress`);
2258 return;
2259 }
2260
2261 try {
2262 this.oAuthFlowInProgress = true;
2263 if (!this._config.userPoolId) {
2264 throw new Error(
2265 `OAuth responses require a User Pool defined in config`
2266 );
2267 }
2268
2269 dispatchAuthEvent(
2270 'parsingCallbackUrl',
2271 { url: URL },
2272 `The callback url is being parsed`
2273 );
2274
2275 const currentUrl =
2276 URL || (JS.browserOrNode().isBrowser ? window.location.href : '');
2277
2278 const hasCodeOrError = !!(parse(currentUrl).query || '')
2279 .split('&')
2280 .map(entry => entry.split('='))
2281 .find(([k]) => k === 'code' || k === 'error');
2282
2283 const hasTokenOrError = !!(parse(currentUrl).hash || '#')
2284 .substr(1)
2285 .split('&')
2286 .map(entry => entry.split('='))
2287 .find(([k]) => k === 'access_token' || k === 'error');
2288
2289 if (hasCodeOrError || hasTokenOrError) {
2290 this._storage.setItem('amplify-redirected-from-hosted-ui', 'true');
2291 try {
2292 const { accessToken, idToken, refreshToken, state } =
2293 await this._oAuthHandler.handleAuthResponse(currentUrl);
2294 const session = new CognitoUserSession({
2295 IdToken: new CognitoIdToken({ IdToken: idToken }),
2296 RefreshToken: new CognitoRefreshToken({
2297 RefreshToken: refreshToken,
2298 }),
2299 AccessToken: new CognitoAccessToken({
2300 AccessToken: accessToken,
2301 }),
2302 });
2303
2304 let credentials;
2305 // Get AWS Credentials & store if Identity Pool is defined
2306 if (this._config.identityPoolId) {
2307 credentials = await this.Credentials.set(session, 'session');
2308 logger.debug('AWS credentials', credentials);
2309 }
2310
2311 /*
2312 Prior to the request we do sign the custom state along with the state we set. This check will verify
2313 if there is a dash indicated when setting custom state from the request. If a dash is contained
2314 then there is custom state present on the state string.
2315 */
2316 const isCustomStateIncluded = /-/.test(state);
2317
2318 /*
2319 The following is to create a user for the Cognito Identity SDK to store the tokens
2320 When we remove this SDK later that logic will have to be centralized in our new version
2321 */
2322 //#region
2323 const currentUser = this.createCognitoUser(
2324 session.getIdToken().decodePayload()['cognito:username']
2325 );
2326
2327 // This calls cacheTokens() in Cognito SDK
2328 currentUser.setSignInUserSession(session);
2329
2330 if (window && typeof window.history !== 'undefined') {
2331 window.history.replaceState(
2332 {},
2333 null,
2334 (this._config.oauth as AwsCognitoOAuthOpts).redirectSignIn
2335 );
2336 }
2337
2338 dispatchAuthEvent(
2339 'signIn',
2340 currentUser,
2341 `A user ${currentUser.getUsername()} has been signed in`
2342 );
2343 dispatchAuthEvent(
2344 'cognitoHostedUI',
2345 currentUser,
2346 `A user ${currentUser.getUsername()} has been signed in via Cognito Hosted UI`
2347 );
2348
2349 if (isCustomStateIncluded) {
2350 const customState = state.split('-').splice(1).join('-');
2351
2352 dispatchAuthEvent(
2353 'customOAuthState',
2354 urlSafeDecode(customState),
2355 `State for user ${currentUser.getUsername()}`
2356 );
2357 }
2358 //#endregion
2359
2360 return credentials;
2361 } catch (err) {
2362 logger.debug('Error in cognito hosted auth response', err);
2363
2364 // Just like a successful handling of `?code`, replace the window history to "dispose" of the `code`.
2365 // Otherwise, reloading the page will throw errors as the `code` has already been spent.
2366 if (window && typeof window.history !== 'undefined') {
2367 window.history.replaceState(
2368 {},
2369 null,
2370 (this._config.oauth as AwsCognitoOAuthOpts).redirectSignIn
2371 );
2372 }
2373
2374 dispatchAuthEvent(
2375 'signIn_failure',
2376 err,
2377 `The OAuth response flow failed`
2378 );
2379 dispatchAuthEvent(
2380 'cognitoHostedUI_failure',
2381 err,
2382 `A failure occurred when returning to the Cognito Hosted UI`
2383 );
2384 dispatchAuthEvent(
2385 'customState_failure',
2386 err,
2387 `A failure occurred when returning state`
2388 );
2389 }
2390 }
2391 } finally {
2392 this.oAuthFlowInProgress = false;
2393 }
2394 }
2395
2396 /**
2397 * Compact version of credentials
2398 * @param {Object} credentials
2399 * @return {Object} - Credentials
2400 */
2401 public essentialCredentials(credentials): ICredentials {
2402 return {
2403 accessKeyId: credentials.accessKeyId,
2404 sessionToken: credentials.sessionToken,
2405 secretAccessKey: credentials.secretAccessKey,
2406 identityId: credentials.identityId,
2407 authenticated: credentials.authenticated,
2408 };
2409 }
2410
2411 private attributesToObject(attributes) {
2412 const obj = {};
2413 if (attributes) {
2414 attributes.map(attribute => {
2415 if (
2416 attribute.Name === 'email_verified' ||
2417 attribute.Name === 'phone_number_verified'
2418 ) {
2419 obj[attribute.Name] =
2420 this.isTruthyString(attribute.Value) || attribute.Value === true;
2421 } else {
2422 obj[attribute.Name] = attribute.Value;
2423 }
2424 });
2425 }
2426 return obj;
2427 }
2428
2429 private isTruthyString(value: any): boolean {
2430 return (
2431 typeof value.toLowerCase === 'function' && value.toLowerCase() === 'true'
2432 );
2433 }
2434
2435 private createCognitoUser(username: string): CognitoUser {
2436 const userData: ICognitoUserData = {
2437 Username: username,
2438 Pool: this.userPool,
2439 };
2440 userData.Storage = this._storage;
2441
2442 const { authenticationFlowType } = this._config;
2443
2444 const user = new CognitoUser(userData);
2445 if (authenticationFlowType) {
2446 user.setAuthenticationFlowType(authenticationFlowType);
2447 }
2448 return user;
2449 }
2450
2451 private _isValidAuthStorage(obj) {
2452 // We need to check if the obj has the functions of Storage
2453 return (
2454 !!obj &&
2455 typeof obj.getItem === 'function' &&
2456 typeof obj.setItem === 'function' &&
2457 typeof obj.removeItem === 'function' &&
2458 typeof obj.clear === 'function'
2459 );
2460 }
2461
2462 private noUserPoolErrorHandler(config: AuthOptions): AuthErrorTypes {
2463 if (config) {
2464 if (!config.userPoolId || !config.identityPoolId) {
2465 return AuthErrorTypes.MissingAuthConfig;
2466 }
2467 }
2468 return AuthErrorTypes.NoConfig;
2469 }
2470
2471 private rejectAuthError(type: AuthErrorTypes): Promise<never> {
2472 return Promise.reject(new AuthError(type));
2473 }
2474
2475 private rejectNoUserPool(): Promise<never> {
2476 const type = this.noUserPoolErrorHandler(this._config);
2477 return Promise.reject(new NoUserPoolError(type));
2478 }
2479
2480 public async rememberDevice(): Promise<string | AuthError> {
2481 let currUser;
2482
2483 try {
2484 currUser = await this.currentUserPoolUser();
2485 } catch (error) {
2486 logger.debug('The user is not authenticated by the error', error);
2487 return Promise.reject('The user is not authenticated');
2488 }
2489
2490 currUser.getCachedDeviceKeyAndPassword();
2491 return new Promise((res, rej) => {
2492 currUser.setDeviceStatusRemembered({
2493 onSuccess: data => {
2494 res(data);
2495 },
2496 onFailure: err => {
2497 if (err.code === 'InvalidParameterException') {
2498 rej(new AuthError(AuthErrorTypes.DeviceConfig));
2499 } else if (err.code === 'NetworkError') {
2500 rej(new AuthError(AuthErrorTypes.NetworkError));
2501 } else {
2502 rej(err);
2503 }
2504 },
2505 });
2506 });
2507 }
2508
2509 public async forgetDevice(): Promise<void> {
2510 let currUser;
2511
2512 try {
2513 currUser = await this.currentUserPoolUser();
2514 } catch (error) {
2515 logger.debug('The user is not authenticated by the error', error);
2516 return Promise.reject('The user is not authenticated');
2517 }
2518
2519 currUser.getCachedDeviceKeyAndPassword();
2520 return new Promise((res, rej) => {
2521 currUser.forgetDevice({
2522 onSuccess: data => {
2523 res(data);
2524 },
2525 onFailure: err => {
2526 if (err.code === 'InvalidParameterException') {
2527 rej(new AuthError(AuthErrorTypes.DeviceConfig));
2528 } else if (err.code === 'NetworkError') {
2529 rej(new AuthError(AuthErrorTypes.NetworkError));
2530 } else {
2531 rej(err);
2532 }
2533 },
2534 });
2535 });
2536 }
2537
2538 public async fetchDevices(): Promise<IAuthDevice[]> {
2539 let currUser;
2540
2541 try {
2542 currUser = await this.currentUserPoolUser();
2543 } catch (error) {
2544 logger.debug('The user is not authenticated by the error', error);
2545 throw new Error('The user is not authenticated');
2546 }
2547
2548 currUser.getCachedDeviceKeyAndPassword();
2549 return new Promise((res, rej) => {
2550 const cb = {
2551 onSuccess(data) {
2552 const deviceList: IAuthDevice[] = data.Devices.map(device => {
2553 const deviceName =
2554 device.DeviceAttributes.find(
2555 ({ Name }) => Name === 'device_name'
2556 ) || {};
2557
2558 const deviceInfo: IAuthDevice = {
2559 id: device.DeviceKey,
2560 name: deviceName.Value,
2561 };
2562 return deviceInfo;
2563 });
2564 res(deviceList);
2565 },
2566 onFailure: err => {
2567 if (err.code === 'InvalidParameterException') {
2568 rej(new AuthError(AuthErrorTypes.DeviceConfig));
2569 } else if (err.code === 'NetworkError') {
2570 rej(new AuthError(AuthErrorTypes.NetworkError));
2571 } else {
2572 rej(err);
2573 }
2574 },
2575 };
2576 currUser.listDevices(MAX_DEVICES, null, cb);
2577 });
2578 }
2579}
2580
2581export const Auth = new AuthClass(null);
2582
2583Amplify.register(Auth);
2584
\No newline at end of file