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

Add OAuth2::Client#make_token_request returning HTTP response #12921

55 changes: 55 additions & 0 deletions spec/std/oauth2/client_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,34 @@ describe OAuth2::Client do
token.access_token.should eq "access_token"
end
end

it "#make_token_request" do
handler = HTTP::Handler::HandlerProc.new do |context|
body = context.request.body.not_nil!.gets_to_end
dpop = context.request.headers.get?("DPoP")
response = {access_token: "access_token", body: body, dpop: dpop}
context.response.print response.to_json
end

run_handler(handler) do |http_client|
client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", scheme: "http"
client.http_client = http_client

token_response = client.make_token_request do |form, headers|
form.add("redirect_uri", client.redirect_uri)
form.add("grant_type", "authorization_code")
form.add("code", "some_authorization_code")
form.add("code_verifier", "a_code_verifier")
form.add("nonce", "a_nonce")
headers.add("DPoP", "a_DPoP_jwt_token")
end
token_response.status_code.should eq(200)
token = OAuth2::AccessToken.from_json(token_response.body)
token.extra.not_nil!["body"].should eq %("redirect_uri=&grant_type=authorization_code&code=some_authorization_code&code_verifier=a_code_verifier&nonce=a_nonce")
token.extra.not_nil!["dpop"].should eq %(["a_DPoP_jwt_token"])
token.access_token.should eq "access_token"
end
end
end
describe "using Request Body to pass credentials" do
it "#get_access_token_using_authorization_code" do
Expand Down Expand Up @@ -197,6 +225,33 @@ describe OAuth2::Client do
token.access_token.should eq "access_token"
end
end

it "#make_token_request" do
handler = HTTP::Handler::HandlerProc.new do |context|
body = context.request.body.not_nil!.gets_to_end
dpop = context.request.headers.get?("DPoP")
response = {access_token: "access_token", body: body, dpop: dpop}
context.response.print response.to_json
end

run_handler(handler) do |http_client|
client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody
client.http_client = http_client

token_response = client.make_token_request do |form, headers|
form.add("grant_type", "refresh_token")
form.add("refresh_token", "some_refresh_token")
form.add("scope", "read_posts")
form.add("nonce", "a_nonce")
headers.add("DPoP", "a_DPoP_jwt_token")
end
token_response.status_code.should eq(200)
token = OAuth2::AccessToken.from_json(token_response.body)
token.extra.not_nil!["body"].should eq %("client_id=client_id&client_secret=client_secret&grant_type=refresh_token&refresh_token=some_refresh_token&scope=read_posts&nonce=a_nonce")
token.extra.not_nil!["dpop"].should eq %(["a_DPoP_jwt_token"])
token.access_token.should eq "access_token"
end
end
end
end

Expand Down
42 changes: 27 additions & 15 deletions src/oauth2/client.cr
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,17 @@
# You can also use an `OAuth2::Session` to automatically refresh expired
# tokens before each request.
class OAuth2::Client
DEFAULT_HEADERS = HTTP::Headers{
"Accept" => "application/json",
"Content-Type" => "application/x-www-form-urlencoded",
}

# Sets the `HTTP::Client` to use with this client.
setter http_client : HTTP::Client?

# Gets the redirect_uri
getter redirect_uri : String?

# Returns the `HTTP::Client` to use with this client.
#
# By default, this returns a new instance every time. To reuse the same instance,
Expand Down Expand Up @@ -105,13 +113,13 @@ class OAuth2::Client
end

uri.query = URI::Params.build do |form|
form.add "client_id", @client_id
form.add "redirect_uri", @redirect_uri
form.add "response_type", "code"
form.add "scope", scope unless scope.nil?
form.add "state", state unless state.nil?
form.add("client_id", @client_id)
form.add("redirect_uri", @redirect_uri)
form.add("response_type", "code")
form.add("scope", scope) unless scope.nil?
form.add("state", state) unless state.nil?
uri.query_params.each do |key, value|
form.add key, value
form.add(key, value)
end
yield form
end
Expand Down Expand Up @@ -155,16 +163,14 @@ class OAuth2::Client
get_access_token do |form|
form.add("grant_type", "refresh_token")
form.add("refresh_token", refresh_token)
form.add "scope", scope unless scope.nil?
form.add("scope", scope) unless scope.nil?
end
end

private def get_access_token : AccessToken
headers = HTTP::Headers{
"Accept" => "application/json",
"Content-Type" => "application/x-www-form-urlencoded",
}

# Makes a token exchange request with custom headers and form fields
# Returns a HTTP::Client::Response
Copy link
Member

Choose a reason for hiding this comment

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

This is superfluous. The return type is sufficiently indicated in the method signature.

Suggested change
# Returns a HTTP::Client::Response

def make_token_request(&block : URI::Params::Builder, HTTP::Headers -> _) : HTTP::Client::Response
headers = DEFAULT_HEADERS.dup
body = URI::Params.build do |form|
case @auth_scheme
when .request_body?
Expand All @@ -176,10 +182,16 @@ class OAuth2::Client
"Basic #{Base64.strict_encode("#{@client_id}:#{@client_secret}")}"
)
end
yield form
yield form, headers
end

response = http_client.post token_uri.request_target, form: body, headers: headers
http_client.post token_uri.request_target, form: body, headers: headers
end

private def get_access_token : AccessToken
response = make_token_request do |form, _headers|
yield form
end
case response.status
when .ok?, .created?
OAuth2::AccessToken.from_json(response.body)
Expand Down