Skip to content

Commit e5a8569

Browse files
authored
feat: refresh token rotation (#14427)
* feat: add GetTokensFromRefreshToken service clients * feat: use GetTokensFromRefreshToken instead of initiateAuth to refresh tokens * test: update tests for refreshing tokens * chore: add GetTokensFromRefreshToken to dts-bundler
1 parent 0bfba9e commit e5a8569

File tree

8 files changed

+178
-104
lines changed

8 files changed

+178
-104
lines changed

packages/auth/__tests__/providers/cognito/refreshToken.test.ts

Lines changed: 15 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
oAuthTokenRefreshException,
77
tokenRefreshException,
88
} from '../../../src/providers/cognito/utils/types';
9-
import { createInitiateAuthClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider';
9+
import { createGetTokensFromRefreshTokenClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider';
1010
import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories';
1111

1212
import { mockAccessToken, mockRequestId } from './testUtils/data';
@@ -16,35 +16,37 @@ jest.mock(
1616
);
1717
jest.mock('../../../src/providers/cognito/factories');
1818

19-
const mockCreateInitiateAuthClient = jest.mocked(createInitiateAuthClient);
19+
const mockCreateGetTokensFromRefreshTokenClient = jest.mocked(
20+
createGetTokensFromRefreshTokenClient,
21+
);
2022
const mockCreateCognitoUserPoolEndpointResolver = jest.mocked(
2123
createCognitoUserPoolEndpointResolver,
2224
);
2325

2426
describe('refreshToken', () => {
2527
const mockedUsername = 'mockedUsername';
2628
const mockedRefreshToken = 'mockedRefreshToken';
27-
const mockInitiateAuth = jest.fn();
29+
const mockGetTokensFromRefreshToken = jest.fn();
2830

2931
beforeEach(() => {
30-
mockCreateInitiateAuthClient.mockReturnValueOnce(mockInitiateAuth);
32+
mockCreateGetTokensFromRefreshTokenClient.mockReturnValueOnce(
33+
mockGetTokensFromRefreshToken,
34+
);
3135
});
3236

3337
afterEach(() => {
34-
mockCreateInitiateAuthClient.mockClear();
3538
mockCreateCognitoUserPoolEndpointResolver.mockClear();
3639
});
3740

3841
describe('positive cases', () => {
3942
beforeEach(() => {
40-
mockInitiateAuth.mockResolvedValue({
43+
mockGetTokensFromRefreshToken.mockResolvedValue({
4144
AuthenticationResult: {
4245
AccessToken: mockAccessToken,
4346
ExpiresIn: 3600,
4447
IdToken: mockAccessToken,
4548
TokenType: 'Bearer',
4649
},
47-
ChallengeParameters: {},
4850
$metadata: {
4951
attempts: 1,
5052
httpStatusCode: 200,
@@ -54,7 +56,7 @@ describe('refreshToken', () => {
5456
});
5557

5658
afterEach(() => {
57-
mockInitiateAuth.mockReset();
59+
mockGetTokensFromRefreshToken.mockReset();
5860
});
5961

6062
it('should refresh token', async () => {
@@ -86,54 +88,13 @@ describe('refreshToken', () => {
8688
expect(JSON.parse(JSON.stringify(response))).toMatchObject(
8789
JSON.parse(JSON.stringify(expectedOutput)),
8890
);
89-
expect(mockInitiateAuth).toHaveBeenCalledWith(
90-
expect.objectContaining({ region: 'us-east-1' }),
91-
expect.objectContaining({
92-
ClientId: 'aaaaaaaaaaaa',
93-
AuthFlow: 'REFRESH_TOKEN_AUTH',
94-
AuthParameters: {
95-
REFRESH_TOKEN: mockedRefreshToken,
96-
},
97-
}),
98-
);
99-
});
100-
101-
it('should send UserContextData', async () => {
102-
(window as any).AmazonCognitoAdvancedSecurityData = {
103-
getData() {
104-
return 'abcd';
105-
},
106-
};
107-
await refreshAuthTokens({
108-
username: 'username',
109-
tokens: {
110-
accessToken: decodeJWT(
111-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0',
112-
),
113-
idToken: decodeJWT(
114-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0',
115-
),
116-
clockDrift: 0,
117-
refreshToken: 'refreshtoken',
118-
username: 'username',
119-
},
120-
authConfig: {
121-
Cognito: {
122-
userPoolId: 'us-east-1_aaaaaaa',
123-
userPoolClientId: 'aaaaaaaaaaaa',
124-
},
125-
},
126-
});
127-
expect(mockInitiateAuth).toHaveBeenCalledWith(
91+
expect(mockGetTokensFromRefreshToken).toHaveBeenCalledWith(
12892
expect.objectContaining({ region: 'us-east-1' }),
12993
expect.objectContaining({
130-
AuthFlow: 'REFRESH_TOKEN_AUTH',
131-
AuthParameters: { REFRESH_TOKEN: 'refreshtoken' },
13294
ClientId: 'aaaaaaaaaaaa',
133-
UserContextData: { EncodedData: 'abcd' },
95+
RefreshToken: mockedRefreshToken,
13496
}),
13597
);
136-
(window as any).AmazonCognitoAdvancedSecurityData = undefined;
13798
});
13899

139100
it('invokes mockCreateCognitoUserPoolEndpointResolver with expected parameters', async () => {
@@ -164,7 +125,7 @@ describe('refreshToken', () => {
164125
expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({
165126
endpointOverride: expectedParam,
166127
});
167-
expect(mockCreateInitiateAuthClient).toHaveBeenCalledWith({
128+
expect(mockCreateGetTokensFromRefreshTokenClient).toHaveBeenCalledWith({
168129
endpointResolver: expectedEndpointResolver,
169130
});
170131
});
@@ -174,13 +135,13 @@ describe('refreshToken', () => {
174135
const mockedError = new Error('Refresh Token has expired');
175136
mockedError.name = 'NotAuthorizedException';
176137
beforeEach(() => {
177-
mockInitiateAuth.mockImplementationOnce(() => {
138+
mockGetTokensFromRefreshToken.mockImplementationOnce(() => {
178139
throw mockedError;
179140
});
180141
});
181142

182143
afterEach(() => {
183-
mockInitiateAuth.mockReset();
144+
mockGetTokensFromRefreshToken.mockReset();
184145
});
185146

186147
it('should throw an exception when refresh_token is not available', async () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers';
5+
6+
import {
7+
GetTokensFromRefreshTokenCommandInput,
8+
GetTokensFromRefreshTokenCommandOutput,
9+
ServiceClientFactoryInput,
10+
} from './types';
11+
import {
12+
createUserPoolDeserializer,
13+
createUserPoolSerializer,
14+
} from './shared/serde';
15+
import { cognitoUserPoolTransferHandler } from './shared/handler';
16+
import { DEFAULT_SERVICE_CLIENT_API_CONFIG } from './constants';
17+
18+
export const createGetTokensFromRefreshTokenClient = (
19+
config: ServiceClientFactoryInput,
20+
) =>
21+
composeServiceApi(
22+
cognitoUserPoolTransferHandler,
23+
createUserPoolSerializer<GetTokensFromRefreshTokenCommandInput>(
24+
'GetTokensFromRefreshToken',
25+
),
26+
createUserPoolDeserializer<GetTokensFromRefreshTokenCommandOutput>(),
27+
{
28+
...DEFAULT_SERVICE_CLIENT_API_CONFIG,
29+
...config,
30+
},
31+
);

packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
export { createInitiateAuthClient } from './createInitiateAuthClient';
5+
export { createGetTokensFromRefreshTokenClient } from './createGetTokensFromRefreshTokenClient';
56
export { createRevokeTokenClient } from './createRevokeTokenClient';
67
export { createSignUpClient } from './createSignUpClient';
78
export { createConfirmSignUpClient } from './createConfirmSignUpClient';

packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/shared/serde/createUserPoolSerializer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type ClientOperation =
1313
| 'ForgotPassword'
1414
| 'ConfirmForgotPassword'
1515
| 'InitiateAuth'
16+
| 'GetTokensFromRefreshToken'
1617
| 'RespondToAuthChallenge'
1718
| 'ResendConfirmationCode'
1819
| 'VerifySoftwareToken'

packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/types/sdk.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,99 @@ declare namespace DeleteUserAttributesResponse {
408408
*/
409409
const filterSensitiveLog: (obj: DeleteUserAttributesResponse) => any;
410410
}
411+
412+
/**
413+
* @public
414+
*
415+
* The input for {@link GetTokensFromRefreshTokenCommand}.
416+
*/
417+
export interface GetTokensFromRefreshTokenCommandInput
418+
extends GetTokensFromRefreshTokenRequest {}
419+
/**
420+
* @public
421+
*
422+
* The output of {@link GetTokensFromRefreshTokenCommand}.
423+
*/
424+
export interface GetTokensFromRefreshTokenCommandOutput
425+
extends GetTokensFromRefreshTokenResponse,
426+
__MetadataBearer {}
427+
/**
428+
* @public
429+
*/
430+
export interface GetTokensFromRefreshTokenRequest {
431+
/**
432+
* <p>A valid refresh token that can authorize the request for new tokens. When refresh
433+
* token rotation is active in the requested app client, this token is invalidated after
434+
* the request is complete.</p>
435+
* @public
436+
*/
437+
RefreshToken: string | undefined;
438+
/**
439+
* <p>The app client that issued the refresh token to the user who wants to request new
440+
* tokens.</p>
441+
* @public
442+
*/
443+
ClientId: string | undefined;
444+
/**
445+
* <p>The client secret of the requested app client, if the client has a secret.</p>
446+
* @public
447+
*/
448+
ClientSecret?: string | undefined;
449+
/**
450+
* <p>When you enable device remembering, Amazon Cognito issues a device key that you can use for
451+
* device authentication that bypasses multi-factor authentication (MFA). To implement
452+
* <code>GetTokensFromRefreshToken</code> in a user pool with device remembering, you
453+
* must capture the device key from the initial authentication request. If your application
454+
* doesn't provide the key of a registered device, Amazon Cognito issues a new one. You must
455+
* provide the confirmed device key in this request if device remembering is
456+
* enabled in your user pool.</p>
457+
* <p>For more information about device remembering, see <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html">Working with devices</a>.</p>
458+
* @public
459+
*/
460+
DeviceKey?: string | undefined;
461+
/**
462+
* <p>A map of custom key-value pairs that you can provide as input for certain custom
463+
* workflows that this action triggers.</p>
464+
* <p>You create custom workflows by assigning Lambda functions to user pool triggers.
465+
* When you use the <code>GetTokensFromRefreshToken</code> API action, Amazon Cognito invokes the
466+
* Lambda function the pre token generation trigger.</p>
467+
* <p>For more information, see <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html">
468+
* Using Lambda triggers</a> in the <i>Amazon Cognito Developer Guide</i>.</p>
469+
* <note>
470+
* <p>When you use the <code>ClientMetadata</code> parameter, note that Amazon Cognito won't do the
471+
* following:</p>
472+
* <ul>
473+
* <li>
474+
* <p>Store the <code>ClientMetadata</code> value. This data is available only
475+
* to Lambda triggers that are assigned to a user pool to support custom
476+
* workflows. If your user pool configuration doesn't include triggers, the
477+
* <code>ClientMetadata</code> parameter serves no purpose.</p>
478+
* </li>
479+
* <li>
480+
* <p>Validate the <code>ClientMetadata</code> value.</p>
481+
* </li>
482+
* <li>
483+
* <p>Encrypt the <code>ClientMetadata</code> value. Don't send sensitive
484+
* information in this parameter.</p>
485+
* </li>
486+
* </ul>
487+
* </note>
488+
* @public
489+
*/
490+
ClientMetadata?: Record<string, string> | undefined;
491+
}
492+
/**
493+
* @public
494+
*/
495+
export interface GetTokensFromRefreshTokenResponse {
496+
/**
497+
* <p>The object that your application receives after authentication. Contains tokens and
498+
* information for device authentication.</p>
499+
* @public
500+
*/
501+
AuthenticationResult?: AuthenticationResultType | undefined;
502+
}
503+
411504
/**
412505
* <p>An Amazon Pinpoint analytics endpoint.</p>
413506
* <p>An endpoint uniquely identifies a mobile device, email address, or phone number that can receive messages from Amazon Pinpoint analytics.</p>

packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ import { CognitoAuthTokens, TokenRefresher } from '../tokenProvider/types';
1212
import { getRegionFromUserPoolId } from '../../../foundation/parsers';
1313
import { assertAuthTokensWithRefreshToken } from '../utils/types';
1414
import { AuthError } from '../../../errors/AuthError';
15-
import { createInitiateAuthClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider';
1615
import { createCognitoUserPoolEndpointResolver } from '../factories';
17-
18-
import { getUserContextData } from './userContextData';
16+
import { createGetTokensFromRefreshTokenClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider';
1917

2018
const refreshAuthTokensFunction: TokenRefresher = async ({
2119
tokens,
@@ -30,34 +28,19 @@ const refreshAuthTokensFunction: TokenRefresher = async ({
3028
const { userPoolId, userPoolClientId, userPoolEndpoint } = authConfig.Cognito;
3129
const region = getRegionFromUserPoolId(userPoolId);
3230
assertAuthTokensWithRefreshToken(tokens);
33-
const refreshTokenString = tokens.refreshToken;
34-
35-
const AuthParameters: Record<string, string> = {
36-
REFRESH_TOKEN: refreshTokenString,
37-
};
38-
if (tokens.deviceMetadata?.deviceKey) {
39-
AuthParameters.DEVICE_KEY = tokens.deviceMetadata.deviceKey;
40-
}
41-
42-
const UserContextData = getUserContextData({
43-
username,
44-
userPoolId,
45-
userPoolClientId,
46-
});
4731

48-
const initiateAuth = createInitiateAuthClient({
32+
const getTokensFromRefreshToken = createGetTokensFromRefreshTokenClient({
4933
endpointResolver: createCognitoUserPoolEndpointResolver({
5034
endpointOverride: userPoolEndpoint,
5135
}),
5236
});
5337

54-
const { AuthenticationResult } = await initiateAuth(
38+
const { AuthenticationResult } = await getTokensFromRefreshToken(
5539
{ region },
5640
{
5741
ClientId: userPoolClientId,
58-
AuthFlow: 'REFRESH_TOKEN_AUTH',
59-
AuthParameters,
60-
UserContextData,
42+
RefreshToken: tokens.refreshToken,
43+
DeviceKey: tokens.deviceMetadata?.deviceKey,
6144
},
6245
);
6346

@@ -79,7 +62,7 @@ const refreshAuthTokensFunction: TokenRefresher = async ({
7962
accessToken,
8063
idToken,
8164
clockDrift,
82-
refreshToken: refreshTokenString,
65+
refreshToken: AuthenticationResult?.RefreshToken ?? tokens.refreshToken,
8366
username,
8467
};
8568
};

0 commit comments

Comments
 (0)