Documentation
¶
Overview ¶
Package tokenexchange holds the handlers for the token endpoint's token-exchange and JWT-bearer grants. The RFC 8693 token exchange handlers validate subject tokens issued by this authorization server and trusted external JWTs, and issue tokens from them. The RFC 7523 JWT-bearer and ID-JAG redemption handlers validate assertions from trusted issuers. Both groups share the trust layer: the issuer and JWKS validation code that checks where a token came from.
The package also mints ID-JAGs through one generic core that checks the request, selects the subject claims, runs the policy, signs the token and writes the response, whatever the subject came from.
Index ¶
- Constants
- func ActIssuerProblem(s string) error
- func Factory(delegationLifespan time.Duration, trustedIssuers []TrustedIssuer, ...) (server.Factory, error)
- func FactoryWithSharedTrustedIssuerValidator(delegationLifespan time.Duration, trustedIssuers []TrustedIssuer, ...) (server.Factory, error)
- func IDJAGIssuanceFactory(trustedIssuers []TrustedIssuer, shared *MultiIssuerTokenValidator, ...) (server.Factory, error)
- func IsIDJAGSubjectClaimToken(token string) bool
- func JWTBearerIssuanceFactory(trustedIssuers []TrustedIssuer, shared *MultiIssuerTokenValidator) (server.Factory, error)
- func RefreshTokenFactory(clientPolicies map[string]ConfiguredClientPolicy, signer *server.Signer, ...) (server.Factory, error)
- func ValidateJWKSURL(jwksURL string, insecureAllowHTTP, allowPrivateIPs bool) error
- func ValidateTrustedIssuerURL(issuerURL string, insecureAllowHTTP bool) error
- func ValidateTrustedIssuers(trustedIssuers []TrustedIssuer, selfIssuer string, allowedAudiences []string) error
- type ActorDeniedError
- type ActorDeniedReason
- type ActorProfile
- type ActorRequest
- type ActorResult
- type ActorTarget
- type ActorTokenValidator
- type ActorValidatorRegistration
- type ConfiguredClientPolicy
- type FactoryOption
- type GrantOwner
- type Handler
- func (*Handler) CanHandleTokenEndpointRequest(_ context.Context, requester fosite.AccessRequester) bool
- func (*Handler) CanSkipClientAuth(_ context.Context, _ fosite.AccessRequester) bool
- func (h *Handler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error
- func (h *Handler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, ...) error
- type IDJAGHandler
- func (*IDJAGHandler) CanHandleTokenEndpointRequest(_ context.Context, requester fosite.AccessRequester) bool
- func (*IDJAGHandler) CanSkipClientAuth(context.Context, fosite.AccessRequester) bool
- func (h *IDJAGHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error
- func (h *IDJAGHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, ...) error
- type IDJAGTarget
- type JWTBearerAssertionValidator
- type JWTBearerGrantPolicy
- type JWTBearerHandler
- func (*JWTBearerHandler) CanHandleTokenEndpointRequest(_ context.Context, requester fosite.AccessRequester) bool
- func (*JWTBearerHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool
- func (h *JWTBearerHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error
- func (h *JWTBearerHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, ...) error
- type JWTBearerSubjectBinding
- type MayActClaim
- type MultiIssuerTokenValidator
- func (v *MultiIssuerTokenValidator) Close() error
- func (v *MultiIssuerTokenValidator) Validate(ctx context.Context, rawToken string) (*ValidatedClaims, error)
- func (v *MultiIssuerTokenValidator) ValidateJWTBearerAssertion(ctx context.Context, rawToken string, defaultAudiences []string) (*ValidatedClaims, error)
- type RefreshTokenHandler
- func (*RefreshTokenHandler) CanHandleTokenEndpointRequest(_ context.Context, requester fosite.AccessRequester) bool
- func (*RefreshTokenHandler) CanSkipClientAuth(_ context.Context, _ fosite.AccessRequester) bool
- func (h *RefreshTokenHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error
- func (h *RefreshTokenHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, ...) error
- type RefreshTokenOption
- type SelfIssuedTokenValidator
- type SubjectTokenValidator
- type TrustedIssuer
- type UpstreamIdentity
- type ValidatedClaims
Constants ¶
const ( IDJAGSubjectClaimUpstreamSubject = "upstream_subject" IDJAGSubjectClaimSubID = "sub_id" )
The tokens a target may list in RequiredSubjectClaims. They are the only valid ones; IsIDJAGSubjectClaimToken is the check to use.
Variables ¶
This section is empty.
Functions ¶
func ActIssuerProblem ¶ added in v0.65.0
ActIssuerProblem returns nil when s is an acceptable ActorValidatorRegistration ActIssuer syntax: an absolute URI of at most 2048 bytes that follows the same rules as an AgentID URI (printable ASCII, RFC 3986 characters, valid percent-escapes, something after the scheme, no userinfo, no fragment) and also has no query. Otherwise it describes the problem without quoting s. It checks syntax only, not the canonical spelling or which schemes are allowed.
func Factory ¶
func Factory( delegationLifespan time.Duration, trustedIssuers []TrustedIssuer, configuredDelegateClients []string, clientPolicies map[string]ConfiguredClientPolicy, options ...FactoryOption, ) (server.Factory, error)
Factory returns a server.Factory that creates a token exchange Handler. RefreshTokenFactory must be registered alongside it: Handler does not claim requests that involve the refresh-token type, and without that handler they would be unhandled. The delegationLifespan parameter sets the maximum lifetime for delegated tokens; the actual lifetime is the minimum of this value and the subject token's remaining lifetime. Returns an error if delegationLifespan is not in (0, server.MaxAccessTokenLifespan]: a zero or negative value would produce delegated tokens with an expiry already in the past, and a value above the access token ceiling would only be caught at request time by the per-request cap.
trustedIssuers must be empty here. A MultiIssuerTokenValidator owns per-issuer JWKS refresh worker pools that only its Close releases, and the bare Factory has no way to hand that instance back to the caller for shutdown (it is built at fosite-compose time, from config not available at this call). Passing a non-empty set therefore returns an error rather than silently building a validator whose workers leak; callers with trusted issuers must build it via NewSharedTrustedIssuerValidator, hold it for Close, and pass it to FactoryWithSharedTrustedIssuerValidator. With no trusted issuers the self-issued validator is used directly, preserving prior behavior exactly.
configuredDelegateClients is the operator-configured list of delegate client IDs (Config.DelegateClients, projected down to just their ClientIDs by the caller). An empty list preserves existing behavior exactly. The trust source here is server config, not client storage: the set is read once at process construction, so removing a client from config revokes its trust on the next restart rather than requiring any explicit revocation step against storage.
clientPolicies holds the source policy of the configured delegate clients whose self_issued_subject_tokens is restricted, keyed by client ID. A configured client without an entry keeps blanket trust of self-issued subject tokens. It is read-only after construction.
func FactoryWithSharedTrustedIssuerValidator ¶ added in v0.45.0
func FactoryWithSharedTrustedIssuerValidator( delegationLifespan time.Duration, trustedIssuers []TrustedIssuer, configuredDelegateClients []string, clientPolicies map[string]ConfiguredClientPolicy, shared *MultiIssuerTokenValidator, options ...FactoryOption, ) (server.Factory, error)
FactoryWithSharedTrustedIssuerValidator is Factory with a shared external-issuer validator. shared is used as the subject-token validator when non-nil; it is REQUIRED whenever trustedIssuers is non-empty (an error is returned otherwise), because a locally-built MultiIssuerTokenValidator's JWKS refresh workers would have no owner to Close them — see the error below. Callers build it once with NewSharedTrustedIssuerValidator, hold it for Close, and can reuse the same instance across the RFC 8693 token-exchange and RFC 7523 JWT-bearer grants.
func IDJAGIssuanceFactory ¶ added in v0.50.0
func IDJAGIssuanceFactory( trustedIssuers []TrustedIssuer, shared *MultiIssuerTokenValidator, explicitGrantClientIDs []string, ) (server.Factory, error)
IDJAGIssuanceFactory builds the production bound ID-JAG handler. It is registered alongside JWTBearerIssuanceFactory whenever any trusted issuer enables the JWT-bearer grant: the two handlers share the per-issuer policy and split one grant type by assertion typ, so enabling the grant enables both assertion forms. explicitGrantClientIDs names the clients that must list the jwt-bearer grant in their registered grant types. See JWTBearerIssuanceFactory for the shared validator's ownership contract.
func IsIDJAGSubjectClaimToken ¶ added in v0.65.0
IsIDJAGSubjectClaimToken reports whether token is a valid entry of IDJAGTarget.RequiredSubjectClaims.
func JWTBearerIssuanceFactory ¶ added in v0.45.0
func JWTBearerIssuanceFactory(trustedIssuers []TrustedIssuer, shared *MultiIssuerTokenValidator) (server.Factory, error)
JWTBearerIssuanceFactory builds the production RFC 7523 handler. It is only registered by composition when a trusted issuer opts into the grant.
shared is used as the JWTBearerAssertionValidator and is REQUIRED whenever trustedIssuers is non-empty (an error is returned otherwise). The RFC 8693 token-exchange Factory and this one are usually enabled for the same trusted issuers, and each MultiIssuerTokenValidator registers its own per-issuer jwkfetch.Cache and background refresh goroutines; sharing one instance avoids doubling that cost, and — since the validator's JWKS workers are released only by its Close — keeps them owned by the caller rather than built and abandoned inside this compose-time closure. Build it once with NewSharedTrustedIssuerValidator and hold it for Close.
func RefreshTokenFactory ¶ added in v0.60.0
func RefreshTokenFactory( clientPolicies map[string]ConfiguredClientPolicy, signer *server.Signer, options ...RefreshTokenOption, ) (server.Factory, error)
RefreshTokenFactory returns a server.Factory that creates the RefreshTokenHandler. It is registered whenever token exchange is enabled, so a request for a refresh token always reaches this handler, which refuses it for a client that has not enabled the feature. clientPolicies is the same read-only map Factory receives. The exchange-grant store is passed with WithExchangeGrantStorage; it may be nil only if no client has exchange_refresh_tokens.
signer is required and must come from server.NewSigner. Construction also fails for any IDJAGTarget in clientPolicies that is not structurally usable (empty issuer, a map key that is not the target's issuer, empty client_id, no or an empty resource, a lifetime that is not positive).
The authorization_details registry is built here, once, and startup fails if that fails. It is private to this handler.
func ValidateJWKSURL ¶ added in v0.42.1
ValidateJWKSURL checks that jwksURL parses, has a host, uses HTTPS unless insecureAllowHTTP permits plain HTTP — and only exactly the "http" scheme, not any other non-https scheme such as "file" or "ftp" — and, when the host is an IP literal, is not a private or loopback address unless allowPrivateIPs permits that. Both flags come from the specific TrustedIssuer being fetched (see ensureRegistered), never from a validator-wide or self-issuer setting. This prevents SSRF attacks where a compromised discovery document — or a hand-configured jwks_url — points to internal services.
This is shared by endpoint-only config-time validation and the runtime fetch choke point. Both use the issuer's allowPrivateIPs policy for literal IP hosts; runtime additionally protects DNS resolution on every outbound fetch.
func ValidateTrustedIssuerURL ¶ added in v0.47.0
ValidateTrustedIssuerURL checks that issuerURL is a valid trusted external OIDC issuer identifier. Trusted issuers require HTTPS unless their own insecureAllowHTTP opt-in is set; unlike this server's issuer, localhost is not exempt. Query, fragment, and userinfo are forbidden, while a trailing slash is permitted for providers such as Microsoft Entra ID v1.
func ValidateTrustedIssuers ¶ added in v0.42.1
func ValidateTrustedIssuers(trustedIssuers []TrustedIssuer, selfIssuer string, allowedAudiences []string) error
ValidateTrustedIssuers runs every structural check NewMultiIssuerTokenValidator performs on trustedIssuers — required fields, self-issuer collision, duplicate issuers, ActorClaim reachability, and ActorMatcher compilation — without constructing a validator or any per-issuer HTTP client. Config validation calls this to fail before the live upstream DCR registration and storage creation that run between RunConfig.Validate and server construction; NewMultiIssuerTokenValidator repeats the same checks at server startup as defence in depth. Both route through validateTrustedIssuer, so the two can't drift out of sync.
Types ¶
type ActorDeniedError ¶ added in v0.65.0
type ActorDeniedError struct {
// Reason is the client-facing reason. It must be one of the ActorDeniedReason
// constants. Any other value, including the zero value, is a server error.
Reason ActorDeniedReason
// contains filtered or unexported fields
}
ActorDeniedError is returned by a validator to refuse an actor. Build it with a keyed literal. The zero value has an empty Reason, which ToolHive reports as a server error, because the reason is not one of the documented values.
func (*ActorDeniedError) Error ¶ added in v0.65.0
func (e *ActorDeniedError) Error() string
Error returns "actor denied: <reason>", or "actor denied" when Reason is empty. It is safe to call on a nil receiver, which also returns "actor denied".
type ActorDeniedReason ¶ added in v0.65.0
type ActorDeniedReason string
ActorDeniedReason is the client-facing reason for refusing an actor.
const ( // ActorDeniedReasonActorTokenExpired means the actor credential has expired. The client // should renew the credential and retry. ActorDeniedReasonActorTokenExpired ActorDeniedReason = "actor_token_expired" //nolint:gosec // G101: not a credential // ActorDeniedReasonApprovalRequired means the user has not approved this agent. // Asking the user may help. Retrying alone will not. ActorDeniedReasonApprovalRequired ActorDeniedReason = "approval_required" // ActorDeniedReasonDenied means the agent is refused and retrying will not help. ActorDeniedReasonDenied ActorDeniedReason = "denied" )
type ActorProfile ¶ added in v0.65.0
type ActorProfile struct {
// Name is the validator's registered name, for logs.
Name string
// ActorTokenType is the actor_token_type the client must send.
ActorTokenType string
// ActorValidatorRegistration is the registration that Name selects.
ActorValidatorRegistration
}
ActorProfile is the actor-token check resolved for one delegate client at startup. It carries the registration the client's actor_profile selects, so Validator and ActIssuer come from the same entry.
type ActorRequest ¶ added in v0.65.0
type ActorRequest struct {
// ActorToken is the raw actor credential presented by the client. It is always set
// when the validator is called. Do not log it or put it in an error.
ActorToken string
// ActorTokenType is the actor_token_type parameter. The caller guarantees it is a
// type the client is allowed to present.
ActorTokenType string
// ClientID is the authenticated client that asked for the ID-JAG. It is always set.
ClientID string
// Grant describes the user the ID-JAG is issued for.
Grant GrantOwner
// Target describes the gateway the ID-JAG is for. It is always set when the
// validator is called. A validator that does not decide by target may ignore it.
Target ActorTarget
}
ActorRequest is the input to ActorTokenValidator.ValidateActor.
type ActorResult ¶ added in v0.65.0
type ActorResult struct {
// AgentID names the agent instance. ToolHive places it in the act claim as sub, next
// to the issuer registered for the validator. It must follow the rules in
// ActorTokenValidator. The zero value is a server error.
AgentID string
// ExpiresAt is when the actor credential stops being valid. The issuing code caps
// the ID-JAG's lifetime at this time. Times are compared in whole seconds. A value
// at or before the time ToolHive checks the result, but less than 60 seconds
// before it, is answered as actor_token_expired. A value 60 or more seconds before
// it is a server error, as is the zero value.
ExpiresAt time.Time
// CredentialSubject identifies the credential for audit logs. It never enters a
// token. The zero value means no subject is recorded.
CredentialSubject string
}
ActorResult is a validator's answer for an actor that was accepted.
type ActorTarget ¶ added in v0.65.0
type ActorTarget struct {
// Issuer is the issuer of the target authorization server as configured. It
// becomes the ID-JAG's aud claim. It is always set when the validator is called.
Issuer string
// Resource is the resource the client requested, which is one of the target's
// configured resources. It is always set.
Resource string
}
ActorTarget describes the gateway that the ID-JAG is issued for.
type ActorTokenValidator ¶ added in v0.65.0
type ActorTokenValidator interface {
ValidateActor(ctx context.Context, req ActorRequest) (ActorResult, error)
}
ActorTokenValidator checks the actor token that a client presents during ID-JAG issuance and reports which agent it belongs to. How a validator is registered and selected is not part of this interface.
ValidateActor is called once per issuance, after the grant checks and before the ID-JAG is signed. ToolHive builds the act claim from the answer: sub is AgentID, and iss is the issuer registered for this validator. A validator never sets claims itself, and it never sets the issuer.
Result ¶
On success, return an ActorResult with a non-empty AgentID and a non-zero ExpiresAt. ExpiresAt is the time at which the actor credential stops being valid.
To refuse, return an *ActorDeniedError, optionally wrapped with %w. Use one of the reasons ActorDeniedReasonActorTokenExpired, ActorDeniedReasonApprovalRequired or ActorDeniedReasonDenied. Return a pointer: an ActorDeniedError value does not implement error. A nil *ActorDeniedError returned as the error is a server error, not a refusal.
Return a plain error when the validator cannot answer. That includes a validator that is not ready yet, for example one bound late at startup. A plain error is reported as a server error, never as a denial.
Requirements ¶
- Refuse with ActorDeniedReasonActorTokenExpired when the actor credential has expired. If the validator accepts a credential within its own clock-skew allowance, it may return that credential's past expiry in ExpiresAt, as long as it is less than 60 seconds before the time ToolHive checks the result. ToolHive then answers actor_token_expired, so the client renews the credential and retries. An expiry 60 or more seconds in the past is a server error, because it points to a fault in the validator and a client retry would not help.
- Refuse with ActorDeniedReasonApprovalRequired when the user has not approved this agent. Use it when asking the user could help. For a validator that decides per target, the approval is for the target in ActorRequest.Target.
- Refuse with ActorDeniedReasonDenied for everything else. That includes an agent with no record and a record that is disabled. A denial is final for the client.
- Use ActorRequest.Target for decisions that depend on the gateway or connector. It holds the configured target, not text from the request.
- Find the user by ActorRequest.Grant.Upstream when the validator binds users by a per-issuer claim. Check that StableClaim is the claim the validator expects for that Issuer, then look the user up by StableValue. A mismatch, or an issuer the validator does not know, is a configuration fault. Return a plain error for it, not a denial, because a denial is final for the client and would hide the fault from operators.
- The whole validation, including the validator's own calls to remote services, has a budget of currently 5 seconds; ToolHive may raise it but will not lower it. The budget also covers any time the call waits for a free slot when many validations run at once, so the validator may get less than the full budget. A validator that calls a remote service should set shorter deadlines of its own.
- Do not join a refusal with other errors (errors.Join, or several %w verbs): ToolHive answers a multi-error as a server error. Detection covers errors that implement Unwrap() []error, such as errors.Join and fmt.Errorf with several %w verbs. A multi-error type that exposes its errors through a single-Unwrap chain is not detected, so do not combine a refusal with other errors in any form.
- Honor ctx. ToolHive stops waiting when the budget is spent, but a validator that ignores ctx keeps running in its goroutine until it returns.
- Never set claims. The issuing code builds act from the registered issuer and AgentID, and caps the ID-JAG's exp at ExpiresAt.
- Return an AgentID that uses only printable ASCII characters from '!' to '~' (no spaces or control characters) and is one of two forms. A plain token is 1 to 255 bytes long and contains no ':'. A value that contains ':' must be an absolute URI per RFC 3986 without userinfo or fragment, of up to 2048 bytes, such as a SPIFFE ID, because RFC 7519 requires a StringOrURI value with ':' to be a URI. ToolHive rejects any other value as a server error and never rewrites it. The AgentID must be unique within the issuer registered for this validator. Two validators registered under the same issuer must not return the same AgentID.
- Parse Grant.SubjectIdentifier as RFC 9493 JSON. Never compare it as text.
- Use ActorResult.CredentialSubject for audit only. It never enters a token.
- Keep credentials out of error text.
- Be safe for concurrent use.
Stability ¶
This API is experimental. It is ready to use, but it may change in a minor release while ToolHive gains its first embedders. ToolHive lists each change in the release notes. Build ActorResult and ActorDeniedError with keyed literals so that added fields do not break your code.
Other rules ¶
- A reason outside the documented set is a server error, so a validator cannot break clients by returning a value they do not know.
- Composition belongs to the embedder. A client uses one validator, and ToolHive offers no chain of validators. An embedder with several checks wraps them in one validator. A wrapper passes the request through unchanged and returns the first refusal.
- Not promised: how many times the validator is called for each tool call, the order of this check relative to other server checks beyond what is stated above, or that the validator sees requests that ToolHive refuses earlier.
type ActorValidatorRegistration ¶ added in v0.65.0
type ActorValidatorRegistration struct {
// Validator checks the actor token. Required.
Validator ActorTokenValidator
// ActIssuer is the value of act.iss in every ID-JAG built from this
// validator's answer. It is the namespace in which AgentID is unique
// (RFC 7519 section 4.1.2). It is often the actor token's issuer, but it does
// not have to be: ToolHive never compares it to the token. Consumers may key
// agent policy on (act.iss, act.sub), so changing ActIssuer renames every
// agent. ToolHive emits it exactly as configured. Several registrations may
// share an ActIssuer. Their validators must then not return the same AgentID,
// which ToolHive cannot check.
//
// ToolHive does not check that a SPIFFE AgentID belongs to the trust domain
// named by a spiffe:// ActIssuer. A validator that accepts JWT-SVIDs must
// verify each SVID against the bundle of the trust domain in its sub; that
// check is the binding. A later release may refuse, at issuance, an AgentID
// outside the trust domain of a spiffe:// ActIssuer.
ActIssuer string
}
ActorValidatorRegistration is a validator registered with the issuer that ToolHive writes as act.iss for the agents it accepts.
type ConfiguredClientPolicy ¶ added in v0.60.0
type ConfiguredClientPolicy struct {
// AllowedSourceClients are the client_id values, besides the client
// itself, whose self-issued subject tokens the client may exchange on the
// default path. The exchange-grant path accepts only these clients, not the
// client's own tokens, so startup requires the list with
// exchange_refresh_tokens.
AllowedSourceClients []string
// SubjectTokenAudiences are the audiences a self-issued subject token must
// carry (one of them, matched exactly) on every exchange path, including
// the client's own tokens. They are input only and never grant an
// access-token audience. Empty means no requirement on the default path
// but refuses on the exchange-grant path, which is why startup requires
// the list with exchange_refresh_tokens.
SubjectTokenAudiences []string
// ExchangeRefreshTokens caps the lifetime of refresh tokens this client may
// obtain through token exchange, keyed by the token-type URN of the grant
// (oauthproto.TokenTypeIDJAG or oauthproto.TokenTypeRefreshToken). A missing
// key means that grant kind is refused, so look up with the comma-ok form.
// Every present value is > 0 and <= 720h, as config validation enforces. Nil
// when the client has no exchange_refresh_tokens block.
ExchangeRefreshTokens map[string]time.Duration
// IDJAGTargets are the authorization servers this client may ask for an
// ID-JAG for, keyed by the issuer exactly as configured. Request-time
// lookup of the audience parameter is exact. Nil when the client has no
// id_jag block.
IDJAGTargets map[string]IDJAGTarget
// ActorProfile is the actor-token check ID-JAG issuance runs for this
// client. Nil when the client has no actor profile. It is set only for
// clients configured at startup, never for dynamically registered ones.
ActorProfile *ActorProfile
}
ConfiguredClientPolicy is the token-exchange policy of a delegate client whose self_issued_subject_tokens is restricted: an entry means restricted. A client with any, or without a token_exchange block, has no entry and keeps blanket trust of self-issued subject tokens.
type FactoryOption ¶ added in v0.57.0
type FactoryOption func(*factoryOptions)
FactoryOption configures Factory and FactoryWithSharedTrustedIssuerValidator. RefreshTokenFactory takes RefreshTokenOption instead.
func WithAuthorizationDetailsRegistry ¶ added in v0.57.0
func WithAuthorizationDetailsRegistry(registry *authorizationdetails.Registry) FactoryOption
WithAuthorizationDetailsRegistry configures the factory to pass the registry through to the token-exchange Handler. With no registry (or a nil one) the authorization_details parameter is ignored.
type GrantOwner ¶ added in v0.65.0
type GrantOwner struct {
// Subject is the user's sub claim as ToolHive knows it. It is always set.
Subject string
// SubjectIdentifier is the grant's sub_id as the raw RFC 9493 JSON text. Parse it;
// do not compare it as text. The zero value means the grant has none.
SubjectIdentifier string
// Upstream is the user's identity at the upstream provider. It is the zero value
// when the server does not record upstream identities (see
// authserver.Config.SubjectIdentifierAudiences). A validator that needs it returns a plain error
// when it is missing, not a denial. ToolHive passes either a complete identity,
// with all four members set, or the zero value.
Upstream UpstreamIdentity
}
GrantOwner identifies the user that a grant belongs to.
type Handler ¶
type Handler struct {
*oauth2.HandleHelper
// contains filtered or unexported fields
}
Handler implements RFC 8693 token exchange for user-to-agent delegation.
When an authenticated OAuth client (the acting agent) presents a user's JWT as subject_token, the handler validates the token and issues a delegated JWT with sub=user and an act claim containing the client's identity, per RFC 8693 Section 4.1.
Subject tokens are intentionally reusable within their lifetime: per RFC 8693's security considerations, a token exchange does not invalidate the subject token, so the same subject token may be exchanged more than once. Replay is bounded by the delegated token's lifetime cap (min(subject-remaining, delegation)), not by single-use tracking; per-jti single-use enforcement is deferred to the broader M2M/sender-constrained- token effort. Single-use applies only to requests for a refresh token (see RefreshTokenHandler).
func (*Handler) CanHandleTokenEndpointRequest ¶
func (*Handler) CanHandleTokenEndpointRequest(_ context.Context, requester fosite.AccessRequester) bool
CanHandleTokenEndpointRequest returns true if the request's grant_type is the RFC 8693 token exchange grant type and the request does not involve a refresh-token type. RefreshTokenHandler claims the opposite, through the same isRefreshTokenExchange predicate, so the two never both handle a request.
func (*Handler) CanSkipClientAuth ¶
CanSkipClientAuth always returns false: RFC 8693 token exchange requires client authentication, and other handlers decide their own requirements.
func (*Handler) HandleTokenEndpointRequest ¶
func (h *Handler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error
HandleTokenEndpointRequest validates the token exchange request parameters, verifies the subject token, and constructs a delegated session with the act claim.
The delegated token's lifetime is the minimum of the subject token's remaining lifetime and the configured delegation lifespan.
func (*Handler) PopulateTokenEndpointResponse ¶
func (h *Handler) PopulateTokenEndpointResponse( ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder, ) error
PopulateTokenEndpointResponse issues the delegated access token and sets the RFC 8693 issued_token_type in the response. When an authorization_details registry is configured and details were granted, it also echoes the granted details as the RFC 9396 authorization_details response member.
type IDJAGHandler ¶ added in v0.50.0
type IDJAGHandler struct {
// contains filtered or unexported fields
}
IDJAGHandler implements the bound RFC 7523 JWT-bearer grant for Identity Assertion Authorization Grant assertions (draft-ietf-oauth-identity-assertion-authz-grant, "ID-JAG" — the inbound half of Cross App Access; the requesting half is pkg/vmcp/auth/strategies/xaa.go). It claims exactly the assertions JWTBearerHandler declines: those whose JOSE typ header is "oauth-id-jag+jwt" (oauthproto.JWTTypeIDJAG).
"Bound" is the trust-model difference from the plain handler. An ID-JAG names the OAuth client that may redeem it — the IdP mints the draft §4.4.1 client_id claim from its administrator-configured resource connection — so this handler never skips client authentication (CanSkipClientAuth) and requires the authenticated client to be that client. Confidential clients authenticate fully; a public client is identified by client_id, which is accepted here because the assertion itself is the primary credential: single-use (required jti), short-lived (maxAssertionAge), and audience-pinned to this server (acceptedAudiences). Draft §4.4 does not allow this: it requires client authentication or a sender-constrained ID-JAG. This is a known departure that is not logged; see the departures paragraph in docs/arch/17-token-exchange-delegation.md. The plain handler's synthetic client is not needed — the redeeming client is a real registered one.
The redeeming client's registered grant_types metadata is deliberately not consulted, except for the operator-configured clients that set grant_types: with open dynamic client registration, self-asserted metadata authorizes nothing, but a configured client's grants are operator policy. What authorizes the grant is the per-issuer policy plus the client_id binding the IdP administrator configured. A configured client that omits grant_types is stored with token exchange alone but is not gated, for backward compatibility; one that sets grant_types must list the jwt-bearer grant.
func (*IDJAGHandler) CanHandleTokenEndpointRequest ¶ added in v0.50.0
func (*IDJAGHandler) CanHandleTokenEndpointRequest(_ context.Context, requester fosite.AccessRequester) bool
CanHandleTokenEndpointRequest claims only recognized ID-JAG assertions — the exact complement of JWTBearerHandler's matcher, so for any given jwt-bearer request at most one of the two handlers is responsible.
func (*IDJAGHandler) CanSkipClientAuth ¶ added in v0.50.0
func (*IDJAGHandler) CanSkipClientAuth(context.Context, fosite.AccessRequester) bool
CanSkipClientAuth never permits skipping client authentication: an ID-JAG is issued TO a specific client (its client_id claim), so fosite must have authenticated — or, for a public client, at least identified via client_id — the caller before HandleTokenEndpointRequest can check the binding. fosite.NewAccessRequest returns the client-authentication error whenever this is false and authentication failed, so the handler below only ever runs with a resolved client.
func (*IDJAGHandler) HandleTokenEndpointRequest ¶ added in v0.50.0
func (h *IDJAGHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error
HandleTokenEndpointRequest validates the ID-JAG assertion, its client binding, and per-issuer policy, then prepares a bounded access-token session.
func (*IDJAGHandler) PopulateTokenEndpointResponse ¶ added in v0.50.0
func (h *IDJAGHandler) PopulateTokenEndpointResponse( ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder, ) error
PopulateTokenEndpointResponse issues only an access token, sharing the plain handler's session-bounded issuance.
type IDJAGTarget ¶ added in v0.65.0
type IDJAGTarget struct {
// Issuer is the target authorization server's issuer as configured. It is
// the request's audience and the ID-JAG's aud.
Issuer string
// ClientID is the delegate client's client_id at the target.
ClientID string
// Resources are the resource values the client may request.
Resources []string
// Lifetime is how long each ID-JAG lives.
Lifetime time.Duration
// RequiredSubjectClaims are the subject claims every ID-JAG for this target
// must carry, as configuration tokens (upstream_subject, sub_id).
RequiredSubjectClaims []string
}
IDJAGTarget is one authorization server a delegate client may ask for an ID-JAG for, and the limits of what it may ask.
type JWTBearerAssertionValidator ¶ added in v0.45.0
type JWTBearerAssertionValidator interface {
ValidateJWTBearerAssertion(
ctx context.Context, rawToken string, defaultAudiences []string,
) (*ValidatedClaims, error)
}
JWTBearerAssertionValidator verifies cryptographic and registered claims of a plain RFC 7523 JWT-bearer assertion.
type JWTBearerGrantPolicy ¶ added in v0.45.0
type JWTBearerGrantPolicy struct {
MaxAssertionAge string `json:"max_assertion_age" yaml:"max_assertion_age"`
SubjectBindings []JWTBearerSubjectBinding `json:"subject_bindings" yaml:"subject_bindings"`
// AcceptedAudiences is the set of "this AS" identity strings an
// assertion's "aud" claim must intersect — e.g. to support migrating
// this server's issuer/token-endpoint URL, or exposing it under more
// than one valid name. Each value uniquely identifies this
// authorization server for this grant; it is NOT a resource/API
// identifier — a bare resource audience is deliberately not accepted
// here, that would let any RFC 8707 resource-scoped token satisfy the
// grant instead of only tokens minted for this AS. When empty, the plain
// JWT-bearer grant accepts the token endpoint, and the ID-JAG grant accepts
// the token endpoint and this server's issuer identifier, unless the issuer
// is also an allowed resource audience. A configured list replaces both
// defaults.
AcceptedAudiences []string `json:"accepted_audiences,omitempty" yaml:"accepted_audiences,omitempty"`
// contains filtered or unexported fields
}
JWTBearerGrantPolicy enables RFC 7523 JWT-bearer assertions for an issuer. MaxAssertionAge uses Go's duration syntax in serialized RunConfig.
type JWTBearerHandler ¶ added in v0.45.0
type JWTBearerHandler struct {
*oauth2.HandleHelper
// contains filtered or unexported fields
}
JWTBearerHandler implements the unbound RFC 7523 JWT-bearer grant.
func (*JWTBearerHandler) CanHandleTokenEndpointRequest ¶ added in v0.45.0
func (*JWTBearerHandler) CanHandleTokenEndpointRequest(_ context.Context, requester fosite.AccessRequester) bool
CanHandleTokenEndpointRequest only claims plain assertions. A recognized ID-JAG assertion belongs to the bound handler (IDJAGHandler); malformed and unsupported typ values remain this handler's responsibility to reject.
func (*JWTBearerHandler) CanSkipClientAuth ¶ added in v0.45.0
func (*JWTBearerHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool
CanSkipClientAuth permits a credential-free plain JWT-bearer assertion. fosite.NewAccessRequest always calls AuthenticateClient first and only discards its error when this returns true (access_request_handler.go), and AuthenticateClient falls back to reading HTTP Basic credentials off the raw request when no form-based client_assertion is present (client_authentication.go). If this only inspected the form, a request carrying a valid assertion plus a wrong HTTP Basic password would be silently accepted, since the resulting clientErr would be discarded without ever being looked at. So the raw *http.Request that fosite stashes under RequestContextKey must be checked for Basic credentials too; treat a missing/malformed request as "credentials might be present" so this fails closed rather than open.
func (*JWTBearerHandler) HandleTokenEndpointRequest ¶ added in v0.45.0
func (h *JWTBearerHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error
HandleTokenEndpointRequest validates policy and prepares a bounded access-token session.
func (*JWTBearerHandler) PopulateTokenEndpointResponse ¶ added in v0.45.0
func (h *JWTBearerHandler) PopulateTokenEndpointResponse( ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder, ) error
PopulateTokenEndpointResponse issues only an access token.
type JWTBearerSubjectBinding ¶ added in v0.45.0
type JWTBearerSubjectBinding struct {
Subject string `json:"subject" yaml:"subject"`
AllowedResources []string `json:"allowed_resources" yaml:"allowed_resources"`
}
JWTBearerSubjectBinding restricts a JWT-bearer assertion subject to the resources for which it may obtain an access token.
type MayActClaim ¶
type MayActClaim struct {
Sub string `json:"sub"`
// Iss, when present, qualifies Sub's namespace. RFC 8693 §4.4: "the
// combination of the two claims 'iss' and 'sub' are sometimes necessary
// to uniquely identify an authorized actor." Without it, Sub alone could
// name a party in some other issuer's namespace while still being
// compared against a ToolHive client ID by checkDelegationConsent — a
// namespace-confusion bug, not just a missing feature. validateMayActShape
// requires Iss, when present, to equal this authorization server's own
// issuer, so by the time checkDelegationConsent reads Sub it is
// guaranteed to be in ToolHive's own client namespace.
Iss string `json:"iss,omitempty"`
}
MayActClaim represents the RFC 8693 §4.4 may_act claim from a subject token. It identifies the party authorized to act on behalf of the subject.
type MultiIssuerTokenValidator ¶ added in v0.41.0
type MultiIssuerTokenValidator struct {
// contains filtered or unexported fields
}
MultiIssuerTokenValidator validates subject tokens from the authorization server itself or from configured external OIDC issuers, delegating self-issued tokens to SelfIssuedTokenValidator and resolving external issuers' JWKS (via OIDC discovery if needed) to verify signature and claims.
A valid signature and audience alone would authorize ToolHive as a resource, not any particular client, as a delegate — a confused-deputy risk (CWE-863). validateExternalToken therefore requires a "may_act" claim or authorization by AllowedActors or ActorMatcher before returning successfully. See docs/arch/17-token-exchange-delegation.md for the full consent-signal precedence and trust model.
func NewMultiIssuerTokenValidator ¶ added in v0.41.0
func NewMultiIssuerTokenValidator( selfValidator *SelfIssuedTokenValidator, selfIssuer string, trustedIssuers []TrustedIssuer, allowedAudiences []string, ) (_ *MultiIssuerTokenValidator, retErr error)
NewMultiIssuerTokenValidator creates a validator that accepts tokens from the authorization server itself and from the provided trusted external issuers. Returns an error if selfValidator is nil, selfIssuer is empty, any TrustedIssuer is invalid (see validateTrustedIssuer), or an issuer's dedicated HTTP client cannot be built (see newExternalIssuerConfig for why each issuer gets its own).
func NewSharedTrustedIssuerValidator ¶ added in v0.45.0
func NewSharedTrustedIssuerValidator( config *server.AuthorizationServerConfig, trustedIssuers []TrustedIssuer, ) (*MultiIssuerTokenValidator, error)
NewSharedTrustedIssuerValidator builds the single MultiIssuerTokenValidator that backs a server's trusted-issuer handling. buildProvider constructs it whenever any trusted issuer is configured — not only when the RFC 7523 JWT-bearer grant is also enabled — and passes it to FactoryWithSharedTrustedIssuerValidator (and, when the JWT-bearer grant is enabled, JWTBearerIssuanceFactory), both of which now require it for a non-empty trusted-issuer set rather than building their own. One shared instance keeps a single JWKS cache/goroutine set per issuer and, crucially, gives the server one validator to Close on shutdown so those goroutines are released (see MultiIssuerTokenValidator.Close). Returns (nil, nil) when trustedIssuers is empty, in which case no external validator is built.
func (*MultiIssuerTokenValidator) Close ¶ added in v0.47.0
func (v *MultiIssuerTokenValidator) Close() error
Close shuts down every per-issuer jwkfetch.Cache, stopping the background JWKS refresh worker pool (and its ~3 goroutines) each one runs. It cancels the validator-scoped context those pools share — signalling them all to stop at once — then waits, under a single shared httpTimeout budget, for each cache to drain. Cancelling up front (rather than after the loop) means the pools unwind in parallel, and the one shared deadline bounds the total wait by httpTimeout rather than httpTimeout×N even if a pool ignores cancellation. This is the same order the construction-failure path uses. It is safe to call more than once and on a validator with no external issuers; the validator must not be used after Close.
A server holds its MultiIssuerTokenValidator and calls this from Close and its construction error path (see pkg/authserver), so neither a normal shutdown nor a failed reconstruction leaks these workers. authserver.CloseIdleConnections deliberately does NOT reach it: that path must stay safe to call on a still-serving server, which needs the workers to keep refreshing external issuers' keys.
func (*MultiIssuerTokenValidator) Validate ¶ added in v0.41.0
func (v *MultiIssuerTokenValidator) Validate(ctx context.Context, rawToken string) (*ValidatedClaims, error)
Validate parses the raw JWT to extract the issuer claim, then routes validation to either the self-issued validator or the appropriate external issuer validator. Returns an error if the issuer is not trusted.
func (*MultiIssuerTokenValidator) ValidateJWTBearerAssertion ¶ added in v0.45.0
func (v *MultiIssuerTokenValidator) ValidateJWTBearerAssertion( ctx context.Context, rawToken string, defaultAudiences []string, ) (*ValidatedClaims, error)
ValidateJWTBearerAssertion verifies a plain RFC 7523 assertion issued by an external issuer explicitly enabled for the JWT-bearer grant. It deliberately does not apply RFC 8693 delegation consent (AllowedActors, ActorMatcher, or may_act): a JWT-bearer assertion's authorization policy is separate from delegation policy.
The assertion audience must intersect the issuer's configured JWTBearerGrant.AcceptedAudiences, or the caller's defaultAudiences when that is unset. The caller is responsible for the phase-specific subject binding, maximum age, replay, and issuance policy after this cryptographic verification succeeds.
type RefreshTokenHandler ¶ added in v0.60.0
type RefreshTokenHandler struct {
// contains filtered or unexported fields
}
RefreshTokenHandler implements RFC 8693 token exchange for refresh tokens: a configured client presents a user's login access token and receives an opaque, non-rotating refresh token whose allowed use is fixed at issuance (see exchangeGrantType). The token is not stored as a fosite refresh token. Nothing is written to fosite's store, so grant_type=refresh_token cannot redeem it.
Unlike Handler, the subject access token is single-use: the grant records its (iss, jti), and a second exchange of the same token is refused with token_used. The check and the write are one atomic store call, made in PopulateTokenEndpointResponse, so two concurrent exchanges produce one grant.
A refresh-token subject is exchanged for a child grant (decideChild). Two derives from one grant give two independent children, since a child has no replay marker. If the response of a derive is lost, the child stays until its parent ends; this is accepted.
A client without exchange_refresh_tokens is refused with the unsupported requested_token_type text (unsupportedTokenType), or with the unsupported subject_token_type text that validateFormParams returns when its subject is a refresh token. A refresh-token subject asking for any type but a refresh token is refused with the same subject text. All are invalid_request.
The exchange grant is the only profile this handler issues today; a later profile would be chosen where decide checks the client's exchange_refresh_tokens.
func (*RefreshTokenHandler) CanHandleTokenEndpointRequest ¶ added in v0.60.0
func (*RefreshTokenHandler) CanHandleTokenEndpointRequest(_ context.Context, requester fosite.AccessRequester) bool
CanHandleTokenEndpointRequest claims token-exchange requests that involve the refresh-token type; see isRefreshTokenExchange.
func (*RefreshTokenHandler) CanSkipClientAuth ¶ added in v0.60.0
func (*RefreshTokenHandler) CanSkipClientAuth(_ context.Context, _ fosite.AccessRequester) bool
CanSkipClientAuth always returns false: token exchange requires client authentication.
func (*RefreshTokenHandler) HandleTokenEndpointRequest ¶ added in v0.60.0
func (h *RefreshTokenHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error
HandleTokenEndpointRequest decides the request only to fail early. Nothing is written and nothing is carried over: PopulateTokenEndpointResponse decides again, so the subject token is verified right before it is consumed.
func (*RefreshTokenHandler) PopulateTokenEndpointResponse ¶ added in v0.60.0
func (h *RefreshTokenHandler) PopulateTokenEndpointResponse( ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder, ) error
PopulateTokenEndpointResponse decides the request again and issues the refresh token. The second decision verifies the subject token right before it is consumed. The grant carries the subject token's (iss, jti), so a reused subject token fails in issue with token_used. A child grant has no (iss, jti) marker, so token_used applies only to top-level grants. The response is the RFC 8693 shape with token_type N_A and no refresh_token member; fosite writes Cache-Control: no-store and Pragma: no-cache.
type RefreshTokenOption ¶ added in v0.65.0
type RefreshTokenOption func(*refreshTokenOptions)
RefreshTokenOption configures RefreshTokenFactory. Factory takes FactoryOption instead, so an option cannot be passed to the wrong factory.
func WithExchangeGrantStorage ¶ added in v0.60.0
func WithExchangeGrantStorage(store storage.ExchangeGrantStorage) RefreshTokenOption
WithExchangeGrantStorage gives RefreshTokenFactory the store that holds the grants behind issued refresh tokens. It may be nil when no client enables exchange_refresh_tokens; the handler then refuses every request for a refresh token before it needs the store.
type SelfIssuedTokenValidator ¶ added in v0.41.0
type SelfIssuedTokenValidator struct {
// contains filtered or unexported fields
}
SelfIssuedTokenValidator validates subject tokens presented during RFC 8693 token exchange. It verifies that the token was issued by this authorization server by checking the signature against the server's own JWKS, and validates standard JWT claims.
func NewSelfIssuedTokenValidator ¶ added in v0.41.0
func NewSelfIssuedTokenValidator( jwks *jose.JSONWebKeySet, issuer string, allowedAudiences []string, ) (*SelfIssuedTokenValidator, error)
NewSelfIssuedTokenValidator creates a new validator for subject tokens. The jwks parameter must be non-nil and contain only the authorization server's public signing keys (e.g. AuthorizationServerConfig.PublicJWKS) — the validator only ever verifies signatures, so it must not be handed private key material. The issuer parameter is the expected "iss" claim value. allowedAudiences is the set of audiences this server accepts in a subject token's "aud" claim; per the same secure default as AuthorizationServerConfig.AllowedAudiences, an empty allowedAudiences rejects every subject token rather than skipping the check.
func (*SelfIssuedTokenValidator) Validate ¶ added in v0.41.0
func (v *SelfIssuedTokenValidator) Validate(_ context.Context, rawToken string) (*ValidatedClaims, error)
Validate parses and verifies a raw JWT subject token. It checks the signature against the server's JWKS, validates issuer and audience, ensures the token is not expired, and requires a subject claim for delegation.
The subject token's "aud" claim is checked against allowedAudiences, but this validator deliberately does not require that the authorization server itself (i.e. the token endpoint) be among those audiences. RFC 8693 leaves subject-token validation criteria out of scope, and ToolHive's vMCP flow legitimately exchanges tokens addressed to a downstream/upstream resource rather than the AS. The residual cross-resource risk is mitigated elsewhere: the token is still pinned to the server-wide allowedAudiences, and the handler enforces the requested resource against the client's registered audiences.
Returns the validated claims on success, or a descriptive error on failure.
type SubjectTokenValidator ¶
type SubjectTokenValidator interface {
Validate(ctx context.Context, rawToken string) (*ValidatedClaims, error)
}
SubjectTokenValidator validates subject tokens presented during RFC 8693 token exchange.
type TrustedIssuer ¶ added in v0.41.0
type TrustedIssuer struct {
// Name optionally identifies this trust declaration for canonical issuer_ref references.
Name string `json:"name,omitempty" yaml:"name,omitempty"`
// IssuerURL is the expected "iss" claim value (exact match).
IssuerURL string `json:"issuer_url" yaml:"issuer_url"`
// ExpectedAudience is the expected "aud" claim value that must appear
// in an RFC 8693 subject token's audience list (a resource/API identifier,
// not a client ID — required for delegation unless JWTBearerGrant is
// configured; see looksLikeResourceIdentifier). RFC 7523 assertions use
// the token endpoint as their audience instead.
//
// This legacy field is deprecated; configure RFC 8693 policy under
// inbound_grants.token_exchange.issuer_policies.
// See docs/arch/17-token-exchange-delegation.md ("ID/access-token
// discrimination") for why and its limits.
ExpectedAudience string `json:"expected_audience" yaml:"expected_audience"`
// JWKSURL is the URL to fetch the issuer's JSON Web Key Set from.
// If empty, it is resolved via OIDC discovery at {IssuerURL}/.well-known/openid-configuration.
JWKSURL string `json:"jwks_url,omitempty" yaml:"jwks_url,omitempty"`
// InsecureAllowHTTP permits plain-HTTP OIDC discovery and JWKS fetches
// for THIS issuer only. Development and testing only — never set in
// production. Does not relax the private-IP guard; see AllowPrivateIPs.
// Deliberately per-issuer: this server's own InsecureAllowHTTP must not
// silently permit plaintext discovery for every trusted external issuer
// too — a network attacker who can intercept that traffic could
// substitute a JWKS and forge subject tokens for that issuer's
// namespace.
InsecureAllowHTTP bool `json:"insecure_allow_http,omitempty" yaml:"insecure_allow_http,omitempty"`
// AllowPrivateIPs permits OIDC discovery and JWKS fetches for THIS
// issuer to resolve to a private or loopback address. Use only when the
// issuer is hosted inside the same cluster and has no public endpoint.
AllowPrivateIPs bool `json:"allow_private_ips,omitempty" yaml:"allow_private_ips,omitempty"`
// CAFilePath is the path to a PEM CA bundle added to the system roots when
// fetching this issuer's OIDC discovery document and JWKS. Trust is additive
// and scoped to this issuer: the public roots still apply, and no other
// issuer's client is affected.
CAFilePath string `json:"ca_file_path,omitempty" yaml:"ca_file_path,omitempty"`
// ActorClaim names the claim identifying the client that requested the
// subject token from THIS EXTERNAL ISSUER (used by AllowedActors below).
// Values are in the external issuer's namespace, NOT ToolHive client
// IDs. Defaults to "azp"; use "appid" for Microsoft Entra v1, "cid" for
// Okta. The special value "client_id" reads ValidatedClaims.ClientID
// instead of Extra (assignClaim routes it to that field) — it is still
// the external token's client_id claim, not a ToolHive one.
ActorClaim string `json:"actor_claim,omitempty" yaml:"actor_claim,omitempty"`
// AllowedActors is the allowlist of ActorClaim values authorized to
// exchange a subject token from this issuer when it carries no
// "may_act" claim. ActorMatcher can additionally authorize a token by
// matching its complete verified claims map; either signal is sufficient.
// When both are empty, only may_act-bearing tokens are accepted, and only
// if AllowMayAct is also true for this issuer. By itself names no
// ToolHive client — see AllowedDelegateClients and
// docs/arch/17-token-exchange-delegation.md ("Accepted limitations" #1).
AllowedActors []string `json:"allowed_actors,omitempty" yaml:"allowed_actors,omitempty"`
// ActorMatcher is an admin-authored CEL expression evaluated against the
// complete signature-verified JWT claims map as "claims". A true result
// authorizes delegation alongside AllowedActors; a syntax or type error
// fails configuration validation. An expression that compiles but does
// not return bool is NOT caught at that point, though — it compiles
// successfully and is only rejected the first time it is evaluated
// against a real token, denying that token (and every one after it, since
// the expression will never return bool). Any other runtime evaluation
// error denies the token the same way.
ActorMatcher string `json:"actor_matcher,omitempty" yaml:"actor_matcher,omitempty"`
// AllowedDelegateClients restricts which ToolHive client IDs may
// exchange a subject token from this issuer, for BOTH consent paths.
// Required (validateTrustedIssuer rejects empty/absent); "*" permits
// any confidential client holding the grant. See
// docs/arch/17-token-exchange-delegation.md ("Accepted limitations" #1).
//nolint:lll // field tags require full JSON+YAML names
AllowedDelegateClients []string `json:"allowed_delegate_clients,omitempty" yaml:"allowed_delegate_clients,omitempty"`
// AllowMayAct permits this external issuer's may_act claim to authorize
// delegation. It defaults to false; external issuers must be opted in
// explicitly because may_act bypasses AllowedActors and ActorMatcher. It
// does not affect self-issued subject tokens. When enabled,
// AllowedDelegateClients must name specific ToolHive clients rather than
// use the wildcard.
AllowMayAct bool `json:"allow_may_act,omitempty" yaml:"allow_may_act,omitempty"`
// JWTBearerGrant optionally enables the plain RFC 7523 JWT-bearer grant.
// It accepts assertions from this issuer without client authentication and
// limits their maximum age, subjects, and RFC 8707 resources. It is
// independent from RFC 8693 delegation policy.
//
// This legacy field is deprecated; configure RFC 7523 policy under
// inbound_grants.jwt_bearer.issuer_policies.
JWTBearerGrant *JWTBearerGrantPolicy `json:"jwt_bearer_grant,omitempty" yaml:"jwt_bearer_grant,omitempty"`
}
TrustedIssuer configures an external OIDC issuer whose tokens are accepted as RFC 8693 subject tokens or RFC 7523 JWT-bearer assertions.
This type is reused verbatim as the wire schema for authserver.RunConfig.TrustedIssuers (deliberately, to avoid a parallel type that drifts — see the go-style rule against that). Its JSON/YAML tags are therefore part of the serialized RunConfig, which is reflected into docs/server/swagger.*; adding, renaming, or retagging a field here is a schema change, not a purely internal one.
func ResolveJWTBearerGrantPolicies ¶ added in v0.45.0
func ResolveJWTBearerGrantPolicies(issuers []TrustedIssuer) ([]TrustedIssuer, error)
ResolveJWTBearerGrantPolicies clones grant policies and parses their duration fields once at the RunConfig-to-runtime boundary.
type UpstreamIdentity ¶ added in v0.65.0
type UpstreamIdentity struct {
// Issuer is the upstream's issuer URL.
Issuer string
// Subject is the upstream's raw sub from its validated ID token.
Subject string
// StableClaim names the upstream claim ToolHive keys users on, for example
// "uid" or "oid", or "sub".
StableClaim string
// StableValue is that claim's value for this user. A validator can bind users by
// a claim that is specific to one issuer.
StableValue string
}
UpstreamIdentity is a user's identity at the upstream provider.
type ValidatedClaims ¶
type ValidatedClaims struct {
// Subject is the user identity from the "sub" claim (required for delegation).
Subject string
// Issuer is the token issuer from the "iss" claim.
Issuer string
// Audience is the list of intended recipients from the "aud" claim.
Audience []string
// Expiry is the token expiration time from the "exp" claim.
Expiry time.Time
// IssuedAt is the token issuance time from the "iat" claim.
IssuedAt time.Time
// JWTID is the unique token identifier from the "jti" claim.
JWTID string
// Name is the user's display name from the custom "name" claim.
Name string
// Email is the user's email address from the custom "email" claim.
Email string
// ClientID is the OAuth client ID from the custom "client_id" claim.
ClientID string
// Scopes is the space-delimited scope string assembled from the "scope"
// or "scp" claim. RFC 9068 §2.2.1 spells it "scope" as a JSON string, but
// fosite's default JWT claims strategy (token/jwt/claims_jwt.go) writes
// scopes as a JSON array under "scp" instead, unless ScopeField is
// explicitly set to String or Both — this server does not set it, so a
// genuine ToolHive-issued access token used as a subject token carries
// "scp", not "scope". When both are present, "scope" wins. Empty if the
// subject token carries neither claim.
Scopes string
// MayAct holds the authorized actor from the "may_act" claim (RFC 8693 §4.4).
// Nil when the subject token does not carry a may_act claim.
MayAct *MayActClaim
// ExternalActor is the external actor claim value when the allowlist
// authorization path matched. It is empty when ActorMatcher alone
// authorized the token, because a matcher need not identify an actor.
// It is never populated from token claims by buildValidatedClaims or
// assignClaim, and is empty for self-issued and may_act-bearing tokens.
ExternalActor string
// ExternalActorAuthorized reports that the external issuer authorized
// delegation through AllowedActors or ActorMatcher. It is separate from
// ExternalActor because ActorMatcher can authorize without an actor claim.
// It is false for self-issued and may_act-bearing tokens.
ExternalActorAuthorized bool
// ExternalIssuer is set by the external-issuer validation path
// (validateExternalToken in multi_issuer_validator.go) to that issuer's
// IssuerURL, for EVERY external token it validates — unlike
// ExternalActor, this is set regardless of whether the token carries a
// may_act claim. It exists so the handler can record provenance (which
// issuer a delegation actually originated from) even for a
// may_act-bearing external token, which leaves ExternalActor unset.
// Empty for self-issued tokens. Like ExternalActor, it is never
// populated from token claims by buildValidatedClaims or assignClaim —
// it comes from the already-validated issuer config the token matched,
// not from anything token-supplied, so it cannot be spoofed via claims.
ExternalIssuer string
// AllowedDelegateClients is set for EVERY external token — like
// ExternalIssuer and unlike ExternalActor, it does not depend on whether
// the token carries a may_act claim (see validateExternalToken). That
// matters: the may_act path bypasses AllowedActors and ActorMatcher
// entirely, so it is the path that most needs this restriction to still
// apply. It is set to that issuer's configured
// TrustedIssuer.AllowedDelegateClients, and is never populated
// from token claims by buildValidatedClaims or assignClaim, and the
// validator never compares it against anything: the validator does not
// know the authenticated ToolHive client, so checkDelegationConsent
// (handler.go) is the one that checks actorID against this list. Nil
// means the issuer did not configure AllowedDelegateClients, which is
// permissive — any ToolHive client may use the allowlisted external
// actor, same as before this field existed.
AllowedDelegateClients []string
// Extra contains all non-standard claims not captured by other fields.
Extra map[string]any
// Claims holds every verified claim as decoded, registered ones included.
// It is shared with the validator's decode result: treat it as read-only
// and deep-copy before handing it to code that may modify it.
Claims map[string]any
}
ValidatedClaims holds the verified claims extracted from a subject token. All fields are populated from a successfully validated JWT that was issued by this authorization server.