From febdf67f64462c5622c0a234497ed739355deaa1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 26 Oct 2023 13:37:17 +0200 Subject: [PATCH 1/8] `createTokenRequest` returns `HttpRequest.Builder` --- .../tinyoauth2client/AuthorizationCodeGrant.java | 4 ++-- .../tinyoauth2client/TinyOAuth2Client.java | 11 +++++------ .../AuthorizationCodeGrantTest.java | 6 ++++-- .../tinyoauth2client/TinyOAuth2ClientTest.java | 15 +++++++++------ 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/java/io/github/coffeelibs/tinyoauth2client/AuthorizationCodeGrant.java b/src/main/java/io/github/coffeelibs/tinyoauth2client/AuthorizationCodeGrant.java index 34cfef7..d80e0c3 100644 --- a/src/main/java/io/github/coffeelibs/tinyoauth2client/AuthorizationCodeGrant.java +++ b/src/main/java/io/github/coffeelibs/tinyoauth2client/AuthorizationCodeGrant.java @@ -256,13 +256,13 @@ class WithAuthorizationCode { @VisibleForTesting HttpRequest buildTokenRequest() { - return client.buildTokenRequest(Map.of( // + return client.createTokenRequest(Map.of( // "grant_type", "authorization_code", // "client_id", client.clientId, // "code_verifier", pkce.getVerifier(), // "code", authorizationCode, // "redirect_uri", redirectUri // - )); + )).build(); } public CompletableFuture> getAccessTokenAsync(HttpClient httpClient) { diff --git a/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java b/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java index 554ff30..ef852f6 100644 --- a/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java +++ b/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java @@ -147,28 +147,27 @@ public HttpResponse refresh(HttpClient httpClient, String refreshToken, @VisibleForTesting HttpRequest buildRefreshTokenRequest(String refreshToken, String... scopes) { - return buildTokenRequest(Map.of(// + return createTokenRequest(Map.of(// "grant_type", "refresh_token", // "refresh_token", refreshToken, // "client_id", clientId, // "scope", String.join(" ", scopes) - )); + )).build(); } /** * Creates a new HTTP request targeting the {@link #tokenEndpoint}. * * @param parameters Parameters to send in an {@code application/x-www-form-urlencoded} request body - * @return A new http request + * @return A new http request builder */ @Contract("_ -> new") - HttpRequest buildTokenRequest(Map parameters) { + HttpRequest.Builder createTokenRequest(Map parameters) { var urlencodedParams = URIUtil.buildQueryString(parameters); return HttpRequest.newBuilder(tokenEndpoint) // .header("Content-Type", "application/x-www-form-urlencoded") // .POST(HttpRequest.BodyPublishers.ofString(urlencodedParams)) // - .timeout(requestTimeout) // - .build(); + .timeout(requestTimeout); } } diff --git a/src/test/java/io/github/coffeelibs/tinyoauth2client/AuthorizationCodeGrantTest.java b/src/test/java/io/github/coffeelibs/tinyoauth2client/AuthorizationCodeGrantTest.java index 60b5f69..ef1ada2 100644 --- a/src/test/java/io/github/coffeelibs/tinyoauth2client/AuthorizationCodeGrantTest.java +++ b/src/test/java/io/github/coffeelibs/tinyoauth2client/AuthorizationCodeGrantTest.java @@ -270,13 +270,15 @@ public class WithAuthCode { @DisplayName("buildTokenRequest() builds new http request") public void testBuildTokenRequest() { var grantWithCode = grant.new WithAuthorizationCode("redirect-uri", "auth-code"); + var requestBuilder = Mockito.mock(HttpRequest.Builder.class); var request = Mockito.mock(HttpRequest.class); - Mockito.doReturn(request).when(client).buildTokenRequest(Mockito.any()); + Mockito.doReturn(requestBuilder).when(client).createTokenRequest(Mockito.any()); + Mockito.doReturn(request).when(requestBuilder).build(); var result = grantWithCode.buildTokenRequest(); Assertions.assertEquals(request, result); - Mockito.verify(client).buildTokenRequest(Map.of(// + Mockito.verify(client).createTokenRequest(Map.of(// "grant_type", "authorization_code", // "client_id", client.clientId, // "code_verifier", pkce.getVerifier(), // diff --git a/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java b/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java index 84b64ac..1be7345 100644 --- a/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java +++ b/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java @@ -14,7 +14,6 @@ import java.time.Duration; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; public class TinyOAuth2ClientTest { @@ -143,13 +142,15 @@ public void testRefreshWithScopes() throws IOException, InterruptedException { public void testBuildRefreshTokenRequest() { var tokenEndpoint = URI.create("http://example.com/oauth2/token"); var client = Mockito.spy(new TinyOAuth2Client("my-client", tokenEndpoint)); + var requestBuilder = Mockito.mock(HttpRequest.Builder.class); var request = Mockito.mock(HttpRequest.class); - Mockito.doReturn(request).when(client).buildTokenRequest(Mockito.any()); + Mockito.doReturn(requestBuilder).when(client).createTokenRequest(Mockito.any()); + Mockito.doReturn(request).when(requestBuilder).build(); var result = client.buildRefreshTokenRequest("r3fr3sh70k3n"); Assertions.assertEquals(request, result); - Mockito.verify(client).buildTokenRequest(Map.of(// + Mockito.verify(client).createTokenRequest(Map.of(// "grant_type", "refresh_token", // "refresh_token", "r3fr3sh70k3n", // "client_id", "my-client", // @@ -162,13 +163,15 @@ public void testBuildRefreshTokenRequest() { public void testBuildRefreshTokenRequestWithScopes() { var tokenEndpoint = URI.create("http://example.com/oauth2/token"); var client = Mockito.spy(new TinyOAuth2Client("my-client", tokenEndpoint)); + var requestBuilder = Mockito.mock(HttpRequest.Builder.class); var request = Mockito.mock(HttpRequest.class); - Mockito.doReturn(request).when(client).buildTokenRequest(Mockito.any()); + Mockito.doReturn(requestBuilder).when(client).createTokenRequest(Mockito.any()); + Mockito.doReturn(request).when(requestBuilder).build(); var result = client.buildRefreshTokenRequest("r3fr3sh70k3n", "foo", "bar"); Assertions.assertEquals(request, result); - Mockito.verify(client).buildTokenRequest(Map.of(// + Mockito.verify(client).createTokenRequest(Map.of(// "grant_type", "refresh_token", // "refresh_token", "r3fr3sh70k3n", // "client_id", "my-client", // @@ -188,7 +191,7 @@ public void testBuildTokenRequest() { uriUtilClass.when(() -> URIUtil.buildQueryString(Mockito.any())).thenReturn("query=string&mock=true"); bodyPublishersClass.when(() -> HttpRequest.BodyPublishers.ofString(Mockito.any())).thenReturn(bodyPublisher); - var request = client.buildTokenRequest(params); + var request = client.createTokenRequest(params).build(); uriUtilClass.verify(() -> URIUtil.buildQueryString(Mockito.same(params))); bodyPublishersClass.verify(() -> HttpRequest.BodyPublishers.ofString("query=string&mock=true")); From 922bad4fcfef54d520affef5aa13c70c3c5d338d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 26 Oct 2023 13:37:31 +0200 Subject: [PATCH 2/8] adjusted JavaDoc --- .../github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java b/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java index ef852f6..4f360ff 100644 --- a/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java +++ b/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java @@ -63,7 +63,7 @@ public TinyOAuth2Client withRequestTimeout(Duration requestTimeout) { * Initializes a new Authorization Code Flow with PKCE * * @param authEndpoint The URI of the Authorization Endpoint - * @return A new Authentication Flow + * @return A new Authorization Code Grant * @see Authorization Code Flow * @deprecated Use {@link #authorizationCodeGrant(URI)} instead, will be removed in a future version */ @@ -76,7 +76,7 @@ public AuthorizationCodeGrant authFlow(URI authEndpoint) { * Initializes a new Authorization Code Grant with PKCE * * @param authEndpoint The URI of the Authorization Endpoint - * @return A new Authentication Flow + * @return A new Authorization Code Grant * @see Authorization Code Flow */ public AuthorizationCodeGrant authorizationCodeGrant(URI authEndpoint) { From addb7d8e38ef6c1f3d6d42ebadeec049a483420a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 26 Oct 2023 17:35:04 +0200 Subject: [PATCH 3/8] implemented `clientCredentialsGrant(...)` --- .../ClientCredentialsGrant.java | 102 ++++++++++++++++++ .../tinyoauth2client/TinyOAuth2Client.java | 14 +++ .../ClientCredentialsGrantTest.java | 90 ++++++++++++++++ .../TinyOAuth2ClientTest.java | 12 +++ 4 files changed, 218 insertions(+) create mode 100644 src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java create mode 100644 src/test/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrantTest.java diff --git a/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java b/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java new file mode 100644 index 0000000..70dddcc --- /dev/null +++ b/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java @@ -0,0 +1,102 @@ +package io.github.coffeelibs.tinyoauth2client; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.VisibleForTesting; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +/** + * Simple OAuth 2.0 Client Credentials Grant + * + * @see TinyOAuth2Client#clientCredentialsGrant(Charset, CharSequence) () + * @see RFC 6749, Section 4.4 + * @see RFC 6749, Section 3.2.1 + */ +@ApiStatus.Experimental +public class ClientCredentialsGrant { + + @VisibleForTesting + final TinyOAuth2Client client; + + @VisibleForTesting + final String basicAuthHeader; + + public ClientCredentialsGrant(TinyOAuth2Client client, Charset charset, CharSequence clientSecret) { + this.client = client; + this.basicAuthHeader = buildBasicAuthHeader(charset, client.clientId, clientSecret); + } + + /** + * Requests a new access token, using the pre-shared client credentials to authenticate against the authorization server. + * + * @param httpClient The http client used to recieve the authorization code + * @param scopes The desired scopes + * @return The Access Token Response + * @throws IOException In case of I/O errors when communicating with the token endpoint + * @throws InterruptedException When this thread is interrupted before a response is received + * @see #authorizeAsync(HttpClient, String...) + */ + public HttpResponse authorize(HttpClient httpClient, String... scopes) throws IOException, InterruptedException { + var req = buildTokenRequest(Set.of(scopes)); + return httpClient.send(req, HttpResponse.BodyHandlers.ofString()); + } + + /** + * Requests a new access token, using the pre-shared client credentials to authenticate against the authorization server. + * + * @param httpClient The http client used to recieve the authorization code + * @param scopes The desired scopes + * @return The future Access Token Response + * @see #authorize(HttpClient, String...) + */ + public CompletableFuture> authorizeAsync(HttpClient httpClient, String... scopes) { + var req = buildTokenRequest(Set.of(scopes)); + return httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofString()); + } + + @VisibleForTesting + static String buildBasicAuthHeader(Charset charset, String clientId, CharSequence clientSecret) { + // while it is inevitable to have a String copy of the encoded header in memory during the http request, + // this is an attempt to at least avoid unnecessary copies of the clientSecret: + var userPassChars = CharBuffer.allocate(clientId.length() + 1 + clientSecret.length()); + userPassChars.put(clientId).put(':').put(CharBuffer.wrap(clientSecret)).flip(); + var userPassBytes = charset.encode(userPassChars); + var base64Bytes = Base64.getEncoder().encode(userPassBytes); + try { + return "Basic " + StandardCharsets.US_ASCII.decode(base64Bytes); + } finally { + Arrays.fill(userPassChars.array(), ' '); + Arrays.fill(userPassBytes.array(), (byte) 0x00); + Arrays.fill(base64Bytes.array(), (byte) 0x00); + } + } + + @VisibleForTesting + HttpRequest buildTokenRequest(Collection scopes) { + var params = scopes.isEmpty() + ? Map.of("grant_type", "client_credentials") + : Map.of("grant_type", "client_credentials", "scope", String.join(" ", scopes)); + var req = client.createTokenRequest(params); + + // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1: + // The authorization server MUST support the HTTP Basic authentication [...] + // Alternatively, the authorization server MAY support including the client credentials in the request-body [...] + // Including the client credentials in the request-body using the two parameters is NOT RECOMMENDED and + // SHOULD be limited to clients unable to directly utilize the HTTP Basic authentication scheme + req.setHeader("Authorization", basicAuthHeader); + return req.build(); + } + +} diff --git a/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java b/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java index 4f360ff..6a11e8b 100644 --- a/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java +++ b/src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java @@ -8,6 +8,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.Charset; import java.time.Duration; import java.util.Map; import java.util.Objects; @@ -83,6 +84,18 @@ public AuthorizationCodeGrant authorizationCodeGrant(URI authEndpoint) { return new AuthorizationCodeGrant(this, authEndpoint, new PKCE()); } + /** + * Initializes a new Client Credentials Grant + * + * @param charset The charset used to encode the credentials, as expected by the authorization server + * @param clientSecret The client secret + * @return A new Client Credentials Grant + * @see Client Credentials Grant + */ + public ClientCredentialsGrant clientCredentialsGrant(Charset charset, CharSequence clientSecret) { + return new ClientCredentialsGrant(this, charset, clientSecret); + } + /** * Refreshes an access token using the given {@code refreshToken}. The request is made by the default http client returned by * {@link HttpClient#newHttpClient()} but on the given executor. @@ -147,6 +160,7 @@ public HttpResponse refresh(HttpClient httpClient, String refreshToken, @VisibleForTesting HttpRequest buildRefreshTokenRequest(String refreshToken, String... scopes) { + // TODO: https://datatracker.ietf.org/doc/html/rfc6749#section-6: If the client type is confidential or the client was issued client credentials [...] the client MUST authenticate return createTokenRequest(Map.of(// "grant_type", "refresh_token", // "refresh_token", refreshToken, // diff --git a/src/test/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrantTest.java b/src/test/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrantTest.java new file mode 100644 index 0000000..f702555 --- /dev/null +++ b/src/test/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrantTest.java @@ -0,0 +1,90 @@ +package io.github.coffeelibs.tinyoauth2client; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mockito; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class ClientCredentialsGrantTest { + + private final TinyOAuth2Client client = Mockito.spy(new TinyOAuth2Client("Aladdin", URI.create("http://example.com/oauth2/token"))); + + @DisplayName("build basic auth header") + @ParameterizedTest(name = "{0}:{1} -> {2}") + @CsvSource({ + // from https://datatracker.ietf.org/doc/html/rfc7617: + "Aladdin, open sesame, Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", + "test, 123£, Basic dGVzdDoxMjPCow==", + }) + public void testBuildBasicAuthHeader(String username, String password, String expectedResult) { + var result = ClientCredentialsGrant.buildBasicAuthHeader(StandardCharsets.UTF_8, username, password); + + Assertions.assertEquals(expectedResult, result); + } + + @Test + @DisplayName("buildTokenRequest() builds new http request") + public void testBuildTokenRequest() { + var grant = new ClientCredentialsGrant(client, StandardCharsets.UTF_8, "open sesame"); + var requestBuilder = Mockito.mock(HttpRequest.Builder.class); + var request = Mockito.mock(HttpRequest.class); + Mockito.doReturn(requestBuilder).when(client).createTokenRequest(Mockito.any()); + Mockito.doReturn(request).when(requestBuilder).build(); + + var result = grant.buildTokenRequest(List.of("foo", "bar")); + + Assertions.assertEquals(request, result); + Mockito.verify(client).createTokenRequest(Map.of(// + "grant_type", "client_credentials", // + "scope", "foo bar" + )); + Mockito.verify(requestBuilder).setHeader("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + } + + @Test + @DisplayName("authorize(...) sends access token request") + @SuppressWarnings("unchecked") + public void testAuthorize() throws IOException, InterruptedException { + var grant = Mockito.spy(new ClientCredentialsGrant(client, StandardCharsets.UTF_8, "open sesame")); + var httpClient = Mockito.mock(HttpClient.class); + var httpRequest = Mockito.mock(HttpRequest.class); + var httpRespone = Mockito.mock(HttpResponse.class); + Mockito.doReturn(httpRequest).when(grant).buildTokenRequest(Mockito.any()); + Mockito.doReturn(httpRespone).when(httpClient).send(Mockito.any(), Mockito.any()); + + var result = grant.authorize(httpClient); + + Assertions.assertEquals(httpRespone, result); + Mockito.verify(httpClient).send(httpRequest, HttpResponse.BodyHandlers.ofString()); + } + + @Test + @DisplayName("authorizeAsync(...) sends access token request") + @SuppressWarnings("unchecked") + public void testAuthorizeAsync() throws IOException, InterruptedException { + var grant = Mockito.spy(new ClientCredentialsGrant(client, StandardCharsets.UTF_8, "open sesame")); + var httpClient = Mockito.mock(HttpClient.class); + var httpRequest = Mockito.mock(HttpRequest.class); + var httpRespone = Mockito.mock(HttpResponse.class); + Mockito.doReturn(httpRequest).when(grant).buildTokenRequest(Mockito.any()); + Mockito.doReturn(CompletableFuture.completedFuture(httpRespone)).when(httpClient).sendAsync(Mockito.any(), Mockito.any()); + + var result = grant.authorizeAsync(httpClient); + + Assertions.assertEquals(httpRespone, result.join()); + Mockito.verify(httpClient).sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()); + } + +} \ No newline at end of file diff --git a/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java b/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java index 1be7345..9887e98 100644 --- a/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java +++ b/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java @@ -11,6 +11,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -31,6 +32,17 @@ public void testAuthorizationCodeGrant() { Assertions.assertNotNull(grant.pkce); } + @Test + @DisplayName("clientCredentialsGrant(...)") + public void testClientCredentialsGrant() { + var client = new TinyOAuth2Client("Aladdin", URI.create("http://example.com/oauth2/token")); + + var grant = client.clientCredentialsGrant(StandardCharsets.UTF_8, "open sesame"); + + Assertions.assertSame(grant.client, client); + Assertions.assertEquals(grant.basicAuthHeader, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + } + @Test @DisplayName("withRequestTimeout(...)") public void testWithRequestTimeout() { From 12f40525e5c4ab1df41f73a8f3e4935ad48e910b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 26 Oct 2023 17:35:10 +0200 Subject: [PATCH 4/8] adjusted README --- README.md | 65 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 96efe09..532eb86 100644 --- a/README.md +++ b/README.md @@ -17,40 +17,57 @@ support for [PKCE](https://datatracker.ietf.org/doc/html/rfc8252#section-8.1) an ## Usage -Configure your authorization server to allow `http://127.0.0.1/*` as a redirect target and look up these configuration values: - -* client identifier -* token endpoint -* authorization endpoint +You begin building an OAuth 2.0 Client via the fluent API: ```java -// this library will just perform the Authorization Flow: -var httpResponse = TinyOAuth2.client("oauth-client-id") - .withTokenEndpoint(URI.create("https://login.example.com/oauth2/token")) +var oauthClient = TinyOAuth2.client("oauth-client-id") // The client identifier + .withTokenEndpoint(URI.create("https://login.example.com/oauth2/token")) // The token endpoint .withRequestTimeout(Duration.ofSeconds(10)) // optional - .authorizationCodeGrant(URI.create("https://login.example.com/oauth2/authorize")) - .authorize(uri -> System.out.println("Please login on " + uri)); - -// from this point onwards, please proceed with the JSON/JWT parser of your choice: -if (httpResponse.statusCode() == 200) { - var jsonString = httpResponse.body() - var bearerToken = parseJson(jsonString).get("access_token"); - // ... -} + // ... ``` -If you wish to use a proxy or your own set of root certificates, provide your own JDK [http client](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html): +Next, continue with a specific grant type by invoking `.authorizationCodeGrant(...)` or `.clientCredentialsGrant(...)` (more may be added eventually). + +This library requires you to provide an instance of [`java.net.http.HttpClient`](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html). +This allows you to configure it to your needs, e.g. by applying proxy settings: + ```java var httpClient = HttpClient.newBuilder() - .proxy(ProxySelector.of(InetSocketAddress.createUnresolved("https:\\example.com",1337))) - .build(); -var httpResponse = TinyOAuth2.client("oauth-client-id") - .withTokenEndpoint(URI.create("https://login.example.com/oauth2/token")) - .authorizationCodeGrant(URI.create("https://login.example.com/oauth2/authorize")) + .proxy(ProxySelector.of(InetSocketAddress.createUnresolved("https:\\example.com",1337))) + .build(); +``` + +### Authorization Code Grant +Usually, you would want to use the [Authorization Code Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) type to obtain access tokens. +Configure your Authorization Server to allow `http://127.0.0.1/*` as a redirect target and look up the authorization endpoint: + +```java +// this library will just perform the Authorization Flow: +var httpResponse = oauthClient.authorizationCodeGrant(URI.create("https://login.example.com/oauth2/authorize")) .authorize(httpClient, uri -> System.out.println("Please login on " + uri)); ``` -If your authorization server doesn't allow wildcards, you can also configure a fixed path (and even port) via e.g. `setRedirectPath("/callback")` and `setRedirectPorts(8080)`. +If your authorization server doesn't allow wildcards, you can also configure a fixed path (and even port) via e.g. `setRedirectPath("/callback")` and `setRedirectPorts(8080)` before calling `authorize(...)`. + +### Client Credentials Grant +Alternatively, if your client shall act on behalf of a service account, use the [Client Credentials Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) type, +which allows the client to authenticate directly without further user interaction: + +```java +var httpResponse = oauthClient.clientCredentialsGrant(UTF_8, "client secret") + .authorize(httpClient); +``` + +### Parsing the Response +For maximum flexibility and minimal attack surface, this library does not include or depend on a specific parser. Instead, use a JSON or JWT parser of your choice to parse the Authorization Server's response: + +```java +if (httpResponse.statusCode() == 200) { + var jsonString = httpResponse.body() + var bearerToken = parseJson(jsonString).get("access_token"); + // ... +} +``` ## Why this library? From dba708a1933197b97050659c47aa4b9832b9c7a3 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 26 Oct 2023 17:40:42 +0200 Subject: [PATCH 5/8] fix parameter order --- .../coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java b/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java index 9887e98..8389c36 100644 --- a/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java +++ b/src/test/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2ClientTest.java @@ -40,7 +40,7 @@ public void testClientCredentialsGrant() { var grant = client.clientCredentialsGrant(StandardCharsets.UTF_8, "open sesame"); Assertions.assertSame(grant.client, client); - Assertions.assertEquals(grant.basicAuthHeader, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + Assertions.assertEquals("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", grant.basicAuthHeader); } @Test From 308a7f4f1c53e880044f0f8c72e5054315098d50 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 26 Oct 2023 20:53:06 +0200 Subject: [PATCH 6/8] reduce visibility --- .../coffeelibs/tinyoauth2client/ClientCredentialsGrant.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java b/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java index 70dddcc..077bd73 100644 --- a/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java +++ b/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java @@ -33,7 +33,7 @@ public class ClientCredentialsGrant { @VisibleForTesting final String basicAuthHeader; - public ClientCredentialsGrant(TinyOAuth2Client client, Charset charset, CharSequence clientSecret) { + ClientCredentialsGrant(TinyOAuth2Client client, Charset charset, CharSequence clientSecret) { this.client = client; this.basicAuthHeader = buildBasicAuthHeader(charset, client.clientId, clientSecret); } From be95b9f40a03cb1fdf242fc3727c9bc89c272c18 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 26 Oct 2023 20:53:16 +0200 Subject: [PATCH 7/8] add jetbrains annotations --- .../coffeelibs/tinyoauth2client/ClientCredentialsGrant.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java b/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java index 077bd73..f8b9f8c 100644 --- a/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java +++ b/src/main/java/io/github/coffeelibs/tinyoauth2client/ClientCredentialsGrant.java @@ -1,6 +1,8 @@ package io.github.coffeelibs.tinyoauth2client; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.NonBlocking; import org.jetbrains.annotations.VisibleForTesting; import java.io.IOException; @@ -48,6 +50,7 @@ public class ClientCredentialsGrant { * @throws InterruptedException When this thread is interrupted before a response is received * @see #authorizeAsync(HttpClient, String...) */ + @Blocking public HttpResponse authorize(HttpClient httpClient, String... scopes) throws IOException, InterruptedException { var req = buildTokenRequest(Set.of(scopes)); return httpClient.send(req, HttpResponse.BodyHandlers.ofString()); @@ -61,6 +64,7 @@ public HttpResponse authorize(HttpClient httpClient, String... scopes) t * @return The future Access Token Response * @see #authorize(HttpClient, String...) */ + @NonBlocking public CompletableFuture> authorizeAsync(HttpClient httpClient, String... scopes) { var req = buildTokenRequest(Set.of(scopes)); return httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofString()); From 8028a7f8b9dc318081d006e58cda11c066486643 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 27 Oct 2023 12:50:47 +0200 Subject: [PATCH 8/8] add some OIDC scopes to usage examples [ci skip] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f8cb9a8..17144fc 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Configure your Authorization Server to allow `http://127.0.0.1/*` as a redirect ```java // this library will just perform the Authorization Flow: var httpResponse = oauthClient.authorizationCodeGrant(URI.create("https://login.example.com/oauth2/authorize")) - .authorize(httpClient, uri -> System.out.println("Please login on " + uri)); + .authorize(httpClient, uri -> System.out.println("Please login on " + uri), "openid", "profile"); // optionally add scopes here); ``` If your authorization server doesn't allow wildcards, you can also configure a fixed path (and even port) via e.g. `setRedirectPath("/callback")` and `setRedirectPorts(8080)` before calling `authorize(...)`. @@ -58,7 +58,7 @@ which allows the client to authenticate directly without further user interactio ```java var httpResponse = oauthClient.clientCredentialsGrant(UTF_8, "client secret") - .authorize(httpClient); + .authorize(httpClient, "openid", "profile"); // optionally add scopes here ``` ### Parsing the Response