Class: Ask::Auth::Providers::DeviceOAuth
- Defined in:
- lib/ask/auth/providers/device_oauth.rb
Overview
RFC 8628 device authorization grant (OAuth 2.0 device flow) — the headless-friendly alternative to authorization-code PKCE: no redirect_uri, no callback route. The user opens a URL on any device, types a short code, and we long-poll the token endpoint.
provider = Ask::Auth::Providers::Xai.new
device = provider.start_device_flow(user: current_user)
# render device[:verification_uri] + device[:user_code]
tokens = provider.complete_device_flow(user:, device_code: device[:device_code])
# raises PendingAuthorization until the user authorizes
Subclasses set client_id, token_url, device_code_url, and scope.
Direct Known Subclasses
Defined Under Namespace
Classes: PendingAuthorization
Constant Summary collapse
- DEVICE_CODE_GRANT_TYPE =
"urn:ietf:params:oauth:grant-type:device_code"
Instance Attribute Summary
Attributes inherited from OAuth
Instance Method Summary collapse
-
#complete_device_flow(user: nil, device_code:) ⇒ Object
Step 2: poll the token endpoint.
-
#initialize(storage: nil, client_id: nil, token_url: nil, device_code_url: nil, scope: nil, http: Ask::Auth::OAuth::HTTP) ⇒ DeviceOAuth
constructor
A new instance of DeviceOAuth.
-
#start_device_flow(user: nil) ⇒ Object
Step 1: ask the provider for a device + user code.
Methods inherited from OAuth
#authorize!, #authorize_url, #call, #generate_code_challenge, #generate_code_verifier, #refresh
Constructor Details
#initialize(storage: nil, client_id: nil, token_url: nil, device_code_url: nil, scope: nil, http: Ask::Auth::OAuth::HTTP) ⇒ DeviceOAuth
Returns a new instance of DeviceOAuth.
27 28 29 30 31 32 |
# File 'lib/ask/auth/providers/device_oauth.rb', line 27 def initialize(storage: nil, client_id: nil, token_url: nil, device_code_url: nil, scope: nil, http: Ask::Auth::OAuth::HTTP) super(storage: storage, client_id: client_id, authorize_url: nil, token_url: token_url, redirect_uri: nil, scope: scope, http: http) @device_code_url = device_code_url end |
Instance Method Details
#complete_device_flow(user: nil, device_code:) ⇒ Object
Step 2: poll the token endpoint. Returns the token hash on success (same shape as #authorize!), or raises PendingAuthorization when the user hasn't authorized yet.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/ask/auth/providers/device_oauth.rb', line 57 def complete_device_flow(user: nil, device_code:) status, body = @http.post_form(token_url_for, { grant_type: DEVICE_CODE_GRANT_TYPE, device_code: device_code, client_id: @client_id }) data = parse_json(body) case status when 200 parse_token_response(body) when 400 case data["error"] when "authorization_pending", "slow_down" raise PendingAuthorization when "access_denied", "expired_token" raise OAuthError, "device authorization #{data["error"]}" else raise OAuthError, "device authorization failed (#{status}): #{data["error_description"] || data["error"]}" end else raise OAuthError, "device authorization failed (#{status}): #{body.to_s[0, 200]}" end end |
#start_device_flow(user: nil) ⇒ Object
Step 1: ask the provider for a device + user code. Returns { device_code:, user_code:, verification_uri:, interval:, expires_at: }.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/ask/auth/providers/device_oauth.rb', line 36 def start_device_flow(user: nil) raise OAuthError, "device_code_url not configured" if @device_code_url.to_s.empty? status, body = @http.post_form(@device_code_url, {client_id: @client_id, scope: @scope.to_s}) raise OAuthError, "device authorization request failed (#{status}): #{body.to_s[0, 200]}" unless status == 200 data = JSON.parse(body) { device_code: data["device_code"], user_code: data["user_code"], verification_uri: data["verification_uri"] || data["verification_url"], interval: data["interval"].to_i, expires_at: data["expires_in"].to_i > 0 ? Time.now + data["expires_in"].to_i : nil } rescue JSON::ParserError => e raise OAuthError, "bad device authorization response: #{e.}" end |