Documentation
¶
Index ¶
- Constants
- Variables
- func BuildVerificationURIs(baseURI, customURI, userCode string) (uri string, uriComplete string)
- func DefaultGenerateDeviceCode(length int) (string, error)
- func DefaultGenerateUserCode(length int) (string, error)
- func NormalizeUserCode(code string) string
- type ApproveDeviceCodeParams
- type Config
- type DenyDeviceCodeParams
- type DeviceCode
- type DeviceCodeApprovedPayload
- type DeviceCodeDeniedPayload
- type DeviceCodeRequestedPayload
- type DeviceCodeResponse
- type DeviceCodeStatus
- type DeviceTokenExchangedPayload
- type ExchangeDeviceTokenParams
- type Option
- func WithCustomURI(uri string) Option
- func WithDeviceCodeLength(length int) Option
- func WithExpiresIn(d time.Duration) Option
- func WithGenerateDeviceCode(fn func(length int) (string, error)) Option
- func WithGenerateUserCode(fn func(length int) (string, error)) Option
- func WithInterval(d time.Duration) Option
- func WithOnDeviceAuthRequest(fn func(ctx context.Context, clientID string, scope *string) error) Option
- func WithSessionExpiry(d time.Duration) Option
- func WithUserCodeLength(length int) Option
- func WithValidateClient(fn func(ctx context.Context, clientID string) (bool, error)) Option
- func WithVerificationURI(uri string) Option
- type Plugin
- func (p *Plugin) ApproveDeviceCode(ctx context.Context, params ApproveDeviceCodeParams) error
- func (p *Plugin) Config() Config
- func (p *Plugin) DenyDeviceCode(ctx context.Context, params DenyDeviceCodeParams) error
- func (p *Plugin) ExchangeDeviceToken(ctx context.Context, params ExchangeDeviceTokenParams) (*TokenResponse, error)
- func (p *Plugin) GetVerificationState(ctx context.Context, rawUserCode string) (*DeviceCode, error)
- func (p *Plugin) ID() string
- func (p *Plugin) Init(ctx *plugin.Context) error
- func (p *Plugin) RequestDeviceCode(ctx context.Context, params RequestDeviceCodeParams) (*DeviceCodeResponse, error)
- func (p *Plugin) ServeApprove(w http.ResponseWriter, r *http.Request)
- func (p *Plugin) ServeDeviceCode(w http.ResponseWriter, r *http.Request)
- func (p *Plugin) ServeTokenExchange(w http.ResponseWriter, r *http.Request)
- type RFCErrorResponse
- type Repository
- type RequestDeviceCodeParams
- type TokenResponse
Constants ¶
const ( // EventDeviceCodeRequested is published when a new device authorization request is initiated. EventDeviceCodeRequested = "deviceauth:code_requested" // EventDeviceCodeApproved is published when an authenticated user approves a device code. EventDeviceCodeApproved = "eventauth:code_approved" // EventDeviceCodeDenied is published when a user explicitly rejects a device code request. EventDeviceCodeDenied = "deviceauth:code_denied" // EventDeviceTokenExchanged is published when an approved device code is successfully exchanged for a session token. EventDeviceTokenExchanged = "deviceauth:token_exchanged" )
const DefaultCharset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
DefaultCharset contains human-friendly uppercase letters and numbers, excluding ambiguous characters (0, O, 1, I, L).
const OAuth2DeviceGrantType = "urn:ietf:params:oauth:grant-type:device_code"
OAuth2DeviceGrantType is the standard RFC 8628 grant type string.
const PluginID = "device-authorization"
PluginID is the unique string identifier for the Device Authorization plugin ("device-authorization").
Variables ¶
var ( // ErrInvalidDeviceCode is returned when a provided device code does not exist in storage. ErrInvalidDeviceCode = errors.New("deviceauth: invalid device code") // ErrInvalidUserCode is returned when a provided user verification code does not exist in storage. ErrInvalidUserCode = errors.New("deviceauth: invalid user verification code") // ErrCodeExpired is returned when attempting to exchange or authorize a device code that has passed its expiration time. ErrCodeExpired = errors.New("deviceauth: device code has expired") // ErrSlowDown is returned when client polling frequency exceeds the configured interval. ErrSlowDown = errors.New("deviceauth: polling rate limit exceeded (slow_down)") // ErrAuthorizationPending is returned when polling a device code that is still waiting for user approval. ErrAuthorizationPending = errors.New("deviceauth: authorization pending") // ErrAccessDenied is returned when polling a device code that has been explicitly denied by the user. ErrAccessDenied = errors.New("deviceauth: authorization request denied") // ErrAlreadyConsumed is returned when attempting to consume a device code that was already exchanged. ErrAlreadyConsumed = errors.New("deviceauth: device code has already been consumed") // ErrInvalidGrantType is returned when the exchange request grant_type is not "urn:ietf:params:oauth:grant-type:device_code". ErrInvalidGrantType = errors.New("deviceauth: invalid grant_type") // ErrUserNotFound is returned when the owner user record cannot be found upon token exchange. ErrUserNotFound = errors.New("deviceauth: user not found") // ErrInvalidClientID is returned when client_id validation fails or mismatch occurs. ErrInvalidClientID = errors.New("deviceauth: invalid client_id") // ErrInvalidParameter is returned when required parameters are missing or malformed. ErrInvalidParameter = errors.New("deviceauth: required parameter is missing or invalid") )
Sentinel errors for the Device Authorization plugin.
Functions ¶
func BuildVerificationURIs ¶
BuildVerificationURIs constructs the standard verification_uri and verification_uri_complete URLs.
func DefaultGenerateDeviceCode ¶
DefaultGenerateDeviceCode generates a cryptographically secure random hex device code string.
func DefaultGenerateUserCode ¶
DefaultGenerateUserCode generates a cryptographically secure user verification code using DefaultCharset.
func NormalizeUserCode ¶
NormalizeUserCode strips spaces, hyphens, and converts the input string to uppercase.
Types ¶
type ApproveDeviceCodeParams ¶
type ApproveDeviceCodeParams struct {
// UserID is the authenticated user approving the grant.
UserID string `json:"user_id"`
// UserCode is the user verification code submitted by the user.
UserCode string `json:"user_code"`
}
ApproveDeviceCodeParams holds input parameters when an authenticated user approves a device authorization.
type Config ¶
type Config struct {
// ExpiresIn specifies the lifetime duration of a device code grant (default: 30 minutes).
ExpiresIn time.Duration
// Interval specifies the minimum polling interval expected between token requests (default: 5 seconds).
Interval time.Duration
// DeviceCodeLength specifies the random character length for generated device codes (default: 40).
DeviceCodeLength int
// UserCodeLength specifies the random character length for generated user codes (default: 8).
UserCodeLength int
// VerificationURI specifies the relative or absolute user verification path (default: "/device").
VerificationURI string
// CustomURI optionally specifies a custom base URI for completing verification URLs.
CustomURI string
// SessionExpiry specifies the default duration of sessions created upon token exchange (default: 24 hours).
SessionExpiry time.Duration
// GenerateDeviceCode allows overriding the default device code generator.
GenerateDeviceCode func(length int) (string, error)
// GenerateUserCode allows overriding the default user code generator.
GenerateUserCode func(length int) (string, error)
// ValidateClient is an optional hook to validate client_id during device code authorization requests.
ValidateClient func(ctx context.Context, clientID string) (bool, error)
// OnDeviceAuthRequest is an optional hook executed during device code authorization requests.
OnDeviceAuthRequest func(ctx context.Context, clientID string, scope *string) error
}
Config holds all configuration settings for the Device Authorization plugin.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns a Config struct pre-populated with standard RFC 8628 defaults.
type DenyDeviceCodeParams ¶
type DenyDeviceCodeParams struct {
// UserCode is the user verification code submitted by the user.
UserCode string `json:"user_code"`
}
DenyDeviceCodeParams holds input parameters when an authenticated user rejects a device authorization.
type DeviceCode ¶
type DeviceCode struct {
// ID is the unique database record identifier.
ID string `json:"id"`
// DeviceCode is the high-entropy secret issued to the device for polling.
DeviceCode string `json:"device_code"`
// UserCode is the short, human-readable code presented to the user for verification.
UserCode string `json:"user_code"`
// UserID is the owner user's unique identifier, populated upon user authorization.
UserID *string `json:"user_id,omitempty"`
// ExpiresAt specifies the exact timestamp after which this device code grant is invalid.
ExpiresAt time.Time `json:"expires_at"`
// Status represents the current state of authorization (pending, approved, denied).
Status DeviceCodeStatus `json:"status"`
// LastPolledAt records the timestamp of the most recent token polling request.
LastPolledAt *time.Time `json:"last_polled_at,omitempty"`
// PollingInterval specifies the minimum duration between consecutive polling attempts.
PollingInterval time.Duration `json:"polling_interval"`
// ClientID optionally identifies the client application requesting authorization.
ClientID *string `json:"client_id,omitempty"`
// Scope optionally specifies the requested access scope.
Scope *string `json:"scope,omitempty"`
// CreatedAt records when the device authorization grant was generated.
CreatedAt time.Time `json:"created_at"`
// UpdatedAt records when the device authorization grant state was last modified.
UpdatedAt time.Time `json:"updated_at"`
}
DeviceCode represents a persistent device authorization request grant (RFC 8628).
type DeviceCodeApprovedPayload ¶
type DeviceCodeApprovedPayload struct {
UserCode string `json:"user_code"`
UserID string `json:"user_id"`
}
DeviceCodeApprovedPayload defines the event bus payload dispatched when a user authorizes a grant.
type DeviceCodeDeniedPayload ¶
type DeviceCodeDeniedPayload struct {
UserCode string `json:"user_code"`
}
DeviceCodeDeniedPayload defines the event bus payload dispatched when a user rejects a grant.
type DeviceCodeRequestedPayload ¶
type DeviceCodeRequestedPayload struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
ClientID *string `json:"client_id,omitempty"`
Scope *string `json:"scope,omitempty"`
}
DeviceCodeRequestedPayload defines the event bus payload dispatched on device code creation.
type DeviceCodeResponse ¶
type DeviceCodeResponse struct {
// DeviceCode is the verification code issued to the client.
DeviceCode string `json:"device_code"`
// UserCode is the short verification code for human entry.
UserCode string `json:"user_code"`
// VerificationURI is the end-user verification URL on the authorization server.
VerificationURI string `json:"verification_uri"`
// VerificationURIComplete is the optional verification URL pre-filled with the user_code.
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
// ExpiresIn indicates the lifetime in seconds of the device_code and user_code.
ExpiresIn int64 `json:"expires_in"`
// Interval indicates the minimum number of seconds the client should wait between polling requests.
Interval int64 `json:"interval"`
}
DeviceCodeResponse represents the successful RFC 8628 response returned to the device.
type DeviceCodeStatus ¶
type DeviceCodeStatus string
DeviceCodeStatus represents the authorization status of a device code.
const ( // StatusPending indicates the user has not yet approved or denied the device authorization request. StatusPending DeviceCodeStatus = "pending" // StatusApproved indicates the user has successfully authorized the device request. StatusApproved DeviceCodeStatus = "approved" // StatusDenied indicates the user has explicitly rejected the device authorization request. StatusDenied DeviceCodeStatus = "denied" )
type DeviceTokenExchangedPayload ¶
type DeviceTokenExchangedPayload struct {
DeviceCode string `json:"device_code"`
UserID string `json:"user_id"`
SessionToken string `json:"session_token"`
}
DeviceTokenExchangedPayload defines the event bus payload dispatched on successful token issuance.
type ExchangeDeviceTokenParams ¶
type ExchangeDeviceTokenParams struct {
// GrantType must be "urn:ietf:params:oauth:grant-type:device_code".
GrantType string `json:"grant_type"`
// DeviceCode is the device code issued in the initial authorization response.
DeviceCode string `json:"device_code"`
// ClientID optionally identifies the client making the request.
ClientID *string `json:"client_id,omitempty"`
}
ExchangeDeviceTokenParams holds parameters submitted by the device when polling for a session token.
type Option ¶
type Option func(*Config)
Option represents a functional option for configuring the Device Authorization plugin.
func WithCustomURI ¶
WithCustomURI sets a custom domain/host URI used for completing verification URLs.
func WithDeviceCodeLength ¶
WithDeviceCodeLength sets the character length of generated device codes.
func WithExpiresIn ¶
WithExpiresIn sets the expiration duration of issued device code grants.
func WithGenerateDeviceCode ¶
WithGenerateDeviceCode overrides the default cryptographic device code generator function.
func WithGenerateUserCode ¶
WithGenerateUserCode overrides the default cryptographic user code generator function.
func WithInterval ¶
WithInterval sets the minimum polling interval requirement.
func WithOnDeviceAuthRequest ¶
func WithOnDeviceAuthRequest(fn func(ctx context.Context, clientID string, scope *string) error) Option
WithOnDeviceAuthRequest registers a hook executed when a device code is requested.
func WithSessionExpiry ¶
WithSessionExpiry sets the duration of sessions generated when exchanging an approved device code.
func WithUserCodeLength ¶
WithUserCodeLength sets the character length of generated user verification codes.
func WithValidateClient ¶
WithValidateClient registers a callback to validate client_id strings.
func WithVerificationURI ¶
WithVerificationURI sets the base verification URI returned in device code responses.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin implements the RFC 8628 Device Authorization Flow plugin for go-modular-auth.
func New ¶
func New(repo Repository, opts ...Option) *Plugin
New instantiates a new Device Authorization plugin configured with the given repository and options.
func (*Plugin) ApproveDeviceCode ¶
func (p *Plugin) ApproveDeviceCode(ctx context.Context, params ApproveDeviceCodeParams) error
ApproveDeviceCode approves a pending device authorization request for an authenticated user.
func (*Plugin) DenyDeviceCode ¶
func (p *Plugin) DenyDeviceCode(ctx context.Context, params DenyDeviceCodeParams) error
DenyDeviceCode rejects a pending device authorization request.
func (*Plugin) ExchangeDeviceToken ¶
func (p *Plugin) ExchangeDeviceToken(ctx context.Context, params ExchangeDeviceTokenParams) (*TokenResponse, error)
ExchangeDeviceToken polls for authorization and exchanges an approved device_code for a session access token.
func (*Plugin) GetVerificationState ¶
GetVerificationState retrieves the device authorization grant state by user_code.
func (*Plugin) ID ¶
ID returns the unique string identifier for the plugin ("device-authorization").
func (*Plugin) RequestDeviceCode ¶
func (p *Plugin) RequestDeviceCode(ctx context.Context, params RequestDeviceCodeParams) (*DeviceCodeResponse, error)
RequestDeviceCode initiates a new device authorization grant request.
func (*Plugin) ServeApprove ¶ added in v0.20.0
func (p *Plugin) ServeApprove(w http.ResponseWriter, r *http.Request)
ServeApprove is a net/http handler for user approval of pending device codes.
func (*Plugin) ServeDeviceCode ¶ added in v0.20.0
func (p *Plugin) ServeDeviceCode(w http.ResponseWriter, r *http.Request)
ServeDeviceCode is a net/http handler for processing RFC 8628 device authorization requests.
func (*Plugin) ServeTokenExchange ¶ added in v0.20.0
func (p *Plugin) ServeTokenExchange(w http.ResponseWriter, r *http.Request)
ServeTokenExchange is a net/http handler for polling and exchanging approved device codes for tokens.
type RFCErrorResponse ¶
type RFCErrorResponse struct {
// Error is the RFC error string (e.g. "authorization_pending", "slow_down", "expired_token", "access_denied").
Error string `json:"error"`
// ErrorDescription provides human-readable details regarding the error.
ErrorDescription string `json:"error_description,omitempty"`
}
RFCErrorResponse represents a standard OAuth 2.0 / RFC 8628 error payload.
type Repository ¶
type Repository interface {
// CreateDeviceCode persists a new device authorization grant record in storage.
//
// Function:
// Called during RFC 8628 device authorization initiation (`/device/code`).
//
// Storage:
// Cache (Redis / In-Memory TTL) - Short-lived device code state with expiration TTL.
//
// Arguments:
// - ctx: Request cancellation context.
// - code: DeviceCode entity containing device_code, user_code, verification_uri, and expiration.
//
// Returns:
// - error: Nil on success, or database error.
//
// Example SQL:
// INSERT INTO device_codes (id, device_code, user_code, verification_uri, status, expires_at, created_at)
// VALUES ($1, $2, $3, $4, $5, $6, $7);
//
// Example Cache (Redis):
// err := rdb.Set(ctx, "device:code:" + code.DeviceCode, bytes, ttl).Err()
CreateDeviceCode(ctx context.Context, code *DeviceCode) error
// FindByDeviceCode retrieves an active device code record by its device_code secret.
//
// Function:
// Used during client polling (`/device/token`) to inspect grant status and rate limits.
//
// Storage:
// Cache (Redis / In-Memory TTL) - Ephemeral device code lookup during client polling.
//
// Arguments:
// - ctx: Request cancellation context.
// - deviceCode: Raw device code secret string.
//
// Returns:
// - *DeviceCode: Matching device code record if found.
// - error: ErrInvalidDeviceCode if missing, or database error.
//
// Example SQL:
// SELECT id, device_code, user_code, status, user_id, last_polled_at, expires_at, created_at FROM device_codes WHERE device_code = $1 LIMIT 1;
//
// Example Cache (Redis):
// val, err := rdb.Get(ctx, "device:code:" + deviceCode).Bytes()
FindByDeviceCode(ctx context.Context, deviceCode string) (*DeviceCode, error)
// FindByUserCode retrieves an active device code record by its normalized user_code.
//
// Function:
// Used when a end-user enters their short user code on the device authorization web page.
//
// Storage:
// Cache (Redis / In-Memory TTL) - Lookup device grant state by user_code string.
//
// Arguments:
// - ctx: Request cancellation context.
// - userCode: User verification code string (e.g. "WDJB-MJHT").
//
// Returns:
// - *DeviceCode: Matching device code record if found.
// - error: ErrInvalidUserCode if missing, or database error.
//
// Example SQL:
// SELECT id, device_code, user_code, status, user_id, expires_at, created_at FROM device_codes WHERE user_code = $1 LIMIT 1;
//
// Example Cache (Redis):
// val, err := rdb.Get(ctx, "device:usercode:" + userCode).Bytes()
FindByUserCode(ctx context.Context, userCode string) (*DeviceCode, error)
// UpdateLastPolledAt updates the last_polled_at timestamp for rate limiting checks.
//
// Function:
// Called during client polling to enforce minimum polling intervals (preventing spam).
//
// Storage:
// Cache (Redis / In-Memory TTL) - Poll timestamp update.
//
// Arguments:
// - ctx: Request cancellation context.
// - deviceCode: Target device code secret.
// - polledAt: Timestamp of current poll request.
//
// Returns:
// - error: Nil on success.
//
// Example SQL:
// UPDATE device_codes SET last_polled_at = $1 WHERE device_code = $2;
//
// Example Cache (Redis):
// err := rdb.Set(ctx, "device:polled:" + deviceCode, polledAt.Unix(), ttl).Err()
UpdateLastPolledAt(ctx context.Context, deviceCode string, polledAt time.Time) error
// UpdateStatus updates the status (approved/denied) and optional owner userID for a device code by userCode.
//
// Function:
// Called when an authenticated user approves or denies the device authorization request.
//
// Storage:
// Cache (Redis / In-Memory TTL) - Grant status approval update.
//
// Arguments:
// - ctx: Request cancellation context.
// - userCode: User verification code string.
// - status: StatusApproved or StatusDenied.
// - userID: Pointer to approving user ID.
//
// Returns:
// - error: Nil on success.
//
// Example SQL:
// UPDATE device_codes SET status = $1, user_id = $2 WHERE user_code = $3;
//
// Example Cache (Redis):
// err := rdb.Set(ctx, "device:status:" + userCode, status, ttl).Err()
UpdateStatus(ctx context.Context, userCode string, status DeviceCodeStatus, userID *string) error
// ConsumeDeviceCode atomically retrieves and removes/invalidates an approved device code record.
// This operation MUST be single-use to protect against race conditions during concurrent polls.
//
// Function:
// Called when client polling detects approval and exchanges the device code for access tokens.
//
// Storage:
// Cache (Redis GETDEL / Memory) - Atomic read-and-delete single-use consumption.
//
// Arguments:
// - ctx: Request cancellation context.
// - deviceCode: Raw device code secret.
//
// Returns:
// - *DeviceCode: Consumed device code grant entity.
// - error: ErrAlreadyConsumed if already exchanged, ErrAuthorizationPending if not approved.
//
// Example SQL:
// DELETE FROM device_codes WHERE device_code = $1 AND status = 'approved' RETURNING id, device_code, user_code, status, user_id, expires_at, created_at;
//
// Example Cache (Redis):
// val, err := rdb.GetDel(ctx, "device:code:" + deviceCode).Bytes()
ConsumeDeviceCode(ctx context.Context, deviceCode string) (*DeviceCode, error)
// DeleteDeviceCode removes a device code record from persistent storage.
//
// Function:
// Called during explicit cancellation or removal.
//
// Storage:
// Cache (Redis / In-Memory TTL) - Device code eviction.
//
// Arguments:
// - ctx: Request cancellation context.
// - deviceCode: Device code secret.
//
// Returns:
// - error: Nil on success.
//
// Example SQL:
// DELETE FROM device_codes WHERE device_code = $1;
//
// Example Cache (Redis):
// err := rdb.Del(ctx, "device:code:" + deviceCode).Err()
DeleteDeviceCode(ctx context.Context, deviceCode string) error
// DeleteExpiredDeviceCodes purges all expired device code records.
//
// Function:
// Called by background cleanup crons.
//
// Storage:
// Database (GORM / SQL) - Bulk record deletion.
//
// Arguments:
// - ctx: Request cancellation context.
//
// Returns:
// - error: Nil on success.
//
// Example SQL:
// DELETE FROM device_codes WHERE expires_at <= $1;
DeleteExpiredDeviceCodes(ctx context.Context) error
// GetUserByID retrieves a user entity by unique user identifier.
//
// Function:
// Used after successful device code exchange to create user session and populate identity.
//
// Storage:
// Database (GORM / SQL) - User primary key lookup.
//
// Arguments:
// - ctx: Request cancellation context.
// - userID: Target user primary key.
//
// Returns:
// - *entity.User: Matching user entity if found.
// - error: ErrUserNotFound if missing.
//
// Example SQL:
// SELECT id, email, name, email_verified, created_at, updated_at FROM users WHERE id = $1 LIMIT 1;
GetUserByID(ctx context.Context, userID string) (*entity.User, error)
// CreateSession initializes and persists an active user session entity.
//
// Function:
// Called during successful token exchange to establish a new user session.
//
// Storage:
// Database (GORM / SQL) - Active session creation.
//
// Arguments:
// - ctx: Request cancellation context.
// - session: Active session entity to persist.
//
// Returns:
// - *entity.Session: Created session entity.
// - error: Nil on success.
//
// Example SQL:
// INSERT INTO sessions (id, user_id, token, expires_at, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6);
CreateSession(ctx context.Context, session *entity.Session) (*entity.Session, error)
}
Repository defines the persistent storage contract required by the Device Authorization plugin. Implement this interface on your custom database adapter (e.g. PostgreSQL, MySQL, SQLite, MongoDB, GORM).
Implementation Example (GORM / database/sql): ¶
type GormDeviceAuthRepository struct {
db *gorm.DB
}
func (r *GormDeviceAuthRepository) FindByDeviceCode(ctx context.Context, deviceCode string) (*deviceauth.DeviceCode, error) {
var dc deviceauth.DeviceCode
if err := r.db.WithContext(ctx).Where("device_code = ?", deviceCode).First(&dc).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, deviceauth.ErrInvalidDeviceCode
}
return nil, err
}
return &dc, nil
}
type RequestDeviceCodeParams ¶
type RequestDeviceCodeParams struct {
// ClientID identifies the client application requesting authorization.
ClientID string `json:"client_id"`
// UserID optionally pre-associates a user ID with the request.
UserID *string `json:"user_id,omitempty"`
// Scope optionally requests specific access permissions.
Scope *string `json:"scope,omitempty"`
}
RequestDeviceCodeParams holds the input parameters for requesting a device code authorization grant.
type TokenResponse ¶
type TokenResponse struct {
// AccessToken is the generated session or access token.
AccessToken string `json:"access_token"`
// TokenType is the token authorization scheme (typically "Bearer").
TokenType string `json:"token_type"`
// ExpiresIn is the session lifetime in seconds.
ExpiresIn int64 `json:"expires_in"`
// Scope optionally returns the granted access scope.
Scope string `json:"scope,omitempty"`
// UserID is the authenticated user ID associated with the session.
UserID string `json:"user_id,omitempty"`
}
TokenResponse represents a successful token issuance response following device authorization.