Skip to content

Commit

Permalink
[SDK-4142] Add support for /oauth/par (#470)
Browse files Browse the repository at this point in the history
  • Loading branch information
stevehobbsdev authored Apr 24, 2023
1 parent 7d6bfa7 commit 2075bb7
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 79 deletions.
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|
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

0 comments on commit 2075bb7

Please sign in to comment.