|
6 | 6 | import io.github.coffeelibs.tinyoauth2client.util.URIUtil;
|
7 | 7 | import org.jetbrains.annotations.ApiStatus;
|
8 | 8 | import org.jetbrains.annotations.Blocking;
|
| 9 | +import org.jetbrains.annotations.BlockingExecutor; |
9 | 10 | import org.jetbrains.annotations.Contract;
|
10 | 11 | import org.jetbrains.annotations.VisibleForTesting;
|
11 | 12 |
|
12 | 13 | import java.io.IOException;
|
13 |
| -import java.io.InterruptedIOException; |
| 14 | +import java.io.UncheckedIOException; |
14 | 15 | import java.net.URI;
|
15 | 16 | import java.net.http.HttpClient;
|
16 | 17 | import java.net.http.HttpRequest;
|
17 | 18 | import java.net.http.HttpResponse;
|
18 | 19 | import java.util.Map;
|
19 | 20 | import java.util.Objects;
|
20 | 21 | import java.util.Set;
|
| 22 | +import java.util.concurrent.CompletableFuture; |
| 23 | +import java.util.concurrent.Executor; |
21 | 24 | import java.util.concurrent.ForkJoinPool;
|
22 | 25 | import java.util.function.Consumer;
|
23 | 26 |
|
@@ -119,22 +122,54 @@ public AuthFlow setRedirectPort(int... ports) {
|
119 | 122 | /**
|
120 | 123 | * Asks the given {@code browser} to browse the authorization URI. This method will block until the browser is
|
121 | 124 | * <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>. |
122 | 128 | *
|
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 |
124 | 131 | * @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> |
126 | 155 | * @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...) |
127 | 158 | */
|
128 | 159 | @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(); |
136 | 162 | }
|
137 | 163 |
|
| 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 | + */ |
138 | 173 | @Blocking
|
139 | 174 | @VisibleForTesting
|
140 | 175 | AuthFlowWithCode requestAuthCode(Consumer<URI> browser, String... scopes) throws IOException {
|
@@ -181,33 +216,26 @@ class AuthFlowWithCode {
|
181 | 216 | this.authorizationCode = authorizationCode;
|
182 | 217 | }
|
183 | 218 |
|
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( // |
195 | 222 | "grant_type", "authorization_code", //
|
196 | 223 | "client_id", client.clientId, //
|
197 | 224 | "code_verifier", pkce.getVerifier(), //
|
198 | 225 | "code", authorizationCode, //
|
199 | 226 | "redirect_uri", redirectUri //
|
200 | 227 | ));
|
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()); |
211 | 239 | }
|
212 | 240 |
|
213 | 241 | }
|
|
0 commit comments