Skip to content

Commit c464d82

Browse files
Merge branch 'release/0.5.0'
2 parents 74dd2cc + edc9e1e commit c464d82

File tree

6 files changed

+348
-181
lines changed

6 files changed

+348
-181
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<groupId>io.github.coffeelibs</groupId>
77
<artifactId>tiny-oauth2-client</artifactId>
8-
<version>0.4.0</version>
8+
<version>0.5.0</version>
99
<name>Tiny OAuth2 Client</name>
1010
<description>Zero Dependency RFC 8252 Authorization Flow</description>
1111
<inceptionYear>2022</inceptionYear>

src/main/java/io/github/coffeelibs/tinyoauth2client/AuthFlow.java

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@
66
import io.github.coffeelibs.tinyoauth2client.util.URIUtil;
77
import org.jetbrains.annotations.ApiStatus;
88
import org.jetbrains.annotations.Blocking;
9+
import org.jetbrains.annotations.BlockingExecutor;
910
import org.jetbrains.annotations.Contract;
1011
import org.jetbrains.annotations.VisibleForTesting;
1112

1213
import java.io.IOException;
13-
import java.io.InterruptedIOException;
14+
import java.io.UncheckedIOException;
1415
import java.net.URI;
1516
import java.net.http.HttpClient;
1617
import java.net.http.HttpRequest;
1718
import java.net.http.HttpResponse;
1819
import java.util.Map;
1920
import java.util.Objects;
2021
import java.util.Set;
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.Executor;
2124
import java.util.concurrent.ForkJoinPool;
2225
import java.util.function.Consumer;
2326

@@ -119,22 +122,54 @@ public AuthFlow setRedirectPort(int... ports) {
119122
/**
120123
* Asks the given {@code browser} to browse the authorization URI. This method will block until the browser is
121124
* <a href="https://datatracker.ietf.org/doc/html/rfc8252#section-4.1">redirected back to this application</a>.
125+
* <p>
126+
* Then, the received authorization code is used to make an
127+
* <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3">Access Token Request</a>.
122128
*
123-
* @param browser An async callback that opens a web browser with the URI it consumes
129+
* @param executor The executor to run the async tasks
130+
* @param browser An async callback (not blocking the executor) that opens a web browser with the URI it consumes
124131
* @param scopes The desired <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.3">scopes</a>
125-
* @return The raw <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
132+
* @return The future <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
133+
* @see #authorize(Consumer, String...)
134+
*/
135+
public CompletableFuture<HttpResponse<String>> authorizeAsync(@BlockingExecutor Executor executor, Consumer<URI> browser, String... scopes) {
136+
return CompletableFuture.supplyAsync(() -> {
137+
try {
138+
return requestAuthCode(browser, scopes);
139+
} catch (IOException e) {
140+
throw new UncheckedIOException(e);
141+
}
142+
}, executor).thenCompose(authorizedFlow -> authorizedFlow.getAccessTokenAsync(executor));
143+
}
144+
145+
/**
146+
* Asks the given {@code browser} to browse the authorization URI. This method will block until the browser is
147+
* <a href="https://datatracker.ietf.org/doc/html/rfc8252#section-4.1">redirected back to this application</a>.
148+
* <p>
149+
* Then, the received authorization code is used to make an
150+
* <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3">Access Token Request</a>.
151+
*
152+
* @param browser An async callback (not blocking this thread) that opens a web browser with the URI it consumes
153+
* @param scopes The desired <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.3">scopes</a>
154+
* @return The <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
126155
* @throws IOException In case of I/O errors when communicating with the token endpoint
156+
* @throws InterruptedException When this thread is interrupted before a response is received
157+
* @see #authorizeAsync(Executor, Consumer, String...)
127158
*/
128159
@Blocking
129-
public String authorize(Consumer<URI> browser, String... scopes) throws IOException {
130-
try {
131-
return requestAuthCode(browser, scopes).getAccessToken();
132-
} catch (InterruptedException e) {
133-
Thread.currentThread().interrupt();
134-
throw new InterruptedIOException("Interrupted while awaiting token response");
135-
}
160+
public HttpResponse<String> authorize(Consumer<URI> browser, String... scopes) throws IOException, InterruptedException {
161+
return requestAuthCode(browser, scopes).getAccessToken();
136162
}
137163

164+
/**
165+
* Asks the given {@code browser} to browse the authorization URI. This method will block until the browser is
166+
* <a href="https://datatracker.ietf.org/doc/html/rfc8252#section-4.1">redirected back to this application</a>.
167+
*
168+
* @param browser An async callback that opens a web browser with the URI it consumes
169+
* @param scopes The desired <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.3">scopes</a>
170+
* @return An authorized instance holding the received <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2">authorization code</a>
171+
* @throws IOException In case of I/O errors when communicating with the token endpoint
172+
*/
138173
@Blocking
139174
@VisibleForTesting
140175
AuthFlowWithCode requestAuthCode(Consumer<URI> browser, String... scopes) throws IOException {
@@ -181,33 +216,26 @@ class AuthFlowWithCode {
181216
this.authorizationCode = authorizationCode;
182217
}
183218

184-
/**
185-
* Requests an access token from the {@link TinyOAuth2Client#tokenEndpoint}.
186-
*
187-
* @return The raw <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
188-
* @throws IOException In case of I/O errors when communicating with the token endpoint
189-
* @throws InterruptedException When this thread is interrupted before a response is received
190-
* @see <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3">RFC 6749 Section 4.1.3: Access Token Request</a>
191-
*/
192-
@Blocking
193-
public String getAccessToken() throws IOException, InterruptedException {
194-
var requestBody = URIUtil.buildQueryString(Map.of( //
219+
@VisibleForTesting
220+
HttpRequest buildTokenRequest() {
221+
return client.buildTokenRequest(Map.of( //
195222
"grant_type", "authorization_code", //
196223
"client_id", client.clientId, //
197224
"code_verifier", pkce.getVerifier(), //
198225
"code", authorizationCode, //
199226
"redirect_uri", redirectUri //
200227
));
201-
var request = HttpRequest.newBuilder(client.tokenEndpoint) //
202-
.header("Content-Type", "application/x-www-form-urlencoded") //
203-
.POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) //
204-
.build();
205-
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
206-
if (response.statusCode() == 200) {
207-
return response.body();
208-
} else {
209-
throw new IOException("Unexpected HTTP response code " + response.statusCode());
210-
}
228+
}
229+
230+
public CompletableFuture<HttpResponse<String>> getAccessTokenAsync(@BlockingExecutor Executor executor) {
231+
return HttpClient.newBuilder().executor(executor).build().sendAsync(buildTokenRequest(), HttpResponse.BodyHandlers.ofString());
232+
}
233+
234+
@Blocking
235+
public HttpResponse<String> getAccessToken() throws IOException, InterruptedException {
236+
// see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
237+
// and https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4
238+
return HttpClient.newHttpClient().send(buildTokenRequest(), HttpResponse.BodyHandlers.ofString());
211239
}
212240

213241
}

src/main/java/io/github/coffeelibs/tinyoauth2client/TinyOAuth2Client.java

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package io.github.coffeelibs.tinyoauth2client;
22

33
import io.github.coffeelibs.tinyoauth2client.util.URIUtil;
4+
import org.jetbrains.annotations.ApiStatus;
45
import org.jetbrains.annotations.Blocking;
6+
import org.jetbrains.annotations.BlockingExecutor;
7+
import org.jetbrains.annotations.Contract;
8+
import org.jetbrains.annotations.VisibleForTesting;
59

610
import java.io.IOException;
711
import java.net.URI;
@@ -10,12 +14,15 @@
1014
import java.net.http.HttpResponse;
1115
import java.util.Map;
1216
import java.util.Objects;
17+
import java.util.concurrent.CompletableFuture;
18+
import java.util.concurrent.Executor;
1319

1420
/**
1521
* An OAuth2 <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-2.1">public client</a> capable of making requests to a token endpoint.
16-
*
22+
*
1723
* @see TinyOAuth2#client(String)
1824
*/
25+
@ApiStatus.Experimental
1926
public class TinyOAuth2Client {
2027

2128
/**
@@ -46,31 +53,55 @@ public AuthFlow authFlow(URI authEndpoint) {
4653
/**
4754
* Refreshes an access token using the given {@code refreshToken}.
4855
*
56+
* @param executor The executor to run the async tasks
4957
* @param refreshToken The refresh token
5058
* @param scopes The desired access token <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.3">scopes</a>
51-
* @return The raw <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
59+
* @return The future <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
60+
* @see #refresh(String, String...)
61+
*/
62+
public CompletableFuture<HttpResponse<String>> refreshAsync(@BlockingExecutor Executor executor, String refreshToken, String... scopes) {
63+
return HttpClient.newBuilder().executor(executor).build().sendAsync(buildRefreshTokenRequest(refreshToken, scopes), HttpResponse.BodyHandlers.ofString());
64+
}
65+
66+
/**
67+
* Refreshes an access token using the given {@code refreshToken}.
68+
*
69+
* @param refreshToken The refresh token
70+
* @param scopes The desired access token <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.3">scopes</a>
71+
* @return The <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
5272
* @throws IOException In case of I/O errors when communicating with the token endpoint
5373
* @throws InterruptedException When this thread is interrupted before a response is received
5474
* @see <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-6">RFC 6749 Section 6: Refreshing an Access Token</a>
75+
* @see #refreshAsync(Executor, String, String...)
5576
*/
5677
@Blocking
57-
public String refresh(String refreshToken, String... scopes) throws IOException, InterruptedException {
58-
var requestBody = URIUtil.buildQueryString(Map.of(//
78+
public HttpResponse<String> refresh(String refreshToken, String... scopes) throws IOException, InterruptedException {
79+
return HttpClient.newHttpClient().send(buildRefreshTokenRequest(refreshToken, scopes), HttpResponse.BodyHandlers.ofString());
80+
}
81+
82+
@VisibleForTesting
83+
HttpRequest buildRefreshTokenRequest(String refreshToken, String... scopes) {
84+
return buildTokenRequest(Map.of(//
5985
"grant_type", "refresh_token", //
6086
"refresh_token", refreshToken, //
6187
"client_id", clientId, //
6288
"scope", String.join(" ", scopes)
6389
));
64-
var request = HttpRequest.newBuilder(tokenEndpoint) //
90+
}
91+
92+
/**
93+
* Creates a new HTTP request targeting the {@link #tokenEndpoint}.
94+
*
95+
* @param parameters Parameters to send in an {@code application/x-www-form-urlencoded} request body
96+
* @return A new http request
97+
*/
98+
@Contract("_ -> new")
99+
HttpRequest buildTokenRequest(Map<String, String> parameters) {
100+
var urlencodedParams = URIUtil.buildQueryString(parameters);
101+
return HttpRequest.newBuilder(tokenEndpoint) //
65102
.header("Content-Type", "application/x-www-form-urlencoded") //
66-
.POST(HttpRequest.BodyPublishers.ofString(requestBody)) //
103+
.POST(HttpRequest.BodyPublishers.ofString(urlencodedParams)) //
67104
.build();
68-
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
69-
if (response.statusCode() == 200) {
70-
return response.body();
71-
} else {
72-
throw new IOException("Unexpected HTTP response code " + response.statusCode());
73-
}
74105
}
75106

76107
}

src/main/java/module-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module io.github.coffeelibs.tinyoauth2client {
22
requires static org.jetbrains.annotations;
3-
requires java.net.http;
3+
requires transitive java.net.http;
44

55
exports io.github.coffeelibs.tinyoauth2client;
66
exports io.github.coffeelibs.tinyoauth2client.http.response;

0 commit comments

Comments
 (0)