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 26 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": "prerelease",
"comment": "Extend proactive token refresh to client_credentials #6102",
"packageName": "@azure/msal-common",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Extend proactive token refresh to client_credentials #6102",
"packageName": "@azure/msal-node",
"email": "[email protected]",
"dependentChangeType": "patch"
}
2 changes: 1 addition & 1 deletion lib/msal-common/src/request/RequestValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export class RequestValidator {
}

// Remove any query parameters already included in SSO params
queryParams.forEach((value, key) => {
queryParams.forEach((_value, key) => {
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
if (eQParams[key]) {
delete eQParams[key];
}
Expand Down
4 changes: 3 additions & 1 deletion lib/msal-common/src/response/AuthenticationResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { AccountInfo } from "../account/AccountInfo";
* - fromCache - Boolean denoting whether token came from cache
* - expiresOn - Javascript Date object representing relative expiration of access token
* - extExpiresOn - Javascript Date object representing extended relative expiration of access token in case of server outage
* - refreshOn - Javascript Date object representing relative time until an access token must be refreshed
* - state - Value passed in by user in request
* - familyId - Family ID identifier, usually only used for refresh tokens
* - requestId - Request ID returned as part of the response
Expand All @@ -32,10 +33,11 @@ export type AuthenticationResult = {
accessToken: string;
fromCache: boolean;
expiresOn: Date | null;
extExpiresOn?: Date;
refreshOn: Date | null;
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
tokenType: string;
correlationId: string;
requestId?: string;
extExpiresOn?: Date;
state?: string;
familyId?: string;
cloudGraphHostName?: string;
Expand Down
41 changes: 39 additions & 2 deletions lib/msal-common/src/response/ResponseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
AuthenticationScheme,
Constants,
THE_FAMILY_ID,
ErrorCodeBounds,
} from "../utils/Constants";
import { PopTokenGenerator } from "../crypto/PopTokenGenerator";
import { AppMetadataEntity } from "../cache/entities/AppMetadataEntity";
Expand Down Expand Up @@ -151,16 +152,47 @@ export class ResponseHandler {
/**
* Function which validates server authorization token response.
* @param serverResponse
* @param refreshAccessToken
*/
validateTokenResponse(
serverResponse: ServerAuthorizationTokenResponse
serverResponse: ServerAuthorizationTokenResponse,
refreshAccessToken?: boolean
): void {
// Check for error
if (
serverResponse.error ||
serverResponse.error_description ||
serverResponse.suberror
) {

// check if 500 error
if (
refreshAccessToken &&
serverResponse.status &&
(serverResponse.status >= ErrorCodeBounds.stsErrorLowerLimit) &&
(serverResponse.status <= ErrorCodeBounds.stsErrorUpperLimit)
) {
this.logger.warning(
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
"executeTokenRequest:validateTokenResponse - AAD is currently unavailable and the access token is unable to be refreshed."
);

// don't throw an exception, but alert the user via a log that the token was unable to be refreshed
return;
// check if 400 error
} else if (
refreshAccessToken &&
serverResponse.status &&
(serverResponse.status >= ErrorCodeBounds.clientErrorLowerLimit) &&
(serverResponse.status <= ErrorCodeBounds.clientErrorUpperLimit)
) {
this.logger.warning(
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
"executeTokenRequest:validateTokenResponse - AAD is currently available but is unable to refresh the access token."
);

// don't throw an exception, but alert the user via a log that the token was unable to be refreshed
return;
}

if (
InteractionRequiredAuthError.isInteractionRequiredError(
serverResponse.error,
Expand Down Expand Up @@ -551,6 +583,7 @@ export class ResponseHandler {
let responseScopes: Array<string> = [];
let expiresOn: Date | null = null;
let extExpiresOn: Date | undefined;
let refreshOn: Date | null = null;
let familyId: string = Constants.EMPTY_STRING;

if (cacheRecord.accessToken) {
Expand Down Expand Up @@ -582,6 +615,9 @@ export class ResponseHandler {
extExpiresOn = new Date(
Number(cacheRecord.accessToken.extendedExpiresOn) * 1000
);
refreshOn = new Date(
Number(cacheRecord.accessToken.refreshOn) * 1000
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
);
}

if (cacheRecord.appMetadata) {
Expand Down Expand Up @@ -615,9 +651,10 @@ export class ResponseHandler {
accessToken: accessToken,
fromCache: fromTokenCache,
expiresOn: expiresOn,
extExpiresOn: extExpiresOn,
refreshOn: refreshOn,
correlationId: request.correlationId,
requestId: requestId || Constants.EMPTY_STRING,
extExpiresOn: extExpiresOn,
familyId: familyId,
tokenType:
cacheRecord.accessToken?.tokenType || Constants.EMPTY_STRING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import { AuthenticationScheme } from "../utils/Constants";
* - timestamp: The time at which the error occurred.
* - trace_id: A unique identifier for the request that can help in diagnostics.
* - correlation_id: A unique identifier for the request that can help in diagnostics across components.
* - status: the network request's response status
*/
export type ServerAuthorizationTokenResponse = {
status?: number;
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
// Success
token_type?: AuthenticationScheme;
scope?: string;
Expand Down
8 changes: 8 additions & 0 deletions lib/msal-common/src/utils/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ export const Constants = {
INVALID_INSTANCE: "invalid_instance",
};

export const ErrorCodeBounds = {
clientErrorLowerLimit: 400,
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
clientErrorUpperLimit: 499,
stsErrorLowerLimit: 500,
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
stsErrorUpperLimit: 599,
} as const;
export type ErrorCodeBounds = typeof ErrorCodeBounds[keyof typeof ErrorCodeBounds];

export const OIDC_DEFAULT_SCOPES = [
Constants.OPENID_SCOPE,
Constants.PROFILE_SCOPE,
Expand Down
10 changes: 10 additions & 0 deletions lib/msal-common/test/response/ResponseHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const testServerTokenResponse = {
scope: TEST_CONFIG.DEFAULT_SCOPES.join(" "),
expires_in: TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN,
ext_expires_in: TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN,
refresh_in: TEST_TOKEN_LIFETIMES.DEFAULT_REFESH_IN,
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
access_token: TEST_TOKENS.ACCESS_TOKEN,
refresh_token: TEST_TOKENS.REFRESH_TOKEN,
id_token: TEST_TOKENS.IDTOKEN_V2,
Expand Down Expand Up @@ -265,6 +266,9 @@ describe("ResponseHandler.ts", () => {
expiresOn: new Date(
Date.now() + testServerTokenResponse.body.expires_in * 1000
),
refreshOn: new Date(
Date.now() + testServerTokenResponse.body.refresh_in * 1000
), // one day in the future
account: testAccount,
tokenType: AuthenticationScheme.BEARER,
};
Expand Down Expand Up @@ -331,6 +335,9 @@ describe("ResponseHandler.ts", () => {
expiresOn: new Date(
Date.now() + testServerTokenResponse.body.expires_in * 1000
),
refreshOn: new Date(
Date.now() + testServerTokenResponse.body.refresh_in * 1000
), // one day in the future
account: testAccount,
tokenType: AuthenticationScheme.BEARER,
};
Expand Down Expand Up @@ -396,6 +403,9 @@ describe("ResponseHandler.ts", () => {
expiresOn: new Date(
Date.now() + testServerTokenResponse.body.expires_in * 1000
),
refreshOn: new Date(
Date.now() + testServerTokenResponse.body.refresh_in * 1000
), // one day in the future
account: testAccount,
tokenType: AuthenticationScheme.BEARER,
};
Expand Down
1 change: 1 addition & 0 deletions lib/msal-common/test/test_kit/StringConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const ID_TOKEN_CLAIMS = {
// Test Expiration Vals
export const TEST_TOKEN_LIFETIMES = {
DEFAULT_EXPIRES_IN: 3599,
DEFAULT_REFESH_IN: 86400,
TEST_ID_TOKEN_EXP: 1536361411,
TEST_ACCESS_TOKEN_EXP: 1537234948,
};
Expand Down
44 changes: 40 additions & 4 deletions lib/msal-node/src/client/ClientCredentialClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
export class ClientCredentialClient extends BaseClient {
private scopeSet: ScopeSet;
private readonly appTokenProvider?: IAppTokenProvider;
private lastCacheOutcome: CacheOutcome;
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved

constructor(
configuration: ClientConfiguration,
Expand All @@ -59,7 +60,23 @@ export class ClientCredentialClient extends BaseClient {

const cachedAuthenticationResult =
await this.getCachedAuthenticationResult(request);

if (cachedAuthenticationResult) {
// if the token is not expired but must be refreshed; get a new one in the background
if (this.lastCacheOutcome === 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."
);

// return the cached token, don't wait for the result of this request
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
const refreshAccessToken = true;
this.executeTokenRequest(request, this.authority, refreshAccessToken);
}

// reset the last cache outcome
this.lastCacheOutcome = CacheOutcome.NO_CACHE_HIT;
bgavrilMS marked this conversation as resolved.
Show resolved Hide resolved

// otherwise, the token is not expired and does not need to be refreshed
return cachedAuthenticationResult;
} else {
return await this.executeTokenRequest(request, this.authority);
Expand All @@ -78,32 +95,48 @@ export class ClientCredentialClient extends BaseClient {
cacheContext = new TokenCacheContext(this.config.serializableCache, false);
await this.config.persistencePlugin.beforeCacheAccess(cacheContext);
}

const cachedAccessToken = this.readAccessTokenFromCache();

if (this.config.serializableCache && this.config.persistencePlugin && cacheContext) {
await this.config.persistencePlugin.afterCacheAccess(cacheContext);
}

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


// must refresh due to the expires_in value
if (
TimeUtils.isTokenExpired(
cachedAccessToken.expiresOn,
this.config.systemOptions.tokenRenewalOffsetSeconds
)
) {
this.lastCacheOutcome = CacheOutcome.CACHED_ACCESS_TOKEN_EXPIRED;
this.serverTelemetryManager?.setCacheOutcome(
CacheOutcome.CACHED_ACCESS_TOKEN_EXPIRED
);

return null;
}

// must refresh (in the background) due to the refresh_in value
if (
cachedAccessToken.refreshOn &&
TimeUtils.isTokenExpired(cachedAccessToken.refreshOn.toString(), 0)
) {
this.lastCacheOutcome = CacheOutcome.REFRESH_CACHED_ACCESS_TOKEN;
this.serverTelemetryManager?.setCacheOutcome(
CacheOutcome.REFRESH_CACHED_ACCESS_TOKEN
);
}

return await ResponseHandler.generateAuthenticationResult(
this.cryptoUtils,
this.authority,
Expand Down Expand Up @@ -150,7 +183,8 @@ export class ClientCredentialClient extends BaseClient {
*/
private async executeTokenRequest(
request: CommonClientCredentialRequest,
authority: Authority
authority: Authority,
refreshAccessToken?: boolean,
): Promise<AuthenticationResult | null> {
let serverTokenResponse: ServerAuthorizationTokenResponse;
let reqTimestamp: number;
Expand Down Expand Up @@ -205,7 +239,9 @@ export class ClientCredentialClient extends BaseClient {
headers,
thumbprint
);

serverTokenResponse = response.body;
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
serverTokenResponse.status = response.status;
}

const responseHandler = new ResponseHandler(
Expand All @@ -217,7 +253,7 @@ export class ClientCredentialClient extends BaseClient {
this.config.persistencePlugin
);

responseHandler.validateTokenResponse(serverTokenResponse);
responseHandler.validateTokenResponse(serverTokenResponse, refreshAccessToken);

const tokenResponse = await responseHandler.handleServerTokenResponse(
serverTokenResponse,
Expand Down
Loading
Loading