Skip to content

Add Device Authorization Flow support to the Authentication API #792

Description

@gagalago

Checklist

  • I have looked into the Readme and Examples, and have not found a suitable solution or answer.
  • I have looked into the API documentation and have not found a suitable solution or answer.
  • I have searched the issues and have not found a suitable solution or answer.
  • I have searched the Auth0 Community forums and have not found a suitable solution or answer.
  • I agree to the terms within the Auth0 Code of Conduct.

Describe the problem you'd like to have solved

ruby-auth0 has no support for the Device Authorization Flow. As of v6.1.0 there is no way to call POST /oauth/device/code, and no way to exchange a device code at POST /oauth/token with grant_type=urn:ietf:params:oauth:grant-type:device_code.

To be clear about a near-miss: Auth0::Api::V2::DeviceCredentials is the Management API device-credentials resource (listing and deleting device public keys). It is unrelated to the OAuth 2.0 Device Authorization Grant.

Auth0 documents this flow and ships it in at least one other SDK — Auth0.NET exposes StartDeviceFlowAsync(DeviceCodeRequest) returning DeviceCodeResponse, and GetTokenAsync(DeviceCodeTokenRequest), declared on IAuthenticationApiClient and covered by integration tests.

We run a small Rails service that acts as an OAuth proxy for our command-line client. It starts a Device Authorization Flow, hands user_code and verification_uri back to the CLI, then polls for the token. Because the flow is not reachable from this SDK, that service talks to /oauth/device/code and /oauth/token through a hand-written Faraday client instead — including re-implementing the four polling states (authorization_pending, slow_down, expired_token, access_denied).

We would rather depend on this SDK than keep maintaining that.

Describe the ideal solution

Two methods on Auth0::Api::AuthenticationEndpoints, named after the existing start_passwordless_sms_flow / exchange_sms_otp_for_tokens pair so they read like their neighbours:

# Start a Device Authorization flow.
# @see https://auth0.com/docs/api/authentication#device-authorization-flow
# @param scope [string] Space-separated list of requested scopes.
# @param audience [string] Unique identifier of the target API.
# @param client_id [string] Client ID for the application
# @return [json] Returns device_code, user_code, verification_uri,
#   verification_uri_complete, expires_in and interval.
def start_device_flow(scope: nil, audience: nil, client_id: @client_id)
  request_params = {
    client_id: client_id,
    scope: scope,
    audience: audience
  }

  request_with_retry(:post, '/oauth/device/code', request_params)
end

# Get access and ID tokens using a device code.
# @see https://auth0.com/docs/api/authentication#device-authorization-flow
# @param device_code [string] The device code returned by start_device_flow.
# @param client_id [string] Client ID for the application
# @return [Auth0::AccessToken] Returns the access_token and id_token
def exchange_device_code_for_tokens(device_code, client_id: @client_id)
  raise Auth0::InvalidParameter, 'Must provide a device code' if device_code.to_s.empty?

  request_params = {
    grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
    client_id: client_id,
    device_code: device_code
  }

  ::Auth0::AccessToken.from_response request_with_retry(:post, '/oauth/token', request_params)
end

Two things that keep this small:

  • No new exception classes are required. While polling, Auth0 answers with an HTTP error whose body carries an error code. Mixins::HTTPProxy#request already raises Auth0::HTTPError subclasses with the raw response body as the message, so a caller can distinguish authorization_pending from slow_down today. A documented accessor for that code would be a nice follow-up, but it is not a prerequisite.
  • This does not touch the Fern-generated half. The v6 migration guide states that "This major version change does not affect the Authentication API", and lib/auth0/api/ is listed in .fernignore, so these additions live in the hand-maintained module and should not be affected by regeneration.

Alternatives and current workarounds

There is no workaround within the SDK — the endpoint is simply not there. request_with_retry could be called directly, but it is not part of the documented public API, so we do not want to build on it.

Our current answer is the separate Faraday client described above.

Additional context

Happy to open a pull request implementing the above, following the shape and conventions in authentication_endpoints.rb, with unit tests alongside the existing ones in test/unit/authentication_endpoints_test.rb. It is essentially the code we already run in production, rewritten to match this codebase.

I am asking first rather than opening it directly because of how the last comparable contribution went: #305 asked for PKCE support, #308 implemented it and was reviewed positively, and it was closed in January 2022 in favour of "a more holistic approach to PKCE support across multiple endpoints" that does not appear to have landed. I would rather know whether targeted additions to Auth0::Api::AuthenticationEndpoints are welcome before writing the code.

(I have separately opened #791 for the PKCE half, which is a single optional keyword on an existing method.)

Environment: auth0 v5.20.0 and v6.1.0, Ruby 3.3 and 4.0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions