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

[Tech Spec] Supporting Bearer Auth #14174

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from

Conversation

jgonz120
Copy link
Contributor

Tech spec for #12877

@jgonz120 jgonz120 requested a review from a team as a code owner March 10, 2025 21:21
@jgonz120 jgonz120 requested review from Copilot and removed request for a team March 10, 2025 21:21

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.

@jgonz120 jgonz120 requested review from joelverhagen, kartheekp-ms and a team March 10, 2025 21:22

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.
Copy link
Contributor

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?

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

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 know Artifactory allows for pushing to subdirectories in the package repo.

Copy link
Contributor

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.

Copy link
Contributor Author

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.
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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."

Copy link
Contributor Author

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.
Copy link
Contributor

@donnie-msft donnie-msft Mar 11, 2025

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.
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

@joelverhagen joelverhagen left a 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.

Copy link
Member

@zivkan zivkan left a 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.
Copy link
Member

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.

Copy link
Contributor Author

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?

Suggested change
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.
Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated~

Copy link
Member

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.
Copy link
Member

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?

Copy link
Contributor Author

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.
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated the wording

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants