device

package module
v0.0.0-...-3c5567c Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 14 Imported by: 0

README

Device authorization

Optional RFC 8628 device authorization composed with the actual oauthserver.Service and its native PostgreSQL owner. The child contains no client registry, identity/session engine, consent authority or token store. Base OAuth construction does not require this module or device migrations.

Quick start

Given an already constructed core service and native store, select the optional native owner before constructing the child:

native, err := store.DeviceStore(oauthpostgres.DeviceConfig{
    Policy: config, LookupKeys: lookupKeys, ActiveLookupKeyVersion: activeVersion,
})
if err != nil {
    return err
}
flow, err := device.NewService(core, config, native)

core is the existing *oauthserver.Service, not a replacement authority. config is the immutable device.Config; lookupKeys are dedicated opaque root-owned digest keys. The full executable native composition and genuine authenticated journey are in postgres/device_flow_integration_test.go. The disabled construction example compiles without either owner.

Selected profile and adoption

The first profile issues unbound device requests to explicitly registered public (none) or confidential (client_secret_basic) clients with Client.DeviceAuthorization=true. Zero preserves the existing deny default and legacy registration fingerprints. Managed creation additionally requires the configured ManagedClientPolicy.DeviceAuthorization ceiling. Redirect registration remains mandatory for this selected client-registration profile; redirect possession is never used as device-grant proof. Changing a static registration's permission requires a new client version and fresh approval; never reuse an earlier version to restore withdrawn authority.

Compose the existing root service, then select the native store's optional DeviceStore(DeviceConfig) factory and pass it to device.NewService. The immutable Config fixes tenant, same-issuer HTTPS verification, authorization and token endpoints, expiry, initial polling interval, complete verification-URI byte bound and user-code lookup limit. No proxy or request parameter can override those authorities. Retain lookup-key versions while their live or recoverable rows remain. Rolling lookup-key configurations must retain overlapping original keys. Collision checks use every retained namespace and at most eight globally ordered transaction advisory locks, independently of the active version and native caller-intent key. Disjoint retained sets cannot interoperate and are unsupported while original requests remain live/recoverable. A collision rejects that issuance without replacing the original request; a new issuance generates new codes. Changing the frozen device policy invalidates existing requests rather than silently renewing or reinterpreting them.

Install the existing shared rate-limit migration and append native DeviceMigrationSource after the ordinary OAuth/consent history before enabling writers. This first forward history does not support existing workload or silent-OIDC artifact histories. Stop writers, install the forward history, replace unaware writers, then activate the selected capability. Do not roll back to unaware writers after activation. No constructor migrates or starts a background worker. See the native adapter.

API

  • ParseDeviceRequest and ParseTokenExchange bound form input to 16 KiB, reject duplicated/conflicting credential channels and preserve only the RFC-defined issuance or extension-grant request.
  • Authorize authenticates the actual current registered client and returns reveal-once device custody only after fresh known commitment. Device codes contain 32 random bytes in canonical 43-character base64url. User codes use eight nonconfusable characters, displayed as XXXX-XXXX.
  • Inspect requires a genuine recent verified password session and returns safe client, scopes, resource, displayed code and original expiry. It cannot approve, issue consent or create a domain command.
  • Approve additionally requires explicit matching ConfirmedUserCode, exact issuer origin and the real session owner's CSRF token. Deny uses the same current session/origin/CSRF admission.
  • Poll preserves committed interval metadata alongside typed pending, slow-down, denied and expired errors. Only the sole fresh atomic winner receives actual root-owned access/refresh tokens.
  • Cancel permanently withdraws the request using the same client/device custody. Canceling a Go context aborts that call; it does not withdraw the durable request.
  • Recover accepts original command correlation and appropriate current client or reviewing-session custody. It delegates metadata-only reconciliation to the existing sole coordinator; it never reconstructs a lost credential reply.

Polling and security

RFC 8628 section 3.5 mandates a five-second interval increase for every slow_down. The selected server policy starts at 5–60 seconds. An early poll that would exceed the 60-second policy bound permanently cancels the request with access_denied; it never returns a clamped slow_down interval. Clients must stop on terminal errors and respect the last returned interval.

The complete verification URI contains only the user code, never the device bearer. Per RFC 8628 section 5.4, opening that URI is not proof of the user's intent: the displayed code still needs explicit confirmation. Codes are stored only as independently versioned issuer/environment/tenant/endpoint-bound keyed digests. Ordinary formatting and serialization of credential-bearing values are redacted or rejected. Reveal credentials only at the intended transport boundary; never log them.

Native mutations retain the actual client actor separately from the approved human's original session provenance. Current client version, directory, credential, authorization and consent authority are fenced by their existing owners. Withdrawal cannot be repaired by replaying an old approval. Required audit and lifecycle effects share the same transaction as device/grant state.

Boundaries and FAQ

Does this provide a device UI or HTTP routes? No. A transport must render the safe prompt and implement its own explicit interaction, authenticated session and CSRF channel. Native capability selection alone is not discovery or HTTP registration.

Are pre-bound identities, OIDC claims, workload clients or MFA downgrade supported? No. This is the selected unbound, recent verified-password-human profile. A pre-bound identity profile remains a separate required profile, not an approximation with a caller-supplied user. Existing MFA sources are never downgraded into password-only admission.

Can I blindly retry an unknown response? No credential replay is provided. Recover the original command's metadata. For lost issuance, start a new request after resolving that original command; the inaccessible old request expires.

This profile does not by itself qualify the complete device milestone. HTTP, reference composition and other selected identity profiles need their own whole-profile execution evidence.

Specification: RFC 8628, August 2019.

Documentation

Overview

Package device owns the optional RFC8628 device authorization protocol. It composes the actual OAuth Service and native execution owner; it owns no replacement authentication, consent, grant, token or transaction engine.

Index

Examples

Constants

View Source
const GrantType = "urn:ietf:params:oauth:grant-type:device_code"

GrantType is RFC8628 section3.4's exact extension-grant discriminator.

Variables

This section is empty.

Functions

This section is empty.

Types

type Approval

type Approval struct {
	Verification
	CSRF                      session.CSRFToken
	Origin, ConfirmedUserCode string
}

Approval requires an explicit user action comparing the displayed canonical code. A complete verification URI never supplies this confirmation or CSRF.

func (Approval) Format

func (Approval) Format(f fmt.State, v rune)

func (Approval) MarshalJSON

func (Approval) MarshalJSON() ([]byte, error)

func (Approval) MarshalText

func (Approval) MarshalText() ([]byte, error)

type Authorization

type Authorization struct {
	Completion                                         oauth.Completion
	DeviceCode                                         DeviceCode
	UserCode, VerificationURI, VerificationURIComplete string
	ExpiresAt                                          time.Time
	Interval                                           time.Duration
}

Authorization contains fresh known-committed reveal-once device custody and bounded UI data. Completion alone never establishes response delivery.

func (Authorization) Format

func (Authorization) Format(f fmt.State, v rune)

func (Authorization) MarshalJSON

func (Authorization) MarshalJSON() ([]byte, error)

func (Authorization) MarshalText

func (Authorization) MarshalText() ([]byte, error)

type Config

type Config = oauth.DevicePolicy

Config is the child's immutable policy projection shared with its native owner. Enabled=false permits dependency-free disabled construction. Enabled policy requires trusted tenant and same-issuer exact HTTPS endpoint locations, TTL1..15minutes, initial interval5..60seconds, complete-URI bound128..2048 and lookup limit1..100/hour. No request/proxy header may replace these values.

type Denial

type Denial struct {
	Verification
	CSRF   session.CSRFToken
	Origin string
}

Denial requires the same genuine fresh session/origin/CSRF admission. Repeated denial is idempotent without disclosing a previous reviewing subject.

func (Denial) Format

func (Denial) Format(f fmt.State, v rune)

func (Denial) MarshalJSON

func (Denial) MarshalJSON() ([]byte, error)

func (Denial) MarshalText

func (Denial) MarshalText() ([]byte, error)

type DeviceCode

type DeviceCode = oauth.Token

DeviceCode retains the root owner's canonical32byte reveal-once secret custody. Parsing or copying one never creates durable grant authority.

type DeviceRequest

type DeviceRequest struct {
	ClientID, Resource, Network string
	Scopes                      []string
	ClientAuthentication        oauth.ClientAuthentication
}

DeviceRequest supplies no subject/tenant/consent selectors. Nil Scopes selects the current registered OAuth-only ceiling; client credentials remain borrowed.

func ParseDeviceRequest

func ParseDeviceRequest(raw string, authorization []string) (DeviceRequest, error)

ParseDeviceRequest parses bounded RFC8628 section3.1 public-none or exclusive confidential-Basic custody. Unknown parameters are ignored; duplicates and conflicting credential channels deny. Trusted Network is never form input.

func (DeviceRequest) Format

func (DeviceRequest) Format(f fmt.State, v rune)

func (DeviceRequest) MarshalJSON

func (DeviceRequest) MarshalJSON() ([]byte, error)

func (DeviceRequest) MarshalText

func (DeviceRequest) MarshalText() ([]byte, error)

type Error

type Error string

Error is a stable redacted protocol condition, suitable for errors.Is and explicit OAuth transport mapping. It contains no submitted diagnostic values.

const (
	ErrInvalidRequest       Error = "invalid_request"
	ErrAuthorizationPending Error = "authorization_pending"
	ErrSlowDown             Error = "slow_down"
	ErrAccessDenied         Error = "access_denied"
	ErrExpiredToken         Error = "expired_token"
	ErrUnavailable          Error = "temporarily_unavailable"
)

func (Error) Error

func (e Error) Error() string

type Metadata

type Metadata struct{ DeviceAuthorizationEndpoint, TokenEndpoint, GrantType string }

Metadata advertises only a completely selected optional device profile. It is not base OAuth discovery or HTTP route registration.

type PollPolicy

type PollPolicy struct{ Initial, SlowDown, Maximum time.Duration }

PollPolicy describes server-owned polling policy. SlowDown is fixed by RFC8628 section3.5, not configuration; every early poll adds exactly5seconds.

type PollResult

type PollResult struct {
	Completion oauth.Completion
	Interval   time.Duration
	Tokens     oauth.TokenResult
}

PollResult preserves committed polling interval/durability even for pending and slow_down errors. Tokens exist only for the sole fresh atomic winner.

func (PollResult) Format

func (PollResult) Format(f fmt.State, v rune)

func (PollResult) MarshalJSON

func (PollResult) MarshalJSON() ([]byte, error)

func (PollResult) MarshalText

func (PollResult) MarshalText() ([]byte, error)

type Recovery

type Recovery struct {
	OperationID                    string
	CommandID                      identity.CommandID
	ClientID, DeviceCode, UserCode string
	ClientAuthentication           oauth.ClientAuthentication
	Session                        auth.BearerCredential
}

Recovery carries only original-command correlation plus the appropriate current client or human custody. No user/tenant/subject override is accepted.

func (Recovery) Format

func (Recovery) Format(f fmt.State, v rune)

func (Recovery) MarshalJSON

func (Recovery) MarshalJSON() ([]byte, error)

func (Recovery) MarshalText

func (Recovery) MarshalText() ([]byte, error)

type Service

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

Service is immutable, concurrent-safe and borrows all owner dependencies. It owns no pool, goroutine, background recovery loop or resource to Close.

func NewService

func NewService(core *oauth.Service, c Config, store Store) (*Service, error)

NewService freezes exact optional composition without IO or generation. Disabled construction exposes no capability or metadata and needs no owners. The selected profile supports unbound issuance and recent verified password humans only; pre-binding, OIDC claims, workload/device UI are not approximated.

Example (Disabled)
package main

import (
	"fmt"

	device "github.com/faustbrian/go-oauth-server/device"
)

func main() {
	service, err := device.NewService(nil, device.Config{}, nil)
	if err != nil {
		return
	}
	fmt.Println(service.Enabled(), service.Metadata().GrantType == "")
}
Output:
false true

func (*Service) Approve

func (s *Service) Approve(ctx context.Context, r Approval) (oauth.Completion, error)

Approve binds actual reviewing subject/current consent atomically only after explicit canonical-code comparison and native current session/origin/CSRF fences.

func (*Service) Authorize

func (s *Service) Authorize(ctx context.Context, r DeviceRequest) (out Authorization, err error)

Authorize authenticates an explicitly permitted current registration before native issuance. Core/native retain current policy/transaction authority; the device cannot select a subject. Codes are delivered only on known commitment.

func (*Service) Cancel

func (s *Service) Cancel(ctx context.Context, r TokenExchange) (oauth.Completion, error)

Cancel is an authenticated explicit client extension. It permanently withdraws this request; cancelling a Go context alone does not imply durable withdrawal.

func (*Service) Deny

func (s *Service) Deny(ctx context.Context, r Denial) (oauth.Completion, error)

Deny permanently wins over later approval/exchange and remains idempotent.

func (*Service) Enabled

func (s *Service) Enabled() bool

Enabled reports complete selected composition, not deployment/readiness.

func (*Service) Format

func (*Service) Format(f fmt.State, v rune)

func (*Service) Inspect

func (s *Service) Inspect(ctx context.Context, r Verification) (out oauth.DevicePrompt, err error)

Inspect returns only a current safe prompt. It never approves, creates a command or emits a domain audit/lifecycle; native rate admission precedes lookup.

func (*Service) Metadata

func (s *Service) Metadata() Metadata

Metadata returns no endpoints/grant when disabled.

func (*Service) Poll

func (s *Service) Poll(ctx context.Context, r TokenExchange) (PollResult, error)

Poll implements RFC8628 pending/slow_down/denied/expired and one token winner.

func (*Service) PollPolicy

func (s *Service) PollPolicy() PollPolicy

PollPolicy returns the selected bounds, not a mutable current device interval.

func (*Service) Recover

func (s *Service) Recover(ctx context.Context, r Recovery) (oauth.Completion, error)

Recover delegates metadata-only reconciliation to the sole native coordinator.

type Store

type Store = oauth.DeviceStore

Store is the actual native execution interface. Base OAuth does not require it. Applications compose the owned native factory, not private domain stores.

type TokenExchange

type TokenExchange struct {
	ClientID, DeviceCode, Network string
	ClientAuthentication          oauth.ClientAuthentication
}

TokenExchange borrows exact client and device-code custody. It permits no subject/scope/resource override. Cancel uses this same authenticated custody to permanently withdraw the device request; context cancellation is distinct.

func ParseTokenExchange

func ParseTokenExchange(raw string, authorization []string) (TokenExchange, error)

ParseTokenExchange admits only the exact device grant and canonical bearer. Scope is not an allowed polling override. Parsing grants no client/device authority; the native owner independently fences both on every request.

func (TokenExchange) Format

func (TokenExchange) Format(f fmt.State, v rune)

func (TokenExchange) MarshalJSON

func (TokenExchange) MarshalJSON() ([]byte, error)

func (TokenExchange) MarshalText

func (TokenExchange) MarshalText() ([]byte, error)

type UserCode

type UserCode string

UserCode is the fixed reference nonconfusable8-character code, displayed4-4. It is not an authenticated actor, an OAuth bearer or an approval instruction.

func ParseUserCode

func ParseUserCode(wire string) (UserCode, error)

ParseUserCode accepts only8ASCII alphabet characters with optional middle hyphen. ASCII case folding is explicit; whitespace/Unicode/confusables deny. Missing, malformed, expired and foreign codes share the same public denial.

type Verification

type Verification struct {
	Session           auth.BearerCredential
	UserCode, Network string
}

Verification borrows a genuine recent browser session and submitted user code. Tenant is fixed at construction. Network is trusted transport attribution.

func (Verification) Format

func (Verification) Format(f fmt.State, v rune)

func (Verification) MarshalJSON

func (Verification) MarshalJSON() ([]byte, error)

func (Verification) MarshalText

func (Verification) MarshalText() ([]byte, error)

Jump to

Keyboard shortcuts

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