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

[SDK-4142] Add support for /oauth/par #470

Merged
merged 6 commits into from
Apr 24, 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
37 changes: 37 additions & 0 deletions lib/auth0/api/authentication_endpoints.rb
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,21 @@ def authorization_url(redirect_uri, options = {})
URI::HTTPS.build(host: @domain, path: '/authorize', query: to_query(request_params))
end

# Return an authorization URL for PAR requests
# @see https://www.rfc-editor.org/rfc/rfc9126.html
# @param request_uri [string] The request_uri as obtained by calling `pushed_authorization_request`
# @param additional_parameters Any additional parameters to send
def par_authorization_url(request_uri)
raise Auth0::InvalidParameter, 'Must supply a valid request_uri' if request_uri.to_s.empty?

request_params = {
client_id: @client_id,
request_uri: request_uri,
}

URI::HTTPS.build(host: @domain, path: '/authorize', query: to_query(request_params))
end

# Returns an Auth0 logout URL with a return URL.
# @see https://auth0.com/docs/api/authentication#logout
# @see https://auth0.com/docs/logout
Expand All @@ -344,6 +359,28 @@ def logout_url(return_to, include_client: false, federated: false)
)
end

# Make a request to the PAR endpoint and receive a `request_uri` to send to the '/authorize' endpoint.
# @see https://auth0.com/docs/api/authentication#authorization-code-grant
# @param redirect_uri [string] URL to redirect after authorization
# @param options [hash] Can contain response_type, connection, state, organization, invitation, and additional_parameters.
# @return [url] Authorization URL.
def pushed_authorization_request(parameters = {})
request_params = {
client_id: @client_id,
response_type: parameters.fetch(:response_type, 'code'),
connection: parameters.fetch(:connection, nil),
redirect_uri: parameters.fetch(:redirect_uri, nil),
state: parameters.fetch(:state, nil),
scope: parameters.fetch(:scope, nil),
organization: parameters.fetch(:organization, nil),
invitation: parameters.fetch(:invitation, nil)
}.merge(parameters.fetch(:additional_parameters, {}))

populate_client_assertion_or_secret(request_params)

request_with_retry(:post_form, '/oauth/par', request_params, {})
end

# Return a SAMLP URL.
# The SAML Request AssertionConsumerServiceURL will be used to POST back
# the assertion and it must match with the application callback URL.
Expand Down
7 changes: 5 additions & 2 deletions lib/auth0/mixins/httpproxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module HTTPProxy
BASE_DELAY = 100

# proxying requests from instance methods to HTTP class methods
%i(get post post_file put patch delete delete_with_body).each do |method|
%i(get post post_file post_form put patch delete delete_with_body).each do |method|
Copy link
Contributor Author

@stevehobbsdev stevehobbsdev Apr 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until now the SDK didn't have a way to send a POST request with application/x-www-form-urlencoded, it always assumed JSON. To support this, I added a new method type here (which ultimately ends up as a POST, similar to how :post_file is used).

The alternative was to add an option to the method that signals the developer wants to use form data, but there was no way to do this without introducing a potential breaking change.

define_method(method) do |uri, body = {}, extra_headers = {}|
body = body.delete_if { |_, v| v.nil? }
token = get_token()
Expand Down Expand Up @@ -85,9 +85,12 @@ def request(method, uri, body = {}, extra_headers = {})
elsif method == :post_file
body.merge!(multipart: true)
# Ignore the default Content-Type headers and let the HTTP client define them
post_file_headers = headers.slice(*headers.keys - ['Content-Type'])
post_file_headers = headers.except('Content-Type') if headers != nil
# Actual call with the altered headers
call(:post, encode_uri(uri), timeout, post_file_headers, body)
elsif method == :post_form
form_post_headers = headers.except('Content-Type') if headers != nil
call(:post, encode_uri(uri), timeout, form_post_headers, body.compact)
else
call(method, encode_uri(uri), timeout, headers, body.to_json)
end
Expand Down
90 changes: 90 additions & 0 deletions spec/lib/auth0/api/authentication_endpoints_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
let(:client_secret) { 'test-client-secret' }
let(:api_identifier) { 'test-audience' }
let(:domain) { 'samples.auth0.com' }
let(:request_uri) { 'urn:ietf:params:oauth:request_uri:the.request.uri' }

let(:client_secret_config) { {
domain: domain,
Expand Down Expand Up @@ -628,5 +629,94 @@
client_assertion_instance.send :start_passwordless_sms_flow, '123456789'
end
end

context 'par_authorization_url' do
it 'throws an exception if request_uri is nil' do
expect { client_secret_instance.send :par_authorization_url, nil}.to raise_error Auth0::InvalidParameter
end

it 'throws an exception if request_uri is empty' do
expect { client_secret_instance.send :par_authorization_url, ''}.to raise_error Auth0::InvalidParameter
end

it 'builds a URL containing the request_uri' do
url = client_secret_instance.send :par_authorization_url, request_uri
expect(CGI.unescape(url.to_s)).to eq("https://samples.auth0.com/authorize?client_id=#{client_id}&request_uri=#{request_uri}")
end
end

context 'pushed_authorization_request' do
it 'sends the request as a form post' do
expect(RestClient::Request).to receive(:execute) do |arg|
expect(arg[:url]).to eq('https://samples.auth0.com/oauth/par')
expect(arg[:method]).to eq(:post)

expect(arg[:payload]).to eq({
client_id: client_id,
client_secret: client_secret,
response_type: 'code',
})

StubResponse.new({}, true, 200)
end

client_secret_instance.send :pushed_authorization_request
end

it 'allows the RestClient to handle the correct header defaults' do
expect(RestClient::Request).to receive(:execute) do |arg|
expect(arg[:headers]).not_to have_key('Content-Type')

StubResponse.new({}, true, 200)
end

client_secret_instance.headers['Content-Type'] = 'application/x-www-form-urlencoded'
client_secret_instance.send :pushed_authorization_request
end

it 'sends the request as a form post with all known overrides' do
expect(RestClient::Request).to receive(:execute) do |arg|
expect(arg[:url]).to eq('https://samples.auth0.com/oauth/par')
expect(arg[:method]).to eq(:post)

expect(arg[:payload]).to eq({
client_id: client_id,
client_secret: client_secret,
connection: 'google-oauth2',
organization: 'org_id',
invitation: 'http://invite.url',
redirect_uri: 'http://localhost:3000',
response_type: 'id_token',
scope: 'openid',
state: 'random_value'
})

StubResponse.new({}, true, 200)
end

client_secret_instance.send(:pushed_authorization_request,
response_type: 'id_token',
redirect_uri: 'http://localhost:3000',
organization: 'org_id',
invitation: 'http://invite.url',
scope: 'openid',
state: 'random_value',
connection: 'google-oauth2')
end

it 'sends the request as a form post using client assertion' do
expect(RestClient::Request).to receive(:execute) do |arg|
expect(arg[:url]).to eq('https://samples.auth0.com/oauth/par')
expect(arg[:method]).to eq(:post)
expect(arg[:payload][:client_secret]).to be_nil
expect(arg[:payload][:client_assertion]).not_to be_nil
expect(arg[:payload][:client_assertion_type]).to eq Auth0::ClientAssertion::CLIENT_ASSERTION_TYPE

StubResponse.new({}, true, 200)
end

client_assertion_instance.send :pushed_authorization_request
end
end
end
end
Loading