deviceflow

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 deviceflow is an RFC 8628 OAuth 2.0 Device Authorization Grant client.

Construct a Client with the issuer's BaseURL plus the paths and client_id it expects, then call StartDeviceAuth followed by repeated PollDeviceAuth calls until either a TokenSet comes back or a terminal error is returned. Caller drives the polling loop and adjusts the interval on ErrSlowDown per RFC 8628 §3.5.

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 DefaultRequestTimeout = 30 * time.Second

DefaultRequestTimeout caps a single device-flow HTTP round-trip (StartDeviceAuth or one PollDeviceAuth call). Set conservatively: healthy device-flow endpoints respond in sub-seconds, so the cap mainly defends against slow-loris responses dripping bytes within MaxResponseBytes — see Client.RequestTimeout for the per-Client override. The polling-loop interval is the caller's concern; this timeout governs only the individual HTTP request.

Variables

View Source
var (
	// ErrAuthorizationPending — user has not yet approved or denied.
	// Caller polls again at the existing interval.
	ErrAuthorizationPending = errors.New("authorization_pending")

	// ErrSlowDown — caller is polling too fast. Caller bumps the
	// interval (per RFC 8628 §3.5, by at least 5 seconds) and tries
	// again.
	ErrSlowDown = errors.New("slow_down")

	// ErrAccessDenied — user denied the request. Terminal.
	ErrAccessDenied = errors.New("access_denied")

	// ErrExpiredToken — device code expired before the user approved.
	// Terminal; restart with a fresh StartDeviceAuth.
	ErrExpiredToken = errors.New("expired_token")

	// ErrInvalidGrant — device code already redeemed, malformed, or
	// otherwise rejected. Terminal.
	ErrInvalidGrant = errors.New("invalid_grant")
)

Sentinel errors returned by PollDeviceAuth when the token endpoint responds with a recognised RFC 8628 §3.5 error code. Callers branch on these with errors.Is and adjust their polling loop accordingly.

View Source
var ErrAbsolutePath = oauthhttp.ErrAbsolutePath

ErrAbsolutePath is returned when DeviceCodePath or TokenPath is an absolute or scheme-relative URL rather than a path relative to BaseURL. Go's url.ResolveReference *replaces* the base when handed an absolute reference, so accepting an absolute path would let any caller who can influence the configuration (env var, config file, server-discovery doc) redirect the device-code or token request to an attacker — and in the token-endpoint case, capture the user's access token. Re-exported from internal/oauthhttp.

View Source
var ErrInsecureBaseURL = oauthhttp.ErrInsecureBaseURL

ErrInsecureBaseURL is returned when device-flow requests are made against an http:// BaseURL without AllowInsecureHTTP set. The token endpoint returns the user's access token in the response body — over plain HTTP that's a credential in the clear. Re-exported from internal/oauthhttp so callers can errors.Is(err, deviceflow.ErrInsecureBaseURL) regardless of which package raised it.

View Source
var ErrUnsafeVerificationURI = errors.New("unsafe verification_uri")

ErrUnsafeVerificationURI is returned when the authorization server returns a verification_uri that fails minimum-trust checks. Defense against a compromised or misconfigured AS pointing users at a phishing page: the URL we'd otherwise echo to the user and open in their browser carries the user code, so a wrong destination is a direct credential-harvesting vector.

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 PollDeviceAuth / PollUntil.

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 a misconfigured TLS bypass (the prior
	// HTTP *http.Client field made that a one-liner). Custom
	// RoundTrippers that wrap or replace TLS verification remain the
	// caller's responsibility; this hook is for observability
	// (request/response logging) and per-environment proxies, not
	// security bypass.
	Transport http.RoundTripper

	BaseURL        string
	ClientID       string
	Scope          string
	UserAgent      string
	DeviceCodePath string
	TokenPath      string

	// RequestTimeout is the per-request 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 — the device-flow token endpoint returns the user's
	// freshly-minted access token in the response body and must be
	// TLS-protected end to end. Production callers MUST leave this
	// false; only tests and local development pinned to loopback
	// should flip it.
	AllowInsecureHTTP bool
	// contains filtered or unexported fields
}

Client polls an RFC 8628 device authorization grant.

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 8628 mechanics.

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 request call. Returns an error if BaseURL, ClientID, DeviceCodePath, or TokenPath is empty — these would otherwise surface as a confusing "POST to :///oauth/token: ..." or "missing client_id" error from the AS at the worst moment (the user is mid-login).

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 (`&deviceflow.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) PollDeviceAuth

func (c *Client) PollDeviceAuth(ctx context.Context, deviceCode string) (*tokens.TokenSet, error)

PollDeviceAuth exchanges deviceCode for a TokenSet at the token endpoint.

On success, returns a TokenSet with absolute expiry derived from the server's expires_in. On any RFC 8628 §3.5 error code, returns the matching sentinel error from this package. Other failures (network, malformed responses) are wrapped with context.

Most callers want PollUntil instead — it drives the poll loop, honours the interval, applies the RFC 8628 §3.5 +5s slow_down bump, and stops at the device-code's ExpiresIn ceiling. Use PollDeviceAuth directly only when you need to render the per-tick state yourself (e.g. animating a TUI spinner).

func (*Client) PollUntil added in v0.3.1

func (c *Client) PollUntil(ctx context.Context, dc *DeviceCode) (*tokens.TokenSet, error)

PollUntil drives the device-flow poll loop end-to-end. Most embedders want this helper rather than calling PollDeviceAuth manually — it owns the loop discipline that RFC 8628 §5.5 calls out as the difference between a polite client and a DoS source.

Behaviour:

  • Waits dc.Interval (defaulting to 5s, clamped to 1s minimum) between successive poll calls.
  • On ErrSlowDown, bumps the interval by 5s permanently per RFC 8628 §3.5. Subsequent slow_down responses bump again.
  • Stops with the most-recent error when dc.ExpiresIn elapses since PollUntil was called. If the AS omitted expires_in (zero or negative), falls back to defaultExpiresIn so the loop is always bounded — closes a hostile-AS DoS vector parallel to the pollInterval clamp.
  • Returns the TokenSet on success.
  • Returns ctx.Err() (wrapped) when the caller cancels.
  • Returns terminal sentinels (ErrAccessDenied, ErrExpiredToken, ErrInvalidGrant) unwrapped, plus any unknown OAuth error verbatim, so callers can errors.Is.

func (*Client) StartDeviceAuth

func (c *Client) StartDeviceAuth(ctx context.Context) (*DeviceCode, error)

StartDeviceAuth requests a fresh device code from the authorization server. The returned DeviceCode is opaque to the client; pass it back unmodified on every PollDeviceAuth.

type DeviceCode

type DeviceCode struct {
	DeviceCode              string `json:"device_code"`
	UserCode                string `json:"user_code"`
	VerificationURI         string `json:"verification_uri"`
	VerificationURIComplete string `json:"verification_uri_complete"`
	ExpiresIn               int    `json:"expires_in"`
	Interval                int    `json:"interval"`
}

DeviceCode is the response from the device authorization endpoint (RFC 8628 §3.2). Pass DeviceCode through to subsequent PollDeviceAuth calls and show UserCode + VerificationURI to the user.

func (DeviceCode) GoString added in v0.3.1

func (d DeviceCode) GoString() string

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

func (DeviceCode) String added in v0.3.1

func (d DeviceCode) String() string

String redacts DeviceCode (the device-flow secret that a hostile observer could redeem against the token endpoint) and VerificationURIComplete (which embeds the UserCode and is also a credential during the auth window). UserCode itself is intentionally shown to the user, so it's preserved. Without this, a stray `fmt.Printf("%+v", dc)` in caller code would log the device code.

type TestingTB added in v0.3.1

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

TestingTB is the subset of testing.TB used by SetNowForTest. It's minimal so tests can use *testing.T directly without the seam helper having to import "testing" into production builds.

Production code should never construct one of these by hand. The presence of the Cleanup method is the signal: misusing the seam requires manufacturing a fake t.Cleanup, which is awkward enough to trip a reviewer.

Jump to

Keyboard shortcuts

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