Skip to content

Commit 5bf5685

Browse files
authored
feat(auth): support clientmetadata for token refresh (#14556)
1 parent 0cf2160 commit 5bf5685

File tree

10 files changed

+158
-8
lines changed

10 files changed

+158
-8
lines changed

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@ describe('fetchAuthSession behavior for IdentityPools only', () => {
7575
});
7676

7777
describe('fetchAuthSession behavior for UserPools only', () => {
78+
let getTokensSpy: jest.SpyInstance;
79+
7880
beforeAll(() => {
79-
jest
81+
getTokensSpy = jest
8082
.spyOn(cognitoUserPoolsTokenProvider, 'getTokens')
8183
.mockImplementation(async () => {
8284
return {
@@ -136,4 +138,28 @@ describe('fetchAuthSession behavior for UserPools only', () => {
136138
userSub: '1234567890',
137139
});
138140
});
141+
142+
test('should pass clientMetadata option to token provider', async () => {
143+
Amplify.configure(
144+
{
145+
Auth: {
146+
Cognito: {
147+
userPoolClientId: 'userPoolCliendIdValue',
148+
userPoolId: 'userpoolIdvalue',
149+
},
150+
},
151+
},
152+
{
153+
Auth: {
154+
credentialsProvider: cognitoCredentialsProvider,
155+
tokenProvider: cognitoUserPoolsTokenProvider,
156+
},
157+
},
158+
);
159+
160+
const clientMetadata = { 'app-version': '1.0.0' };
161+
await fetchAuthSession({ clientMetadata });
162+
163+
expect(getTokensSpy).toHaveBeenCalledWith({ clientMetadata });
164+
});
139165
});

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
14
import { decodeJWT } from '@aws-amplify/core/internals/utils';
25

36
import { refreshAuthTokens } from '../../../src/providers/cognito/utils/refreshAuthTokens';
@@ -60,6 +63,7 @@ describe('refreshToken', () => {
6063
});
6164

6265
it('should refresh token', async () => {
66+
const clientMetadata = { 'app-version': '1.0.0' };
6367
const expectedOutput = {
6468
accessToken: decodeJWT(mockAccessToken),
6569
idToken: decodeJWT(mockAccessToken),
@@ -82,6 +86,7 @@ describe('refreshToken', () => {
8286
},
8387
},
8488
username: mockedUsername,
89+
clientMetadata,
8590
});
8691

8792
// stringify and re-parse for JWT equality
@@ -93,6 +98,7 @@ describe('refreshToken', () => {
9398
expect.objectContaining({
9499
ClientId: 'aaaaaaaaaaaa',
95100
RefreshToken: mockedRefreshToken,
101+
ClientMetadata: clientMetadata,
96102
}),
97103
);
98104
});

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,81 @@ describe('TokenOrchestrator', () => {
151151
expect(tokens?.accessToken).toEqual(validAuthTokens.accessToken);
152152
});
153153
});
154+
155+
describe('setClientMetadataProvider', () => {
156+
it('should use clientMetadataProvider for token refresh', async () => {
157+
const clientMetadata = { 'app-version': '1.0.0' };
158+
const clientMetadataProvider = () => Promise.resolve(clientMetadata);
159+
160+
mockTokenRefresher.mockResolvedValue({
161+
accessToken: { payload: {} },
162+
idToken: { payload: {} },
163+
clockDrift: 0,
164+
refreshToken: 'newRefreshToken',
165+
username: 'testuser',
166+
});
167+
168+
tokenOrchestrator.setTokenRefresher(mockTokenRefresher);
169+
tokenOrchestrator.setAuthTokenStore(mockAuthTokenStore);
170+
tokenOrchestrator.setClientMetadataProvider(clientMetadataProvider);
171+
172+
mockAuthTokenStore.loadTokens.mockResolvedValue({
173+
accessToken: { payload: { exp: 1 } },
174+
idToken: { payload: { exp: 1 } },
175+
clockDrift: 0,
176+
refreshToken: 'refreshToken',
177+
username: 'testuser',
178+
});
179+
mockAuthTokenStore.getLastAuthUser.mockResolvedValue('testuser');
180+
181+
await tokenOrchestrator.getTokens({ forceRefresh: true });
182+
183+
expect(mockTokenRefresher).toHaveBeenCalledWith(
184+
expect.objectContaining({
185+
clientMetadata,
186+
}),
187+
);
188+
});
189+
190+
it('should prioritize clientMetadata from options over clientMetadataProvider', async () => {
191+
const providerMetadata = { 'app-version': '1.0.0' };
192+
const optionsMetadata = {
193+
'app-version': '2.0.0',
194+
'device-id': 'test-device',
195+
};
196+
const clientMetadataProvider = () => Promise.resolve(providerMetadata);
197+
198+
mockTokenRefresher.mockResolvedValue({
199+
accessToken: { payload: {} },
200+
idToken: { payload: {} },
201+
clockDrift: 0,
202+
refreshToken: 'newRefreshToken',
203+
username: 'testuser',
204+
});
205+
206+
tokenOrchestrator.setTokenRefresher(mockTokenRefresher);
207+
tokenOrchestrator.setAuthTokenStore(mockAuthTokenStore);
208+
tokenOrchestrator.setClientMetadataProvider(clientMetadataProvider);
209+
210+
mockAuthTokenStore.loadTokens.mockResolvedValue({
211+
accessToken: { payload: { exp: 1 } },
212+
idToken: { payload: { exp: 1 } },
213+
clockDrift: 0,
214+
refreshToken: 'refreshToken',
215+
username: 'testuser',
216+
});
217+
mockAuthTokenStore.getLastAuthUser.mockResolvedValue('testuser');
218+
219+
await tokenOrchestrator.getTokens({
220+
forceRefresh: true,
221+
clientMetadata: optionsMetadata,
222+
});
223+
224+
expect(mockTokenRefresher).toHaveBeenCalledWith(
225+
expect.objectContaining({
226+
clientMetadata: optionsMetadata,
227+
}),
228+
);
229+
});
230+
});
154231
});

packages/auth/src/providers/cognito/tokenProvider/CognitoUserPoolsTokenProvider.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import {
55
AuthConfig,
66
AuthTokens,
7+
ClientMetadataProvider,
78
FetchAuthSessionOptions,
89
KeyValueStorageInterface,
910
defaultStorage,
@@ -28,16 +29,20 @@ export class CognitoUserPoolsTokenProvider
2829
this.tokenOrchestrator.setTokenRefresher(refreshAuthTokens);
2930
}
3031

31-
getTokens(
32-
{ forceRefresh }: FetchAuthSessionOptions = { forceRefresh: false },
33-
): Promise<AuthTokens | null> {
34-
return this.tokenOrchestrator.getTokens({ forceRefresh });
32+
getTokens(options: FetchAuthSessionOptions = {}): Promise<AuthTokens | null> {
33+
return this.tokenOrchestrator.getTokens(options);
3534
}
3635

3736
setKeyValueStorage(keyValueStorage: KeyValueStorageInterface): void {
3837
this.authTokenStore.setKeyValueStorage(keyValueStorage);
3938
}
4039

40+
setClientMetadataProvider(
41+
clientMetadataProvider: ClientMetadataProvider,
42+
): void {
43+
this.tokenOrchestrator.setClientMetadataProvider(clientMetadataProvider);
44+
}
45+
4146
setAuthConfig(authConfig: AuthConfig) {
4247
this.authTokenStore.setAuthConfig(authConfig);
4348
this.tokenOrchestrator.setAuthConfig(authConfig);

packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import {
44
AuthConfig,
55
AuthTokens,
6+
ClientMetadataProvider,
67
CognitoUserPoolConfig,
78
FetchAuthSessionOptions,
89
Hub,
@@ -19,7 +20,7 @@ import { assertServiceError } from '../../../errors/utils/assertServiceError';
1920
import { AuthError } from '../../../errors/AuthError';
2021
import { oAuthStore } from '../utils/oauth/oAuthStore';
2122
import { addInflightPromise } from '../utils/oauth/inflightPromise';
22-
import { CognitoAuthSignInDetails } from '../types';
23+
import { ClientMetadata, CognitoAuthSignInDetails } from '../types';
2324

2425
import {
2526
AuthTokenOrchestrator,
@@ -32,6 +33,7 @@ import {
3233

3334
export class TokenOrchestrator implements AuthTokenOrchestrator {
3435
private authConfig?: AuthConfig;
36+
clientMetadataProvider?: ClientMetadataProvider;
3537
tokenStore?: AuthTokenStore;
3638
tokenRefresher?: TokenRefresher;
3739
inflightPromise: Promise<void> | undefined;
@@ -94,6 +96,12 @@ export class TokenOrchestrator implements AuthTokenOrchestrator {
9496
return this.tokenRefresher;
9597
}
9698

99+
setClientMetadataProvider(
100+
clientMetadataProvider: ClientMetadataProvider,
101+
): void {
102+
this.clientMetadataProvider = clientMetadataProvider;
103+
}
104+
97105
async getTokens(
98106
options?: FetchAuthSessionOptions,
99107
): Promise<
@@ -130,6 +138,8 @@ export class TokenOrchestrator implements AuthTokenOrchestrator {
130138
tokens = await this.refreshTokens({
131139
tokens,
132140
username,
141+
clientMetadata:
142+
options?.clientMetadata ?? (await this.clientMetadataProvider?.()),
133143
});
134144

135145
if (tokens === null) {
@@ -147,16 +157,19 @@ export class TokenOrchestrator implements AuthTokenOrchestrator {
147157
private async refreshTokens({
148158
tokens,
149159
username,
160+
clientMetadata,
150161
}: {
151162
tokens: CognitoAuthTokens;
152163
username: string;
164+
clientMetadata?: ClientMetadata;
153165
}): Promise<CognitoAuthTokens | null> {
154166
try {
155167
const { signInDetails } = tokens;
156168
const newTokens = await this.getTokenRefresher()({
157169
tokens,
158170
authConfig: this.authConfig,
159171
username,
172+
clientMetadata,
160173
});
161174
newTokens.signInDetails = signInDetails;
162175
await this.setTokens({ tokens: newTokens });

packages/auth/src/providers/cognito/tokenProvider/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,24 @@
33
import {
44
AuthConfig,
55
AuthTokens,
6+
ClientMetadataProvider,
67
FetchAuthSessionOptions,
78
KeyValueStorageInterface,
89
TokenProvider,
910
} from '@aws-amplify/core';
1011

11-
import { CognitoAuthSignInDetails } from '../types';
12+
import { ClientMetadata, CognitoAuthSignInDetails } from '../types';
1213

1314
export type TokenRefresher = ({
1415
tokens,
1516
authConfig,
1617
username,
18+
clientMetadata,
1719
}: {
1820
tokens: CognitoAuthTokens;
1921
authConfig?: AuthConfig;
2022
username: string;
23+
clientMetadata?: ClientMetadata;
2124
}) => Promise<CognitoAuthTokens>;
2225

2326
export type AuthKeys<AuthKey extends string> = Record<AuthKey, string>;
@@ -66,6 +69,9 @@ export interface AuthTokenOrchestrator {
6669
export interface CognitoUserPoolTokenProviderType extends TokenProvider {
6770
setKeyValueStorage(keyValueStorage: KeyValueStorageInterface): void;
6871
setAuthConfig(authConfig: AuthConfig): void;
72+
setClientMetadataProvider(
73+
clientMetadataProvider: ClientMetadataProvider,
74+
): void;
6975
}
7076

7177
export type CognitoAuthTokens = AuthTokens & {

packages/auth/src/providers/cognito/types/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const cognitoHostedUIIdentityProviderMap: Record<AuthProvider, string> =
3838
/**
3939
* Arbitrary key/value pairs that may be passed as part of certain Cognito requests
4040
*/
41-
export type ClientMetadata = Record<string, string>;
41+
export type { ClientMetadata } from '@aws-amplify/core';
4242

4343
/**
4444
* Allowed values for preferredChallenge

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@ import { assertAuthTokensWithRefreshToken } from '../utils/types';
1414
import { AuthError } from '../../../errors/AuthError';
1515
import { createCognitoUserPoolEndpointResolver } from '../factories';
1616
import { createGetTokensFromRefreshTokenClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider';
17+
import { ClientMetadata } from '../types';
1718

1819
const refreshAuthTokensFunction: TokenRefresher = async ({
1920
tokens,
2021
authConfig,
2122
username,
23+
clientMetadata,
2224
}: {
2325
tokens: CognitoAuthTokens;
2426
authConfig?: AuthConfig;
2527
username: string;
28+
clientMetadata?: ClientMetadata;
2629
}): Promise<CognitoAuthTokens> => {
2730
assertTokenProviderConfig(authConfig?.Cognito);
2831
const { userPoolId, userPoolClientId, userPoolEndpoint } = authConfig.Cognito;
@@ -41,6 +44,7 @@ const refreshAuthTokensFunction: TokenRefresher = async ({
4144
ClientId: userPoolClientId,
4245
RefreshToken: tokens.refreshToken,
4346
DeviceKey: tokens.deviceMetadata?.deviceKey,
47+
ClientMetadata: clientMetadata,
4448
},
4549
);
4650

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export {
2020
OAuthConfig,
2121
CognitoUserPoolConfig,
2222
JWT,
23+
ClientMetadata,
24+
ClientMetadataProvider,
2325
} from './singleton/Auth/types';
2426
export { decodeJWT } from './singleton/Auth/utils';
2527
export {

packages/core/src/singleton/Auth/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
import { StrictUnion } from '../../types';
55
import { AtLeastOne } from '../types';
66

7+
/**
8+
* Arbitrary key/value pairs that may be passed as part of certain Cognito requests
9+
*/
10+
export type ClientMetadata = Record<string, string>;
11+
12+
/**
13+
* Function type for providing client metadata for Cognito operations
14+
*/
15+
export type ClientMetadataProvider = () => Promise<ClientMetadata>;
16+
717
// From https://github.com/awslabs/aws-jwt-verify/blob/main/src/safe-json-parse.ts
818
// From https://github.com/awslabs/aws-jwt-verify/blob/main/src/jwt-model.ts
919
interface JwtPayloadStandardFields {
@@ -66,6 +76,7 @@ export interface TokenProvider {
6676

6777
export interface FetchAuthSessionOptions {
6878
forceRefresh?: boolean;
79+
clientMetadata?: ClientMetadata;
6980
}
7081

7182
export interface AuthTokens {

0 commit comments

Comments
 (0)