UNPKG

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