deviceauth

package
v1.3.0 Latest Latest
Warning

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

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

README

GoModularAuth Device Authorization Plugin (gomodularauth-device-authorization)

gomodularauth-device-authorization implements the RFC 8628 OAuth 2.0 Device Authorization Grant for GoModularAuth. It enables authentication on internet-connected devices with limited user input capabilities (Smart TVs, CLI tools, media streamers, game consoles, IoT devices) through a secondary browser session.

Installation

go get github.com/BladiCreator/go-modular-auth/plugins/device-authorization

Quick Start

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	auth "github.com/BladiCreator/go-modular-auth"
	deviceauth "github.com/BladiCreator/go-modular-auth/plugins/device-authorization"
	"github.com/BladiCreator/go-modular-auth/plugins/device-authorization/plugins"
	adaptermem "github.com/BladiCreator/go-modular-auth/adapters/memory"
)

func main() {
	ctx := context.Background()

	// 1. Initialize storage adapters

	// 2. Configure GoModularAuth with Device Authorization Plugin
	a, err := auth.New(auth.Config{
		Adapter: adaptermem.NewAdapter(),
		Plugins: []auth.PluginModule{
			deviceauth.New(
				deviceauth.WithVerificationURI("https://auth.example.com/device"),
				deviceauth.WithExpiresIn(10*time.Minute),
				deviceauth.WithPollingInterval(5*time.Second),
			),
		},
	})
	if err != nil {
		log.Fatalf("failed to initialize auth: %v", err)
	}

	// 3. Resolve DeviceAuthorization Capability
	devAuth, err := auth.Get[plugins.DeviceAuthorization](a)
	if err != nil {
		log.Fatalf("failed to resolve capability: %v", err)
	}

	// 4. Device starts authorization flow (CLI or Smart TV)
	resp, err := devAuth.Start(ctx, deviceauth.DeviceAuthorizationRequestInput{
		ClientID: "smart_tv_app",
		Scope:    "read_profile stream_video",
	})
	if err != nil {
		log.Fatalf("failed to start device flow: %v", err)
	}

	fmt.Printf("Please visit: %s\n", resp.VerificationURI)
	fmt.Printf("Enter Code:   %s\n", resp.UserCode)

	// 5. Device polls token endpoint
	for {
		time.Sleep(time.Duration(resp.Interval) * time.Second)

		pollResp, err := devAuth.Poll(ctx, deviceauth.PollInput{
			DeviceCode: resp.DeviceCode,
			ClientID:   "smart_tv_app",
		})
		if errors.Is(err, deviceauth.ErrAuthorizationPending) {
			fmt.Println("Waiting for user authorization...")
			continue
		}
		if errors.Is(err, deviceauth.ErrSlowDown) {
			fmt.Println("Polling too quickly; slowing down...")
			resp.Interval += 5
			continue
		}
		if err != nil {
			log.Fatalf("polling failed: %v", err)
		}

		fmt.Printf("Authorization successful! Session Token: %s\n", pollResp.Token)
		break
	}
}

Detailed guide: doc/plugins/device-authorization.md.

Documentation

Overview

Package deviceauth provides RFC 8628 OAuth 2.0 Device Authorization Grant capabilities (reserved module).

Index

Constants

View Source
const (
	DefaultExpiresIn          = 10 * time.Minute
	DefaultPollingInterval    = 5 * time.Second
	DefaultSlowDownIncrement  = 5 * time.Second
	DefaultMaxFailedAttempts  = 5
	DefaultVerificationURI    = "https://example.com/device"
	DefaultRateLimitPerMinute = 10
)

Default configuration parameters according to RFC 8628 recommendations.

View Source
const (
	// OAuthErrAuthorizationPending indicates the user has not yet completed the authorization flow.
	OAuthErrAuthorizationPending = "authorization_pending"

	// OAuthErrSlowDown indicates the client is polling too frequently; interval must be increased by 5s.
	OAuthErrSlowDown = "slow_down"

	// OAuthErrAccessDenied indicates the user explicitly denied the authorization request.
	OAuthErrAccessDenied = "access_denied"

	// OAuthErrExpiredToken indicates the device code or user code has expired.
	OAuthErrExpiredToken = "expired_token"

	// OAuthErrInvalidGrant indicates the device code is invalid, revoked, or already consumed.
	OAuthErrInvalidGrant = "invalid_grant"
)

RFC 8628 Standard Device Authorization Error Codes

View Source
const (
	EventDeviceAuthStarted    auth.EventName = "deviceauth.started"
	EventDeviceAuthAuthorized auth.EventName = "deviceauth.authorized"
	EventDeviceAuthDenied     auth.EventName = "deviceauth.denied"
	EventDeviceAuthConsumed   auth.EventName = "deviceauth.consumed"
)

Audit and lifecycle event identifiers for the Device Authorization Grant.

View Source
const (
	ModelDeviceGrants = "device_grants"
)
View Source
const PluginID = "device-authorization"

PluginID identifies this plugin in the GoModularAuth runtime.

Variables

View Source
var (
	// ErrAuthorizationPending indicates the authorization request is pending user approval.
	ErrAuthorizationPending = errors.New(OAuthErrAuthorizationPending)

	// ErrSlowDown indicates the client polled before the allowed interval; interval increases by 5 seconds.
	ErrSlowDown = errors.New(OAuthErrSlowDown)

	// ErrAccessDenied indicates the user explicitly denied the authorization request.
	ErrAccessDenied = errors.New(OAuthErrAccessDenied)

	// ErrExpiredToken indicates the device authorization request has expired.
	ErrExpiredToken = errors.New(OAuthErrExpiredToken)

	// ErrInvalidGrant indicates the device code is invalid or has already been consumed (replay prevented).
	ErrInvalidGrant = errors.New(OAuthErrInvalidGrant)

	// ErrInvalidUserCode indicates the user-entered verification code was not found or is invalid.
	ErrInvalidUserCode = errors.New("device auth: invalid or unknown user code")

	// ErrMaxAttemptsExceeded indicates too many failed attempts were made against a user code.
	ErrMaxAttemptsExceeded = errors.New("device auth: maximum verification attempts exceeded")

	// ErrAlreadyAuthorized indicates the user code was already approved.
	ErrAlreadyAuthorized = errors.New("device auth: request is already authorized")

	// ErrAlreadyDenied indicates the user code was already denied.
	ErrAlreadyDenied = errors.New("device auth: request is already denied")

	// ErrAlreadyConsumed indicates the grant has already been exchanged for tokens.
	ErrAlreadyConsumed = errors.New("device auth: request has already been consumed")

	// ErrRateLimited indicates request rate limit has been exceeded.
	ErrRateLimited = errors.New("device auth: rate limit exceeded")

	// ErrInvalidClient indicates the client ID is missing or does not match the initiated grant.
	ErrInvalidClient = errors.New("device auth: invalid client")

	// ErrInvalidInput indicates missing or invalid request parameters.
	ErrInvalidInput = errors.New("device auth: invalid input parameters")

	// ErrAdapterRequired indicates that a database adapter was not configured.
	ErrAdapterRequired = errors.New("device auth: adapter is required")

	// ErrStoreRequired is an alias for ErrAdapterRequired for backward compatibility.
	ErrStoreRequired = ErrAdapterRequired
)

Functions

func RegisterSchema

func RegisterSchema(s *schema.Schema) error

RegisterSchema registers the device_grants model into the compiled schema.

Types

type AuthorizeInput

type AuthorizeInput struct {
	// UserCode is the target user code being authorized. Required.
	UserCode string `json:"user_code"`

	// Principal identifies the authenticated user approving the request. Required.
	Principal auth.Principal `json:"principal"`

	// Scope specifies approved scopes (can be subset of requested).
	Scope string `json:"scope,omitempty"`
}

AuthorizeInput contains parameters for the end user approving the device request.

type Capability

type Capability = DeviceAuthorization

Capability is an alias for DeviceAuthorization to support standard capability discovery conventions.

type Config

type Config struct {
	VerificationURI         string
	VerificationURIComplete string
	ExpiresIn               time.Duration
	PollingInterval         time.Duration
	SlowDownIncrement       time.Duration
	MaxFailedAttempts       int
	RateLimitPerMinute      int64
}

Config encapsulates configuration settings for the device authorization plugin.

type DenyInput

type DenyInput struct {
	// UserCode is the target user code being denied. Required.
	UserCode string `json:"user_code"`

	// Principal identifies the authenticated user denying the request. Required.
	Principal auth.Principal `json:"principal"`
}

DenyInput contains parameters for the end user rejecting the device request.

type DeviceAuthAuthorizedPayload

type DeviceAuthAuthorizedPayload struct {
	GrantID      string    `json:"grant_id"`
	ClientID     string    `json:"client_id"`
	UserCode     string    `json:"user_code"`
	UserID       string    `json:"user_id"`
	AuthorizedAt time.Time `json:"authorized_at"`
}

DeviceAuthAuthorizedPayload describes user authorization of a device grant.

type DeviceAuthConsumedPayload

type DeviceAuthConsumedPayload struct {
	GrantID    string    `json:"grant_id"`
	ClientID   string    `json:"client_id"`
	UserID     string    `json:"user_id"`
	ConsumedAt time.Time `json:"consumed_at"`
}

DeviceAuthConsumedPayload describes single-use token issuance for a device grant.

type DeviceAuthDeniedPayload

type DeviceAuthDeniedPayload struct {
	GrantID  string    `json:"grant_id"`
	ClientID string    `json:"client_id"`
	UserCode string    `json:"user_code"`
	DeniedAt time.Time `json:"denied_at"`
}

DeviceAuthDeniedPayload describes user rejection of a device grant.

type DeviceAuthStartedPayload

type DeviceAuthStartedPayload struct {
	GrantID         string    `json:"grant_id"`
	ClientID        string    `json:"client_id"`
	UserCode        string    `json:"user_code"`
	ExpiresAt       time.Time `json:"expires_at"`
	PollingInterval int       `json:"polling_interval"`
	CreatedAt       time.Time `json:"created_at"`
}

DeviceAuthStartedPayload describes the initiation of a device flow. CRITICAL: NEVER includes the plaintext device_code in events.

type DeviceAuthorization

type DeviceAuthorization interface {
	// Start initiates the device flow and returns device and user codes.
	Start(ctx context.Context, in DeviceAuthorizationRequestInput) (*DeviceAuthorizationResponse, error)

	// Poll checks device authorization status and issues tokens upon user approval.
	Poll(ctx context.Context, in PollInput) (*PollResponse, error)

	// VerifyUserCode inspects a user code submitted by the user on the verification page.
	VerifyUserCode(ctx context.Context, in VerifyUserCodeInput) (*UserCodeVerificationResult, error)

	// Authorize records user consent and associates the grant with the authorizing user.
	Authorize(ctx context.Context, in AuthorizeInput) error

	// Deny records user rejection of the authorization request.
	Deny(ctx context.Context, in DenyInput) error
}

DeviceAuthorization defines the RFC 8628 OAuth 2.0 Device Authorization Grant capability. Applications access it via auth.Get[plugins.DeviceAuthorization](a) or auth.Get[deviceauth.DeviceAuthorization](a).

type DeviceAuthorizationRequestInput

type DeviceAuthorizationRequestInput struct {
	// ClientID identifies the client application running on the device. Required.
	ClientID string `json:"client_id"`

	// Scope indicates the requested permission scopes.
	Scope string `json:"scope,omitempty"`

	// Metadata carries optional device or client properties (e.g. model, OS).
	Metadata map[string]string `json:"metadata,omitempty"`
}

DeviceAuthorizationRequestInput contains parameters to initiate a device authorization flow.

type DeviceAuthorizationResponse

type DeviceAuthorizationResponse struct {
	// DeviceCode is the high-entropy verification code for device polling.
	DeviceCode string `json:"device_code"`

	// UserCode is the human-friendly code entered by the user on the verification page.
	UserCode string `json:"user_code"`

	// VerificationURI is the web URL where the end user enters the UserCode.
	VerificationURI string `json:"verification_uri"`

	// VerificationURIComplete is the optional URL containing pre-filled user_code.
	VerificationURIComplete string `json:"verification_uri_complete,omitempty"`

	// ExpiresIn is the lifetime in seconds for the device_code and user_code.
	ExpiresIn int `json:"expires_in"`

	// Interval is the minimum polling interval in seconds that the device must respect.
	Interval int `json:"interval"`
}

DeviceAuthorizationResponse represents the response returned to the device (RFC 8628 Section 3.2).

type DeviceGrant

type DeviceGrant struct {
	ID              string            `json:"id"`
	ClientID        string            `json:"client_id"`
	DeviceCodeHash  string            `json:"device_code_hash"`
	UserCode        string            `json:"user_code"`
	Status          DeviceStatus      `json:"status"`
	UserID          string            `json:"user_id,omitempty"`
	Scope           string            `json:"scope,omitempty"`
	Metadata        map[string]string `json:"metadata,omitempty"`
	ExpiresAt       time.Time         `json:"expires_at"`
	LastPolledAt    *time.Time        `json:"last_polled_at,omitempty"`
	PollingInterval int               `json:"polling_interval"`
	FailedAttempts  int               `json:"failed_attempts"`
	AuthorizedAt    *time.Time        `json:"authorized_at,omitempty"`
	ConsumedAt      *time.Time        `json:"consumed_at,omitempty"`
	CreatedAt       time.Time         `json:"created_at"`
	UpdatedAt       time.Time         `json:"updated_at"`
}

DeviceGrant represents the authoritative state of an RFC 8628 device authorization session.

func (*DeviceGrant) Expired

func (g *DeviceGrant) Expired(now time.Time) bool

Expired reports whether the device grant has passed its validity window.

type DeviceStatus

type DeviceStatus string

DeviceStatus represents the lifecycle state of a device authorization grant.

const (
	// StatusPending indicates the authorization request is awaiting user interaction.
	StatusPending DeviceStatus = "pending"

	// StatusAuthorized indicates the user approved the request, ready for device token exchange.
	StatusAuthorized DeviceStatus = "authorized"

	// StatusDenied indicates the user rejected the authorization request.
	StatusDenied DeviceStatus = "denied"

	// StatusConsumed indicates the device code has been exchanged for credentials (single-use fulfilled).
	StatusConsumed DeviceStatus = "consumed"
)

type Module

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

Module implements auth.PluginModule using plugin-sdk.

func New

func New(opts ...Option) *Module

New creates an installable device authorization PluginModule with options.

func (*Module) ID

func (m *Module) ID() string

ID returns the module's unique identifier.

func (*Module) Init

func (m *Module) Init(h *auth.Host) error

Init configures infrastructure and registers the typed Capability in GoModularAuth.

func (*Module) RegisterSchema

func (m *Module) RegisterSchema(s *schema.Schema) error

RegisterSchema registers the device_grants model into the schema.

type Option

type Option func(*Config)

Option configures device authorization plugin settings.

func WithExpiresIn

func WithExpiresIn(d time.Duration) Option

WithExpiresIn sets the validity lifetime of issued device and user codes.

func WithMaxFailedAttempts

func WithMaxFailedAttempts(max int) Option

WithMaxFailedAttempts sets the maximum bad user-code attempts before locking the grant.

func WithPollingInterval

func WithPollingInterval(d time.Duration) Option

WithPollingInterval sets the minimum interval between device poll requests.

func WithRateLimit

func WithRateLimit(limit int64) Option

WithRateLimit sets the maximum verification requests allowed per minute per IP.

func WithVerificationURI

func WithVerificationURI(uri string) Option

WithVerificationURI sets the base verification URL where users enter their code.

func WithVerificationURIComplete

func WithVerificationURIComplete(uri string) Option

WithVerificationURIComplete sets the complete verification URL format.

type PollInput

type PollInput struct {
	// DeviceCode is the secret code issued to the device. Required.
	DeviceCode string `json:"device_code"`

	// ClientID is the client application ID. Required.
	ClientID string `json:"client_id"`

	// Session holds optional metadata (IP address, User Agent) recorded with the created session.
	Session auth.SessionOptions `json:"session,omitempty"`
}

PollInput contains parameters for the device polling token request.

type PollResponse

type PollResponse struct {
	// Status indicates the current lifecycle status of the authorization request.
	Status DeviceStatus `json:"status"`

	// UserID is the subject identifier of the authorizing user when Status == StatusAuthorized or StatusConsumed.
	UserID string `json:"user_id,omitempty"`

	// Session is the newly established session when authorization is fulfilled.
	Session *session.Session `json:"session,omitempty"`

	// Token is the plaintext session token returned exactly once upon successful completion.
	Token string `json:"token,omitempty"`
}

PollResponse represents the outcome of a device token poll request.

type UserCodeVerificationResult

type UserCodeVerificationResult struct {
	// GrantID is the internal grant identifier.
	GrantID string `json:"grant_id"`

	// ClientID is the client requesting authorization.
	ClientID string `json:"client_id"`

	// UserCode is the normalized user code.
	UserCode string `json:"user_code"`

	// Scope indicates requested permissions.
	Scope string `json:"scope,omitempty"`

	// Metadata contains additional context for display.
	Metadata map[string]string `json:"metadata,omitempty"`

	// ExpiresAt is the expiration instant.
	ExpiresAt time.Time `json:"expires_at"`
}

UserCodeVerificationResult presents grant metadata to the user consent interface.

type VerifyUserCodeInput

type VerifyUserCodeInput struct {
	// UserCode is the code entered by the user. Required.
	UserCode string `json:"user_code"`

	// ClientIP is the caller's IP address used for rate limiting brute-force protection.
	ClientIP string `json:"client_ip,omitempty"`
}

VerifyUserCodeInput contains parameters for the user entering the code on the web page.

Directories

Path Synopsis
Package cache provides rate limiting and temporary throttle helpers for device authorization.
Package cache provides rate limiting and temporary throttle helpers for device authorization.
Package events re-exports device authorization audit event identifiers and payload types.
Package events re-exports device authorization audit event identifiers and payload types.
Package migrations contains SQL schema migrations for the device-authorization plugin.
Package migrations contains SQL schema migrations for the device-authorization plugin.
Package plugins exposes capability aliases for GoModularAuth plugins.
Package plugins exposes capability aliases for GoModularAuth plugins.

Jump to

Keyboard shortcuts

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