Skip to content

Commit 81213b9

Browse files
authored
feat: Re-use Cognito identity id (#437)
1 parent 460770b commit 81213b9

File tree

3 files changed

+155
-6
lines changed

3 files changed

+155
-6
lines changed

src/dispatch/CognitoIdentityClient.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
/* eslint-disable no-underscore-dangle */
12
import { HttpHandler, HttpRequest } from '@aws-sdk/protocol-http';
23
import { Credentials } from '@aws-sdk/types';
34
import { responseToJson } from './utils';
5+
import { IDENTITY_KEY } from '../utils/constants';
46

57
const METHOD = 'POST';
68
const CONTENT_TYPE = 'application/x-amz-json-1.1';
@@ -62,16 +64,40 @@ export class CognitoIdentityClient {
6264
}
6365

6466
public getId = async (request: { IdentityPoolId: string }) => {
67+
let getIdResponse: GetIdResponse | null = null;
68+
69+
try {
70+
getIdResponse = JSON.parse(
71+
localStorage.getItem(IDENTITY_KEY)!
72+
) as GetIdResponse | null;
73+
} catch (e) {
74+
// Ignore -- we will get a new identity Id from Cognito
75+
}
76+
77+
if (getIdResponse && getIdResponse.IdentityId) {
78+
return Promise.resolve(getIdResponse);
79+
}
80+
6581
try {
6682
const requestPayload = JSON.stringify(request);
6783
const idRequest = this.getHttpRequest(
6884
GET_ID_TARGET,
6985
requestPayload
7086
);
71-
const { response } = await this.fetchRequestHandler.handle(
72-
idRequest
73-
);
74-
return (await responseToJson(response)) as GetIdResponse;
87+
const getIdResponse = (await responseToJson(
88+
(
89+
await this.fetchRequestHandler.handle(idRequest)
90+
).response
91+
)) as GetIdResponse;
92+
try {
93+
localStorage.setItem(
94+
IDENTITY_KEY,
95+
JSON.stringify({ IdentityId: getIdResponse.IdentityId })
96+
);
97+
} catch (e) {
98+
// Ignore
99+
}
100+
return getIdResponse;
75101
} catch (e) {
76102
throw new Error(`CWR: Failed to retrieve Cognito identity: ${e}`);
77103
}
@@ -107,9 +133,11 @@ export class CognitoIdentityClient {
107133
const { response } = await this.fetchRequestHandler.handle(
108134
credentialRequest
109135
);
110-
const { Credentials } = (await responseToJson(
136+
const credentialsResponse = (await responseToJson(
111137
response
112138
)) as CredentialsResponse;
139+
this.validateCredenentialsResponse(credentialsResponse);
140+
const Credentials = credentialsResponse.Credentials;
113141
const { AccessKeyId, Expiration, SecretKey, SessionToken } =
114142
Credentials;
115143
return {
@@ -125,6 +153,22 @@ export class CognitoIdentityClient {
125153
}
126154
};
127155

156+
private validateCredenentialsResponse = (cr: any) => {
157+
if (
158+
cr &&
159+
cr.__type &&
160+
(cr.__type === 'ResourceNotFoundException' ||
161+
cr.__type === 'ValidationException')
162+
) {
163+
// The request may have failed because of ValidationException or
164+
// ResourceNotFoundException, which means the identity Id is bad. In
165+
// any case, we invalidate the identity Id so the entire process can
166+
// be re-tried.
167+
localStorage.removeItem(IDENTITY_KEY);
168+
throw new Error(`${cr.__type}: ${cr.message}`);
169+
}
170+
};
171+
128172
private getHttpRequest = (target: string, payload: string) =>
129173
new HttpRequest({
130174
method: METHOD,

src/dispatch/__tests__/CognitoIdentityClient.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { advanceTo } from 'jest-date-mock';
44
import { CognitoIdentityClient } from '../CognitoIdentityClient';
55
import { Credentials } from '@aws-sdk/types';
66
import { getReadableStream } from '../../test-utils/test-utils';
7+
import { IDENTITY_KEY } from '../../utils/constants';
78

89
const mockCredentials =
910
'{ "IdentityId": "a", "Credentials": { "AccessKeyId": "x", "SecretKey": "y", "SessionToken": "z" } }';
@@ -22,6 +23,7 @@ describe('CognitoIdentityClient tests', () => {
2223
beforeEach(() => {
2324
advanceTo(0);
2425
fetchHandler.mockClear();
26+
localStorage.clear();
2527

2628
// @ts-ignore
2729
FetchHttpHandler.mockImplementation(() => {
@@ -74,7 +76,7 @@ describe('CognitoIdentityClient tests', () => {
7476
});
7577

7678
// Assert
77-
return expect(
79+
await expect(
7880
client.getCredentialsForIdentity('my-fake-identity-id')
7981
).rejects.toEqual(expected);
8082
});
@@ -175,4 +177,106 @@ describe('CognitoIdentityClient tests', () => {
175177
})
176178
).rejects.toEqual(expected);
177179
});
180+
181+
test('when identity Id is retrieved from Cognito then next identity Id is retrieved from localStorage', async () => {
182+
fetchHandler.mockResolvedValueOnce({
183+
response: {
184+
body: getReadableStream(mockIdCommand)
185+
}
186+
});
187+
188+
// Init
189+
const client: CognitoIdentityClient = new CognitoIdentityClient({
190+
fetchRequestHandler: new FetchHttpHandler(),
191+
region: Utils.AWS_RUM_REGION
192+
});
193+
194+
// Run
195+
await client.getId({ IdentityPoolId: 'my-fake-identity-pool-id' });
196+
const idCommand = await client.getId({
197+
IdentityPoolId: 'my-fake-identity-pool-id'
198+
});
199+
200+
// Assert
201+
expect(fetchHandler).toHaveBeenCalledTimes(1);
202+
expect(idCommand).toMatchObject({
203+
IdentityId: 'mockId'
204+
});
205+
});
206+
207+
test('when getCredentialsForIdentity returns a ResourceNotFoundException then an error is thrown', async () => {
208+
fetchHandler.mockResolvedValueOnce({
209+
response: {
210+
body: getReadableStream(
211+
'{"__type": "ResourceNotFoundException", "message": ""}'
212+
)
213+
}
214+
});
215+
const expected: Error = new Error(
216+
`CWR: Failed to retrieve credentials for Cognito identity: Error: ResourceNotFoundException: `
217+
);
218+
219+
// Init
220+
const client: CognitoIdentityClient = new CognitoIdentityClient({
221+
fetchRequestHandler: new FetchHttpHandler(),
222+
region: Utils.AWS_RUM_REGION
223+
});
224+
225+
// Assert
226+
await expect(
227+
client.getCredentialsForIdentity('my-fake-identity-id')
228+
).rejects.toEqual(expected);
229+
});
230+
231+
test('when getCredentialsForIdentity returns a ValidationException then an error is thrown', async () => {
232+
fetchHandler.mockResolvedValueOnce({
233+
response: {
234+
body: getReadableStream(
235+
'{"__type": "ValidationException", "message": ""}'
236+
)
237+
}
238+
});
239+
const expected: Error = new Error(
240+
`CWR: Failed to retrieve credentials for Cognito identity: Error: ValidationException: `
241+
);
242+
243+
// Init
244+
const client: CognitoIdentityClient = new CognitoIdentityClient({
245+
fetchRequestHandler: new FetchHttpHandler(),
246+
region: Utils.AWS_RUM_REGION
247+
});
248+
249+
// Assert
250+
await expect(
251+
client.getCredentialsForIdentity('my-fake-identity-id')
252+
).rejects.toEqual(expected);
253+
});
254+
255+
test('when getCredentialsForIdentity returns a ResourceNotFoundException then identity id is removed from localStorage ', async () => {
256+
localStorage.setItem(IDENTITY_KEY, 'my-fake-identity-id');
257+
258+
fetchHandler.mockResolvedValueOnce({
259+
response: {
260+
body: getReadableStream(
261+
'{"__type": "ResourceNotFoundException", "message": ""}'
262+
)
263+
}
264+
});
265+
266+
// Init
267+
const client: CognitoIdentityClient = new CognitoIdentityClient({
268+
fetchRequestHandler: new FetchHttpHandler(),
269+
region: Utils.AWS_RUM_REGION
270+
});
271+
272+
// Run
273+
try {
274+
await client.getCredentialsForIdentity('my-fake-identity-id');
275+
} catch (e) {
276+
// Ignore
277+
}
278+
279+
// Assert
280+
expect(localStorage.getItem(IDENTITY_KEY)).toBe(null);
281+
});
178282
});

src/utils/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const CRED_KEY = 'cwr_c';
2+
export const IDENTITY_KEY = 'cwr_i';
23
export const SESSION_COOKIE_NAME = 'cwr_s';
34
export const USER_COOKIE_NAME = 'cwr_u';
45
export const CRED_RENEW_MS = 30000;

0 commit comments

Comments
 (0)