devicecodes

package
v0.0.9 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package devicecodes implements the RFC 8628 device authorization grant state machine: code generation, persistence, and approval/redemption.

Index

Constants

View Source
const DeviceCodePrefix = "slm_dc_"

DeviceCodePrefix is the literal prefix returned to clients in /oauth/device/code.

View Source
const DeviceCodeTTL = 10 * time.Minute

DeviceCodeTTL is how long a device_code remains valid.

View Source
const PurgeOlderThan = 1 * time.Hour

PurgeOlderThan is the grace window used by PurgeExpired.

View Source
const UserCodeLength = 8

UserCodeLength is the number of characters in a user_code (before display formatting).

Variables

View Source
var (
	// ErrNotFound is returned when a device_code or user_code lookup misses.
	ErrNotFound = errors.New("device code not found")
	// ErrExpired is returned when the row exists but expires_at is in the past.
	ErrExpired = errors.New("device code expired")
	// ErrAlreadyApproved is returned when the device code has already been approved.
	ErrAlreadyApproved = errors.New("device code already approved")
	// ErrAlreadyDenied is returned when the device code has already been denied.
	ErrAlreadyDenied = errors.New("device code already denied")
	// ErrNotApproved is returned when a device code is redeemed before approval.
	ErrNotApproved = errors.New("device code not yet approved")
	// ErrPolledTooFast is returned by the poll guard when /oauth/token is
	// hit again before the enforced interval has elapsed.
	ErrPolledTooFast = errors.New("polled too fast")
)

Functions

func Approve

func Approve(ctx context.Context, id, userID uuid.UUID, tokenName string) error

Approve stamps approval onto the row. Errors if the row no longer exists, is denied, or is already approved (state-changed protection).

func DeleteByID

func DeleteByID(ctx context.Context, id uuid.UUID) error

DeleteByID removes a row regardless of state. Used after the CLI's poll observes a terminal state (expired or denied) so it isn't re-observed.

func Deny

func Deny(ctx context.Context, id uuid.UUID) error

Deny stamps denial onto the row. Errors if the row no longer exists, is approved, or is already denied.

func FormatUserCode

func FormatUserCode(code string) string

FormatUserCode inserts a dash in the middle of an 8-char code for display. Codes that aren't UserCodeLength are returned unchanged.

func HashDeviceCode

func HashDeviceCode(plain string) []byte

HashDeviceCode returns the SHA-256 hash for a plaintext device_code. Used by the /oauth/token handler to look up the row.

func NormalizeUserCode

func NormalizeUserCode(s string) string

NormalizeUserCode strips dashes and whitespace and uppercases. Use it on any user-supplied user_code before lookup.

func PurgeExpired

func PurgeExpired(ctx context.Context) error

PurgeExpired removes rows whose expires_at is more than PurgeOlderThan in the past. Called opportunistically from Create.

func RedeemTx

func RedeemTx(ctx context.Context, id uuid.UUID) (plain string, tokenRow clitokens.Token, err error)

RedeemTx is the transactional finalization step: re-read FOR UPDATE, verify the row is approved and not expired, insert the cli_tokens row, delete the device_codes row, return the token plaintext.

Types

type CreateResult

type CreateResult struct {
	DeviceCodePlain string
	UserCodePlain   string
	Row             DeviceCode
}

CreateResult is what Create returns: the row plus the plaintext device_code (only available at creation time).

func Create

func Create(ctx context.Context, clientName string) (CreateResult, error)

Create opportunistically purges old rows and inserts a fresh device_code + user_code. Retries on user_code collision.

type DeviceCode

type DeviceCode struct {
	ExpiresAt         time.Time  `db:"expires_at"`
	ClientName        *string    `db:"client_name"`
	ApprovedAt        *time.Time `db:"approved_at"`
	ApprovedUserID    *uuid.UUID `db:"approved_user_id"`
	ApprovedTokenName *string    `db:"approved_token_name"`
	DeniedAt          *time.Time `db:"denied_at"`
	UserCode          string     `db:"user_code"`
	DeviceCodeHash    []byte     `db:"device_code_hash"`
	ID                uuid.UUID  `db:"id"`
}

DeviceCode is the persistent shape of a device_codes row.

func LookupByDeviceCodeHash

func LookupByDeviceCodeHash(ctx context.Context, hash []byte) (DeviceCode, error)

LookupByDeviceCodeHash fetches the row by hash. Returns ErrNotFound, ErrExpired, ErrAlreadyDenied as appropriate (callers care about state). ErrAlreadyApproved is not returned — approved rows are a normal step in the redemption flow and the caller distinguishes via ApprovedAt.

func LookupByUserCode

func LookupByUserCode(ctx context.Context, userCode string) (DeviceCode, error)

LookupByUserCode fetches a row by user_code, normalized. Same error shape as LookupByDeviceCodeHash.

type PollGuard

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

PollGuard is an in-memory per-device_code poll-rate gate. Single instance only by design.

func NewPollGuard

func NewPollGuard(baseline time.Duration, clk func() time.Time) *PollGuard

NewPollGuard returns a guard with the given baseline interval. Pass time.Now in production.

func (*PollGuard) Allow

func (g *PollGuard) Allow(key string) (bool, time.Duration)

Allow reports whether a poll for the given key may proceed. The second return is how long the caller should advise the client to wait (zero if ok is true).

func (*PollGuard) Bump

func (g *PollGuard) Bump(key string)

Bump adds 5s to the enforced interval for this key. Called after a slow_down response so subsequent polls are spaced further.

func (*PollGuard) Forget

func (g *PollGuard) Forget(key string)

Forget removes a key's entry. Call this after the device_code reaches a terminal state (redeemed, denied, expired).

func (*PollGuard) Recorded

func (g *PollGuard) Recorded(key string)

Recorded must be called after a successful Allow when the poll actually happened.

func (*PollGuard) StartSweeper

func (g *PollGuard) StartSweeper(ctx context.Context, every, evictAfter time.Duration)

StartSweeper runs Sweep on the given ticker until ctx is canceled. Wire this from cmd/server with a context that ends on shutdown.

func (*PollGuard) Sweep

func (g *PollGuard) Sweep(evictAfter time.Duration)

Sweep drops entries that have not been polled in evictAfter. Suitable for a 1-minute ticker goroutine.

type UserCodeLockout

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

UserCodeLockout is an in-memory per-user_code failed-lookup counter. Single instance per process; mirrors recovery.Limiter's shape.

func NewUserCodeLockout

func NewUserCodeLockout(clk func() time.Time) *UserCodeLockout

NewUserCodeLockout returns an empty lockout. Pass time.Now in production.

func (*UserCodeLockout) Allow

func (l *UserCodeLockout) Allow(userCode string) bool

Allow reports whether a /activate lookup for the given user_code may proceed. Does not record an attempt.

func (*UserCodeLockout) RecordFailure

func (l *UserCodeLockout) RecordFailure(userCode string)

RecordFailure logs a failed lookup for the user_code.

func (*UserCodeLockout) RecordSuccess

func (l *UserCodeLockout) RecordSuccess(userCode string)

RecordSuccess clears the failure history (e.g., after a successful approve/deny so the code's pending lockout is cleared).

Jump to

Keyboard shortcuts

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