sts

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 28, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package sts is an RFC 8693 OAuth 2.0 Token Exchange client.

Construct a Client with the issuer's BaseURL and the token endpoint path, then call Exchange with a populated ExchangeRequest. The package is provider-agnostic: every server-specific value (endpoint path, requested-token-type URIs, custom form fields) is supplied at call time. There is no provider detection.

Index

Constants

View Source
const (
	GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // RFC 8693 grant_type URI, not a credential

	SubjectTokenTypeJWT         = "urn:ietf:params:oauth:token-type:jwt"          //nolint:gosec // RFC 8693 token-type URI, not a credential
	SubjectTokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token" //nolint:gosec // RFC 8693 token-type URI, not a credential
)

RFC 8693 grant_type and standard subject-token type URIs. Caller supplies RequestedTokenType (which is always implementation-specific outside of these RFC 8693 standard values).

View Source
const DefaultRequestTimeout = 30 * time.Second

DefaultRequestTimeout caps a single token-exchange round-trip. Set conservatively: even with a slow auth host plus TLS handshake, a healthy exchange completes in sub-seconds. The cap mainly defends against slow-loris responses dripping bytes within MaxResponseBytes — see Client.RequestTimeout for the per-Client override.

Variables

View Source
var ErrAbsolutePath = oauthhttp.ErrAbsolutePath

ErrAbsolutePath is returned when Path is an absolute or scheme-relative URL rather than a path relative to BaseURL. See oauthhttp.ErrAbsolutePath for the rationale; re-exported here so callers can errors.Is on either package's sentinel.

View Source
var ErrInsecureBaseURL = oauthhttp.ErrInsecureBaseURL

ErrInsecureBaseURL is returned when Exchange is called against an http:// BaseURL without AllowInsecureHTTP set. Token exchange ships a subject_token (typically the user's core bearer) in the request body — over plain HTTP that's a credential in the clear. Re-exported from internal/oauthhttp so callers can errors.Is uniformly across deviceflow and sts.

Functions

func SetNowForTest added in v0.3.1

func SetNowForTest(t TestingTB, c *Client, now func() time.Time)

SetNowForTest replaces c.now()'s clock for the lifetime of the test. The previous override (if any) is restored when t.Cleanup runs. Stores go through atomic.Pointer so they don't race the unsynchronised hot-path reads in Exchange.

Replaces the previous package-global `nowFunc` shim, which `t.Parallel`-running tests could race against each other on. With a per-Client field, two parallel tests each freeze their own Client's clock without interference.

Types

type Client

type Client struct {
	// Transport supplies the http.RoundTripper used for all calls.
	// nil → http.DefaultTransport. The library builds its own
	// *http.Client around this transport so callers can't trivially
	// pass a *http.Client with TLS verification disabled. See the
	// deviceflow.Client.Transport doc for the security rationale.
	Transport http.RoundTripper

	BaseURL   string
	Path      string
	UserAgent string

	// RequestTimeout is the per-Exchange deadline applied via
	// context.WithTimeout on top of the caller's context. Zero falls
	// back to DefaultRequestTimeout. Negative disables the cap (useful
	// for tests that want to drive timing via the caller's ctx alone).
	RequestTimeout time.Duration

	// AllowInsecureHTTP permits http:// BaseURLs. Default (false) is
	// reject — token exchanges carry the subject token (a bearer) on
	// the wire and must be TLS-protected end to end. Production callers
	// MUST leave this false; only tests and local development that pin
	// the issuer to loopback should flip it.
	AllowInsecureHTTP bool
	// contains filtered or unexported fields
}

Client exchanges subject tokens for tokens of a different type at an RFC 8693 token endpoint.

All configuration is explicit; the package has no global state and no implicit URLs. Provide BaseURL and Path; the rest is RFC 8693.

func New added in v0.3.1

func New(c *Client) (*Client, error)

New validates a Client's required fields at construction time rather than at the first Exchange call. Returns an error if BaseURL or Path is empty — these would otherwise surface as a confusing "POST to :///token: ..." error from the caller at the worst moment.

Takes a *Client (rather than a Client value) because the struct embeds an atomic.Pointer for the test-clock seam, which can't be copied per the noCopy convention. Returns the same pointer on success.

Field-bag construction (`&sts.Client{...}`) is still supported for callers who want to set optional fields piecemeal, but `New` is the recommended path — it makes misconfiguration a startup error rather than a runtime one.

func (*Client) Exchange

func (c *Client) Exchange(ctx context.Context, req ExchangeRequest) (*tokens.TokenSet, error)

Exchange performs one RFC 8693 token exchange.

Returns a TokenSet with absolute ExpiresAt derived from the server's expires_in. Returns an error wrapping the response body when the server responds with a non-2xx status; callers can match on the returned error message for known OAuth error codes.

type ExchangeRequest

type ExchangeRequest struct {
	SubjectToken       string
	SubjectTokenType   string
	RequestedTokenType string

	Audience string
	Resource string
	Scope    string

	// ClientID, when non-empty, is sent as the username component of an
	// HTTP Basic Authorization header (RFC 6749 §2.3.1). ClientSecret
	// is the password component; leave empty for public clients
	// (Exchange will emit Basic base64("id:"), which §2.3.1 permits).
	//
	// Why both surfaces. Some authorization servers accept client
	// credentials for the token-exchange grant only via Basic Auth and
	// ignore form-body client_id — zitadel-based servers as of
	// 2026-05 are a known example (pkg/op/token_exchange.go reads only
	// from r.BasicAuth()). Callers are also free to set
	// Extra["client_id"] for servers that read from the form body;
	// both surfaces can be populated simultaneously and validate()
	// rejects a divergence between them.
	//
	// Wire encoding. Both values are url.QueryEscape'd before being
	// placed in the header — RFC 6749 §2.3.1 mandates that client
	// credentials are form-urlencoded before going into Basic Auth, so
	// spec-compliant servers decode via QueryUnescape on the way in.
	// Without the escape, credentials containing reserved characters
	// (':', '@', '%', non-ASCII) would not round-trip correctly even
	// against compliant servers. validate() further rejects ClientID
	// values that ':' or non-VSCHAR bytes can't reach via this path
	// (RFC 7617 §2 + RFC 6749 §2.3.1).
	ClientID     string
	ClientSecret string

	Extra url.Values
}

ExchangeRequest is the input to a token exchange.

SubjectToken, SubjectTokenType, and RequestedTokenType are required. Audience, Resource, and Scope map to RFC 8693 §2.1 parameters and are sent only when non-empty. Extra carries implementation-specific form fields (e.g. server-defined parameters not in RFC 8693) that the caller's server expects; the standard fields above always win if Extra also sets them.

Extra values are NOT redacted by the Stringer — only SubjectToken is. Do not stuff bearer-equivalent credentials (client secrets, assertion JWTs, refresh tokens) into Extra; the package is targeted at public-client device flow where no such field is expected. If a caller's server demands a secret-bearing form field via Extra, log hygiene becomes that caller's responsibility.

func (ExchangeRequest) GoString added in v0.3.1

func (r ExchangeRequest) GoString() string

GoString delegates to String so %#v in fmt also redacts.

func (ExchangeRequest) String added in v0.3.1

func (r ExchangeRequest) String() string

String redacts SubjectToken (the user's core bearer) and ClientSecret (bearer-equivalent for confidential clients) so accidental log/print-debug exposure doesn't leak them. ClientID is shown verbatim — it's an identifier, not a credential. Other fields are configuration metadata and shown verbatim.

Redaction here is a log-hygiene defense, not a memory-safety or security boundary. The struct fields remain exported and readable via direct access or reflection; callers handling long-lived credentials are responsible for their own zeroization. When adding new fields to ExchangeRequest, update this method if the field carries bearer-equivalent data.

type TestingTB added in v0.3.1

type TestingTB interface {
	Helper()
	Cleanup(func())
}

TestingTB is the subset of testing.TB used by SetNowForTest.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL