authcode

package
v0.5.2 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package authcode is an RFC 8252 OAuth 2.0 Authorization Code Grant client for native apps: it uses PKCE (RFC 7636, S256) and a loopback redirect so a CLI can authenticate a user through their browser without the user copying a code by hand.

Usage is three steps, driven by the embedding CLI:

  1. Start binds a loopback listener and computes the PKCE verifier + state. It returns a Flow carrying AuthorizationURL — the URL the caller opens in the user's browser.
  2. Wait blocks until the browser is redirected back to the loopback listener, returning the authorization code.
  3. Exchange redeems that code (with the PKCE verifier) at the token endpoint for a TokenSet.

Opening the browser is deliberately the caller's job — this package performs no I/O beyond the loopback listener and the two HTTP calls to the authorization server, mirroring deviceflow (which likewise leaves browser-opening to the embedder).

The client is provider-agnostic: every server-specific value (endpoint paths, client_id, optional scope) is configured at construction time. There is no provider detection.

Index

Constants

View Source
const DefaultCallbackTimeout = 5 * time.Minute

DefaultCallbackTimeout bounds how long Wait blocks for the browser redirect before giving up. Long enough for a user to complete an SSO hop (including MFA), short enough that an abandoned login doesn't park the listener forever.

View Source
const DefaultRequestTimeout = 30 * time.Second

DefaultRequestTimeout caps the token-exchange HTTP round-trip. Set conservatively: healthy token endpoints respond in sub-seconds, so the cap mainly defends against a slow-loris response dribbling bytes.

Variables

View Source
var (
	// ErrAccessDenied — the user declined consent (the authorization
	// server redirected back with error=access_denied), or the token
	// endpoint rejected the exchange with the same code. Terminal.
	ErrAccessDenied = errors.New("access_denied")

	// ErrInvalidGrant — the token endpoint rejected the authorization
	// code (already redeemed, expired, or PKCE verifier mismatch).
	// Terminal.
	ErrInvalidGrant = errors.New("invalid_grant")

	// ErrMissingCode — the authorization server redirected to the
	// callback with neither a code nor an error parameter. Terminal.
	ErrMissingCode = errors.New("authorization callback returned no code")

	// ErrListenerClosed — the loopback listener stopped before any
	// callback arrived (e.g. Close was called concurrently). Terminal.
	ErrListenerClosed = errors.New("loopback listener closed before callback arrived")

	// ErrAuthorizeQuery — AuthorizePath carries query parameters. The client
	// owns the authorization request's query string (response_type, client_id,
	// redirect_uri, the PKCE challenge, state, scope) and sets it wholesale, so
	// a query on the configured path would be silently discarded. Rather than
	// drop it — and issue a request missing whatever the caller intended
	// (audience, resource, access_type, a tenant hint) — Start fails loud.
	ErrAuthorizeQuery = errors.New("AuthorizePath must not carry query parameters")
)

Sentinel errors returned by the flow. Callers branch on these with errors.Is to distinguish user action from transport failure.

View Source
var ErrAbsolutePath = oauthhttp.ErrAbsolutePath

ErrAbsolutePath is re-exported from internal/oauthhttp. See deviceflow's identically-named sentinel: an absolute AuthorizePath/TokenPath would let configuration redirect the user's bearer to an attacker.

View Source
var ErrInsecureBaseURL = oauthhttp.ErrInsecureBaseURL

ErrInsecureBaseURL is re-exported from internal/oauthhttp so callers can errors.Is(err, authcode.ErrInsecureBaseURL) regardless of which package raised it. The token endpoint returns the user's freshly-minted access token in the response body and must be TLS-protected end to end.

Functions

func SetNowForTest

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 expiry read in Exchange. Per-Client (not package-global) so t.Parallel tests with independent Clients don't race each other — the same hazard the deviceflow v0.2.0 review surfaced.

Types

type Client

type Client struct {
	// Transport supplies the http.RoundTripper used for the token-exchange
	// call. nil → http.DefaultTransport. As in deviceflow, this hook is for
	// observability and per-environment proxies, not TLS-verification
	// bypass; the library builds its own *http.Client around it.
	Transport http.RoundTripper

	BaseURL       string
	ClientID      string
	Scope         string
	UserAgent     string
	AuthorizePath string
	TokenPath     string

	// RequestTimeout is the per-request deadline for the token exchange,
	// applied via context.WithTimeout on top of the caller's context. Zero
	// falls back to DefaultRequestTimeout; negative disables the cap.
	RequestTimeout time.Duration

	// CallbackTimeout bounds how long Wait blocks for the browser redirect.
	// Zero falls back to DefaultCallbackTimeout; negative disables the cap
	// (Wait then relies solely on the caller's context).
	CallbackTimeout time.Duration

	// AllowInsecureHTTP permits http:// BaseURLs, restricted to loopback
	// hosts. Production callers MUST leave this false; only tests and local
	// development pinned to loopback should flip it. Note this governs the
	// authorization-server BaseURL, not the loopback redirect — the
	// redirect is always http://127.0.0.1, which RFC 8252 §8.3 permits
	// precisely because loopback traffic never leaves the machine.
	AllowInsecureHTTP bool
	// contains filtered or unexported fields
}

Client performs the RFC 8252 loopback authorization-code flow.

All configuration is explicit; the package has no global state and no implicit URLs. Provide BaseURL, ClientID, and the two endpoint paths; the rest is RFC 8252 / RFC 7636 mechanics.

func New

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

New validates a Client's required fields at construction time rather than at Start/Exchange. Returns an error if BaseURL, ClientID, AuthorizePath, or TokenPath is empty.

Takes a *Client (rather than a value) because the struct embeds an atomic.Pointer for the test-clock seam, which can't be copied. Returns the same pointer on success. Field-bag construction is still supported, but New makes misconfiguration a startup error rather than a runtime one.

func (*Client) Start

func (c *Client) Start(ctx context.Context) (*Flow, error)

Start computes PKCE + state, binds a loopback listener, starts serving the callback, and builds the authorization URL. The returned Flow's AuthorizationURL should be opened in the user's browser.

The provided context governs only the listener bind; the listener and callback server outlive ctx and are torn down by Wait or Close. Pass the browser-wait deadline to Wait, not here.

type Flow

type Flow struct {
	// AuthorizationURL is the URL the caller opens in the user's browser
	// to begin consent.
	AuthorizationURL string

	// RedirectURI is the loopback callback the authorization server
	// redirects to. Exposed mainly for diagnostics/logging; the value is
	// also re-sent on the token exchange (RFC 6749 §4.1.3 requires it to
	// match the authorize request).
	RedirectURI string
	// contains filtered or unexported fields
}

Flow is one in-progress authorization-code login. Start returns it with AuthorizationURL populated; the caller opens that URL, then calls Wait followed by Exchange. The Flow owns a live loopback listener until Wait returns or Close is called — callers MUST call one of the two to avoid leaking the listener.

func (*Flow) Close

func (f *Flow) Close() error

Close shuts down the loopback listener. Safe to call multiple times and safe to call without Wait (e.g. when the caller aborts after Start). Wait calls Close on return, so most callers never invoke it directly.

func (*Flow) Exchange

func (f *Flow) Exchange(ctx context.Context, code string) (*tokens.TokenSet, error)

Exchange redeems code at the token endpoint using the PKCE verifier and redirect URI captured in this Flow (RFC 6749 §4.1.3 + RFC 7636 §4.5).

On success it returns a TokenSet with absolute expiry derived from the server's expires_in. On a recognised OAuth error it returns the matching sentinel (ErrAccessDenied, ErrInvalidGrant); other failures (network, malformed responses) are wrapped with context.

func (*Flow) GoString

func (f *Flow) GoString() string

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

func (*Flow) String

func (f *Flow) String() string

String redacts the PKCE verifier and CSRF state. Both are live secrets during the auth window: the verifier redeems the authorization code, and state is the only gate on which callback this Flow accepts. Without this, a stray fmt.Printf("%+v", flow) in caller code would dump them to logs — the same hazard tokens.TokenSet, deviceflow.DeviceCode, and sts.ExchangeRequest guard against. AuthorizationURL carries the same state in its query by construction, so it's shown with that one parameter scrubbed (see redactedAuthorizationURL); otherwise fmt would be a second, silent path to the bare secret. RedirectURI holds no secret and is shown verbatim.

func (*Flow) Wait

func (f *Flow) Wait(ctx context.Context) (code string, err error)

Wait blocks until the browser is redirected to the loopback callback, the callback timeout elapses, or ctx is cancelled. It returns the authorization code on success. The loopback listener is shut down before Wait returns, so the Flow is single-use.

type TestingTB

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

TestingTB is the subset of testing.TB used by SetNowForTest. Minimal so production builds never import "testing"; the Cleanup method is the signal that misusing the seam requires manufacturing a fake t.

Jump to

Keyboard shortcuts

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