Skip to content

Commit 2075bb7

Browse files
[SDK-4142] Add support for /oauth/par (#470)
1 parent 7d6bfa7 commit 2075bb7

File tree

5 files changed

+171
-79
lines changed

5 files changed

+171
-79
lines changed

lib/auth0/api/authentication_endpoints.rb

+37
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,21 @@ def authorization_url(redirect_uri, options = {})
323323
URI::HTTPS.build(host: @domain, path: '/authorize', query: to_query(request_params))
324324
end
325325

326+
# Return an authorization URL for PAR requests
327+
# @see https://www.rfc-editor.org/rfc/rfc9126.html
328+
# @param request_uri [string] The request_uri as obtained by calling `pushed_authorization_request`
329+
# @param additional_parameters Any additional parameters to send
330+
def par_authorization_url(request_uri)
331+
raise Auth0::InvalidParameter, 'Must supply a valid request_uri' if request_uri.to_s.empty?
332+
333+
request_params = {
334+
client_id: @client_id,
335+
request_uri: request_uri,
336+
}
337+
338+
URI::HTTPS.build(host: @domain, path: '/authorize', query: to_query(request_params))
339+
end
340+
326341
# Returns an Auth0 logout URL with a return URL.
327342
# @see https://auth0.com/docs/api/authentication#logout
328343
# @see https://auth0.com/docs/logout
@@ -344,6 +359,28 @@ def logout_url(return_to, include_client: false, federated: false)
344359
)
345360
end
346361

362+
# Make a request to the PAR endpoint and receive a `request_uri` to send to the '/authorize' endpoint.
363+
# @see https://auth0.com/docs/api/authentication#authorization-code-grant
364+
# @param redirect_uri [string] URL to redirect after authorization
365+
# @param options [hash] Can contain response_type, connection, state, organization, invitation, and additional_parameters.
366+
# @return [url] Authorization URL.
367+
def pushed_authorization_request(parameters = {})
368+
request_params = {
369+
client_id: @client_id,
370+
response_type: parameters.fetch(:response_type, 'code'),
371+
connection: parameters.fetch(:connection, nil),
372+
redirect_uri: parameters.fetch(:redirect_uri, nil),
373+
state: parameters.fetch(:state, nil),
374+
scope: parameters.fetch(:scope, nil),
375+
organization: parameters.fetch(:organization, nil),
376+
invitation: parameters.fetch(:invitation, nil)
377+
}.merge(parameters.fetch(:additional_parameters, {}))
378+
379+
populate_client_assertion_or_secret(request_params)
380+
381+
request_with_retry(:post_form, '/oauth/par', request_params, {})
382+
end
383+
347384
# Return a SAMLP URL.
348385
# The SAML Request AssertionConsumerServiceURL will be used to POST back
349386
# the assertion and it must match with the application callback URL.

lib/auth0/mixins/httpproxy.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ module HTTPProxy
1616
BASE_DELAY = 100
1717

1818
# proxying requests from instance methods to HTTP class methods
19-
%i(get post post_file put patch delete delete_with_body).each do |method|
19+
%i(get post post_file post_form put patch delete delete_with_body).each do |method|
2020
define_method(method) do |uri, body = {}, extra_headers = {}|
2121
body = body.delete_if { |_, v| v.nil? }
2222
token = get_token()
@@ -85,9 +85,12 @@ def request(method, uri, body = {}, extra_headers = {})
8585
elsif method == :post_file
8686
body.merge!(multipart: true)
8787
# Ignore the default Content-Type headers and let the HTTP client define them
88-
post_file_headers = headers.slice(*headers.keys - ['Content-Type'])
88+
post_file_headers = headers.except('Content-Type') if headers != nil
8989
# Actual call with the altered headers
9090
call(:post, encode_uri(uri), timeout, post_file_headers, body)
91+
elsif method == :post_form
92+
form_post_headers = headers.except('Content-Type') if headers != nil
93+
call(:post, encode_uri(uri), timeout, form_post_headers, body.compact)
9194
else
9295
call(method, encode_uri(uri), timeout, headers, body.to_json)
9396
end

spec/lib/auth0/api/authentication_endpoints_spec.rb

+90
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
let(:client_secret) { 'test-client-secret' }
77
let(:api_identifier) { 'test-audience' }
88
let(:domain) { 'samples.auth0.com' }
9+
let(:request_uri) { 'urn:ietf:params:oauth:request_uri:the.request.uri' }
910

1011
let(:client_secret_config) { {
1112
domain: domain,
@@ -628,5 +629,94 @@
628629
client_assertion_instance.send :start_passwordless_sms_flow, '123456789'
629630
end
630631
end
632+
633+
context 'par_authorization_url' do
634+
it 'throws an exception if request_uri is nil' do
635+
expect { client_secret_instance.send :par_authorization_url, nil}.to raise_error Auth0::InvalidParameter
636+
end
637+
638+
it 'throws an exception if request_uri is empty' do
639+
expect { client_secret_instance.send :par_authorization_url, ''}.to raise_error Auth0::InvalidParameter
640+
end
641+
642+
it 'builds a URL containing the request_uri' do
643+
url = client_secret_instance.send :par_authorization_url, request_uri
644+
expect(CGI.unescape(url.to_s)).to eq("https://samples.auth0.com/authorize?client_id=#{client_id}&request_uri=#{request_uri}")
645+
end
646+
end
647+
648+
context 'pushed_authorization_request' do
649+
it 'sends the request as a form post' do
650+
expect(RestClient::Request).to receive(:execute) do |arg|
651+
expect(arg[:url]).to eq('https://samples.auth0.com/oauth/par')
652+
expect(arg[:method]).to eq(:post)
653+
654+
expect(arg[:payload]).to eq({
655+
client_id: client_id,
656+
client_secret: client_secret,
657+
response_type: 'code',
658+
})
659+
660+
StubResponse.new({}, true, 200)
661+
end
662+
663+
client_secret_instance.send :pushed_authorization_request
664+
end
665+
666+
it 'allows the RestClient to handle the correct header defaults' do
667+
expect(RestClient::Request).to receive(:execute) do |arg|
668+
expect(arg[:headers]).not_to have_key('Content-Type')
669+
670+
StubResponse.new({}, true, 200)
671+
end
672+
673+
client_secret_instance.headers['Content-Type'] = 'application/x-www-form-urlencoded'
674+
client_secret_instance.send :pushed_authorization_request
675+
end
676+
677+
it 'sends the request as a form post with all known overrides' do
678+
expect(RestClient::Request).to receive(:execute) do |arg|
679+
expect(arg[:url]).to eq('https://samples.auth0.com/oauth/par')
680+
expect(arg[:method]).to eq(:post)
681+
682+
expect(arg[:payload]).to eq({
683+
client_id: client_id,
684+
client_secret: client_secret,
685+
connection: 'google-oauth2',
686+
organization: 'org_id',
687+
invitation: 'http://invite.url',
688+
redirect_uri: 'http://localhost:3000',
689+
response_type: 'id_token',
690+
scope: 'openid',
691+
state: 'random_value'
692+
})
693+
694+
StubResponse.new({}, true, 200)
695+
end
696+
697+
client_secret_instance.send(:pushed_authorization_request,
698+
response_type: 'id_token',
699+
redirect_uri: 'http://localhost:3000',
700+
organization: 'org_id',
701+
invitation: 'http://invite.url',
702+
scope: 'openid',
703+
state: 'random_value',
704+
connection: 'google-oauth2')
705+
end
706+
707+
it 'sends the request as a form post using client assertion' do
708+
expect(RestClient::Request).to receive(:execute) do |arg|
709+
expect(arg[:url]).to eq('https://samples.auth0.com/oauth/par')
710+
expect(arg[:method]).to eq(:post)
711+
expect(arg[:payload][:client_secret]).to be_nil
712+
expect(arg[:payload][:client_assertion]).not_to be_nil
713+
expect(arg[:payload][:client_assertion_type]).to eq Auth0::ClientAssertion::CLIENT_ASSERTION_TYPE
714+
715+
StubResponse.new({}, true, 200)
716+
end
717+
718+
client_assertion_instance.send :pushed_authorization_request
719+
end
720+
end
631721
end
632722
end

0 commit comments

Comments
 (0)