UNPKG

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