Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,44 @@ client.authorization_url 'http://localhost:3000'
# => #<URI::HTTPS https://YOUR_DOMAIN/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000>
```

## Device Authorization Flow

The [Device Authorization Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow) lets input-constrained devices, such as a CLI or a smart TV, obtain tokens by having the user complete the login in a browser elsewhere.

Start the flow, show the user code to the user, then poll for the tokens until they finish:

```ruby
require 'auth0'

client = Auth0::Client.new(
client_id: ENV['AUTH0_RUBY_CLIENT_ID'],
domain: ENV['AUTH0_RUBY_DOMAIN']
)

flow = client.start_device_flow(
scope: 'openid profile offline_access',
audience: 'https://api.example.com'
)

puts "Go to #{flow['verification_uri']} and enter the code #{flow['user_code']}"

# Poll no more frequently than the interval Auth0 returns, until the code expires.
tokens = loop do
sleep flow['interval']

begin
break client.exchange_device_code_for_tokens(flow['device_code'])
rescue Auth0::HTTPError => e
# While the user has not finished, Auth0 answers with an `error` of
# `authorization_pending` or `slow_down`. Anything else is terminal.
error = JSON.parse(e.message)['error'] rescue nil
raise unless %w[authorization_pending slow_down].include?(error)
end
end

tokens.access_token
```

## Management API Client

As a simple example of how to get started with the Management API, we'll create an admin route to point to a list of all users from Auth0:
Expand Down
34 changes: 34 additions & 0 deletions lib/auth0/api/authentication_endpoints.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,40 @@ def exchange_auth_code_for_tokens(
::Auth0::AccessToken.from_response request_with_retry(:post, '/oauth/token', request_params)
end

# 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

# Get access and ID tokens using a refresh token.
# @see https://auth0.com/docs/api/authentication#refresh-token
# @param refresh_token [string] Refresh token to use. Request this with
Expand Down
60 changes: 60 additions & 0 deletions test/unit/authentication_endpoints_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,66 @@ def test_exchange_auth_code_for_tokens_with_client_assertion
refute_nil result.access_token
end

# --- start_device_flow ---

def test_start_device_flow_requests_a_device_code
stub_request(:post, "https://#{@domain}/oauth/device/code")
.with do |req|
body = JSON.parse(req.body, symbolize_names: true)
body[:client_id] == @client_id &&
body[:scope] == "openid profile" &&
body[:audience] == "https://api.example.com"
end
.to_return(
status: 200,
body: {
"device_code" => "the_device_code",
"user_code" => "ABCD-EFGH",
"verification_uri" => "https://#{@domain}/activate",
"expires_in" => 900,
"interval" => 5
}.to_json,
headers: { "Content-Type" => "application/json" }
)

result = @client_secret_instance.send(
:start_device_flow, scope: "openid profile", audience: "https://api.example.com"
)

assert_equal "the_device_code", result["device_code"]
assert_equal "ABCD-EFGH", result["user_code"]
assert_equal 5, result["interval"]
end

# --- exchange_device_code_for_tokens ---

def test_exchange_device_code_for_tokens
stub_request(:post, "https://#{@domain}/oauth/token")
.with do |req|
body = JSON.parse(req.body, symbolize_names: true)
body[:grant_type] == "urn:ietf:params:oauth:grant-type:device_code" &&
body[:device_code] == "the_device_code" &&
body[:client_id] == @client_id
end
.to_return(
status: 200,
body: { "id_token" => "id_token", "access_token" => "test_access_token", "expires_in" => 86_400 }.to_json,
headers: { "Content-Type" => "application/json" }
)

result = @client_secret_instance.send(:exchange_device_code_for_tokens, "the_device_code")

assert_kind_of Auth0::AccessToken, result
refute_nil result.access_token
refute_nil result.id_token
end

def test_exchange_device_code_for_tokens_raises_on_empty
assert_raises(Auth0::InvalidParameter) do
@client_secret_instance.send(:exchange_device_code_for_tokens, "")
end
end

# --- exchange_refresh_token ---

def test_exchange_refresh_token_with_client_secret
Expand Down
Loading