devicecode

package
v0.22.154 Latest Latest
Warning

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

Go to latest
Published: May 2, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package devicecode is clawtool's issuer-agnostic OAuth 2.0 Device Authorization Grant (RFC 8628) helper. ADR-036 Phase 1 promotes this out of the GitHub-specific shim so:

  • The onboard wizard's auth-prompt phase (DeviceCodeStep) and the existing star-on-GitHub flow share one poll loop instead of each carrying their own copy of the slow_down / authorization_pending / expired_token state machine.
  • Future issuers (per-source OAuth, hosted-clawtool sign-in) drop in by pointing Config at their device + token endpoints and providing the form fields they want submitted.

Token storage is the caller's job (internal/secrets is the only blessed sink); the poller stays stateless so tests can drive it with httptest fixtures.

Index

Constants

View Source
const MinPollInterval = 5 * time.Second

MinPollInterval is the floor RFC 8628 §3.5 recommends and that every major issuer (GitHub, Google, Auth0) enforces. Servers commonly return 5; we never go below it even when the response quotes a smaller number.

Variables

View Source
var ErrAuthorizationDenied = errors.New("devicecode: authorization denied by user")

ErrAuthorizationDenied is returned when the issuer reports access_denied (RFC 8628 §3.5).

View Source
var ErrDeviceCodeExpired = errors.New("devicecode: device code expired before authorisation")

ErrDeviceCodeExpired is returned when the device code's lifetime ran out before the user authorised. Callers typically restart the flow with a fresh code.

View Source
var ErrNoClientID = errors.New("devicecode: client_id is not configured")

ErrNoClientID is returned when Config.ClientID is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// HTTP is the client used for both the device-code request
	// and the token poll. Defaults to a 30s-timeout client.
	HTTP *http.Client
	// DeviceEndpoint is the absolute URL of the issuer's
	// /device/code (or equivalent) endpoint.
	DeviceEndpoint string
	// TokenEndpoint is the absolute URL of the issuer's
	// /token endpoint. Polled with grant_type=device_code.
	TokenEndpoint string
	// ClientID is the OAuth client_id. Public-by-design under
	// device flow; no client_secret is used.
	ClientID string
	// Scopes is the space-separated scope list submitted on the
	// initial device-code request.
	Scopes string
	// UserAgent is the User-Agent header set on every request.
	// Defaults to "clawtool-devicecode/1".
	UserAgent string
	// ExtraDeviceFields lets callers add issuer-specific form
	// fields to the device-code POST (e.g. "audience" for Auth0).
	ExtraDeviceFields url.Values
	// ExtraTokenFields is the same escape hatch for the token
	// poll (rare, but Auth0 + Okta sometimes want it).
	ExtraTokenFields url.Values
	// MinInterval, when non-zero, acts as an absolute override
	// on the poll cadence — replaces both the issuer's
	// suggested interval and the RFC 8628 §3.5 5-second floor.
	// Test-only knob: production callers leave this zero so
	// the spec floor takes effect (and the issuer's value, if
	// higher, wins). Setting it negative is treated the same
	// as zero.
	MinInterval time.Duration
}

Config wires the poller to a specific issuer + client.

type DeviceCode

type DeviceCode struct {
	DeviceCode      string `json:"device_code"`
	UserCode        string `json:"user_code"`
	VerificationURI string `json:"verification_uri"`
	// VerificationURIComplete is the optional pre-filled URL
	// (verification_uri + user_code embedded) some issuers
	// return; preferred for browser-launch when present.
	VerificationURIComplete string        `json:"verification_uri_complete,omitempty"`
	ExpiresIn               int           `json:"expires_in"` // seconds
	Interval                int           `json:"interval"`   // server-recommended poll cadence
	Expires                 time.Time     `json:"-"`          // computed
	PollEvery               time.Duration `json:"-"`          // computed
}

DeviceCode is the response envelope from the device authorization endpoint (RFC 8628 §3.2). The wizard / CLI shows VerificationURI + UserCode to the operator and polls the token endpoint with DeviceCode until the user authorises or the code expires.

func RequestDeviceCode

func RequestDeviceCode(ctx context.Context, cfg Config) (*DeviceCode, error)

RequestDeviceCode performs the initial RFC 8628 §3.1 POST to the device-code endpoint and returns the parsed envelope with computed Expires + PollEvery.

type Token

type Token struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	Scope        string `json:"scope"`
	RefreshToken string `json:"refresh_token,omitempty"`
	ExpiresIn    int    `json:"expires_in,omitempty"`
}

Token is the successful poll response.

func Poll

func Poll(ctx context.Context, cfg Config, dc *DeviceCode) (*Token, error)

Poll runs the RFC 8628 §3.4–§3.5 token poll until the user authorises (returns Token), the issuer denies it (returns ErrAuthorizationDenied), the device code expires (returns ErrDeviceCodeExpired), or ctx is cancelled.

authorization_pending → continue at the current interval. slow_down → bump the interval per the issuer's value (or +5s). expired_token → ErrDeviceCodeExpired. access_denied → ErrAuthorizationDenied.

Tests inject a fake clock by passing an *http.Client whose transport returns canned responses; the polling cadence is driven off PollEvery, which the test can shrink to milliseconds.

Jump to

Keyboard shortcuts

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