When you need it
Some devices can’t host a browser:
- Smart TVs
- Voice assistants
- Kiosks
- Conversational call-centre agents (the agent is the device)
The user authenticates on their phone and the device polls until it gets the token. RFC 8628 standardises the flow.
The dance
- Device calls
/oauth/device/begin→ server returnsuser_code,device_code,verification_uri. - Device displays
user_codeandverification_urito the user. - User opens the URI on their phone, enters the code, signs in, approves.
- Device polls
/oauth/device/tokenwithdevice_codeevery few seconds. - Server returns the token once the user approves.
What it looks like in Genie
// pkg/auth/oauth_device/oauth_device.go
func (s *Service) Begin() (AuthorizationResponse, error) {
deviceCode, _ := randomToken(32)
userCode := randomUserCode() // 6 chars, easy to type
s.pending[deviceCode] = &pendingAuth{
UserCode: userCode,
DeviceCode: deviceCode,
Status: StatusPending,
ExpiresAt: time.Now().Add(s.ttl),
}
return AuthorizationResponse{
DeviceCode: deviceCode,
UserCode: userCode,
VerificationURI: s.verificationURI,
ExpiresIn: int(s.ttl.Seconds()),
Interval: 5, // poll every 5s
}, nil
}
The polling endpoint:
func (s *Service) Poll(deviceCode string) (TokenResponse, error) {
p := s.pending[deviceCode]
if p == nil { return TokenResponse{}, ErrExpiredToken }
switch p.Status {
case StatusPending:
return TokenResponse{}, ErrAuthorizationPending
case StatusDenied:
return TokenResponse{}, ErrAccessDenied
case StatusApproved:
return TokenResponse{Token: p.Token, ...}, nil
}
}
The Approve step is what the user triggers from the phone after signing in:
func (s *Service) Approve(userCode, token string) error {
for _, p := range s.pending {
if p.UserCode == userCode {
p.Token = token
p.Status = StatusApproved
return nil
}
}
return ErrUnknownUserCode
}
What teams get wrong
Long user codes. “EXVHF7K23WP9” is harder to type than “EXVH-F7K2”. 6-8 characters with optional hyphens; alphanumeric without ambiguous chars (no 0, O, I, 1).
Polling intervals too short. The spec says the server can ask the client to back off via the interval field. Honour it. Aggressive polling is the second-most-common reason these flows get rate-limited.
No expiry on the device_code. A device that polls forever is a DDoS vector. 10-15 minutes is the working range; force the user to start over after.
Where Genie uses it
The conversational call-centre agent — the operator’s phone screen displays the user_code; the customer authenticates via their banking app; the agent gets the token. Same pattern works for any voice-driven onboarding flow where a browser isn’t natural.