feat: TokenProvider for externally minted bearer tokens - #121
Open
vegardx wants to merge 3 commits into
Open
Conversation
v4 is in maintenance; v5 is where RegisteredClaims and parser options live. Minimal footprint: the one require line swaps and go.sum carries only the v5 hashes. (go mod tidy is not run here — it fails on main today from the pre-existing monolithic-vs-split genproto ambiguity in the docker example's test closure, unrelated to this change.)
Adds a JWTProvider interface so GitHub App JWTs can be signed outside the client — enabling KMS-backed keys (AWS KMS, GCP Cloud KMS, Azure Key Vault, HSMs) where private key material never leaves the secure boundary. - NewClientWithGitHubApp keeps its exact public API; the PEM path becomes an internal pemJWTProvider and parse failures wrap in the same 'invalid credentials' contract. - NewClientWithJWTProvider is the new entry point for custom signers. - SignerJWTProvider adapts any crypto.Signer whose Public() is *rsa.PublicKey. The token is assembled by jwt/v5 (SigningString) and the digest signed through the Signer, because SignedString requires a concrete *rsa.PrivateKey and cannot drive a remote signer. - JWTProviderFunc is a function type implementing JWTProvider (http.HandlerFunc style). - Both providers share one claims builder and check ctx cancellation before signing. - actionsAuth.validate() reworded for the provider field; error messages and the invalid-credentials wrapping are unchanged.
NewClientWithPersonalAccessToken takes the bearer as a fixed string captured at construction. This generalizes that to a TokenProvider consulted on every re-authentication, so callers using short-lived credentials -- GitHub App installation tokens in particular -- can rotate them without rebuilding the client. Motivation: at fleet scale, installation-token minting is rate limited and worth coordinating across processes, via a shared cache, a single-flight mint, and credential selection by remaining quota. A client that mints its own token cannot participate in that coordination; one that asks for a token can. Backwards compatible: no exported symbol changes signature or is removed, NewClientWithPersonalAccessToken is untouched and is now the degenerate case of a provider returning a constant, and the pre-existing App-plus-PAT conflict keeps its original error wording so callers matching on that string are unaffected. The existing test suite passes unmodified. Stacked on the pluggable-JWT-signing branch, which introduces the jwtProvider field this shares its exactly-one-of validation with.
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.
What
Generalizes PAT auth into a
TokenProviderinterface consulted on every re-authentication, so callers using short-lived credentials, GitHub App installation tokens in particular, can rotate them without rebuilding the client.Two ways in:
NewClientWithPersonalAccessToken: unchanged public API; now the degenerate case of a provider returning a constant.NewClientWithTokenProvider: new constructor accepting anyTokenProvider. PlusTokenProviderFunc, a function adapter (http.HandlerFuncstyle).The token is sent verbatim as
Authorization: Bearerfor a single request; the client neither inspects nor caches it. A provider handing out expiring credentials therefore owns their renewal, which is the point of the seam: renewal can then be shared across clients and processes rather than duplicated inside each one.The doc comment spells out which credential types are valid at which config-URL scope:
https://HOST/enterprises/NAME) requires a PAT withmanage_runners:enterprise. Today a GitHub App is installed on an organization or a repository, never on an enterprise, so no installation token can be minted at this scope. Nothing in the client enforces that: if GitHub ships enterprise-installable Apps, their installation tokens flow through this seam unchanged.admin:org, or an installation token from an App holdingorganization_self_hosted_runners.repo, or an installation token from an App holdingadministration.Why
At fleet scale, installation-token minting is rate limited and worth coordinating across processes: a shared cache, a single-flight mint, credential selection by remaining quota. A client that mints its own token cannot participate in that coordination; one that asks for a token can. Today the bearer is a fixed string captured at construction, so short-lived credentials force client rebuilds.
This is not speculative: we run this in production today against GitHub Enterprise Cloud with data residency, with installation tokens minted and renewed outside the client and handed in through this seam.
Testing
go build ./...andgo test -race ./...green across the repo. New coverage intoken_provider_test.go: nil-provider rejection, the provider being consulted on every re-authentication (not cached), provider-error propagation, andTokenProviderFuncpassthrough.Compatibility
NewClientWithPersonalAccessTokenis untouched.Stacked on #117 (pluggable JWT signing): shares its exactly-one-of validation with the
jwtProviderfield introduced there. Only the last commit (feat(client): TokenProvider for externally minted bearer tokens) is new to review here; this rebases to a single commit once #117 lands.