Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend proactive token refresh to client_credentials #6102

Merged
merged 35 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
df66417
Extend proactive token refresh to client_credentials
rgins16 Jun 7, 2023
7c2aa2f
Change files
rgins16 Jun 7, 2023
27610f9
beachball
rgins16 Jun 7, 2023
2b2390d
small improvement
rgins16 Jun 7, 2023
1f9c3b8
Merge branch 'dev' into refresh
rgins16 Jun 8, 2023
3cfadfd
Implemented Feedback
rgins16 Jun 8, 2023
7d806bd
Implemented Feedback
rgins16 Jun 10, 2023
86f74aa
Implemented Feedback
rgins16 Jun 14, 2023
399b804
Change files
rgins16 Jun 14, 2023
8afa0cd
Fixed some verbiage
rgins16 Jun 14, 2023
0f6094e
Removed unused import
rgins16 Jun 14, 2023
92699af
Fixed verbiage
rgins16 Jun 14, 2023
6b0496f
Implemented feedback and changed some logic
rgins16 Jun 15, 2023
8b1c755
Merge branch 'dev' into refresh
rgins16 Jun 15, 2023
d3617a4
Removed unused code
rgins16 Jun 15, 2023
1bf3a89
Implemented Feedback
rgins16 Jun 20, 2023
06f7a83
Merge branch 'dev' into refresh
rgins16 Jun 20, 2023
45ba315
Modified unit test
rgins16 Jun 20, 2023
599a1ae
enum -> object literal
rgins16 Jun 21, 2023
b2a720c
Merge branch 'dev' into refresh
rgins16 Jul 12, 2023
2995c2e
Merge branch 'dev' into refresh
Robbie-Microsoft Jul 12, 2023
4e74f5c
Implemented Feedback and improved tests
rgins16 Jul 13, 2023
d9113a4
Fixed typo
rgins16 Jul 13, 2023
b0e018a
Merge branch 'dev' into refresh
Robbie-Microsoft Jul 13, 2023
7629ece
Fixed broken tests
rgins16 Jul 13, 2023
422fcfc
Merge branch 'refresh' of https://github.com/AzureAD/microsoft-authen…
rgins16 Jul 13, 2023
af74365
Implemented GitHub Feedback
rgins16 Jul 14, 2023
7f69f90
Improved error message in response handler
rgins16 Jul 14, 2023
00e6773
Implemented GitHub Feedback
rgins16 Jul 14, 2023
10718c1
Merge branch 'dev' into refresh
Robbie-Microsoft Aug 1, 2023
35a17a8
Merge branch 'dev' into refresh
Robbie-Microsoft Aug 4, 2023
ec7eff5
Ran prettier command
Robbie-Microsoft Aug 4, 2023
9420e2f
Merge branch 'dev' into refresh
Robbie-Microsoft Aug 8, 2023
1b20310
Merge branch 'dev' into refresh
Robbie-Microsoft Aug 15, 2023
21ae216
Merge branch 'dev' into refresh
Robbie-Microsoft Aug 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
"comment": "Extend proactive token refresh to client_credentials",
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
"packageName": "@azure/msal-node",
"email": "[email protected]",
"dependentChangeType": "patch"
}
45 changes: 41 additions & 4 deletions lib/msal-node/src/client/ClientCredentialClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,14 @@ export class ClientCredentialClient extends BaseClient {
): Promise<AuthenticationResult | null> {
const cachedAccessToken = this.readAccessTokenFromCache();

// must refresh due to non-existent access_token
if (!cachedAccessToken) {
this.serverTelemetryManager?.setCacheOutcome(
CacheOutcome.NO_CACHED_ACCESS_TOKEN
);
return null;
}

if (
// must refresh due to the expires_in value
} else if (
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
TimeUtils.isTokenExpired(
cachedAccessToken.expiresOn,
this.config.systemOptions.tokenRenewalOffsetSeconds
Expand All @@ -89,7 +89,26 @@ export class ClientCredentialClient extends BaseClient {
this.serverTelemetryManager?.setCacheOutcome(
CacheOutcome.CACHED_ACCESS_TOKEN_EXPIRED
);

return null;
// the token is not expired, but it must be refreshed due to the refresh_in value
} else if (
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
cachedAccessToken.refreshOn &&
TimeUtils.isTokenExpired(cachedAccessToken.refreshOn, 0)
) {
this.serverTelemetryManager?.setCacheOutcome(
CacheOutcome.REFRESH_CACHED_ACCESS_TOKEN
);

this.logger.info(
"ClientCredentialClient:getCachedAuthenticationResult - Cached access token's refreshOn property has been exceeded'. It's not expired, but must be refreshed."
);

// the token's cache key // will be used to remove the token from the cache if necessary
const accessTokenCacheKey = cachedAccessToken.generateCredentialKey();

// start a background request to get a new token
this.executeTokenRequest(request, this.authority, accessTokenCacheKey);
}

return await ResponseHandler.generateAuthenticationResult(
Expand Down Expand Up @@ -138,7 +157,8 @@ export class ClientCredentialClient extends BaseClient {
*/
private async executeTokenRequest(
request: CommonClientCredentialRequest,
authority: Authority
authority: Authority,
accessTokenCacheKey?: string,
): Promise<AuthenticationResult | null> {
let serverTokenResponse: ServerAuthorizationTokenResponse;
let reqTimestamp: number;
Expand Down Expand Up @@ -193,6 +213,23 @@ export class ClientCredentialClient extends BaseClient {
headers,
thumbprint
);

// check if a new token is being requested because the cached token needs to be refreshed, not because it's expired
if (accessTokenCacheKey) {
// check if ESTS is down (status code 429 or 5xx)
if ((response.status === 429) || (response.status >= 500)) {
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
this.logger.warning(
"ClientCredentialClient:executeTokenRequest - AAD is currently unavailable and is unable to refresh the token."
);
// ESTS is not down, the cached token is bad and should be removed from the cache
} else {
this.logger.error(
"ClientCredentialClient:executeTokenRequest - AAD is currently available but is unable to refresh the token. Returning the unexpired token and removing it from the cache."
);
this.cacheManager.removeAccessToken(accessTokenCacheKey);
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
}
}

serverTokenResponse = response.body;
}

Expand Down
82 changes: 82 additions & 0 deletions lib/msal-node/test/client/ClientCredentialClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,88 @@ describe("ClientCredentialClient unit tests", () => {
expect(authResult.state).toHaveLength(0);
});

it("re-acquires a token via a network request, because the cached token's refreshOn value is expired", async () => {
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
sinon
.stub(Authority.prototype, <any>"getEndpointMetadataFromNetwork")
.resolves(DEFAULT_OPENID_CONFIG_RESPONSE.body);
sinon
.stub(
bgavrilMS marked this conversation as resolved.
Show resolved Hide resolved
ClientCredentialClient.prototype,
<any>"executePostToTokenEndpoint"
)
.resolves(CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT);

const createTokenRequestBodySpy = sinon.spy(
ClientCredentialClient.prototype,
<any>"createTokenRequestBody"
);
const client = new ClientCredentialClient(config);
const clientCredentialRequest: CommonClientCredentialRequest = {
authority: TEST_CONFIG.validAuthority,
correlationId: TEST_CONFIG.CORRELATION_ID,
scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE,
};

const expectedAtEntity: AccessTokenEntity =
AccessTokenEntity.createAccessTokenEntity(
"",
"login.microsoftonline.com",
"an_access_token",
config.authOptions.clientId,
TEST_CONFIG.TENANT,
TEST_CONFIG.DEFAULT_GRAPH_SCOPE.toString(),
TimeUtils.nowSeconds() + 4600,
TimeUtils.nowSeconds() + 4600,
mockCrypto,
TimeUtils.nowSeconds() - 4600, // refreshOn
AuthenticationScheme.BEARER
);
console.log(expectedAtEntity);

sinon
.stub(
ClientCredentialClient.prototype,
<any>"readAccessTokenFromCache"
)
.returns(expectedAtEntity);

const authResult = (await client.acquireToken(
clientCredentialRequest
)) as AuthenticationResult;
const expectedScopes = [TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0]];
expect(authResult.fromCache).toBe(false);
expect(authResult.scopes).toEqual(expectedScopes);
expect(authResult.accessToken).toEqual(
CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT.body.access_token
);
expect(authResult.state).toHaveLength(0);

expect(
createTokenRequestBodySpy.calledWith(clientCredentialRequest)
).toBe(true);

const returnVal = (await createTokenRequestBodySpy
.returnValues[0]) as string;
expect(
returnVal.includes(`${TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0]}`)
).toBe(true);
expect(
returnVal.includes(
`${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}`
)
).toBe(true);
expect(
returnVal.includes(
`${AADServerParamKeys.GRANT_TYPE}=${GrantType.CLIENT_CREDENTIALS_GRANT}`
)
).toBe(true);
expect(
returnVal.includes(
`${AADServerParamKeys.CLIENT_SECRET}=${TEST_CONFIG.MSAL_CLIENT_SECRET}`
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
)
).toBe(true);
});

it("acquires a token, skipCache = true", async () => {
sinon
.stub(Authority.prototype, <any>"getEndpointMetadataFromNetwork")
Expand Down