device

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package device implements RFC 8628's OAuth 2.0 Device Authorization Grant: the "visit this URL and enter this code" flow used by headless or browser-less CLIs (no local browser or listening port assumed — see https://datatracker.ietf.org/doc/html/rfc8628).

This package deliberately does not mint a credential itself. A successful PollDeviceToken tells the caller which user approved the request; the host application decides what to hand the CLI in response — a authit/user session, a authit/pat token, or its own credential shape. This keeps device flow reusable regardless of what kind of token a given host application issues.

Rate-limiting guesses against the (short, low-entropy-by-design) user_code at the approval endpoint is the host application's responsibility (RFC 8628 §5.2) — typically ordinary HTTP-layer rate-limiting middleware in front of ApproveDeviceAuthorization/ DenyDeviceAuthorization.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrAuthorizationPending = errors.New("authit/device: authorization pending")
	ErrSlowDown             = errors.New("authit/device: polling too frequently")
	ErrAccessDenied         = errors.New("authit/device: access denied")
	ErrExpiredToken         = errors.New("authit/device: device code expired or invalid")
	ErrInvalidUserCode      = errors.New("authit/device: invalid, already-used, or expired user code")
)

These map directly to RFC 8628 §3.5's token-endpoint error codes; a host application's HTTP layer translates them to the matching "error" field (authorization_pending, slow_down, access_denied, expired_token).

View Source
var ErrRateLimited = ratelimit.ErrRateLimited

ErrRateLimited is returned when Config.RateLimiter refuses a user-code lookup or an approval. Alias for ratelimit.ErrRateLimited.

It is deliberately distinct from ErrInvalidUserCode: a host must not report it as "wrong code", because the caller's code may well have been right. RFC 8628 has no error code for this at the verification endpoint, which is a UI surface rather than a token endpoint — 429 with a try-again message is the sensible rendering.

Functions

This section is empty.

Types

type Authorization

type Authorization struct {
	DeviceCode string
	UserCode   string
	ExpiresIn  time.Duration
	Interval   time.Duration
}

Authorization is what StartDeviceAuthorization hands back to the CLI. Building the full verification_uri / verification_uri_complete is left to the host application, which is the only party that knows its own domain and routing.

type Config

type Config struct {
	// DeviceCodeTTL is how long a device authorization request stays valid
	// before the user must restart it. Defaults to 15 minutes (matches
	// GitHub's device flow).
	DeviceCodeTTL time.Duration
	// PollInterval is the minimum gap the CLI is told to leave between
	// polls. Defaults to 5 seconds.
	PollInterval time.Duration
	// SlowDownIncrement is added to a device authorization's effective
	// interval, permanently, the first time the CLI is caught polling
	// faster than instructed. Defaults to 5 seconds, per RFC 8628 §3.5.
	SlowDownIncrement time.Duration
	// AuditLogger receives security-relevant events (approval, denial).
	// Nil means events are not recorded — see package audit.
	AuditLogger audit.Logger
	// RateLimiter bounds guessing at user codes. Nil means ratelimit.Noop.
	//
	// Setting this is not optional in the way the other nil-safe fields
	// are. A user code carries about 34.5 bits (crypto.GenerateUserCode),
	// which is deliberately low so a human can read it off one screen and
	// type it into another — RFC 8628 §5.2 is explicit that the security
	// of that choice rests on rate-limiting guesses at the verification
	// endpoint. With no limiter, an attacker who can call
	// ApproveDeviceAuthorization or DenyDeviceAuthorization in a loop can
	// search for a pending code.
	//
	// Keys:
	//
	//	device:approve:<caller user id>  per authenticated caller
	//	device:user-code:failures        global, charged on a failed lookup
	//
	// The global key is charged only when a lookup FAILS, so ordinary use
	// never consumes it and an enumeration sweep exhausts it almost
	// immediately. The trade is real and worth stating: an attacker who
	// burns that budget also stops legitimate users from entering a code
	// until it refills. Size Burst for your traffic — a nuisance outage on
	// one flow is the better end of this trade against an exhaustible
	// 34.5-bit space, but it is a trade.
	RateLimiter ratelimit.Limiter
}

Config tunes the device package's flows.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service implements the device-authorization-grant flow.

func NewService

func NewService(stores Stores, cfg Config) (*Service, error)

NewService constructs a Service. Config.AuditLogger may be nil, in which case audit.NoopLogger is used.

func (*Service) ApproveDeviceAuthorization

func (s *Service) ApproveDeviceAuthorization(ctx context.Context, callerUserID, userCode string) error

ApproveDeviceAuthorization marks the device authorization identified by userCode as approved by callerUserID. Call this from an already-authenticated web session after the user types/confirms the code — device does not itself authenticate callerUserID.

func (*Service) DenyDeviceAuthorization

func (s *Service) DenyDeviceAuthorization(ctx context.Context, userCode string) error

DenyDeviceAuthorization marks the device authorization identified by userCode as denied, so the CLI's poll terminates with ErrAccessDenied.

func (*Service) PollDeviceToken

func (s *Service) PollDeviceToken(ctx context.Context, rawDeviceCode string) (userID, scope string, err error)

PollDeviceToken is what the CLI calls repeatedly with the device_code it received from StartDeviceAuthorization. On success it returns the ID of the user who approved the request (and the scope that was requested) — it is then the host application's job to mint whatever credential it wants to hand the CLI. Once a device authorization resolves to a terminal outcome (approved or denied), the record is deleted so it cannot be polled again.

func (*Service) StartDeviceAuthorization

func (s *Service) StartDeviceAuthorization(ctx context.Context, clientID, scope string) (Authorization, error)

StartDeviceAuthorization begins a new device-authorization-grant request for the given clientID/scope (both host-application-defined and optional — pass "" for either if the host doesn't distinguish clients or scopes).

type Stores

type Stores struct {
	Authorizations store.DeviceAuthorizationStore
}

Stores groups the persistence ports the device package needs.

Jump to

Keyboard shortcuts

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