1 | import { ConsoleLogger as Logger } from './Logger';
|
2 | import { StorageHelper } from './StorageHelper';
|
3 | import { makeQuerablePromise } from './JS';
|
4 | import { FacebookOAuth, GoogleOAuth } from './OAuthHelper';
|
5 | import { jitteredExponentialRetry } from './Util';
|
6 | import { ICredentials } from './types';
|
7 | import { getAmplifyUserAgent } from './Platform';
|
8 | import { Amplify } from './Amplify';
|
9 | import {
|
10 | fromCognitoIdentity,
|
11 | FromCognitoIdentityParameters,
|
12 | fromCognitoIdentityPool,
|
13 | FromCognitoIdentityPoolParameters,
|
14 | } from '@aws-sdk/credential-provider-cognito-identity';
|
15 | import {
|
16 | CognitoIdentityClient,
|
17 | GetIdCommand,
|
18 | GetCredentialsForIdentityCommand,
|
19 | } from '@aws-sdk/client-cognito-identity';
|
20 | import { CredentialProvider } from '@aws-sdk/types';
|
21 |
|
22 | const logger = new Logger('Credentials');
|
23 |
|
24 | const CREDENTIALS_TTL = 50 * 60 * 1000;
|
25 |
|
26 | const COGNITO_IDENTITY_KEY_PREFIX = 'CognitoIdentityId-';
|
27 |
|
28 | export 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 |
|
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 |
|
62 |
|
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 |
|
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 |
|
116 |
|
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;
|
135 | } catch (err) {
|
136 |
|
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 |
|
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 |
|
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 |
|
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 |
|
189 |
|
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 | |
224 |
|
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 |
|
276 |
|
277 |
|
278 |
|
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 |
|
310 |
|
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 |
|
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 |
|
427 |
|
428 |
|
429 |
|
430 |
|
431 | const credentialsProvider: CredentialProvider = async () => {
|
432 |
|
433 | const guestIdentityId = await this._getGuestIdentityId();
|
434 |
|
435 | let generatedOrRetrievedIdentityId;
|
436 | if (!guestIdentityId) {
|
437 |
|
438 |
|
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 |
|
456 |
|
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 |
|
468 |
|
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 |
|
474 | await this._removeGuestIdentityId();
|
475 | }
|
476 |
|
477 |
|
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 |
|
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 |
|
611 |
|
612 |
|
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 |
|
625 | export const Credentials = new CredentialsClass(null);
|
626 |
|
627 | Amplify.register(Credentials);
|
628 |
|
629 |
|
630 |
|
631 |
|
632 | export default Credentials;
|