UNPKG

19.3 kBPlain TextView Raw
1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3import { ConsoleLogger as Logger } from './Logger';
4import { StorageHelper } from './StorageHelper';
5import { makeQuerablePromise } from './JS';
6import { FacebookOAuth, GoogleOAuth } from './OAuthHelper';
7import { jitteredExponentialRetry } from './Util';
8import { ICredentials } from './types';
9import { Amplify } from './Amplify';
10import { getId, getCredentialsForIdentity } from './AwsClients/CognitoIdentity';
11import { parseAWSExports } from './parseAWSExports';
12import { Hub } from './Hub';
13
14const logger = new Logger('Credentials');
15
16const CREDENTIALS_TTL = 50 * 60 * 1000; // 50 min, can be modified on config if required in the future
17
18const COGNITO_IDENTITY_KEY_PREFIX = 'CognitoIdentityId-';
19
20const AMPLIFY_SYMBOL = (
21 typeof Symbol !== 'undefined' && typeof Symbol.for === 'function'
22 ? Symbol.for('amplify_default')
23 : '@@amplify_default'
24) as Symbol;
25
26const dispatchCredentialsEvent = (
27 event: string,
28 data: any,
29 message: string
30) => {
31 Hub.dispatch('core', { event, data, message }, 'Credentials', AMPLIFY_SYMBOL);
32};
33
34export class CredentialsClass {
35 private _config;
36 private _credentials;
37 private _credentials_source;
38 private _gettingCredPromise = null;
39 private _refreshHandlers = {};
40 private _storage;
41 private _storageSync;
42 private _identityId;
43 private _nextCredentialsRefresh: number;
44
45 // Allow `Auth` to be injected for SSR, but Auth isn't a required dependency for Credentials
46 Auth = undefined;
47
48 constructor(config) {
49 this.configure(config);
50 this._refreshHandlers['google'] = GoogleOAuth.refreshGoogleToken;
51 this._refreshHandlers['facebook'] = FacebookOAuth.refreshFacebookToken;
52 }
53
54 public getModuleName() {
55 return 'Credentials';
56 }
57
58 public getCredSource() {
59 return this._credentials_source;
60 }
61
62 public configure(config) {
63 if (!config) return this._config || {};
64
65 this._config = Object.assign({}, this._config, config);
66 const { refreshHandlers } = this._config;
67 // If the developer has provided an object of refresh handlers,
68 // then we can merge the provided handlers with the current handlers.
69 if (refreshHandlers) {
70 this._refreshHandlers = {
71 ...this._refreshHandlers,
72 ...refreshHandlers,
73 };
74 }
75
76 this._storage = this._config.storage;
77
78 if (!this._storage) {
79 this._storage = new StorageHelper().getStorage();
80 }
81
82 this._storageSync = Promise.resolve();
83 if (typeof this._storage['sync'] === 'function') {
84 this._storageSync = this._storage['sync']();
85 }
86
87 dispatchCredentialsEvent(
88 'credentials_configured',
89 null,
90 `Credentials has been configured successfully`
91 );
92
93 return this._config;
94 }
95
96 public get() {
97 logger.debug('getting credentials');
98 return this._pickupCredentials();
99 }
100
101 // currently we only store the guest identity in local storage
102 private _getCognitoIdentityIdStorageKey(identityPoolId: string) {
103 return `${COGNITO_IDENTITY_KEY_PREFIX}${identityPoolId}`;
104 }
105
106 private _pickupCredentials() {
107 logger.debug('picking up credentials');
108 if (!this._gettingCredPromise || !this._gettingCredPromise.isPending()) {
109 logger.debug('getting new cred promise');
110 this._gettingCredPromise = makeQuerablePromise(this._keepAlive());
111 } else {
112 logger.debug('getting old cred promise');
113 }
114 return this._gettingCredPromise;
115 }
116
117 private async _keepAlive() {
118 logger.debug('checking if credentials exists and not expired');
119 const cred = this._credentials;
120 if (cred && !this._isExpired(cred) && !this._isPastTTL()) {
121 logger.debug('credentials not changed and not expired, directly return');
122 return Promise.resolve(cred);
123 }
124
125 logger.debug('need to get a new credential or refresh the existing one');
126
127 // Some use-cases don't require Auth for signing in, but use Credentials for guest users (e.g. Analytics)
128 // Prefer locally scoped `Auth`, but fallback to registered `Amplify.Auth` global otherwise.
129 const { Auth = Amplify.Auth } = this;
130
131 if (!Auth || typeof Auth.currentUserCredentials !== 'function') {
132 // If Auth module is not imported, do a best effort to get guest credentials
133 return this._setCredentialsForGuest();
134 }
135
136 if (!this._isExpired(cred) && this._isPastTTL()) {
137 logger.debug('ttl has passed but token is not yet expired');
138 try {
139 const user = await Auth.currentUserPoolUser();
140 const session = await Auth.currentSession();
141 const refreshToken = session.refreshToken;
142 const refreshRequest = new Promise((res, rej) => {
143 user.refreshSession(refreshToken, (err, data) => {
144 return err ? rej(err) : res(data);
145 });
146 });
147 await refreshRequest; // note that rejections will be caught and handled in the catch block.
148 } catch (err) {
149 // should not throw because user might just be on guest access or is authenticated through federation
150 logger.debug('Error attempting to refreshing the session', err);
151 }
152 }
153 return Auth.currentUserCredentials();
154 }
155
156 public refreshFederatedToken(federatedInfo) {
157 logger.debug('Getting federated credentials');
158 const { provider, user, token, identity_id } = federatedInfo;
159 let { expires_at } = federatedInfo;
160
161 // Make sure expires_at is in millis
162 expires_at =
163 new Date(expires_at).getFullYear() === 1970
164 ? expires_at * 1000
165 : expires_at;
166
167 const that = this;
168 logger.debug('checking if federated jwt token expired');
169 if (expires_at > new Date().getTime()) {
170 // if not expired
171 logger.debug('token not expired');
172 return this._setCredentialsFromFederation({
173 provider,
174 token,
175 user,
176 identity_id,
177 expires_at,
178 });
179 } else {
180 // if refresh handler exists
181 if (
182 that._refreshHandlers[provider] &&
183 typeof that._refreshHandlers[provider] === 'function'
184 ) {
185 logger.debug('getting refreshed jwt token from federation provider');
186 return this._providerRefreshWithRetry({
187 refreshHandler: that._refreshHandlers[provider],
188 provider,
189 user,
190 });
191 } else {
192 logger.debug('no refresh handler for provider:', provider);
193 this.clear();
194 return Promise.reject('no refresh handler for provider');
195 }
196 }
197 }
198
199 private _providerRefreshWithRetry({ refreshHandler, provider, user }) {
200 const MAX_DELAY_MS = 10 * 1000;
201 // refreshHandler will retry network errors, otherwise it will
202 // return NonRetryableError to break out of jitteredExponentialRetry
203 return jitteredExponentialRetry<any>(refreshHandler, [], MAX_DELAY_MS)
204 .then(data => {
205 logger.debug('refresh federated token sucessfully', data);
206 return this._setCredentialsFromFederation({
207 provider,
208 token: data.token,
209 user,
210 identity_id: data.identity_id,
211 expires_at: data.expires_at,
212 });
213 })
214 .catch(e => {
215 const isNetworkError =
216 typeof e === 'string' &&
217 e.toLowerCase().lastIndexOf('network error', e.length) === 0;
218
219 if (!isNetworkError) {
220 this.clear();
221 }
222
223 logger.debug('refresh federated token failed', e);
224 return Promise.reject('refreshing federation token failed: ' + e);
225 });
226 }
227
228 private _isExpired(credentials): boolean {
229 if (!credentials) {
230 logger.debug('no credentials for expiration check');
231 return true;
232 }
233 logger.debug('are these credentials expired?', credentials);
234 const ts = Date.now();
235
236 /* returns date object.
237 https://github.com/aws/aws-sdk-js-v3/blob/v1.0.0-beta.1/packages/types/src/credentials.ts#L26
238 */
239 const { expiration } = credentials;
240 return expiration.getTime() <= ts;
241 }
242
243 private _isPastTTL(): boolean {
244 return this._nextCredentialsRefresh <= Date.now();
245 }
246
247 private async _setCredentialsForGuest() {
248 logger.debug('setting credentials for guest');
249 if (!this._config?.identityPoolId) {
250 // If Credentials are not configured thru Auth module,
251 // doing best effort to check if the library was configured
252 this._config = Object.assign(
253 {},
254 this._config,
255 parseAWSExports(this._config || {}).Auth
256 );
257 }
258 const { identityPoolId, region, mandatorySignIn, identityPoolRegion } =
259 this._config;
260
261 if (mandatorySignIn) {
262 return Promise.reject(
263 'cannot get guest credentials when mandatory signin enabled'
264 );
265 }
266
267 if (!identityPoolId) {
268 logger.debug(
269 'No Cognito Identity pool provided for unauthenticated access'
270 );
271 return Promise.reject(
272 'No Cognito Identity pool provided for unauthenticated access'
273 );
274 }
275
276 if (!identityPoolRegion && !region) {
277 logger.debug('region is not configured for getting the credentials');
278 return Promise.reject(
279 'region is not configured for getting the credentials'
280 );
281 }
282
283 const identityId = (this._identityId = await this._getGuestIdentityId());
284
285 const cognitoConfig = { region: identityPoolRegion ?? region };
286
287 const guestCredentialsProvider = async () => {
288 if (!identityId) {
289 const { IdentityId } = await getId(cognitoConfig, {
290 IdentityPoolId: identityPoolId,
291 });
292 this._identityId = IdentityId;
293 }
294 const { Credentials } = await getCredentialsForIdentity(cognitoConfig, {
295 IdentityId: this._identityId,
296 });
297 return {
298 identityId: this._identityId,
299 accessKeyId: Credentials.AccessKeyId,
300 secretAccessKey: Credentials.SecretKey,
301 sessionToken: Credentials.SessionToken,
302 expiration: Credentials.Expiration,
303 };
304 };
305 let credentials = guestCredentialsProvider().catch(async err => {
306 throw err;
307 });
308
309 return this._loadCredentials(credentials, 'guest', false, null)
310 .then(res => {
311 return res;
312 })
313 .catch(async e => {
314 // If identity id is deleted in the console, we make one attempt to recreate it
315 // and remove existing id from cache.
316 if (
317 e.name === 'ResourceNotFoundException' &&
318 e.message === `Identity '${identityId}' not found.`
319 ) {
320 logger.debug('Failed to load guest credentials');
321 await this._removeGuestIdentityId();
322
323 const guestCredentialsProvider = async () => {
324 const { IdentityId } = await getId(cognitoConfig, {
325 IdentityPoolId: identityPoolId,
326 });
327 this._identityId = IdentityId;
328 const { Credentials } = await getCredentialsForIdentity(
329 cognitoConfig,
330 {
331 IdentityId,
332 }
333 );
334
335 return {
336 identityId: IdentityId,
337 accessKeyId: Credentials.AccessKeyId,
338 secretAccessKey: Credentials.SecretKey,
339 sessionToken: Credentials.SessionToken,
340 expiration: Credentials.Expiration,
341 };
342 };
343
344 credentials = guestCredentialsProvider().catch(async err => {
345 throw err;
346 });
347
348 return this._loadCredentials(credentials, 'guest', false, null);
349 } else {
350 return e;
351 }
352 });
353 }
354
355 private _setCredentialsFromFederation(params) {
356 const { provider, token } = params;
357 let { identity_id } = params;
358 const domains = {
359 google: 'accounts.google.com',
360 facebook: 'graph.facebook.com',
361 amazon: 'www.amazon.com',
362 developer: 'cognito-identity.amazonaws.com',
363 };
364
365 // Use custom provider url instead of the predefined ones
366 const domain = domains[provider] || provider;
367 if (!domain) {
368 return Promise.reject('You must specify a federated provider');
369 }
370
371 const logins = {};
372 logins[domain] = token;
373
374 const { identityPoolId, region, identityPoolRegion } = this._config;
375 if (!identityPoolId) {
376 logger.debug('No Cognito Federated Identity pool provided');
377 return Promise.reject('No Cognito Federated Identity pool provided');
378 }
379 if (!identityPoolRegion && !region) {
380 logger.debug('region is not configured for getting the credentials');
381 return Promise.reject(
382 'region is not configured for getting the credentials'
383 );
384 }
385
386 const cognitoConfig = { region: identityPoolRegion ?? region };
387
388 const authenticatedCredentialsProvider = async () => {
389 if (!identity_id) {
390 const { IdentityId } = await getId(cognitoConfig, {
391 IdentityPoolId: identityPoolId,
392 Logins: logins,
393 });
394 identity_id = IdentityId;
395 }
396 const { Credentials } = await getCredentialsForIdentity(cognitoConfig, {
397 IdentityId: identity_id,
398 Logins: logins,
399 });
400 return {
401 identityId: identity_id,
402 accessKeyId: Credentials.AccessKeyId,
403 secretAccessKey: Credentials.SecretKey,
404 sessionToken: Credentials.SessionToken,
405 expiration: Credentials.Expiration,
406 };
407 };
408
409 const credentials = authenticatedCredentialsProvider().catch(async err => {
410 throw err;
411 });
412
413 return this._loadCredentials(credentials, 'federated', true, params);
414 }
415
416 private _setCredentialsFromSession(session): Promise<ICredentials> {
417 logger.debug('set credentials from session');
418 const idToken = session.getIdToken().getJwtToken();
419 const { region, userPoolId, identityPoolId, identityPoolRegion } =
420 this._config;
421 if (!identityPoolId) {
422 logger.debug('No Cognito Federated Identity pool provided');
423 return Promise.reject('No Cognito Federated Identity pool provided');
424 }
425 if (!identityPoolRegion && !region) {
426 logger.debug('region is not configured for getting the credentials');
427 return Promise.reject(
428 'region is not configured for getting the credentials'
429 );
430 }
431 const key = 'cognito-idp.' + region + '.amazonaws.com/' + userPoolId;
432 const logins = {};
433 logins[key] = idToken;
434
435 const cognitoConfig = { region: identityPoolRegion ?? region };
436
437 /*
438 Retreiving identityId with GetIdCommand to mimic the behavior in the following code in aws-sdk-v3:
439 https://git.io/JeDxU
440
441 Note: Retreive identityId from CredentialsProvider once aws-sdk-js v3 supports this.
442 */
443 const credentialsProvider = async () => {
444 // try to fetch the local stored guest identity, if found, we will associate it with the logins
445 const guestIdentityId = await this._getGuestIdentityId();
446
447 let generatedOrRetrievedIdentityId;
448 if (!guestIdentityId) {
449 // for a first-time user, this will return a brand new identity
450 // for a returning user, this will retrieve the previous identity assocaited with the logins
451 const { IdentityId } = await getId(cognitoConfig, {
452 IdentityPoolId: identityPoolId,
453 Logins: logins,
454 });
455 generatedOrRetrievedIdentityId = IdentityId;
456 }
457
458 const {
459 Credentials: { AccessKeyId, Expiration, SecretKey, SessionToken },
460 // single source of truth for the primary identity associated with the logins
461 // only if a guest identity is used for a first-time user, that guest identity will become its primary identity
462 IdentityId: primaryIdentityId,
463 } = await getCredentialsForIdentity(cognitoConfig, {
464 IdentityId: guestIdentityId || generatedOrRetrievedIdentityId,
465 Logins: logins,
466 });
467
468 this._identityId = primaryIdentityId;
469 if (guestIdentityId) {
470 // if guestIdentity is found and used by GetCredentialsForIdentity
471 // it will be linked to the logins provided, and disqualified as an unauth identity
472 logger.debug(
473 `The guest identity ${guestIdentityId} has been successfully linked to the logins`
474 );
475 if (guestIdentityId === primaryIdentityId) {
476 logger.debug(
477 `The guest identity ${guestIdentityId} has become the primary identity`
478 );
479 }
480 // remove it from local storage to avoid being used as a guest Identity by _setCredentialsForGuest
481 await this._removeGuestIdentityId();
482 }
483
484 // https://github.com/aws/aws-sdk-js-v3/blob/main/packages/credential-provider-cognito-identity/src/fromCognitoIdentity.ts#L40
485 return {
486 accessKeyId: AccessKeyId,
487 secretAccessKey: SecretKey,
488 sessionToken: SessionToken,
489 expiration: Expiration,
490 identityId: primaryIdentityId,
491 };
492 };
493
494 const credentials = credentialsProvider().catch(async err => {
495 throw err;
496 });
497
498 return this._loadCredentials(credentials, 'userPool', true, null);
499 }
500
501 private _loadCredentials(
502 credentials,
503 source,
504 authenticated,
505 info
506 ): Promise<ICredentials> {
507 const that = this;
508 return new Promise((res, rej) => {
509 credentials
510 .then(async credentials => {
511 logger.debug('Load credentials successfully', credentials);
512 if (this._identityId && !credentials.identityId) {
513 credentials['identityId'] = this._identityId;
514 }
515
516 that._credentials = credentials;
517 that._credentials.authenticated = authenticated;
518 that._credentials_source = source;
519 that._nextCredentialsRefresh = new Date().getTime() + CREDENTIALS_TTL;
520 if (source === 'federated') {
521 const user = Object.assign(
522 { id: this._credentials.identityId },
523 info.user
524 );
525 const { provider, token, expires_at, identity_id } = info;
526 try {
527 this._storage.setItem(
528 'aws-amplify-federatedInfo',
529 JSON.stringify({
530 provider,
531 token,
532 user,
533 expires_at,
534 identity_id,
535 })
536 );
537 } catch (e) {
538 logger.debug('Failed to put federated info into auth storage', e);
539 }
540 }
541 if (source === 'guest') {
542 await this._setGuestIdentityId(credentials.identityId);
543 }
544 res(that._credentials);
545 return;
546 })
547 .catch(err => {
548 if (err) {
549 logger.debug('Failed to load credentials', credentials);
550 logger.debug('Error loading credentials', err);
551 rej(err);
552 return;
553 }
554 });
555 });
556 }
557
558 public set(params, source): Promise<ICredentials> {
559 if (source === 'session') {
560 return this._setCredentialsFromSession(params);
561 } else if (source === 'federation') {
562 return this._setCredentialsFromFederation(params);
563 } else if (source === 'guest') {
564 return this._setCredentialsForGuest();
565 } else {
566 logger.debug('no source specified for setting credentials');
567 return Promise.reject('invalid source');
568 }
569 }
570
571 public async clear() {
572 this._credentials = null;
573 this._credentials_source = null;
574 logger.debug('removing aws-amplify-federatedInfo from storage');
575 this._storage.removeItem('aws-amplify-federatedInfo');
576 }
577
578 /* operations on local stored guest identity */
579 private async _getGuestIdentityId(): Promise<string> {
580 const { identityPoolId } = this._config;
581 try {
582 await this._storageSync;
583 return this._storage.getItem(
584 this._getCognitoIdentityIdStorageKey(identityPoolId)
585 );
586 } catch (e) {
587 logger.debug('Failed to get the cached guest identityId', e);
588 }
589 }
590
591 private async _setGuestIdentityId(identityId: string) {
592 const { identityPoolId } = this._config;
593 try {
594 await this._storageSync;
595 this._storage.setItem(
596 this._getCognitoIdentityIdStorageKey(identityPoolId),
597 identityId
598 );
599 } catch (e) {
600 logger.debug('Failed to cache guest identityId', e);
601 }
602 }
603
604 private async _removeGuestIdentityId() {
605 const { identityPoolId } = this._config;
606 logger.debug(
607 `removing ${this._getCognitoIdentityIdStorageKey(
608 identityPoolId
609 )} from storage`
610 );
611 this._storage.removeItem(
612 this._getCognitoIdentityIdStorageKey(identityPoolId)
613 );
614 }
615
616 /**
617 * Compact version of credentials
618 * @param {Object} credentials
619 * @return {Object} - Credentials
620 */
621 public shear(credentials) {
622 return {
623 accessKeyId: credentials.accessKeyId,
624 sessionToken: credentials.sessionToken,
625 secretAccessKey: credentials.secretAccessKey,
626 identityId: credentials.identityId,
627 authenticated: credentials.authenticated,
628 };
629 }
630}
631
632export const Credentials = new CredentialsClass(null);
633
634Amplify.register(Credentials);