Skip to content

Commit

Permalink
feat: support exchanging OTP codes for tokens (#438)
Browse files Browse the repository at this point in the history
  • Loading branch information
stevehobbsdev authored Jan 27, 2023
1 parent ab7a193 commit 8eec6ef
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 1 deletion.
43 changes: 43 additions & 0 deletions lib/auth0/api/authentication_endpoints.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module AuthenticationEndpoints

UP_AUTH = 'Username-Password-Authentication'.freeze
JWT_BEARER = 'urn:ietf:params:oauth:grant-type:jwt-bearer'.freeze
GRANT_TYPE_PASSWORDLESS_OPT = 'http://auth0.com/oauth/grant-type/passwordless/otp'.freeze

# Request an API access token using a Client Credentials grant
# @see https://auth0.com/docs/api-auth/tutorials/client-credentials
Expand Down Expand Up @@ -94,6 +95,48 @@ def exchange_refresh_token(
::Auth0::AccessToken.from_response request_with_retry(:post, '/oauth/token', request_params)
end

# Exchange an OTP recieved through SMS for ID and access tokens
# @param phone_number [string] The user's phone number used to receive the OTP
# @param otp [string] The OTP contained in the SMS
# @param audience [string] The audience for the access token (defaults to nil)
# @param scope [string] The scope (defaults to 'openid profile email')
def exchange_sms_otp_for_tokens(phone_number, otp, audience: nil, scope: nil)
request_params = {
grant_type: GRANT_TYPE_PASSWORDLESS_OPT,
client_id: @client_id,
username: phone_number,
otp: otp,
realm: 'sms',
audience: audience,
scope: scope || 'openid profile email'
}

populate_client_assertion_or_secret(request_params)

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

# Exchange an OTP recieved through email for ID and access tokens
# @param email_address [string] The user's email address used to receive the OTP
# @param otp [string] The OTP contained in the email
# @param audience [string] The audience for the access token (defaults to nil)
# @param scope [string] The scope (defaults to 'openid profile email')
def exchange_email_otp_for_tokens(email_address, otp, audience: nil, scope: nil)
request_params = {
grant_type: GRANT_TYPE_PASSWORDLESS_OPT,
client_id: @client_id,
username: email_address,
otp: otp,
realm: 'email',
audience: audience,
scope: scope || 'openid profile email'
}

populate_client_assertion_or_secret(request_params)

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

# rubocop:disable Metrics/ParameterLists
# Get access and ID tokens using Resource Owner Password.
# Requires that your tenant has a Default Audience or Default Directory.
Expand Down
194 changes: 194 additions & 0 deletions spec/lib/auth0/api/authentication_endpoints_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,200 @@
end
end

context 'exchange_sms_otp_for_tokens', focus: true do
it 'requests the tokens using an OTP from SMS' do
expect(RestClient::Request).to receive(:execute) do |arg|
expect(arg).to match(
include(
method: :post,
url: 'https://samples.auth0.com/oauth/token'
)
)

payload = JSON.parse arg[:payload], symbolize_names: true

expect(payload[:grant_type]).to eq 'http://auth0.com/oauth/grant-type/passwordless/otp'
expect(payload[:username]).to eq 'phone_number'
expect(payload[:realm]).to eq 'sms'
expect(payload[:otp]).to eq 'code'
expect(payload[:client_id]).to eq client_id
expect(payload[:client_secret]).to eq client_secret
expect(payload[:scope]).to eq 'openid profile email'
expect(payload[:audience]).to be_nil

StubResponse.new({
"id_token" => "id_token",
"access_token" => "test_access_token",
"expires_in" => 86400},
true,
200)
end

result = client_secret_instance.send :exchange_sms_otp_for_tokens, 'phone_number', 'code'

expect(result).to be_a_kind_of(Auth0::AccessToken)
expect(result.id_token).not_to be_nil
expect(result.access_token).not_to be_nil
expect(result.expires_in).not_to be_nil
end

it 'requests the tokens using OTP from SMS, and overrides scope and audience' do
expect(RestClient::Request).to receive(:execute) do |arg|
expect(arg).to match(
include(
method: :post,
url: 'https://samples.auth0.com/oauth/token'
)
)

payload = JSON.parse arg[:payload], symbolize_names: true

expect(payload[:scope]).to eq 'openid'
expect(payload[:audience]).to eq api_identifier

StubResponse.new({
"id_token" => "id_token",
"access_token" => "test_access_token",
"expires_in" => 86400},
true,
200)
end

result = client_secret_instance.send(:exchange_sms_otp_for_tokens, 'phone_number', 'code',
audience: api_identifier,
scope: 'openid'
)

expect(result).to be_a_kind_of(Auth0::AccessToken)
expect(result.id_token).not_to be_nil
expect(result.access_token).not_to be_nil
expect(result.expires_in).not_to be_nil
end

it 'requests the tokens using an OTP from SMS using client assertion' do
expect(RestClient::Request).to receive(:execute) do |arg|
expect(arg).to match(
include(
method: :post,
url: 'https://samples.auth0.com/oauth/token'
)
)

payload = JSON.parse arg[:payload], symbolize_names: true

expect(payload[:grant_type]).to eq 'http://auth0.com/oauth/grant-type/passwordless/otp'
expect(payload[:client_secret]).to be_nil
expect(payload[:client_assertion]).not_to be_nil
expect(payload[:client_assertion_type]).to eq Auth0::ClientAssertion::CLIENT_ASSERTION_TYPE

StubResponse.new({
"id_token" => "id_token",
"access_token" => "test_access_token",
"expires_in" => 86400},
true,
200)
end

client_assertion_instance.send :exchange_sms_otp_for_tokens, 'phone_number', 'code'
end
end

context 'exchange_email_otp_for_tokens', focus: true do
it 'requests the tokens using email OTP' do
expect(RestClient::Request).to receive(:execute) do |arg|
expect(arg).to match(
include(
method: :post,
url: 'https://samples.auth0.com/oauth/token'
)
)

payload = JSON.parse arg[:payload], symbolize_names: true

expect(payload[:grant_type]).to eq 'http://auth0.com/oauth/grant-type/passwordless/otp'
expect(payload[:username]).to eq 'email_address'
expect(payload[:realm]).to eq 'email'
expect(payload[:otp]).to eq 'code'
expect(payload[:client_id]).to eq client_id
expect(payload[:client_secret]).to eq client_secret
expect(payload[:scope]).to eq 'openid profile email'
expect(payload[:audience]).to be_nil

StubResponse.new({
"id_token" => "id_token",
"access_token" => "test_access_token",
"expires_in" => 86400},
true,
200)
end

result = client_secret_instance.send :exchange_email_otp_for_tokens, 'email_address', 'code'

expect(result).to be_a_kind_of(Auth0::AccessToken)
expect(result.id_token).not_to be_nil
expect(result.access_token).not_to be_nil
expect(result.expires_in).not_to be_nil
end

it 'requests the tokens using OTP from email, and overrides scope and audience' do
expect(RestClient::Request).to receive(:execute) do |arg|
expect(arg).to match(
include(
method: :post,
url: 'https://samples.auth0.com/oauth/token'
)
)

payload = JSON.parse arg[:payload], symbolize_names: true

expect(payload[:scope]).to eq 'openid'
expect(payload[:audience]).to eq api_identifier

StubResponse.new({
"id_token" => "id_token",
"access_token" => "test_access_token",
"expires_in" => 86400},
true,
200)
end

client_secret_instance.send(:exchange_email_otp_for_tokens, 'email_address', 'code',
audience: api_identifier,
scope: 'openid'
)
end

it 'requests the tokens using OTP from email using client assertion' do
expect(RestClient::Request).to receive(:execute) do |arg|
expect(arg).to match(
include(
method: :post,
url: 'https://samples.auth0.com/oauth/token'
)
)

payload = JSON.parse arg[:payload], symbolize_names: true

expect(payload[:grant_type]).to eq 'http://auth0.com/oauth/grant-type/passwordless/otp'
expect(payload[:client_secret]).to be_nil
expect(payload[:client_assertion]).not_to be_nil
expect(payload[:client_assertion_type]).to eq Auth0::ClientAssertion::CLIENT_ASSERTION_TYPE

StubResponse.new({
"id_token" => "id_token",
"access_token" => "test_access_token",
"expires_in" => 86400},
true,
200)
end

client_assertion_instance.send(:exchange_email_otp_for_tokens, 'email_address', 'code',
audience: api_identifier,
scope: 'openid'
)
end
end

context 'login_with_resource_owner' do
it 'logs in using a client secret' do
expect(RestClient::Request).to receive(:execute) do |arg|
Expand Down
2 changes: 1 addition & 1 deletion spec/lib/auth0/api/v2/clients_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
it { expect { @instance.client_credential('1', '') }.to raise_error 'Must specify a credential id' }
end

context '.delete_client_credential', focus: true do
context '.delete_client_credential' do
it { expect(@instance).to respond_to(:delete_client_credential) }

it 'is expected to delete /api/v2/clients/1/credentials/2' do
Expand Down

0 comments on commit 8eec6ef

Please sign in to comment.