1 |
|
2 |
|
3 | import { ConsoleLogger as Logger } from './Logger';
|
4 | import { StorageHelper } from './StorageHelper';
|
5 | import { makeQuerablePromise } from './JS';
|
6 | import { FacebookOAuth, GoogleOAuth } from './OAuthHelper';
|
7 | import { jitteredExponentialRetry } from './Util';
|
8 | import { ICredentials } from './types';
|
9 | import { Amplify } from './Amplify';
|
10 | import { getId, getCredentialsForIdentity } from './AwsClients/CognitoIdentity';
|
11 | import { parseAWSExports } from './parseAWSExports';
|
12 | import { Hub } from './Hub';
|
13 |
|
14 | const logger = new Logger('Credentials');
|
15 |
|
16 | const CREDENTIALS_TTL = 50 * 60 * 1000;
|
17 |
|
18 | const COGNITO_IDENTITY_KEY_PREFIX = 'CognitoIdentityId-';
|
19 |
|
20 | const AMPLIFY_SYMBOL = (
|
21 | typeof Symbol !== 'undefined' && typeof Symbol.for === 'function'
|
22 | ? Symbol.for('amplify_default')
|
23 | : '@@amplify_default'
|
24 | ) as Symbol;
|
25 |
|
26 | const dispatchCredentialsEvent = (
|
27 | event: string,
|
28 | data: any,
|
29 | message: string
|
30 | ) => {
|
31 | Hub.dispatch('core', { event, data, message }, 'Credentials', AMPLIFY_SYMBOL);
|
32 | };
|
33 |
|
34 | export 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 |
|
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 |
|
68 |
|
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 |
|
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 |
|
128 |
|
129 | const { Auth = Amplify.Auth } = this;
|
130 |
|
131 | if (!Auth || typeof Auth.currentUserCredentials !== 'function') {
|
132 |
|
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;
|
148 | } catch (err) {
|
149 |
|
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 |
|
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 |
|
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 |
|
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 |
|
202 |
|
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 | |
237 |
|
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 |
|
251 |
|
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 |
|
315 |
|
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 |
|
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 |
|
439 |
|
440 |
|
441 |
|
442 |
|
443 | const credentialsProvider = async () => {
|
444 |
|
445 | const guestIdentityId = await this._getGuestIdentityId();
|
446 |
|
447 | let generatedOrRetrievedIdentityId;
|
448 | if (!guestIdentityId) {
|
449 |
|
450 |
|
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 |
|
461 |
|
462 | IdentityId: primaryIdentityId,
|
463 | } = await getCredentialsForIdentity(cognitoConfig, {
|
464 | IdentityId: guestIdentityId || generatedOrRetrievedIdentityId,
|
465 | Logins: logins,
|
466 | });
|
467 |
|
468 | this._identityId = primaryIdentityId;
|
469 | if (guestIdentityId) {
|
470 |
|
471 |
|
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 |
|
481 | await this._removeGuestIdentityId();
|
482 | }
|
483 |
|
484 |
|
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 |
|
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 |
|
618 |
|
619 |
|
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 |
|
632 | export const Credentials = new CredentialsClass(null);
|
633 |
|
634 | Amplify.register(Credentials);
|