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

Fix the CAE bug in js core lib #22324

Merged
merged 16 commits into from
Jul 8, 2022
Merged

Fix the CAE bug in js core lib #22324

merged 16 commits into from
Jul 8, 2022

Conversation

MaryGao
Copy link
Member

@MaryGao MaryGao commented Jun 22, 2022

Issue

With CAE, we introduce a new case where a resource provider can reject a token when it isn't expired. To inform clients to bypass their cache even though the cached tokens haven't expired, we introduce a mechanism called claim challenge to indicate that the token was rejected and a new access token need to be issued by Azure AD. CAE requires a client update to understand claim challenge.
https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-continuous-access-evaluation

From client side we are supposed to refresh our token in claim challenge, below are the code snippet:

const accessToken = await onChallengeOptions.getAccessToken(
parsedChallenge.scope ? [parsedChallenge.scope] : scopes,
{
claims: decodeStringToString(parsedChallenge.claims),
} as GetTokenOptions
);

However in token cycler we missed this condition to refresh the token if we have an invalid but not expired token, below are the logic to refresh:

const mustRefresh = tenantId !== tokenOptions.tenantId || cycler.mustRefresh;
if (mustRefresh) return refresh(scopes, tokenOptions);

How to fix

During claim challenge we need to bypass our cached token and refresh a new token in some way. Currently I add the claims in GetTokenOptions and refine the condition mustRefresh if we have claim details:

    // If the tenantId passed in token options is different to the one we have
    // Or if we are in claim challenge and the token was rejected and a new access token need to be issued, we need to
    // refresh the token with the new tenantId or token.
    const mustRefresh = tenantId !== tokenOptions.tenantId || !!tokenOptions.claims || cycler.mustRefresh;

    if (mustRefresh) return refresh(scopes, tokenOptions);

How to reproduce

  • Make sure you have a user with some RBAC on AzureSDKTeam, and it is under “CAE users” group (that group have CAE enabled).
  • Double check that azure-identity already enabled the CAE.
  • Authenticate your client with UsernamePasswordCredential (note the endpoint is different, it is on EUAP).
  • Have some loop to use your client to do something e.g. list subscriptions. Do it e.g. every 10 seconds for an hour.
  • Somewhere else (another thread, or another app), revoke the session, while loop in (4) is running
  • During the loop in (4), make sure your code on CAE works (e.g. have a break point, or compare the new token to the old token).

fix #21820

@azure-sdk
Copy link
Collaborator

API change check

APIView has identified API level changes in this PR and created following API reviews.

azure-core-auth

Copy link
Member

@joheredi joheredi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks good overall! The only thing I wonder is if we should make the "force refresh" decision in the challenge callback rather than in the cycler. @jeremymeng what do you think?

azure-pipelines.yml Outdated Show resolved Hide resolved
// If the tenantId passed in token options is different to the one we have
// Or if we are in claim challenge and the token was rejected and a new access token need to be issued, we need to
// refresh the token with the new tenantId or token.
const mustRefresh = tenantId !== tokenOptions.tenantId || !!tokenOptions.claims || cycler.mustRefresh;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeremymeng, I'm wondering if we should have an option that can set mustRefresh to true instead of adding more logic around specific contents of the token in the cycler. This way the individual callbacks can make the decision about forcing refresh or not. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is usually clearer if you do Boolean(tokenOptions.claims)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaryGao can you please add some tests to validate this behavior? I would like to see specifically a test that validates that the token doesn't get refreshed when there is no need to refresh but we still have claims

Copy link
Member Author

@MaryGao MaryGao Jul 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joheredi Currently I just assume that we must refresh the token if claim details provides. Do you mean we need to support your cases?

Update below existing test cases:

    // Case1: Will refresh token once as the first time token is empty 
    await pipeline.sendRequest(testHttpsClient, pipelineRequest);
    clock.tick(5000);
    // Case2: Will refresh token twice
    // - 1st refreshing because the token is epxired
    // - 2nd refreshing because the response with old token has 401 error with claim details so we need refresh token again
    await pipeline.sendRequest(testHttpsClient, pipelineRequest);
    // Case3: Token is still valid and no need to refresh it
    await pipeline.sendRequest(testHttpsClient, pipelineRequest);

/**
* Claim details to perform the Continuous Access Evaluation authentication flow
*/
claims?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the claims we get from the challenge right? We are already passing it in authorizeRequestOnClaimChallenge callback but forgot to update this interface when the callback was originally added

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly! This parameter is already passed in code

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Thanks for adding it! Could you please add an entry to core-auth CHANGELOG?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to also remove the "as GetTokenOptions" cast https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client/src/authorizeRequestOnClaimChallenge.ts#L87 as it is no longer needed.

Copy link
Member Author

@MaryGao MaryGao Jul 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

@MaryGao MaryGao requested a review from joheredi July 4, 2022 14:48
@check-enforcer
Copy link

check-enforcer bot commented Jul 5, 2022

This pull request is protected by Check Enforcer.

What is Check Enforcer?

Check Enforcer helps ensure all pull requests are covered by at least one check-run (typically an Azure Pipeline). When all check-runs associated with this pull request pass then Check Enforcer itself will pass.

Why am I getting this message?

You are getting this message because Check Enforcer did not detect any check-runs being associated with this pull request within five minutes. This may indicate that your pull request is not covered by any pipelines and so Check Enforcer is correctly blocking the pull request being merged.

What should I do now?

If the check-enforcer check-run is not passing and all other check-runs associated with this PR are passing (excluding license-cla) then you could try telling Check Enforcer to evaluate your pull request again. You can do this by adding a comment to this pull request as follows:
/check-enforcer evaluate
Typically evaulation only takes a few seconds. If you know that your pull request is not covered by a pipeline and this is expected you can override Check Enforcer using the following command:
/check-enforcer override
Note that using the override command triggers alerts so that follow-up investigations can occur (PRs still need to be approved as normal).

What if I am onboarding a new service?

Often, new services do not have validation pipelines associated with them, in order to bootstrap pipelines for a new service, you can issue the following command as a pull request comment:
/azp run prepare-pipelines
This will run a pipeline that analyzes the source tree and creates the pipelines necessary to build and validate your pull request. Once the pipeline has been created you can trigger the pipeline using the following comment:
/azp run js - [service] - ci

@MaryGao
Copy link
Member Author

MaryGao commented Jul 5, 2022

/check-enforcer evaluate

// Or if we are in claim challenge and the token was rejected and a new access token need to be issued, we need to
// refresh the token with the new tenantId or token.
const mustRefresh =
tenantId !== tokenOptions.tenantId || Boolean(tokenOptions.claims) || cycler.mustRefresh;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so whenever we see claims, we know we have to refresh? Can we add a test with claims and one without to make sure those booleans are covered?

Copy link
Member Author

@MaryGao MaryGao Jul 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test to cover that

@jeremymeng
Copy link
Member

I think it looks good overall! The only thing I wonder is if we should make the "force refresh" decision in the challenge callback rather than in the cycler. @jeremymeng what do you think?

I agree that it would be better if we have a way to invalidate the token cache. I logged an issue before for token cache api discussion. I added this to the issue.

For this PR I think it is fine for now, considering that tenantId and claims are special in the flow.

Copy link
Member

@joheredi joheredi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change looks good. Can you please add a test to make sure we are not always refreshing the token?

@MaryGao
Copy link
Member Author

MaryGao commented Jul 8, 2022

Change looks good. Can you please add a test to make sure we are not always refreshing the token?

Added with the test

`it("tests that once the challenge is processed we won't refresh the token again and again", async function () {

@MaryGao MaryGao merged commit 6758bdb into Azure:main Jul 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Customised bearerTokenAuthenticationPolicy will fail to get access token at the first time
5 participants