feat: add bearer credentials and a credential injection seam - #114
Merged
Merged
Conversation
Azure DevOps accepts an Entra ID access token only as `Authorization: Bearer`. Both hosting providers previously had no way to send one: HostingCredentialKind carried Token and UsernamePassword only, and AzureDevOpsProvider applies Token as Basic with an empty username, which is the personal access token scheme. An Entra token in that slot does not authenticate. Credentials also resolved solely from ktsu.CredentialCache via PersonaGUID. That suits a long-lived secret such as a personal access token and suits nothing about a short-lived one: an Entra access token is minted per session and expires within the hour, so writing it to the host's keyring would persist a secret that is stale before it is read again. Two additions: - HostingCredentialKind.BearerToken, with HostingCredential.FromBearerToken. AzureDevOpsProvider sends it as a Bearer header; GitHubProvider maps it to Octokit's AuthenticationType.Bearer, which is separately what a GitHub App installation token requires. FromToken keeps its existing per-host behaviour, and its doc comment is corrected: it was described as a bearer token while being applied as neither host's bearer scheme. - GitProvider.CredentialSource, a Func<HostingCredential> consulted on every resolution rather than cached, so a caller can return a freshly refreshed token each time. It takes precedence over the credential cache when set. Returning HostingCredential.None proceeds unauthenticated; returning null throws from ResolveCredential with a message naming the mistake, while reaching IsAuthenticated as false rather than throwing from a property getter. HostingCredential and HostingCredentialKind become public, since they are now the vocabulary a caller supplies a credential in. They were internal while the credential cache was the only way in. Verified: 603/603 tests pass, including 8 new ones. The Azure DevOps bearer test was mutation-checked by reverting the mapping to Basic, which fails it. Pre-existing and unrelated: `dotnet pack` reports CP0014 net9.0/net10.0 ApiCompat errors on Polyfill's shims, reproduced on main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two comments described what the code used to be rather than what it is. The diff and the pull request already carry that, and a comment describing a prior state goes stale as soon as anything else moves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Problem
Azure DevOps accepts an Entra ID access token only as
Authorization: Bearer. Neither hosting provider could send one.HostingCredentialKindcarriedTokenandUsernamePasswordonly, andAzureDevOpsProvider.ApplyAuthenticationappliesTokenas Basic with an empty username, which is the personal access token scheme. An Entra token in that slot does not authenticate. There is noBeareranywhere in the library today (grep -rn Bearer GitIntegration/returns nothing), despiteHostingCredentialKind.Token's own doc comment describing it as a bearer token.Credentials also resolved solely from
ktsu.CredentialCacheviaPersonaGUID. That suits a long-lived secret such as a personal access token and suits nothing about a short-lived one: an Entra access token is minted per session and expires within the hour, so writing it to the host's keyring would persist a secret that is stale before it is read again.Changes
HostingCredentialKind.BearerToken, withHostingCredential.FromBearerToken.AzureDevOpsProvidersends it as a Bearer header.GitHubProvidermaps it to Octokit'sAuthenticationType.Bearer, which is separately what a GitHub App installation token requires, and which the single-argumentCredentialsconstructor cannot produce (it defaults toOauthand sendsToken <value>).FromTokenkeeps its existing per-host behaviour. Its doc comment is corrected: it was described as a bearer token while being applied as neither host's bearer scheme.GitProvider.CredentialSource, aFunc<HostingCredential>consulted on every resolution rather than cached, so a caller can return a freshly refreshed token each time. It takes precedence over the credential cache when set, so a caller supplying one never has to also clear whatever the keyring holds for its persona.Returning
HostingCredential.Noneproceeds unauthenticated. Returningnullis a caller bug rather than a way to say that, soResolveCredentialthrows with a message naming the mistake — the same philosophy as the existing unrecognised-subtype throw. It reachesIsAuthenticatedasfalseinstead of throwing, since a property getter must not.HostingCredentialandHostingCredentialKindbecome public. They are now the vocabulary a caller supplies a credential in; they were internal while the credential cache was the only way in.Usage
Testing
603/603 pass, 8 of them new:
UsesABearerTokenFromTheCredentialSourcePrefersTheCredentialSourceOverTheCredentialCacheConsultsTheCredentialSourceOnEveryResolution— proves the callback is not cached, which is what makes token refresh workProceedsUnauthenticatedWhenTheCredentialSourceSuppliesNoneReportsAuthenticatedForACredentialSourceSupplyingATokenThrowsWhenTheCredentialSourceReturnsNullSendsBearerAuthForABearerTokenCredentialAsync(Azure DevOps, asserts the header verbatim and un-decoded — any base64 round-trip would mean it went through the Basic path)SendsBearerAuthForABearerTokenCredentialAsync(GitHub)The Azure DevOps bearer test was mutation-checked by reverting the mapping back to Basic, which fails it.
Notes
GitProviderproperties table, and a new "Supplying a credential directly" section.VERSION.mdleft alone, since it is bot-maintained.dotnet packreports CP0014 net9.0/net10.0 ApiCompat errors on Polyfill's shims. Reproduced onmainat480d878, so it is not from this branch.🤖 Generated with Claude Code