Documentation
¶
Overview ¶
Package upstreamauth holds the outbound authentication and transport policy shared by the platform's HTTP-based connection kinds.
A connection kind that reaches an upstream over HTTP has to answer the same questions whatever it carries on the wire: which credential goes on the request, which headers the operator pins and the model may not touch, how long a dial and a call may take, how much of a response may be read, and which TLS material the handshake presents. Those answers are the same for an OpenAPI-described REST API and for a GraphQL endpoint, and keeping one copy of them is what stops a fix to, say, the token-fetch error scrubber from landing in one kind and not the other.
The package deliberately knows nothing about tools, catalogs, operations or documents. It takes the slice of a connection's configuration that concerns credentials and transport, and it produces an Authenticator and an *http.Client. Each kind keeps its own exported Config with its own keys and maps the overlapping fields onto this one, so this package never appears in a kind's public API.
Error text is owned by the caller. Config.ErrPrefix names the kind in every message this package produces ("apigateway: credential is required ..."), because an operator reading a refused connection save should see the surface they configured, not the seam behind it.
Index ¶
- Constants
- Variables
- func NewHTTPClient(cfg Config) *http.Client
- func NewHTTPTransport(cfg Config) *http.Transport
- func ReadBody(prefix string, r io.Reader, maxBytes int64) (body []byte, truncated bool, err error)
- func ReadLimit(limit int64) int64
- func ReserveBodyBudget(b *membudget.Budget, contentLength, readCap int64) (reserved int64, ok bool)
- func SetAuthEvents(a Authenticator, w *authevents.Writer) bool
- func SetConnOAuthStore(a Authenticator, s connoauth.Store) bool
- type Authenticator
- type Config
- func (c Config) AuthHeader() string
- func (c Config) BuildTLSConfig() (*tls.Config, error)
- func (c Config) ConnOAuthConfig() connoauth.Config
- func (c Config) IsOAuthAuthorizationCode() bool
- func (c Config) Validate() error
- func (c Config) ValidateAuth() error
- func (c Config) ValidateCustomHeaders(headers map[string]string) error
- func (c Config) ValidateIdentityPassthrough() error
- func (c Config) ValidateStaticHeaders() error
- func (c Config) ValidateTLSMaterial() error
- func (c Config) ValidateTransport() error
- type OAuth2Config
- type SignedJWTConfig
Constants ¶
const ( // AuthModeNone disables outbound authentication. AuthModeNone = "none" // AuthModeBearer sends "Authorization: Bearer <credential>". AuthModeBearer = "bearer" // AuthModeAPIKey sends the credential as a header (default // "X-API-Key") or as a query parameter; placement and key name are // per-connection so APIs that use non-standard schemes (e.g. an // "api_key" query parameter, or a custom "X-Api-Token" header) can // be onboarded without code changes. AuthModeAPIKey = "api_key" // AuthModeBasic sends "Authorization: Basic base64(username:password)" // per RFC 7617. Required for the long tail of older REST APIs (Jenkins, // on-prem Jira / Confluence Server / DC, internal apps) that never moved // to bearer or OAuth. RFC 7617 §2 forbids ":" in the userid; password // may be empty (some APIs accept "token:" as a bearer-token-in-username // pattern). Password is encrypted at rest via the platform's // FieldEncryptor (the "password" config key is already in the // sensitive-keys list). AuthModeBasic = "basic" // AuthModeOAuth is the canonical OAuth auth_mode shared across // every toolkit kind (see connoauth.AuthModeOAuth). The specific // flow is carried separately in OAuth2Config.Grant. Parse // normalizes the legacy api-only auth_mode values below to this // form, so a parsed Config always reports AuthModeOAuth for an // OAuth connection. AuthModeOAuth = connoauth.AuthModeOAuth // AuthModeOAuth2ClientCredentials is the legacy api-only auth_mode // that encoded the client_credentials grant in the mode string. // Retained so raw config authored before the schema unified (and // hand-built test Configs) still parse; Parse normalizes it to // AuthModeOAuth + Grant=client_credentials. // // client_credentials acquires a bearer token — server-to-server, // no human in the loop. The platform exchanges the configured // client_id + client_secret for a token at OAuth.TokenURL and // applies it as "Authorization: Bearer <token>" on outbound // calls. Tokens are cached + refreshed automatically by the // underlying golang.org/x/oauth2 library; no DB state is // required because every restart can re-acquire from credentials. AuthModeOAuth2ClientCredentials = "oauth2_client_credentials" // #nosec G101 -- mode name, not a credential // AuthModeOAuth2AuthorizationCode runs the user-driven OAuth 2.1 // authorization-code grant: an admin completes a one-time browser // flow at connection setup; the resulting refresh token is // persisted (encrypted) so subsequent platform restarts and // background workloads keep working without further interaction. // Tokens are refreshed automatically before expiry. Requires the // platform's database (refresh-token state survives restarts). AuthModeOAuth2AuthorizationCode = "oauth2_authorization_code" // #nosec G101 -- mode name, not a credential // AuthModeMTLS authenticates by presenting an X.509 client // certificate during the TLS handshake per RFC 5246 / 8446. No // Authorization header is sent: the cert IS the credential. // Used by upstreams that map the cert's subject DN (or a SAN) to // a user identity in their authorizer, including service-mesh // peers, PKI-fronted internal APIs, healthcare integration // engines, financial messaging endpoints, and FedRAMP / DoD- // boundary services. Requires both mtls_client_cert_pem and // mtls_client_key_pem on the connection config; the mTLS material // can also be present alongside other auth modes (bearer + mTLS, // etc.), but auth_mode=mtls is the explicit "no header // credential" signal. AuthModeMTLS = "mtls" // CredentialPlacementHeader (default) sends the credential as an HTTP // header named by APIKeyHeader. CredentialPlacementHeader = "header" // CredentialPlacementQuery sends the credential as a URL query parameter // named by APIKeyParam. CredentialPlacementQuery = "query" // DefaultAPIKeyHeader is the conventional API-key header name when // the connection does not specify one. DefaultAPIKeyHeader = "X-API-Key" // #nosec G101 -- header name, not a credential // DefaultConnectTimeout caps the time spent establishing the // outbound connection (TCP + TLS handshake) on each invocation. DefaultConnectTimeout = 10 * time.Second // DefaultCallTimeout caps the total per-call time including // upstream processing and response read. DefaultCallTimeout = 60 * time.Second // DefaultMaxResponseBytes is the upstream read cap: the most a kind // reads of any one response. It bounds transfer and buffering, not // what reaches the model; a kind's own inline budget does that. DefaultMaxResponseBytes = int64(10 * 1024 * 1024) )
const ( OAuth2AuthStyleHeader = "header" OAuth2AuthStyleParams = "params" )
EndpointAuthStyle values.
const ( // SignedJWTAlgHS256 signs with jwt_client_secret (HMAC-SHA256). SignedJWTAlgHS256 = "HS256" // SignedJWTAlgRS256 signs with an RSA jwt_private_key_pem. SignedJWTAlgRS256 = "RS256" // SignedJWTAlgES256 signs with an ECDSA P-256 jwt_private_key_pem. SignedJWTAlgES256 = "ES256" )
The signing algorithms the mode supports. The algorithm selects both the JWA signing method and the key material the config must carry: HS256 signs with the shared secret, RS256 and ES256 with the PEM private key.
const ( // DefaultSignedJWTTokenLifetime is exp - iat when the connection // sets no jwt_token_lifetime. Five minutes matches the default // lifetime the upstreams in this class hand out. DefaultSignedJWTTokenLifetime = 5 * time.Minute // DefaultSignedJWTIssuedAtSkew is how far back iat is set when the // connection sets no jwt_issued_at_skew. An upstream whose clock // runs behind the platform's refuses a token whose iat is in its // future, so the claim is backdated by default rather than on // request. DefaultSignedJWTIssuedAtSkew = 30 * time.Second )
const AuthModeSignedJWT = "signed_jwt"
AuthModeSignedJWT mints the short-lived JWT the upstream validates, from an identifier and a signing key the operator was issued out of band, and sends it as "Authorization: Bearer <jwt>".
The distinguishing property of this class of upstream is that there is no token endpoint: nothing is exchanged, the client is the issuer, and the token is valid for minutes. That is why neither "bearer" (a fixed string cannot carry a 300-second expiry) nor "oauth" (there is no endpoint to exchange against) can reach one. Sage X3's connected applications, Snowflake key-pair authentication, and Apple's App Store Connect and APNs keys are all this shape, as is the long tail of internal services that issue a client id and a shared secret.
const AuthorizationHeader = "Authorization"
AuthorizationHeader is the HTTP header bearer-mode auth populates. Named so the same literal is not repeated across the auth dispatch and the header-spoof rejection.
Variables ¶
var ErrNeedsReauth = errors.New("oauth2 connection needs admin reconnect")
ErrNeedsReauth is the structured error a kind's invoke tool surfaces when an authorization_code connection's stored refresh token is missing, expired beyond refresh_expires_at, or definitively rejected by the IdP (RFC 6749 §5.2 invalid_grant on the refresh_token grant). Transient failures (network, 5xx, request cancellation) DO NOT produce this error.
The message carries no package prefix of its own: Apply wraps it in the calling kind's voice, so an operator sees "apigateway: oauth2 connection needs admin reconnect" while errors.Is still matches. The wording intentionally points at the platform's reauth path rather than echoing the underlying IdP response (which can include sensitive material from a partial grant exchange).
Functions ¶
func NewHTTPClient ¶
NewHTTPClient builds the per-connection *http.Client: the call timeout as the client deadline, and the connection's transport.
Redirects are explicitly disallowed so a kind does not blindly re-issue a request (and re-attach the connection's credential) to a host the operator did not authorize. The model can follow a redirect manually by reading the upstream Location header from the response and issuing a new call with the redirected URL.
Every request the client sends carries the platform's User-Agent unless the request already names one (useragent.Transport), so a kind's tool call, a page of a walk and a schema introspection all present the product rather than Go's default, which a web application firewall refuses (#1679). The wrapper sits directly over the *http.Transport; metrics wrapping is applied by the caller rather than here so test helpers can construct a bare client without threading a metrics handle through every call site.
TLS-config build errors are intentionally not surfaced from this constructor. Validation has already checked cert + key + CA bundle, so BuildTLSConfig only fails here if a caller has constructed a Config by hand and bypassed it. The fallback returns a transport with the system default tls.Config and the first outbound call will fail loudly with the underlying tls error, which is the same surface a misconfigured transport would produce on any other auth mode.
func NewHTTPTransport ¶
NewHTTPTransport builds the per-connection http.Transport. The dial step (TCP + TLS handshake) is bound by cfg.ConnectTimeout so an unreachable upstream fails fast instead of consuming the full CallTimeout budget. Exposed separately from NewHTTPClient so unit tests can verify the wiring without standing up a network listener.
When the connection carries mTLS material (cfg.MTLSClientCertPEM + cfg.MTLSClientKeyPEM) or a custom CA bundle (cfg.TLSCABundlePEM), the transport's TLSClientConfig is populated accordingly. With neither set, TLSClientConfig stays nil and Go's net/http uses system defaults. BuildTLSConfig errors here are degraded to nil (see NewHTTPClient for the rationale).
func ReadBody ¶
ReadBody reads at most maxBytes of an upstream response, reporting whether it was cut short. One extra byte is read so a body exactly at the cap is distinguishable from one that overruns it.
func ReadLimit ¶
ReadLimit is the most of a response to read given a configured limit, falling back to the default cap when there is none. The one definition a buffered call, a page of a walk, and the memory reservation share, so the three cannot drift.
func ReserveBodyBudget ¶
ReserveBodyBudget computes the worst-case number of bytes a buffered read of this response could hold and tries to reserve them against the shared budget. It returns the amount reserved (to be released by the caller) and whether the reservation was granted.
When the upstream declares a Content-Length below the read cap, only that many bytes are reserved so small (and empty) responses do not each tie up the full per-request cap and falsely exhaust the budget. This is safe because Go's HTTP client bounds resp.Body to the declared Content-Length — a server that writes more than it declared cannot make ReadBody buffer beyond it. Unknown/chunked responses (ContentLength < 0) and over-cap responses reserve the full cap, which is exactly what ReadBody may buffer. A nil/disabled budget always grants the reservation and Release is a no-op, so the buffered path is unchanged when no budget is configured.
func SetAuthEvents ¶
func SetAuthEvents(a Authenticator, w *authevents.Writer) bool
SetAuthEvents wires the audit-event writer onto an authenticator that emits OAuth lifecycle events, and reports whether it did. The companion to SetConnOAuthStore; see its comment.
func SetConnOAuthStore ¶
func SetConnOAuthStore(a Authenticator, s connoauth.Store) bool
SetConnOAuthStore wires the persisted OAuth token store onto an authenticator that needs one, and reports whether it did. Only the authorization_code authenticator does; every other mode returns false, so a kind can call this unconditionally after NewAuthenticator instead of type-switching at each call site.
Types ¶
type Authenticator ¶
Authenticator applies a connection's authentication scheme to an outbound HTTP request before it is sent. Implementations must be safe for concurrent use — a single Authenticator is shared across all in-flight invocations of a connection.
Implementations MUST NOT log credential material. The platform's audit pipeline expects no Authorization or X-API-Key value to ever appear in slog output, error messages, or audit rows; carelessly formatted error strings are the most common leak path.
func NewAuthenticator ¶
func NewAuthenticator(c Config) (Authenticator, error)
NewAuthenticator returns the Authenticator implementation for a validated Config. ValidateAuth has already rejected unknown auth modes, so the default branch only fires if a future mode is added without a matching case here.
type Config ¶
type Config struct {
// Kind is the connoauth connection kind ("api", "graphql"). It
// keys the persisted OAuth token row and identifies the connection
// in connoauth's deduplicated configuration warnings, so two kinds
// with a same-named connection do not share a token.
Kind string
// ErrPrefix names the calling kind in every error this package
// produces. Empty falls back to the package name. Set it: an
// operator whose connection save is refused should read
// "apigateway: credential is required ...", not the name of an
// internal seam they cannot see in any configuration file.
ErrPrefix string
// ConnectionName is the audit-visible connection identifier. Used
// as the OAuth token-row key for the authorization_code grant.
// Kinds populate it from the toolkit instance name after Parse.
ConnectionName string
// AuthMode selects the credential scheme: one of the AuthMode*
// constants.
AuthMode string
// Credential is the bearer token or API key. Ignored when AuthMode
// is "none". Encrypted at rest via the platform's FieldEncryptor.
Credential string
// CredentialPlacement is "header" (default) or "query" — only consulted
// when AuthMode is "api_key".
CredentialPlacement string
// APIKeyHeader is the header name to set when CredentialPlacement is
// "header". Defaults to DefaultAPIKeyHeader.
APIKeyHeader string
// APIKeyParam is the query parameter name when CredentialPlacement is
// "query". No default — required when placement is "query".
APIKeyParam string
// Username is the userid for HTTP Basic auth (RFC 7617). Required
// when AuthMode is "basic". Ignored otherwise. Not a secret on its
// own (per RFC 7617 §2 the userid is sent in clear after base64
// decoding regardless), so it is not encrypted at rest.
Username string
// Password is the password for HTTP Basic auth. May be empty: some
// legacy APIs accept a bearer token in the userid slot with an empty
// password (the "token:" pattern). Encrypted at rest via the
// platform's FieldEncryptor.
Password string
// OAuth2 carries the OAuth 2.1 parameters used when AuthMode is
// AuthModeOAuth. Empty for non-OAuth modes.
OAuth2 OAuth2Config
// SignedJWT carries the assertion parameters used when AuthMode is
// AuthModeSignedJWT. Empty for every other mode.
SignedJWT SignedJWTConfig
// ConnectTimeout caps the dial step (TCP + TLS handshake) on each
// invocation.
ConnectTimeout time.Duration
// CallTimeout caps the total per-invocation time.
CallTimeout time.Duration
// MaxResponseBytes is the upstream read cap: the most a kind reads
// of any one response. Defaults to DefaultMaxResponseBytes.
MaxResponseBytes int64
// StaticHeaders are operator-configured headers attached to every
// outbound request, in addition to whatever AuthMode contributes.
// Operator-supplied; the model never sets or overrides these.
// Values are encrypted at rest.
StaticHeaders map[string]string
// MTLSClientCertPEM is the PEM-encoded X.509 client certificate
// chain (leaf first) presented during the TLS handshake. Public
// material, stored in plain text. Required alongside
// MTLSClientKeyPEM and optional otherwise; an ambiguous config
// (one set, the other empty) is refused.
MTLSClientCertPEM string
// MTLSClientKeyPEM is the PEM-encoded private key matching
// MTLSClientCertPEM. Encrypted at rest via the platform's
// FieldEncryptor. Validation runs the cert + key through
// tls.X509KeyPair so a key that does not match the cert is
// rejected at write time, not on first outbound call.
MTLSClientKeyPEM string
// TLSCABundlePEM is an optional PEM bundle of root CA
// certificates added to the TLS trust store for outbound
// requests on this connection. Appended to the system root
// pool, not substituted: public CAs remain trusted. Required
// when the upstream's TLS certificate is signed by a private
// CA (cluster-internal CA, mesh CA, corporate root) that the
// host's default cert store does not carry.
TLSCABundlePEM string
// IdentityPassthrough forwards the acting caller's inbound bearer
// token as the outbound Authorization header, instead of applying
// this connection's shared credential. When set, AuthMode must be
// "none": the shared-credential Authenticator is skipped, and two
// sources for one header would be ambiguous. Reading the caller's
// token off the request context is the kind's job, not this
// package's — only the invariant lives here.
IdentityPassthrough bool
}
Config is the authentication and transport slice of a connection's configuration. A kind builds one from its own Config and hands it to NewAuthenticator and NewHTTPClient.
func Parse ¶
Parse reads the authentication and transport keys out of a connection's config map and applies the defaults, leaving the kind to read its own keys from the same map. The returned Config is NOT validated: a kind interleaves these checks with its own so the first error an operator sees is the same one it was before this policy was shared (see Validate and the individual validators).
kind is the connoauth connection kind, errPrefix names the kind in error text, and endpointURL identifies the connection in connoauth's deduplicated warnings about legacy keys and cleartext endpoints.
func (Config) AuthHeader ¶
AuthHeader returns the canonical header name this connection's auth mode would set, so ValidateCustomHeaders can reject the model's attempts to spoof or override it. Empty string means no header-based auth (mode=none, mode=api_key with query placement).
func (Config) BuildTLSConfig ¶
BuildTLSConfig returns the *tls.Config this connection's transport presents, or nil when it carries neither a client keypair nor a CA bundle.
func (Config) ConnOAuthConfig ¶
ConnOAuthConfig maps this connection's OAuth settings to the unified connoauth.Config the Source consumes. The CA bundle travels with it so the token-exchange and refresh paths can verify an IdP behind a private CA without falling back to system trust.
Exported because the initial authorization-code exchange (the admin OAuth kind handler) and the per-call silent refresh (the authenticator) must read every field through the same translator: a regression here would otherwise drop CABundlePEM, Prompt, or a future field from one path but not the other.
func (Config) IsOAuthAuthorizationCode ¶
IsOAuthAuthorizationCode reports whether the connection uses the OAuth authorization_code grant (canonical AuthModeOAuth plus that grant). The admin redirect handler and the kind handlers gate the one-time browser flow on this, so they do not depend on the raw auth_mode string shape.
func (Config) Validate ¶
Validate runs every check this package owns, in the order a kind with no additional keys would want them. A kind that interleaves its own checks calls the individual validators instead.
func (Config) ValidateAuth ¶
ValidateAuth enforces the per-mode credential requirements.
func (Config) ValidateCustomHeaders ¶
ValidateCustomHeaders refuses model-supplied headers that would collide with a credential or with an operator-pinned static header. The model never gets to set Authorization, the header its own connection's auth mode owns, or any name the operator fixed in static_headers: those are the operator's to decide, and a header the model set would either be silently overwritten at request build time or, worse, win.
func (Config) ValidateIdentityPassthrough ¶
ValidateIdentityPassthrough enforces that a passthrough connection carries no shared credential. Passthrough forwards the caller's inbound token as the Authorization header, so a configured auth_mode would either be ignored (confusing) or fight for the same header. Requiring auth_mode=none keeps the single-credential-source invariant explicit.
func (Config) ValidateStaticHeaders ¶
ValidateStaticHeaders refuses operator config that would collide with the auth path or with hop-by-hop headers Go forbids on a request. A static header attempting to set Authorization (or the auth-mode-reserved header for api_key+header) would silently lose to the auth layer at request time — fail loudly here instead.
func (Config) ValidateTLSMaterial ¶
ValidateTLSMaterial enforces the per-connection mTLS and CA-trust rules, so a misconfiguration is refused at admin write time rather than on the first outbound call.
func (Config) ValidateTransport ¶
ValidateTransport enforces that the timeouts and the read cap are positive. Zero would mean "no timeout" to net/http and "read nothing" to the response reader, neither of which any operator intends.
type OAuth2Config ¶
type OAuth2Config struct {
// Grant is the OAuth flow, populated by Parse from the canonical
// oauth_grant (or derived from a legacy auth_mode). One of
// connoauth.GrantClientCredentials or
// connoauth.GrantAuthorizationCode. The authenticator and
// validation dispatch on this rather than on the auth_mode string.
Grant string
// TokenURL is the upstream's token endpoint. Required.
TokenURL string
// ClientID is the platform's registered client id. Required.
ClientID string
// ClientSecret is the platform's registered client secret.
// Required. Encrypted at rest via the platform's FieldEncryptor.
ClientSecret string
// Scopes is an optional list of OAuth scopes to request.
Scopes []string
// EndpointAuthStyle controls how the client credentials are
// transmitted at token-fetch time. "header" (default) sends
// them as HTTP Basic auth on the token request; "params"
// sends them as POST body parameters. Some IdPs require one
// or the other; "header" is the OAuth 2.1 default.
EndpointAuthStyle string
// AuthorizationURL is the upstream's authorization endpoint.
// Required only for the authorization_code grant — that's
// where the platform redirects the admin's browser to start
// the flow.
AuthorizationURL string
// Prompt is an optional OIDC prompt parameter (RFC OIDC
// §3.1.2.1). Common values: "login" (force credential prompt),
// "consent" (force consent screen), "select_account",
// "none" (silent auth). Empty by default — the IdP decides.
// Operators of strict OIDC realms (Keycloak, Auth0, Okta)
// typically set this to "login" so admin Reconnect actions
// always re-prompt the user. Pure-OAuth (non-OIDC) providers
// often reject unknown parameters with invalid_request, so
// leave empty for those.
Prompt string
}
OAuth2Config describes the OAuth 2.1 grant parameters. For client_credentials the platform exchanges ClientID + ClientSecret at TokenURL for an access token (cached + refreshed by the golang.org/x/oauth2 library). For authorization_code an admin completes a one-time browser flow and the persisted refresh token is read through connoauth.Source on every call.
type SignedJWTConfig ¶
type SignedJWTConfig struct {
// Algorithm is one of SignedJWTAlgHS256 (the default),
// SignedJWTAlgRS256 or SignedJWTAlgES256. It selects the signing
// method and which key field is required.
Algorithm string
// ClientSecret is the HMAC shared secret. Required for HS256 and
// refused for the asymmetric algorithms. Encrypted at rest via the
// platform's FieldEncryptor.
ClientSecret string
// PrivateKeyPEM is the PEM-encoded private key (PKCS#1 or PKCS#8
// for RSA, SEC 1 or PKCS#8 for ECDSA). Required for RS256 and
// ES256 and refused for HS256. Encrypted at rest via the
// platform's FieldEncryptor.
PrivateKeyPEM string
// KeyID is emitted as the token's "kid" header when set. Upstreams
// that hold several registered keys for one account select by it.
// Optional; no header is emitted when empty.
KeyID string
// Issuer is the "iss" claim, and Subject the "sub" claim. Each is
// omitted from the token when empty, because an upstream that
// registered no value for one rejects a token that carries it: a
// Sage X3 connected application checks both, an Apple App Store
// Connect team key checks iss alone, and an Apple individual key
// checks sub alone. At least one must be set — a token that
// identifies nobody is not one any upstream in this class accepts.
Issuer string
Subject string
// Audience is the "aud" claim. Defaults to the connection's
// endpoint URL at parse time; the value must match what the
// upstream registered, byte for byte.
Audience string
// TokenLifetime is exp - iat. Defaults to
// DefaultSignedJWTTokenLifetime.
TokenLifetime time.Duration
// IssuedAtSkew is how far back iat is set to absorb clock drift
// between the platform and the upstream. It is also the renewal
// margin: a cached assertion is abandoned once it is this close to
// expiry, so one is never presented so near exp that the upstream's
// clock could have passed it in flight. Setting it to zero is an
// operator stating that the two clocks agree, and gives up the
// margin as well as the backdating. Defaults to
// DefaultSignedJWTIssuedAtSkew and must be under TokenLifetime —
// a skew at or past the lifetime mints a token that is already
// expired.
IssuedAtSkew time.Duration
}
SignedJWTConfig describes the assertion the platform mints for an upstream that validates a client-minted JWT.
The claims are stated rather than derived because each upstream registered exact values out of band and checks them literally: a connected application in Sage X3 pins the issuer to the client id it generated and the audience to the API URL byte for byte, and answers a mismatch with a 401 naming the claim.