Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support exchanging OTP codes for tokens #438

Merged
merged 1 commit into from
Jan 27, 2023
Merged
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
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