Skip to content

feat: add Device Authorization Flow to the Authentication API - #794

Open
gagalago wants to merge 3 commits into
auth0:masterfrom
gagalago:feat/device-authorization-flow
Open

feat: add Device Authorization Flow to the Authentication API#794
gagalago wants to merge 3 commits into
auth0:masterfrom
gagalago:feat/device-authorization-flow

Conversation

@gagalago

Copy link
Copy Markdown
Contributor

Changes

Implements the Device Authorization Flow in Auth0::Api::AuthenticationEndpoints. This is the proposal from #792, opened as a pull request per the general contributing guidelines ("PRs to our libraries are always welcome"); happy to close it if you would rather settle the design on the issue first.

Two new methods, additive only — no existing method, endpoint or signature changes:

  • start_device_flow(scope:, audience:, client_id:)POST /oauth/device/code, returning the parsed body (device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval).
  • exchange_device_code_for_tokens(device_code, client_id:)POST /oauth/token with grant_type=urn:ietf:params:oauth:grant-type:device_code, returning an Auth0::AccessToken.

They are named after the existing start_passwordless_sms_flow / exchange_sms_otp_for_tokens pair so they read like their neighbours, and they follow the conventions already in the file: an Auth0::InvalidParameter guard on the required positional argument, a request_params hash, request_with_retry, and ::Auth0::AccessToken.from_response for the token response.

Note this is unrelated to Auth0::Api::V2::DeviceCredentials, which is the Management API device-credentials resource.

No new exception classes. While the user completes their part, Auth0 answers with an HTTP error whose body carries an error of authorization_pending or slow_down. Mixins::HTTPProxy#request already raises Auth0::HTTPError subclasses with the raw response body as the message — AccessDenied for 403 and BadRequest for 400, both HTTPError — so callers can distinguish the polling states with what is already there. A documented accessor for that error code would be a good follow-up, but it is deliberately not in this pull request.

References

For prior art inside the SDK family, Auth0.NET exposes the same two operations as StartDeviceFlowAsync(DeviceCodeRequest) and GetTokenAsync(DeviceCodeTokenRequest) on IAuthenticationApiClient.

Motivation: we run a small Rails service that acts as an OAuth proxy for our command-line client. It starts the flow, hands user_code and verification_uri to the CLI, and polls for the token. With this in place we can delete the hand-written Faraday client that exists only because the flow is not reachable from this SDK.

Testing

Three cases added to test/unit/authentication_endpoints_test.rb, alongside the existing ones and using the same WebMock body-matching style:

  • test_start_device_flow_requests_a_device_code — asserts client_id, scope and audience are sent to /oauth/device/code, and that the response fields come back
  • test_exchange_device_code_for_tokens — asserts the urn:ietf:params:oauth:grant-type:device_code grant type and device_code are sent, and that an Auth0::AccessToken is returned
  • test_exchange_device_code_for_tokens_raises_on_empty — the InvalidParameter guard
bundle exec rake test TEST=test/unit/authentication_endpoints_test.rb TESTOPTS="--name=/device_code|start_device_flow/"

I checked the tests are not vacuous: pointing start_device_flow at a different path makes its test fail, and restoring it makes it pass again.

Ruby full suite
3.3.9 555 runs, 4585 assertions, 0 failures, 0 errors
4.0.6 555 runs, 0 failures, 1 error — the pre-existing CGI.parse failure on master, unrelated to this change and fixed by #793

The three new tests pass on both (3 runs, 7 assertions, 0 failures).

bundle exec rubocop --force-exclusion reports no offences on the changed files. As with the rest of authentication_endpoints.rb, I followed the file's single-quoted style — it is listed under AllCops.Exclude in .rubocop.yml as ported verbatim from the legacy SDK.

Documentation

Added a Device Authorization Flow section to EXAMPLES.md, showing the full loop: start the flow, display the user code, poll on the returned interval, and treat authorization_pending / slow_down as continue-conditions.

  • This change adds unit test coverage
  • This change adds integration test coverage
  • This change has been tested on the latest version of Ruby

Checklist

@gagalago
gagalago requested a review from a team as a code owner August 24, 2026 14:01
@gagalago
gagalago force-pushed the feat/device-authorization-flow branch from 56cf804 to 39cfd27 Compare August 31, 2026 13:00
@gagalago
gagalago force-pushed the feat/device-authorization-flow branch from 39cfd27 to dc54547 Compare September 3, 2026 10:29
Comment thread lib/auth0/api/authentication_endpoints.rb
Comment thread EXAMPLES.md Outdated
Comment thread test/unit/authentication_endpoints_test.rb
Comment thread test/unit/authentication_endpoints_test.rb
Document that device flow runs as a public client and deliberately sends
no client_secret or client assertion, and pin that in both tests so a
later change to the auth behaviour cannot pass unnoticed.

Back off by 5 seconds on slow_down in the polling example, per RFC 8628
section 3.5, and include verification_uri_complete in the stubbed device
code response so it matches what Auth0 actually returns.
The two device tests only covered a client configured with a secret.
Mirror the _with_client_secret / _with_client_assertion pair the sibling
token methods use, so a configured signing key is proven not to be sent
on either device endpoint.
@gagalago

gagalago commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

All four fair

Auth0 refuses device flow if you send credentials: /oauth/device/code returns 200 with just client_id/audience/scope, and 403 unauthorized_client the moment a client_secret is added. So calling the helper here would break any client that has a secret configured. Both methods now say that, and both tests assert no client_secret or client_assertion reaches the body — including on a signing-key client, mirroring the _with_client_secret / _with_client_assertion pairing you pointed at.
I didn't add a runtime raise, though. An app can hold a secret for the authorization-code and refresh paths and still use device flow as a public client — that's our proxy — so raising would reject a valid caller.
slow_down was a genuine bug in the example: it polled straight back at the throttled rate. It now adds 5s to the interval per RFC 8628 §3.5 and keeps it there.

And verification_uri_complete is in the stub and asserted now — real responses do include it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants