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

auth: manual token expiration, better auxiliary token handling, improve tests #665

Merged
merged 15 commits into from
Nov 27, 2023

Conversation

manicminer
Copy link
Contributor

@manicminer manicminer commented Sep 26, 2023

Better Handling of Auxiliary Tokens

Adds the AuthorizeRequest field to client.Client which API clients can define for customized authoriation behavior. This is then utilized by resourcemanager.Client to decorate requests with both Authorization and x-ms-authorization-auxiliary headers.

Adds a helper function SetAuthHeader() which decorates an *http.Request with a bearer token, e.g. adds the Authorization header. This is called by base.Client when AuthorizeRequest is nil.

Manual Token Expiration

This provides the ability to intentionally expire a cached access token, which causes a new token to be automatically acquired the next time the Token() or AuxiliaryTokens() methods are called.

Downstream implementers can test whether an auth.Authorizer implements auth.CachingAuthorizer, in case they receive a standard auth.Authorizer, and then call InvalidateTokenCache() prior to making a new request. This requires that the method is called as-needed when an action is taken that is likely to necessitate a new access token, for example with management groups which make use of token claims for access control.

Closes: #459

Related:

Whilst the new ExpireToken() method only has any meaning or effect when
a token is being cached, in practise this all the existing Authorizers wrap
themselves with a CachedAuthorizer. For consistency this is added to the
Authorizer interface to make downstream implementation simpler and this
should not cause any incompatibility or errors where one of the provided
constructors is used.
@manicminer manicminer added enhancement New feature or request authentication labels Sep 26, 2023
@manicminer manicminer requested a review from a team as a code owner September 26, 2023 15:55
@manicminer manicminer changed the title auth: ability to manually expire a cached token auth: manual token expiration, better auxiliary token handling, improve tests Sep 27, 2023
@manicminer
Copy link
Contributor Author

Unit test results

Screenshot 2023-09-27 at 13 04 58

@industrialzombie
Copy link

Hey,

The client will need to be updated to use this new SetAuthHeaders() function when it executes a request.
Should that be included in the scope of this change?

https://github.com/hashicorp/go-azure-sdk/blob/main/sdk/client/client.go#L337

@manicminer
Copy link
Contributor Author

@industrialzombie Yes it will, I had thought to add that separately for additional safety downstream, but on balance it's probably fine to add it here and merge it all together 👍

@industrialzombie
Copy link

Is it possible to get an estimate on when this might be reviewed?

@manicminer
Copy link
Contributor Author

@industrialzombie We'll look at this after today's release, so that we have a few days to catch any potential bugs before shipping.

@industrialzombie
Copy link

Thanks, I appreciate the feedback!

Copy link
Contributor

@tombuildsstuff tombuildsstuff left a comment

Choose a reason for hiding this comment

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

Left some comments inline but otherwise 👍

sdk/auth/cached_authorizer.go Show resolved Hide resolved
Comment on lines 15 to 17
Token(ctx context.Context, request *http.Request) (*oauth2.Token, error)

AuxiliaryTokens(ctx context.Context, request *http.Request) ([]*oauth2.Token, error)
ExpireTokens() error
Copy link
Contributor

Choose a reason for hiding this comment

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

for this one (but also in retrospect) is it worth adding some documentation for this one? As it stands the "will do nothing" comments on each implementation will show as needed, but don't strictly explain the purpose for this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good shout, will add 👍

)

// SetAuthHeaders decorates a *http.Request with necessary authorization headers for Azure APIs. For more information about the vendor-specific
// `x-ms-authorization-auxiliary` header, see https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/authenticate-multi-tenant
func SetAuthHeaders(ctx context.Context, req *http.Request, authorizer Authorizer) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

since this is specific to Resource Manager, this probably wants to go into the Resource Manager Client?

Copy link
Contributor Author

@manicminer manicminer Oct 23, 2023

Choose a reason for hiding this comment

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

agreed, other APIs may perhaps support the concept of multiple tenants but the implementation will be different

sdk/auth/token.go Show resolved Hide resolved
@@ -327,14 +327,11 @@ func (c *Client) Execute(ctx context.Context, req *Request) (*Response, error) {
return nil, fmt.Errorf("req.Request was nil")
}

// at this point we're ready to send the HTTP Request, as such let's get the Authorization token
// and add that to the request
// Set Authorization and X-Ms-Authorization-Auxiliary headers
Copy link
Contributor

Choose a reason for hiding this comment

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

Doing this will cause issues with Storage Data Plane which is super picky about the headers it supports

Perhaps Client should have a ConfigureAuthorizerForRequest function/method defined against the struct which the implementations can pass-in?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, I've added this and set a custom authorization function for ResourceManagerClient. Where such custom function is not specified, the base client will fall back to setting a standard Authorization header with a bearer token (i.e. its current behavior).

sdk/client/client.go Outdated Show resolved Hide resolved
// ---
// ExpireToken has no effect with shared keys
func (c *SharedKeyAuthorizer) ExpireTokens() error {
return fmt.Errorf("SharedKeyAuthorizer tokens cannot expire")
Copy link
Contributor

Choose a reason for hiding this comment

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

given the other authorizers are a noop, why not do the same here? perhaps this method wants renaming to InvalidateTokenCache or something to better reflect what it's doing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've done away with this ambiguity and added a CachingAuthorizer interface which can be tested for at runtime to call the InvalidateTokenCache() method.

* Add a custom authorization function for ResourceManagerClient, which
  sets the `Authorization` and `x-ms-authorization-auxiliary` headers.
* When custom authorization function is not defined, use the
  auth.SetAuthHeader() function to decorate requests with a standard
  `Authorization` header having a bearer token.
@manicminer
Copy link
Contributor Author

Thanks for the review @tombuildsstuff, I've reworked it to remove the anbiguity over invalidation by adding a CachingAuthorizer interface, and added an AuthorizeRequest property to the base client to allow API clients to supply a custom function for decorating the request. WDYT?

Copy link
Contributor

@tombuildsstuff tombuildsstuff left a comment

Choose a reason for hiding this comment

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

A couple of minor comments but otherwise 👍

// tokens to be acquired when Token() or AuxiliaryTokens() are next called
func (c *CachedAuthorizer) InvalidateCachedTokens() error {
if c.token == nil {
return fmt.Errorf("internal-error: c.token was nil")
Copy link
Contributor

Choose a reason for hiding this comment

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

tbh I think it's worth this being a noop?

Suggested change
return fmt.Errorf("internal-error: c.token was nil")
return nil

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, the end result should be the same 👍

Comment on lines 36 to 40
auxTokenValues := make([]string, 0)
for _, auxToken := range auxTokens {
auxTokenValues = append(auxTokenValues, fmt.Sprintf("%s %s", auxToken.Type(), auxToken.AccessToken))
}
req.Header.Set("X-Ms-Authorization-Auxiliary", strings.Join(auxTokenValues, ", "))
Copy link
Contributor

Choose a reason for hiding this comment

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

if there aren't any, should we omit this?

@manicminer manicminer added the release-once-merged The SDK should be released once this PR is merged label Nov 27, 2023
@manicminer manicminer merged commit 8e462d0 into main Nov 27, 2023
5 checks passed
@manicminer manicminer deleted the f/manual-token-expiry branch November 27, 2023 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
authentication enhancement New feature or request release-once-merged The SDK should be released once this PR is merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support Token Refresh to resolve Azure Management Group issue
3 participants