diff --git a/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/OAuth2API.java b/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/OAuth2API.java index e9791dba..329bd287 100755 --- a/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/OAuth2API.java +++ b/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/OAuth2API.java @@ -18,13 +18,15 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; -import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static one.jpro.platform.auth.core.utils.AuthUtils.BASE64_ENCODER; + /** * OAuth2 API provides the required functionalities to interact with an OAuth2 provider. * @@ -34,18 +36,32 @@ public class OAuth2API { private static final Pattern MAX_AGE = Pattern.compile("max-age=\"?(\\d+)\"?"); private static final String CACHE_CONTROL = "cache-control"; - private static final Base64.Encoder BASE64_ENCODER = AuthUtils.BASE64_ENCODER; @NotNull - private final OAuth2Options options; + protected final OAuth2Options options; @NotNull private final HttpClient httpClient; + /** + * Creates an OAuth2 API object. + * + * @param options the OAuth2 options + */ public OAuth2API(@NotNull final OAuth2Options options) { - this.options = options; + this.options = Objects.requireNonNull(options, "OAuth2 options cannot be null"); this.httpClient = HttpClient.newHttpClient(); } + /** + * Returns the options used to configure this API. + * + * @return an OAuth2 options object + */ + @NotNull + public OAuth2Options getOptions() { + return options; + } + /** * The client sends the end-user's browser to this endpoint to request their authentication and consent. * This endpoint is used in the code and implicit OAuth 2.0 flows which require end-user interaction. @@ -632,8 +648,8 @@ public CompletableFuture logout(final @NotNull String accessToken, final @ * @param payload the payload to send * @return an asynchronous http response wrapped in a completable future */ - private CompletableFuture> fetch(HttpMethod method, String path, - JSONObject headers, String payload) { + protected CompletableFuture> fetch(HttpMethod method, String path, + JSONObject headers, String payload) { if (path == null || path.isEmpty()) { // and this can happen as it is a config option that is dependent on the provider return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid path")); diff --git a/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/OAuth2AuthenticationProvider.java b/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/OAuth2AuthenticationProvider.java index 64743450..4758a33e 100755 --- a/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/OAuth2AuthenticationProvider.java +++ b/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/OAuth2AuthenticationProvider.java @@ -47,28 +47,38 @@ public class OAuth2AuthenticationProvider implements AuthenticationProvider discover(@Nullable .setUserInfoParams(new JSONObject().put("alt", "json"))) .discover(); } + + /** + * Inner class to handle OAuth2 API specific to Google. + */ + private static class GoogleOAuth2API extends OAuth2API { + + public GoogleOAuth2API(@NotNull final OAuth2Options options) { + super(options); + } + + @Override + public CompletableFuture tokenIntrospection(String tokenType, String token) { + final JSONObject headers = new JSONObject(); + + final boolean confidentialClient = options.getClientId() != null && options.getClientSecret() != null; + if (confidentialClient) { + String basic = options.getClientId() + ":" + options.getClientSecret(); + headers.put("Authorization", "Basic " + + AuthUtils.BASE64_ENCODER.encodeToString(basic.getBytes(StandardCharsets.UTF_8))); + } + + final String path = options.getIntrospectionPath() + "?" + tokenType + "=" + token; + + headers.put("Content-Type", "application/x-www-form-urlencoded"); + // specify preferred accepted accessToken type + headers.put("Accept", "application/json,application/x-www-form-urlencoded;q=0.9"); + + return fetch(HttpMethod.POST, path, headers, null) + .thenCompose(response -> { + if (response.body() == null || response.body().isEmpty()) { + return CompletableFuture.failedFuture(new RuntimeException("No Body")); + } + + JSONObject json; + if (AuthUtils.containsValue(response.headers(), "application/json")) { + json = new JSONObject(response.body()); + } else if (AuthUtils.containsValue(response.headers(), "application/x-www-form-urlencoded") || + AuthUtils.containsValue(response.headers(), "text/plain")) { + json = AuthUtils.queryToJson(response.body()); + } else return CompletableFuture.failedFuture( + new RuntimeException("Cannot handle accessToken type: " + + response.headers().allValues("Content-Type"))); + + if (json == null || json.has("error")) { + return CompletableFuture.failedFuture( + new RuntimeException(AuthUtils.extractErrorDescription(json))); + } else { + AuthUtils.processNonStandardHeaders(json, response, options.getScopeSeparator()); + return CompletableFuture.completedFuture(json); + } + }); + } + } } diff --git a/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/provider/OpenIDAuthenticationProvider.java b/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/provider/OpenIDAuthenticationProvider.java index 7b1650ce..8ee3288c 100644 --- a/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/provider/OpenIDAuthenticationProvider.java +++ b/jpro-auth/core/src/main/java/one/jpro/platform/auth/core/oauth2/provider/OpenIDAuthenticationProvider.java @@ -2,6 +2,7 @@ import javafx.stage.Stage; import one.jpro.platform.auth.core.authentication.User; +import one.jpro.platform.auth.core.oauth2.OAuth2API; import one.jpro.platform.auth.core.oauth2.OAuth2AuthenticationProvider; import one.jpro.platform.auth.core.oauth2.OAuth2Credentials; import one.jpro.platform.auth.core.oauth2.OAuth2Options; @@ -23,12 +24,13 @@ public class OpenIDAuthenticationProvider extends OAuth2AuthenticationProvider { /** * Creates a OAuth2 authentication provider. * - * @param stage the JavaFX application stage - * @param options the OAuth2 options + * @param stage the JavaFX application stage + * @param api the OAuth2 api */ - public OpenIDAuthenticationProvider(@Nullable Stage stage, @NotNull OAuth2Options options) { - super(stage, options); + protected OpenIDAuthenticationProvider(@Nullable Stage stage, @NotNull OAuth2API api) { + super(stage, api); + final OAuth2Options options = api.getOptions(); // Configure credentials scopes if (options.getSupportedScopes() != null && !options.getSupportedScopes().isEmpty()) { credentials.setScopes(options.getSupportedScopes()); @@ -37,6 +39,16 @@ public OpenIDAuthenticationProvider(@Nullable Stage stage, @NotNull OAuth2Option } } + /** + * Creates a OAuth2 authentication provider. + * + * @param stage the JavaFX application stage + * @param options the OAuth2 options + */ + public OpenIDAuthenticationProvider(@Nullable Stage stage, @NotNull OAuth2Options options) { + this(stage, new OAuth2API(options)); + } + @NotNull public OAuth2Credentials getCredentials() { return credentials;