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 silent auth scopes and refresh behavior #327

Merged
merged 6 commits into from
Jul 18, 2022

Conversation

chlowell
Copy link
Collaborator

@chlowell chlowell commented Jul 8, 2022

This fixes two problems with silent authentication:

  1. it ignores the requested scopes (closes [Bug] wrong scope when getting token silently #322; thanks @mkurock)
  2. it uses refresh tokens only to refresh expired access tokens i.e., silent authentication for a given scope succeeds only when the cache contains an access token (expired or not) for that scope

Fixing the first problem is straightforward: just pass the right argument. The second problem is more complex. It arises in the cache's Read method, which assumes the caller requires an access token matching the given parameters and returns an error when it doesn't find one. If the caller could in principle use a previously cached refresh token to authenticate, it never gets the chance because Read doesn't return that refresh token. I fixed this by having Read continue past an access token cache miss to fetch more data, such as a refresh token, before returning. This means Read now returns a zero-valued access token in some cases. This is okay because base.Client is the only caller and already validates access tokens before returning them to the application.

@chlowell chlowell added the bug Something isn't working label Jul 8, 2022
}

var cc *accesstokens.Credential
if silent.RequestType == accesstokens.ATConfidential {
cc = silent.Credential
}

token, err := b.Token.Refresh(ctx, silent.RequestType, b.AuthParams, cc, storageTokenResponse.RefreshToken)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is problem 1. b.AuthParams holds common parameters for all auth requests. Its type has a scopes field but on this instance that field always has a zero value (default scopes are handled elsewhere). authParams is an instance of the same type which carries overriding values for this auth request, and the scopes requested by the user application.

@@ -94,10 +94,7 @@ func (m *Manager) Read(ctx context.Context, authParameters authority.AuthParams,
return TokenResponse{}, err
}

accessToken, err := m.readAccessToken(homeAccountID, metadata.Aliases, realm, clientID, scopes)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is problem 2. This helper returns an error when it misses. A miss isn't fatal and using an error to signal one requires the caller to inspect the error to determine whether it's safe to proceed. I could've used a sentinel error for misses but that seemed excessive because the helper really doesn't need to return an error--it only searches a map.

Copy link
Member

@bgavrilMS bgavrilMS left a comment

Choose a reason for hiding this comment

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

LGTM, at least by reading the tests.

@bgavrilMS
Copy link
Member

This fixes two problems with silent authentication:

  1. it ignores the requested scopes (closes [Bug] wrong scope when getting token silently #322; thanks @mkurock)
  2. it uses refresh tokens only to refresh expired access tokens i.e., silent authentication for a given scope succeeds only when the cache contains an access token (expired or not) for that scope

Fixing the first problem is straightforward: just pass the right argument. The second problem is more complex. It arises in the cache's Read method, which assumes the caller requires an access token matching the given parameters and returns an error when it doesn't find one. If the caller could in principle use a previously cached refresh token to authenticate, it never gets the chance because Read doesn't return that refresh token. I fixed this by having Read continue past an access token cache miss to fetch more data, such as a refresh token, before returning. This means Read now returns a zero-valued access token in some cases. This is okay because base.Client is the only caller and already validates access tokens before returning them to the application.

To clarify some aspects for issue 1.

  • AAD may return more scopes that what you request for. E.g. you request for User.Read, but AAD return User.Read and Mail.Read. This is an AAD optimization to minimize number of requests.
  • MSALs should associate the scopes returned by AAD with the token in the cache. Not the requested scopes.
  • MSAL needs to add "openid profile offline_access" to request in order for caching to work. These should just be ignored when it comes to caching.

openid + profile tells AAD to issue an IdToken
offline_access tells AAD to issue a refresh token.

@chlowell
Copy link
Collaborator Author

Thanks for the additional context. I added a test to verify granted scopes are handled correctly in caching. Clients appear to add "openid profile offline_access" scopes as you describe--at least, I see this in live traffic and the code looks right--but that doesn't have unit test coverage today (#329).

@sonarqubecloud
Copy link

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 1 Code Smell

No Coverage information No Coverage information
0.0% 0.0% Duplication

// Validate validates that this AccessToken can be used.
func (a AccessToken) Validate() error {
if FakeValidate != nil {

Choose a reason for hiding this comment

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

It looks like a bad idea. It should be possible to just pass a valid token in the test.

Copy link
Collaborator Author

@chlowell chlowell Jul 15, 2022

Choose a reason for hiding this comment

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

To give some context, the cache calls this method before writing a token and won't write that token if this method returns an error. This method will return an error if the token expires within the next 5 minutes. A test that requires the cache to contain an expired token therefore can't pass in a valid token unless it then waits for that token to expire. Perhaps the cache shouldn't check expiry before writing tokens (too big a change to contemplate here) but there are of course other ways to work around this. The test could cache a token that expires in 5 minutes and 1 millisecond then time.Sleep(2*time.Millisecond). I like that approach, but I still prefer FakeValidate because it may prove useful in other test cases and doesn't require the one I'm adding in this PR to take a dependency on timing or behavior not under test.

Edit: for anyone not familiar with Go, let me add that FakeValidate isn't part of the public API because it's in an internal package.

@chlowell chlowell merged commit 26c6116 into release-0.5.3 Jul 18, 2022
siddhijain added a commit that referenced this pull request Jul 18, 2022
Fix silent auth scopes and refresh behavior (#327)
@chlowell chlowell deleted the chlowell/fix-silent-auth branch February 25, 2023 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants