-
Notifications
You must be signed in to change notification settings - Fork 320
Add request and challenge callbacks to BearerTokenAuthorizationPolicy #3368
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
base: main
Are you sure you want to change the base?
Conversation
API Change CheckAPIView identified API level changes in this PR and created the following API reviews |
d1a2597 to
1d5a6cd
Compare
1d5a6cd to
4785d13
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds extensible request authorization and authentication challenge handling to BearerTokenAuthorizationPolicy, enabling SDK clients to implement custom authorization and challenge handling logic.
- Introduces three new public traits:
OnRequest,OnChallenge, andAuthorizer(sealed) to define callbacks for request authorization and challenge handling - Adds builder methods
with_on_request()andwith_on_challenge()to configure policy behavior - Adds
Request::body_mut()method to support resetting body streams when retrying after challenges - Adds
WWW_AUTHENTICATEheader constant for accessing authentication challenge headers
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| sdk/core/typespec_client_core/src/http/request/mod.rs | Adds body_mut() method to enable mutable access to request body for stream reset operations |
| sdk/core/typespec_client_core/src/http/headers/common.rs | Adds WWW_AUTHENTICATE header constant following existing header constant patterns |
| sdk/core/typespec_client_core/CHANGELOG.md | Documents the Request::body_mut() API addition |
| sdk/core/azure_core/src/http/policies/mod.rs | Exports new public traits (Authorizer, OnChallenge, OnRequest) from bearer_token_policy module |
| sdk/core/azure_core/src/http/policies/bearer_token_policy.rs | Refactors authorization logic into BearerTokenAuthorizer implementing Authorizer trait; adds OnRequest and OnChallenge trait definitions; implements challenge handling with body stream reset; adds comprehensive test coverage for new functionality |
| sdk/core/azure_core/CHANGELOG.md | Documents new extensible authorization and challenge handling features |
| - Added extensible request authorization and authentication challenge handling to `BearerTokenAuthorizationPolicy`. | ||
| - `OnRequest`, `OnChallenge`, and `Authorizer` traits define callbacks for these features. | ||
| - `with_on_request()` and `with_on_challenge()` builder methods set callbacks for a policy instance. |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the coding guidelines, changes in sdk/core/typespec_client_core/CHANGELOG.md should be reflected in sdk/core/azure_core/CHANGELOG.md. The addition of Request::body_mut() documented in typespec_client_core/CHANGELOG.md should also be mentioned here since it's a public API change that affects azure_core users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot is right about the body_mut() call but, Copilot, you're wrong about the direction of changes reflecting in TSS to AC.
heaths
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, clean design overall but I have a few questions and concerns.
| - Added extensible request authorization and authentication challenge handling to `BearerTokenAuthorizationPolicy`. | ||
| - `OnRequest`, `OnChallenge`, and `Authorizer` traits define callbacks for these features. | ||
| - `with_on_request()` and `with_on_challenge()` builder methods set callbacks for a policy instance. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot is right about the body_mut() call but, Copilot, you're wrong about the direction of changes reflecting in TSS to AC.
| /// Sets a callback for `send` to invoke once on each request it receives, before sending the request. See [`OnRequest`] | ||
| /// for more details. When not set, the policy authorizes each request using the credential and scopes specified to `new`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc comments in Rust are like (good) git commits: first line is a summary, blank line, more info. So this should probably be:
| /// Sets a callback for `send` to invoke once on each request it receives, before sending the request. See [`OnRequest`] | |
| /// for more details. When not set, the policy authorizes each request using the credential and scopes specified to `new`. | |
| /// Sets a callback for `send` to invoke once on each request it receives, before sending the request. | |
| /// | |
| /// See [`OnRequest`] for more details. When not set, the policy authorizes each request using the credential and scopes specified to `new`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc comments for with_on_challenge are good by comparison.
| ) -> PolicyResult { | ||
| let access_token = self.access_token.read().await; | ||
| self.on_request | ||
| .on_request(ctx, request, self.authorizer.as_ref()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this let Key Vault, for example, zero out the request body until it has authenticated? That's what we did in .NET by checking if we had an Authorization header or something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes; request is mutable. My prototype for Key Vault has a single type implementing both traits so an instance can hold the challenge request's body. OnRequest stashes the body and OnChallenge restores it.
| request: &mut Request, | ||
| authorizer: &dyn Authorizer, | ||
| headers: &Headers, | ||
| ) -> Result<bool>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And a bool should always be sufficient? You can't imagine expanding on that such that maybe we should return a result model that is currently just a bool? If you're reasonably certain, I see no problem with this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes; attempting to handle a challenge won't produce useful data for the caller. If the callback handled the challenge, the request is ready to go; the policy needs only to send it. If the callback didn't handle the challenge, the policy can't either. And the policy has the same input data--the challenge--as the callback anyway.
But I suppose this could return simply Result<()>. Then Ok means retry the request, and the policy wraps Err with the response and returns that to the pipeline. 🤔 that's simpler and compels the implementer of the callback to write an error message that may be more helpful than the one check_success would otherwise produce. What do you think?
| /// Invoked once on every [`BearerTokenAuthorizationPolicy::send`] invocation, before the policy sends the request. | ||
| /// `send` doesn't call this method before retrying a request after an authentication challenge (see [`OnChallenge`] | ||
| /// for more about challenge handling). Implementations are responsible for authorizing each request via the provided | ||
| /// [`Authorizer`]. The policy sends the request when this method returns Ok. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This borderlines on the summary + blank line + description format. Something to consider. Maybe it's fine as-is.
| mod sealed { | ||
| pub trait Sealed {} | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't define your own. Just use crate::private::Sealed. Ancestors' private members are visible to all descendants.
| /// be implemented outside of this module. | ||
| #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] | ||
| #[cfg_attr(not(target_arch = "wasm32"), async_trait)] | ||
| pub trait Authorizer: sealed::Sealed + std::fmt::Debug + Send + Sync { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of this being a trait? You end up having to pass it around as dyn (or maybe not; impl is better if possible) which is slower to call and this seems like it's in a hot path.
This isn't .NET. We try to avoid traits (similar to .NET's interfaces) as much as possible, unlike .NET. Why not just expose the BearerTokenAuthorizer, especially if it's the only thing that would implement this trait. Seems like an unnecessary abstraction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this could be something like
pub type Authorize = dyn for<'a> Fn(
&'a mut Request,
&'a [&'a str],
TokenRequestOptions<'a>,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>
+ Send
+ Syncinstead, if that's preferable. The rationale for the trait aside from making the signature more legible and making it easy for an implementation to hold state is that I expect we'll eventually have at least one more implementation, supporting a different auth scheme for a separate auth policy (TokenRequestOptions will capture the differences between schemes).
|
|
||
| let mut response = next[0].send(ctx, request, &next[1..]).await?; | ||
|
|
||
| if response.status() == StatusCode::Unauthorized { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lines 79 to 96 can be simplified using .then - it alleviates the need for the has_challenge variable at all.
Something like:
let response = if let Some(ref on_challenge) = self.on_challenge {
response.headers
.get_optional_str(&WWW_AUTHENTICATE)
.then(|| {
let should_retry = on_challenge.on_challenge(ctx, request.self.authorizer.as_ref(), response.headers()).await?;
if should_retry
:
:
}).or_else(||Ok(response))?;Pseudo code written in the UX, so take it with a grain of salt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't tidier in practice because the signature being and_then<U, F>(self, f: F) -> Option<U> means the closure can't use ?. No problem inlining has_challenge though, and this check can be more terse and a little more efficient by calling get_str instead of get_optional_str
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is that the .and_then closure returns a Result<T> (that's why the Ok(response)) - you then can use ? when it's all done.
Ultimately it's not a huge deal - the code appears to be correct. I'm just a fan of chaining primitives when possible - I find the code slightly easier to understand.
| #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] | ||
| #[cfg_attr(not(target_arch = "wasm32"), async_trait)] | ||
| pub trait Authorizer: sealed::Sealed + std::fmt::Debug + Send + Sync { | ||
| /// Acquire an access token for the provided scopes and options, and set the request's authorization header. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While the trait can only be implemented within this module, the authorize function should still have a decent doccomment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean it's too brief? What do you suggest adding?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arguments mostly - I was thinking of what happens if some service needs its own request/challenge callbacks - they're going to need to access documentation on the Authorizer. And since it's a public trait, it could be used by a service client.
These enable SDK clients to implement custom request authorization and challenge handling (closes #1756).
New API:
OnRequesttrait: defines a callback invoked on each request, responsible for authorizing it before sendingOnChallengetrait: defines a callback invoked upon receiving an auth challenge. Implementations are responsible for parsing challenges and authorizing requests and telling the policy whether to retry themBearerTokenAuthorizationPolicybuilder methods:with_on_request()andwith_on_challenge()set callbacks for a policy instanceAuthorizertrait (sealed): allows callbacks to authorize requests and cache tokens without using the policy's credential directlysend()into a private implementation of thisRequest::body_mut(): lets the policy reset a body stream before retrying after a challenge