·2 min read·← All posts
Go OAuth Device Flow Voice AI

When you need it

Some devices can’t host a browser:

The user authenticates on their phone and the device polls until it gets the token. RFC 8628 standardises the flow.

The dance

  1. Device calls /oauth/device/begin → server returns user_code, device_code, verification_uri.
  2. Device displays user_code and verification_uri to the user.
  3. User opens the URI on their phone, enters the code, signs in, approves.
  4. Device polls /oauth/device/token with device_code every few seconds.
  5. 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.

← Back to all posts