Skip to content

Commit

Permalink
extend api
Browse files Browse the repository at this point in the history
  • Loading branch information
infeo committed Apr 21, 2023
1 parent 30c7a8f commit 91cb8c5
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 27 deletions.
64 changes: 53 additions & 11 deletions src/main/java/io/github/coffeelibs/tinyoauth2client/AuthFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
import io.github.coffeelibs.tinyoauth2client.http.response.Response;
import io.github.coffeelibs.tinyoauth2client.util.RandomUtil;
import io.github.coffeelibs.tinyoauth2client.util.URIUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.BlockingExecutor;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.VisibleForTesting;
import org.jetbrains.annotations.*;

import java.io.IOException;
import java.io.UncheckedIOException;
Expand Down Expand Up @@ -133,13 +129,32 @@ public AuthFlow setRedirectPort(int... ports) {
* @see #authorize(Consumer, String...)
*/
public CompletableFuture<HttpResponse<String>> authorizeAsync(@BlockingExecutor Executor executor, Consumer<URI> browser, String... scopes) {
return authorizeAsync(HttpClient.newBuilder().executor(executor).build(), browser, scopes);
}

/**
* Asks the given {@code browser} to browse the authorization URI. This method will block until the browser is
* <a href="https://datatracker.ietf.org/doc/html/rfc8252#section-4.1">redirected back to this application</a>.
* <p>
* Then, the received authorization code is used to make an
* <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3">Access Token Request</a>.
* <p>
* The executor used to the request the auth code and get the access token is the same specified in {@link HttpClient.Builder#executor(Executor)}. If the given http client does not provide an executor via {@link HttpClient#executor()}, a default single thread executor is used for requesting the authorization code.
*
* @param httpClient The http client used for the access token request
* @param browser An async callback (not blocking the executor) that opens a web browser with the URI it consumes
* @param scopes The desired <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.3">scopes</a>
* @return The future <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
* @see #authorize(Consumer, String...)
*/
public CompletableFuture<HttpResponse<String>> authorizeAsync(HttpClient httpClient, Consumer<URI> browser, String... scopes) {
return CompletableFuture.supplyAsync(() -> {
try {
return requestAuthCode(browser, scopes);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, executor).thenCompose(authorizedFlow -> authorizedFlow.getAccessTokenAsync(executor));
}, httpClient.executor().orElse(ForkJoinPool.commonPool())).thenCompose(authorizedFlow -> authorizedFlow.getAccessTokenAsync(httpClient));
}

/**
Expand All @@ -158,7 +173,27 @@ public CompletableFuture<HttpResponse<String>> authorizeAsync(@BlockingExecutor
*/
@Blocking
public HttpResponse<String> authorize(Consumer<URI> browser, String... scopes) throws IOException, InterruptedException {
return requestAuthCode(browser, scopes).getAccessToken();
return authorize(HttpClient.newHttpClient(), browser, scopes);
}

/**
* Asks the given {@code browser} to browse the authorization URI. This method will block until the browser is
* <a href="https://datatracker.ietf.org/doc/html/rfc8252#section-4.1">redirected back to this application</a>.
* <p>
* Then, the received authorization code is used to make an
* <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3">Access Token Request</a>.
*
* @param httpClient The http client used to recieve the authorization code
* @param browser An async callback (not blocking this thread) that opens a web browser with the URI it consumes
* @param scopes The desired <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.3">scopes</a>
* @return The <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
* @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(Executor, Consumer, String...)
*/
@Blocking
public HttpResponse<String> authorize(HttpClient httpClient, Consumer<URI> browser, String... scopes) throws IOException, InterruptedException {
return requestAuthCode(browser, scopes).getAccessToken(httpClient);
}

/**
Expand Down Expand Up @@ -231,16 +266,23 @@ HttpRequest buildTokenRequest() {
}

public CompletableFuture<HttpResponse<String>> getAccessTokenAsync(@BlockingExecutor Executor executor) {
return HttpClient.newBuilder().executor(executor).build().sendAsync(buildTokenRequest(), HttpResponse.BodyHandlers.ofString());
return getAccessTokenAsync(HttpClient.newBuilder().executor(executor).build());
}

public CompletableFuture<HttpResponse<String>> getAccessTokenAsync(HttpClient httpClient) {
return httpClient.sendAsync(buildTokenRequest(), HttpResponse.BodyHandlers.ofString());
}

@Blocking
public HttpResponse<String> getAccessToken() throws IOException, InterruptedException {
return getAccessToken(HttpClient.newHttpClient());
}

@Blocking
public HttpResponse<String> getAccessToken(HttpClient httpClient) throws IOException, InterruptedException {
// see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
// and https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4
return HttpClient.newHttpClient().send(buildTokenRequest(), HttpResponse.BodyHandlers.ofString());
return httpClient.send(buildTokenRequest(), HttpResponse.BodyHandlers.ofString()); //TODO
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public AuthFlow authFlow(URI authEndpoint) {
}

/**
* Refreshes an access token using the given {@code refreshToken}.
* 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.
*
* @param executor The executor to run the async tasks
* @param refreshToken The refresh token
Expand All @@ -56,11 +57,26 @@ public AuthFlow authFlow(URI authEndpoint) {
* @see #refresh(String, String...)
*/
public CompletableFuture<HttpResponse<String>> refreshAsync(@BlockingExecutor Executor executor, String refreshToken, String... scopes) {
return HttpClient.newBuilder().executor(executor).build().sendAsync(buildRefreshTokenRequest(refreshToken, scopes), HttpResponse.BodyHandlers.ofString());
return refreshAsync(HttpClient.newBuilder().executor(executor).build(), refreshToken, scopes);
}

/**
* Refreshes an access token using the given {@code refreshToken}.
* Refreshes an access token using the given {@code refreshToken}. The request is made by the given {@link HttpClient}.
* <p>
* The executor used to send the request and handle the response is the same specified in {@link HttpClient.Builder#executor(Executor)}
*
* @param httpClient The http client used to send the request
* @param refreshToken The refresh token
* @param scopes The desired access token <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.3">scopes</a>
* @return The future <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
* @see #refresh(String, String...)
*/
public CompletableFuture<HttpResponse<String>> refreshAsync(HttpClient httpClient, String refreshToken, String... scopes) {
return httpClient.sendAsync(buildRefreshTokenRequest(refreshToken, scopes), HttpResponse.BodyHandlers.ofString()); //TODO
}

/**
* Refreshes an access token using the given {@code refreshToken}. The request is made by the default http client returned by {@link HttpClient#newHttpClient()}.
*
* @param refreshToken The refresh token
* @param scopes The desired access token <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.3">scopes</a>
Expand All @@ -72,7 +88,24 @@ public CompletableFuture<HttpResponse<String>> refreshAsync(@BlockingExecutor Ex
*/
@Blocking
public HttpResponse<String> refresh(String refreshToken, String... scopes) throws IOException, InterruptedException {
return HttpClient.newHttpClient().send(buildRefreshTokenRequest(refreshToken, scopes), HttpResponse.BodyHandlers.ofString());
return refresh(HttpClient.newHttpClient(), refreshToken, scopes);
}

/**
* Refreshes an access token using the given {@code refreshToken}. The request is made by the given {@link HttpClient}.
*
* @param httpClient The http client used to send the request
* @param refreshToken The refresh token
* @param scopes The desired access token <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.3">scopes</a>
* @return The <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4">Access Token Response</a>
* @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 <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-6">RFC 6749 Section 6: Refreshing an Access Token</a>
* @see #refreshAsync(Executor, String, String...)
*/
@Blocking
public HttpResponse<String> refresh(HttpClient httpClient, String refreshToken, String... scopes) throws IOException, InterruptedException {
return httpClient.send(buildRefreshTokenRequest(refreshToken, scopes), HttpResponse.BodyHandlers.ofString()); //TODO
}

@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@
import io.github.coffeelibs.tinyoauth2client.http.RedirectTarget;
import io.github.coffeelibs.tinyoauth2client.http.response.Response;
import io.github.coffeelibs.tinyoauth2client.util.URIUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
Expand All @@ -22,6 +16,7 @@
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
Expand Down Expand Up @@ -343,13 +338,15 @@ public void testGetAccessToken() throws IOException, InterruptedException {
@SuppressWarnings("unchecked")
public void testAuthorizeAsync() throws IOException, ExecutionException, InterruptedException {
Consumer<URI> browser = Mockito.mock(Consumer.class);
var httpClient = Mockito.mock(HttpClient.class);
Mockito.when(httpClient.executor()).thenReturn(Optional.empty());
var authFlow = Mockito.spy(new AuthFlow(client, authEndpoint, pkce));
var authFlowWithCode = Mockito.mock(AuthFlow.AuthFlowWithCode.class);
var httpResponse = Mockito.mock(HttpResponse.class);
Mockito.doReturn(authFlowWithCode).when(authFlow).requestAuthCode(Mockito.any(), Mockito.any());
Mockito.doReturn(CompletableFuture.completedFuture(httpResponse)).when(authFlowWithCode).getAccessTokenAsync(Mockito.any());
Mockito.doReturn(CompletableFuture.completedFuture(httpResponse)).when(authFlowWithCode).getAccessTokenAsync((HttpClient) Mockito.any());

var result = authFlow.authorizeAsync(Runnable::run, browser);
var result = authFlow.authorizeAsync(httpClient, browser);

Assertions.assertEquals(httpResponse, result.get());
}
Expand Down Expand Up @@ -377,7 +374,7 @@ public void testAuthorizeAsyncWithError2() throws IOException {
var authFlow = Mockito.spy(new AuthFlow(client, authEndpoint, pkce));
var authFlowWithCode = Mockito.mock(AuthFlow.AuthFlowWithCode.class);
Mockito.doReturn(authFlowWithCode).when(authFlow).requestAuthCode(Mockito.any(), Mockito.any());
Mockito.doReturn(CompletableFuture.failedFuture(new IOException("error"))).when(authFlowWithCode).getAccessTokenAsync(Mockito.any());
Mockito.doReturn(CompletableFuture.failedFuture(new IOException("error"))).when(authFlowWithCode).getAccessTokenAsync((Executor) Mockito.any());

var result = authFlow.authorizeAsync(Runnable::run, browser);

Expand All @@ -389,13 +386,14 @@ public void testAuthorizeAsyncWithError2() throws IOException {
@SuppressWarnings("unchecked")
public void testAuthorize() throws IOException, InterruptedException {
Consumer<URI> browser = Mockito.mock(Consumer.class);
var httpClient = Mockito.mock(HttpClient.class);
var authFlow = Mockito.spy(new AuthFlow(client, authEndpoint, pkce));
var authFlowWithCode = Mockito.mock(AuthFlow.AuthFlowWithCode.class);
var httpResponse = Mockito.mock(HttpResponse.class);
Mockito.doReturn(authFlowWithCode).when(authFlow).requestAuthCode(Mockito.any(), Mockito.any());
Mockito.doReturn(httpResponse).when(authFlowWithCode).getAccessToken();
Mockito.doReturn(httpResponse).when(authFlowWithCode).getAccessToken(httpClient);

var result = authFlow.authorize(browser);
var result = authFlow.authorize(httpClient, browser);

Assertions.assertEquals(httpResponse, result);
}
Expand Down

0 comments on commit 91cb8c5

Please sign in to comment.