UNPKG

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