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 ¶
- Variables
- type Authorization
- type Config
- type Service
- func (s *Service) ApproveDeviceAuthorization(ctx context.Context, callerUserID, userCode string) error
- func (s *Service) DenyDeviceAuthorization(ctx context.Context, userCode string) error
- func (s *Service) PollDeviceToken(ctx context.Context, rawDeviceCode string) (userID, scope string, err error)
- func (s *Service) StartDeviceAuthorization(ctx context.Context, clientID, scope string) (Authorization, error)
- type Stores
Constants ¶
This section is empty.
Variables ¶
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).
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 ¶
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 ¶
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.