-
Notifications
You must be signed in to change notification settings - Fork 256
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
[Tech Spec] Supporting Bearer Auth #14174
base: dev
Are you sure you want to change the base?
Conversation
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.
PR Overview
This PR introduces a tech specification for supporting bearer token authentication for NuGet credential providers. The document outlines the functional and technical changes needed to update the NuGet CLI to support bearer auth, discusses drawbacks, and presents alternative schemes.
Reviewed Changes
File | Description |
---|---|
accepted/2025/supporting-bearer-auth.md | New tech spec document detailing changes to enable bearer token auth |
Copilot reviewed 1 out of 1 changed files in this pull request and generated no comments.
|
||
The push command will no longer display a warning when no API key is specified. | ||
|
||
When the NuGet client receives an unauthorized response, it will check to see if the response header includes bearer, if it does it’ll retry the request passing the credential as a bearer token in the authentication 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.
Would it be beneficial to have a way to check that a feed supports bearer tokens prior to making a request? Can a feed advertise that it supports bearer tokens in index.json which we would have already requested?
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 is a good question. Personally, I am not sure. My understanding of auth in HTTP is that WWW-Authenticate
is the blessed way of advertising the supported authentication methods, but this is returned with a 401 challenge (essentially after a request has failed without proper credential). It seems what you're getting at is whether the client should be able to know about these available auth methods prior to hitting a 401. I think this only matters for public access (anonymous allowed) feeds like NuGet.org where an auth challenge will not be hit on the index.json
or if the index.json
auth challenge differs, say, per resource type. NuGet.org has auth challenge differing on resource type -- specifically the PackagePublish
family of resource types requires auth but none of the other resources do.
I too wonder if it would be beneficial for the client impl to know this in advance, before seeing a 401.
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.
In this situation I think it's best to stick to the defined protocol. It's well documented and standard practice.
Also thinking of the push command, the user can provide the URL to push to, so we might not have the index.json to determine what auth the feed supports.
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.
Good point on the push URL difference. I don't know how widely people push to https://api.nuget.org/v3/index.json
vs. https://www.nuget.org/api/v2/package
. I think we can't know from server side data.
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 know Artifactory allows for pushing to subdirectories in the package repo.
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.
Another place I was going with this as well is can we cache the knowledge in client that this feed supports bearer tokens or that it does not support it, and therefore not make requests with/without bearer depending on that knowledge?
This caching could be a consideration regardless of whether my comment about a feed advertising its support is implemented or not.
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.
Either way if we don't have credentials, we will be making the first request to get the initial 401 response. If we want to cache something, I think the response from the credential provider makes the most since, which we already do. I reworded it though so it doesn't sound so prescriptive on whether or not bearer is returned
|
||
### Functional explanation | ||
|
||
The push command will no longer display a warning when no API key is specified. |
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.
Does this mean we're moving the CLI away from the ApiKey option?
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.
That's the plan, we would need to continue to support it since other feeds might still use it.
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! It's probably worth stating that this is the beginning of slowly encouraging a change in behavior. The motivation doesn't explicitly say "and it's a direction we want to begin moving towards with this spec."
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.
Added a line in the motivation
|
||
Encourage .NET to implement [Issue #91867](https://github.com/dotnet/runtime/issues/91867#issue-1889923702), allowing for native support of bearer tokens. | ||
This would allow us to remove the code required to pass the bearer token in the header and instead let the framework handle it. | ||
This would require moving NuGet HTTP requests out of the devenv.exe into a service hub process or waiting for devenv.exe to move to .NET core. |
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.
Interesting! Are there other reasons that we should start doing out of proc HTTP sooner than later?
|
||
When deciding which protocol to use for authentication we will use a combination of the headers provided and the results from ICredentials.GetCredential. | ||
If the header indicates the server accepts basic, and GetCredential returns a result for basic, we will use that for the request. | ||
Similarly, if the header accepts bearer, and GetCredentials returns a result for bearer, we will use 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.
Where will the token be stuffed? In the password field? Seems like an alternative is to have a simple new AuthorizationHeader
property. This essentially gives the cred provider full control on setting the Authorization
header on the next request side stepping the GetCredential
and ICredential
interface entirely.
I am reading between the lines here, but it seems that ICredential
flow will NOT be used in conjunction with HttpClient
when bearer
is returned. Instead https://learn.microsoft.com/en-us/dotnet/api/system.net.http.headers.httprequestheaders.authorization?view=net-9.0 will be used?
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.
Right, HttpClient doesn't support bearer tokens with ICredential. I love the idea of offloading all the responsibility of setting the headers to the cred provider! I'll check with @embetten to see what they think.
|
||
## Explanation | ||
|
||
### Functional explanation |
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 want to confirm because this is a pleasant surprise to me -- the credential provider protocol works just today on NuGet push? I remember @kartheekp-ms mentioning something about a 401/403 difference in nuget push + credential providers. But I can't remember.
It might be worth expanding which status codes will NOT work in this flow (for example 403) and what happens -- unchanged by this design.
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.
There's a configuration set that determines if we prompt on 403. So it depends on the configuration, do we want to always prompt on 403?
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 am not sure. I think to reduce risk we would keep current behavior for flows that don't use bearer
. If the flow uses bearer
, then I think we should prompt for 401 but not for 403 so we can differentiate "expired credential" = 401 from "valid credential, invalid permissions" = 403. No point in prompting when the user is trying to upload a package they do not have access to. But I am not confident on this approach.
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.
Looks like we set that value to !hasApiKey when publishing packages. Otherwise, it's set from the configuration. So, if the user is using bearer auth, we should expect them not to set the API key, and we would prompt on 403.
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.
Interesting. I wonder how we can improve this. If someone doesn't have the right permissions we shouldn't keep prompting cred provider over and over right?
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.
Yea, I think we could focus the conversation on 403 here? Though it feels a little out of scope... but if we get 403 and request credentials again, my expectation would be that the user would try a different credential. If they were to use the same credential again then we should just fail saying auth failed.
Yes, perhaps in an interactive environment. There are non-interactive environments like CI too where the cred provider will be useful (as is Artifacts cred provider today). For interactive, it would be frustrating signing in over and over when the problem is not your password to, say, your Microsoft account (that turns into an OIDC token -> short lived API key) but is more about signing into the wrong user entirely. Or you as the user just don't have access to that package at all.
Could prompt again and if they get the same credential, they could just return null.
This makes sense. If the cred provider has context to improve the UX and say "nope, sorry, that user was tried already and it didn't work due to the 2nd 401 and the cred provider being launched again, so we'll abort without asking for your username again) -- then this works. I think we should allow ourselves a little grace to iterate this tech design as we write the first cred provider that uses it, i.e. the nuget.org one for OIDC + short lived API keys.
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.
We also have a retry cache, so before we make the request to the credential provider we check if an entry already exists using this key so for a non-interactive request if "IsRetry" is true and we got 403 you know not to try again.
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.
For the interactive experience, who should be responsible for not prompting again? NuGet Client or the Credential Providers?
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.
If possible, NuGet Client so the same logic need not be replicated across multiple cred provider impls. Does that sound reasonable?
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.
That sounds reasonable to me, though right now we don't really account for this, and I don't these changes really make this that much worse, other than now 403 responses could fall under this loop.
|
||
## Explanation | ||
|
||
### Functional explanation |
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.
We should explain that credential expiration should also trigger the cred provider flow again. Suppose a bearer token expires. What does server need to return so that the credential is refreshed? Perhaps this is just 401?
What if the credential is valid but has insufficient privileges? I think cred provider should NOT be invoked and the command should fail. Perhaps this is 403?
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.
Looking at the code, if we get 401 it would automatically go through the cred provider again. I can add a line to make it clear that on 401 we get a new credential and don't use a cached one or something.
For that second one I think returning 403 is what makes sense. Should probably decide what we want to do about prompting and 403 though: https://github.com/NuGet/Home/pull/14174/files/5b0ff0b203d3b3a91ae53713b26e704fcf1ad386#r1990090946
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.
Also looking at the code it does appear that on retry we go to the cred provider. We could pass CredentialRequestType to the cred provider and let them decide what to do with a forbidden response.
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.
Added some wording about the expiration.
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.
Looks good. It is a relatively high level doc with some details left to the implementation. I am fine with this if your team is. Seems like some things are best figured out once you see how the code is factored.
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.
just nitpick comments, but hardly surprising since I talked to you about this before you wrote the spec 😁
|
||
## Summary | ||
|
||
Update the NuGet CLI to allow for bearer token authentication when publishing packages. |
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 NuGet CLI" refers specifically to NuGet.exe. Unless there's something later in the spec that makes it compelling, I'd expect auth changes to affect dotnet CLI, msbuild restore, and all NuGet functionality in VS as well.
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.
Oo good point, does this work?
Update the NuGet CLI to allow for bearer token authentication when publishing packages. | |
Update the NuGet authentication flow to allow for bearer token authentication when publishing packages. |
By removing ApiKey as a requirement feeds will no longer have to document sending a placeholder. | ||
|
||
Additionally due to the [limitation in .NET](https://github.com/dotnet/runtime/issues/91867#issue-1889923702) we can’t send these types of credentials using the HttpClientHandler. | ||
To circumvent this Azure Artifacts sends their key using basic auth, passing the key as a password in a network credential. |
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 suggest "basic auth", rather than "a network credential".
NetworkCredential
is a .NET specific type that I don't think is well known. I think HTTP Basic auth is better known.
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 ICredential can return a network credential for digest and NTLM auth too right (not just basic)? When I was trying to get bearer too work it looks like those two schemes were supported too. But I may be wrong.
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.
To circumvent this Azure Artifacts sends their key using basic auth, passing the key as a password in a network credential. | |
To circumvent this Azure Artifacts sends their key using basic auth, passing the key as a password. |
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.
Updated it to remove network credential, didn't want to say basic auth twice in one sentence.
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 guess what I'm getting at is that the network credential/ICredential
abstraction is what restricting the flow today so it's probably good to call out that, rather than making it specific to basic
auth). I don't feel strongly though.
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.
updated~
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 guess what I'm getting at is that the network credential/ICredential abstraction is what restricting the flow today
Unless I misunderstand something, no, I wouldn't say ICredential
or NetworkCredential
is restricting Bearer auth. HttpClientHandler
doesn't automatically handle it in the same way that it does for Basic, Digest, Negotiate or NTLM.
|
||
When deciding which protocol to use for authentication we will use a combination of the headers provided and the results from ICredentials.GetCredential. | ||
If the header indicates the server accepts basic, and GetCredential returns a result for basic, we will use that for the request. | ||
Similarly, if the header accepts bearer, and GetCredentials returns a result for bearer, we will use 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.
What if the server supports both bearer and basic, and the cred provider provides creds, also specifying both bearer and basic? Which auth scheme wins? Since you listed basic first, does that win?
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.
Yea, basic would win
|
||
## Drawbacks | ||
|
||
Even though we are moving away from sending the bearer token as a password during the request, the ICredential.GetCredential explicitly returns a NetworkCredential, which requires a username and password. |
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.
nitpick, but I don't like the first half of the sentence.
NuGet never supported bearer auth, so I just don't agree that "we're moving away" from anything. If cred providers "lie" to NuGet and give us HTTP basic auth credentials with what happens to be an OAuth token as the password, that's a decision of the cred provider, not NuGet.
Moreover, I feel like saying "moving away from" implies we're breaking backwards compatibility with something. But we're not. If a cred provider continues to give us http basic credentials that happen to use an OAuth token in the password field, that's completely opaque to NuGet, and will continue to work.
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.
updated the wording
Co-authored-by: Andy Zivkovic <[email protected]>
Co-authored-by: Andy Zivkovic <[email protected]>
Co-authored-by: Andy Zivkovic <[email protected]>
Co-authored-by: Andy Zivkovic <[email protected]>
Co-authored-by: Andy Zivkovic <[email protected]>
Tech spec for #12877