UNPKG

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