Documentation
¶
Overview ¶
Package devicecodes implements the RFC 8628 device authorization grant state machine: code generation, persistence, and approval/redemption.
Index ¶
- Constants
- Variables
- func Approve(ctx context.Context, id, userID uuid.UUID, tokenName string) error
- func DeleteByID(ctx context.Context, id uuid.UUID) error
- func Deny(ctx context.Context, id uuid.UUID) error
- func FormatUserCode(code string) string
- func HashDeviceCode(plain string) []byte
- func NormalizeUserCode(s string) string
- func PurgeExpired(ctx context.Context) error
- func RedeemTx(ctx context.Context, id uuid.UUID) (plain string, tokenRow clitokens.Token, err error)
- type CreateResult
- type DeviceCode
- type PollGuard
- func (g *PollGuard) Allow(key string) (bool, time.Duration)
- func (g *PollGuard) Bump(key string)
- func (g *PollGuard) Forget(key string)
- func (g *PollGuard) Recorded(key string)
- func (g *PollGuard) StartSweeper(ctx context.Context, every, evictAfter time.Duration)
- func (g *PollGuard) Sweep(evictAfter time.Duration)
- type UserCodeLockout
Constants ¶
const DeviceCodePrefix = "slm_dc_"
DeviceCodePrefix is the literal prefix returned to clients in /oauth/device/code.
const DeviceCodeTTL = 10 * time.Minute
DeviceCodeTTL is how long a device_code remains valid.
const PurgeOlderThan = 1 * time.Hour
PurgeOlderThan is the grace window used by PurgeExpired.
const UserCodeLength = 8
UserCodeLength is the number of characters in a user_code (before display formatting).
Variables ¶
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 ¶
Approve stamps approval onto the row. Errors if the row no longer exists, is denied, or is already approved (state-changed protection).
func DeleteByID ¶
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 ¶
Deny stamps denial onto the row. Errors if the row no longer exists, is approved, or is already denied.
func FormatUserCode ¶
FormatUserCode inserts a dash in the middle of an 8-char code for display. Codes that aren't UserCodeLength are returned unchanged.
func HashDeviceCode ¶
HashDeviceCode returns the SHA-256 hash for a plaintext device_code. Used by the /oauth/token handler to look up the row.
func NormalizeUserCode ¶
NormalizeUserCode strips dashes and whitespace and uppercases. Use it on any user-supplied user_code before lookup.
func PurgeExpired ¶
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).
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 ¶
NewPollGuard returns a guard with the given baseline interval. Pass time.Now in production.
func (*PollGuard) Allow ¶
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 ¶
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 ¶
Forget removes a key's entry. Call this after the device_code reaches a terminal state (redeemed, denied, expired).
func (*PollGuard) Recorded ¶
Recorded must be called after a successful Allow when the poll actually happened.
func (*PollGuard) StartSweeper ¶
StartSweeper runs Sweep on the given ticker until ctx is canceled. Wire this from cmd/server with a context that ends on shutdown.
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).