From c9287d000c4fe6ef26c78adda9dc08f9e9faf45c Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Tue, 12 Feb 2019 20:50:21 -0500 Subject: [PATCH 1/6] Implement resource owner credentials --- spec/std/oauth2/client_spec.cr | 2 ++ src/oauth2/client.cr | 32 ++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/spec/std/oauth2/client_spec.cr b/spec/std/oauth2/client_spec.cr index 1d8be78efe7b..31ecf4c4b25a 100644 --- a/spec/std/oauth2/client_spec.cr +++ b/spec/std/oauth2/client_spec.cr @@ -44,5 +44,7 @@ describe OAuth2::Client do client.get_access_token_using_refresh_token("some_refresh_token") client.get_access_token_using_refresh_token("some_refresh_token", scope: "some scope") client.get_access_token_using_client_credentials(scope: "some scope") + client.get_access_token_using_resource_owner_credentials(username: "user123", password: "monkey") + client.get_access_token_using_resource_owner_credentials(username: "user123", password: "monkey", scope: "foo") end) end diff --git a/src/oauth2/client.cr b/src/oauth2/client.cr index 0380a6eaa088..ba31ebf5e6f3 100644 --- a/src/oauth2/client.cr +++ b/src/oauth2/client.cr @@ -106,7 +106,7 @@ class OAuth2::Client # Gets an access token using an authorization code, as specified by # [RFC 6749, Section 4.1.1](https://tools.ietf.org/html/rfc6749#section-4.1.3). - def get_access_token_using_authorization_code(authorization_code) : AccessToken + def get_access_token_using_authorization_code(authorization_code : String) : AccessToken get_access_token do |form| form.add("redirect_uri", @redirect_uri) form.add("grant_type", "authorization_code") @@ -114,26 +114,37 @@ class OAuth2::Client end end - # Gets an access token using a refresh token, as specified by - # [RFC 6749, Section 6](https://tools.ietf.org/html/rfc6749#section-6). - def get_access_token_using_refresh_token(refresh_token, scope = nil) : AccessToken + # Gets an access token using the resource owner credentials, as specified by + # [RFC 6749, Section 4.3.2](https://tools.ietf.org/html/rfc6749#section-4.3.2). + def get_access_token_using_resource_owner_credentials(username : String, password : String, scope = nil) : AccessToken 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("grant_type", "password") + form.add("username", username) + form.add("password", password) + form.add("scope", scope) unless scope.nil? end end # Gets an access token using client credentials, as specified by # [RFC 6749, Section 4.4.2](https://tools.ietf.org/html/rfc6749#section-4.4.2). - def get_access_token_using_client_credentials(scope = nil) + def get_access_token_using_client_credentials(scope = nil) : AccessToken get_access_token do |form| form.add("grant_type", "client_credentials") form.add("scope", scope) unless scope.nil? end end - private def get_access_token + # Gets an access token using a refresh token, as specified by + # [RFC 6749, Section 6](https://tools.ietf.org/html/rfc6749#section-6). + def get_access_token_using_refresh_token(refresh_token, scope = nil) : AccessToken + get_access_token do |form| + form.add("grant_type", "refresh_token") + form.add("refresh_token", refresh_token) + form.add "scope", scope unless scope.nil? + end + end + + private def get_access_token : AccessToken body = HTTP::Params.build do |form| form.add("client_id", @client_id) form.add("client_secret", @client_secret) @@ -141,7 +152,8 @@ class OAuth2::Client end headers = HTTP::Headers{ - "Accept" => "application/json", + "Accept" => "application/json", + "Content-Type" => "application/x-www-form-urlencoded", } response = HTTP::Client.post(token_uri, form: body, headers: headers) From 9f4f091c3f275d11bcd9bd66a863ee52597ebcea Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Wed, 13 Feb 2019 08:06:12 -0500 Subject: [PATCH 2/6] Fix link name --- src/oauth2/client.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oauth2/client.cr b/src/oauth2/client.cr index ba31ebf5e6f3..86e941d13d1d 100644 --- a/src/oauth2/client.cr +++ b/src/oauth2/client.cr @@ -105,7 +105,7 @@ class OAuth2::Client end # Gets an access token using an authorization code, as specified by - # [RFC 6749, Section 4.1.1](https://tools.ietf.org/html/rfc6749#section-4.1.3). + # [RFC 6749, Section 4.1.3](https://tools.ietf.org/html/rfc6749#section-4.1.3). def get_access_token_using_authorization_code(authorization_code : String) : AccessToken get_access_token do |form| form.add("redirect_uri", @redirect_uri) From b815b7749c8df511b44fb8d10d605ceb9869afe8 Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Wed, 13 Feb 2019 23:06:23 -0500 Subject: [PATCH 3/6] get_access_token_using_* --- spec/std/oauth2/client_spec.cr | 64 ++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/spec/std/oauth2/client_spec.cr b/spec/std/oauth2/client_spec.cr index 31ecf4c4b25a..04a5b750cf04 100644 --- a/spec/std/oauth2/client_spec.cr +++ b/spec/std/oauth2/client_spec.cr @@ -1,6 +1,20 @@ require "spec" require "oauth2" +private def assert_request + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body, headers: context.request.headers} + context.response.print response.to_json + end + address = server.bind_unused_port "::1" + spawn { server.listen } + + yield address + + server.close +end + describe OAuth2::Client do describe "authorization uri" do it "gets with default endpoint" do @@ -38,6 +52,56 @@ describe OAuth2::Client do end end + describe "get_access_token_using_*" do + it "#get_access_token_using_authorization_code" do + expected = %("client_id=client_id&client_secret=client_secret&redirect_uri=&grant_type=authorization_code&code=SDFhw39fwfg23flSfpawbef") + + assert_request do |address| + client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" + + token = client.get_access_token_using_authorization_code(authorization_code: "SDFhw39fwfg23flSfpawbef") + token.extra.not_nil!["body"].should eq expected + token.access_token.should eq "access_token" + end + end + + it "#get_access_token_using_resource_owner_credentials" do + expected = %("client_id=client_id&client_secret=client_secret&grant_type=password&username=user123&password=monkey&scope=read_posts") + + assert_request do |address| + client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" + + token = client.get_access_token_using_resource_owner_credentials(username: "user123", password: "monkey", scope: "read_posts") + token.extra.not_nil!["body"].should eq expected + token.access_token.should eq "access_token" + end + end + + it "#get_access_token_using_client_credentials" do + expected = %("client_id=client_id&client_secret=client_secret&grant_type=client_credentials&scope=read_posts") + + assert_request do |address| + client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" + + token = client.get_access_token_using_client_credentials(scope: "read_posts") + token.extra.not_nil!["body"].should eq expected + token.access_token.should eq "access_token" + end + end + + it "#get_access_token_using_refresh_token" do + expected = %("client_id=client_id&client_secret=client_secret&grant_type=refresh_token&refresh_token=some_refresh_token&scope=read_posts") + + assert_request do |address| + client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" + + token = client.get_access_token_using_refresh_token(scope: "read_posts", refresh_token: "some_refresh_token") + token.extra.not_nil!["body"].should eq expected + token.access_token.should eq "access_token" + end + end + end + typeof(begin client = OAuth2::Client.new "localhost", "client_id", "client_secret", redirect_uri: "uri", authorize_uri: "/baz" client.get_access_token_using_authorization_code("some_code") From 7880336c5bca1c2b01bb82e78244a7a1eac671bd Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Wed, 13 Feb 2019 23:08:18 -0500 Subject: [PATCH 4/6] remove headers from response --- spec/std/oauth2/client_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/std/oauth2/client_spec.cr b/spec/std/oauth2/client_spec.cr index 04a5b750cf04..f2943d846fd0 100644 --- a/spec/std/oauth2/client_spec.cr +++ b/spec/std/oauth2/client_spec.cr @@ -4,7 +4,7 @@ require "oauth2" private def assert_request server = HTTP::Server.new do |context| body = context.request.body.not_nil!.gets_to_end - response = {access_token: "access_token", body: body, headers: context.request.headers} + response = {access_token: "access_token", body: body} context.response.print response.to_json end address = server.bind_unused_port "::1" From 4582b297f5083f8cac27ac6135b3494dc52f1546 Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Thu, 14 Feb 2019 19:59:02 -0500 Subject: [PATCH 5/6] Address comments --- spec/std/http/spec_helper.cr | 46 ++++++++++++++++++++++++++++++ spec/std/oauth2/client_spec.cr | 51 ++++++++++++++++++++++------------ 2 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 spec/std/http/spec_helper.cr diff --git a/spec/std/http/spec_helper.cr b/spec/std/http/spec_helper.cr new file mode 100644 index 000000000000..96a31a582cfa --- /dev/null +++ b/spec/std/http/spec_helper.cr @@ -0,0 +1,46 @@ +require "spec" + +private def wait_for(timeout = 5.seconds) + now = Time.monotonic + + until yield + Fiber.yield + + if (Time.monotonic - now) > timeout + raise "server failed to start within 5 seconds" + end + end +end + +# Helper method which runs *server* +# 1. Spawns `server.listen` in a new fiber. +# 2. Waits until `server.listening?`. +# 3. Yields to the given block. +# 4. Ensures the server is closed. +# 5. After returning from the block, it waits for the server to gracefully +# shut down before continuing execution in the current fiber. +# 6. If the listening fiber raises an exception, it is rescued and re-raised +# in the current fiber. +def run_server(server) + server_done = Channel(Exception?).new + + spawn do + server.listen + rescue exc + server_done.send exc + else + server_done.send nil + end + + begin + wait_for { server.listening? } + + yield server_done + ensure + server.close unless server.closed? + + if exc = server_done.receive + raise exc + end + end +end diff --git a/spec/std/oauth2/client_spec.cr b/spec/std/oauth2/client_spec.cr index f2943d846fd0..4a30c2b8d2a0 100644 --- a/spec/std/oauth2/client_spec.cr +++ b/spec/std/oauth2/client_spec.cr @@ -1,19 +1,6 @@ require "spec" require "oauth2" - -private def assert_request - server = HTTP::Server.new do |context| - body = context.request.body.not_nil!.gets_to_end - response = {access_token: "access_token", body: body} - context.response.print response.to_json - end - address = server.bind_unused_port "::1" - spawn { server.listen } - - yield address - - server.close -end +require "../http/spec_helper" describe OAuth2::Client do describe "authorization uri" do @@ -54,9 +41,16 @@ describe OAuth2::Client do describe "get_access_token_using_*" do it "#get_access_token_using_authorization_code" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + expected = %("client_id=client_id&client_secret=client_secret&redirect_uri=&grant_type=authorization_code&code=SDFhw39fwfg23flSfpawbef") + address = server.bind_unused_port "::1" - assert_request do |address| + run_server(server) do client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" token = client.get_access_token_using_authorization_code(authorization_code: "SDFhw39fwfg23flSfpawbef") @@ -66,9 +60,16 @@ describe OAuth2::Client do end it "#get_access_token_using_resource_owner_credentials" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + expected = %("client_id=client_id&client_secret=client_secret&grant_type=password&username=user123&password=monkey&scope=read_posts") + address = server.bind_unused_port "::1" - assert_request do |address| + run_server(server) do client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" token = client.get_access_token_using_resource_owner_credentials(username: "user123", password: "monkey", scope: "read_posts") @@ -78,9 +79,16 @@ describe OAuth2::Client do end it "#get_access_token_using_client_credentials" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + expected = %("client_id=client_id&client_secret=client_secret&grant_type=client_credentials&scope=read_posts") + address = server.bind_unused_port "::1" - assert_request do |address| + run_server(server) do client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" token = client.get_access_token_using_client_credentials(scope: "read_posts") @@ -90,9 +98,16 @@ describe OAuth2::Client do end it "#get_access_token_using_refresh_token" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + expected = %("client_id=client_id&client_secret=client_secret&grant_type=refresh_token&refresh_token=some_refresh_token&scope=read_posts") + address = server.bind_unused_port "::1" - assert_request do |address| + run_server(server) do client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" token = client.get_access_token_using_refresh_token(scope: "read_posts", refresh_token: "some_refresh_token") From a3d4bf6177481fd59baefe74f89fb0af2033e939 Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Sat, 23 Feb 2019 10:06:30 -0500 Subject: [PATCH 6/6] Display timeout value in exception --- spec/std/http/spec_helper.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/std/http/spec_helper.cr b/spec/std/http/spec_helper.cr index 96a31a582cfa..db1b3d8eb5f7 100644 --- a/spec/std/http/spec_helper.cr +++ b/spec/std/http/spec_helper.cr @@ -7,7 +7,7 @@ private def wait_for(timeout = 5.seconds) Fiber.yield if (Time.monotonic - now) > timeout - raise "server failed to start within 5 seconds" + raise "server failed to start within #{timeout}" end end end