Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Override the standard OAuth2 API implementation for introspection #28

Merged
merged 1 commit into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
Expand Down Expand Up @@ -632,8 +648,8 @@ public CompletableFuture<Void> logout(final @NotNull String accessToken, final @
* @param payload the payload to send
* @return an asynchronous http response wrapped in a completable future
*/
private CompletableFuture<HttpResponse<String>> fetch(HttpMethod method, String path,
JSONObject headers, String payload) {
protected CompletableFuture<HttpResponse<String>> 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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,28 +47,38 @@ public class OAuth2AuthenticationProvider implements AuthenticationProvider<Cred
@Nullable
private final Stage stage;
@NotNull
private final OAuth2Options options;
@NotNull
private final OAuth2API api;
@NotNull
private final OAuth2Options options;

private HttpServer httpServer;

/**
* Creates a OAuth2 authentication provider.
* Creates an 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 OAuth2AuthenticationProvider(@Nullable final Stage stage, @NotNull final OAuth2Options options) {
public OAuth2AuthenticationProvider(@Nullable final Stage stage, @NotNull final OAuth2API api) {
this.stage = stage;
this.options = Objects.requireNonNull(options, "OAuth2 options cannot be null");
this.api = new OAuth2API(options);
this.api = Objects.requireNonNull(api, "OAuth2 api cannot be null");
this.options = api.getOptions();
this.options.validate();

// Create a new http server
this.httpServer = HttpServer.create(stage);
}

/**
* Creates an OAuth2 authentication provider.
*
* @param stage the JavaFX application stage
* @param options the OAuth2 options
*/
public OAuth2AuthenticationProvider(@Nullable final Stage stage, @NotNull final OAuth2Options options) {
this(stage, new OAuth2API(options));
}

/**
* Returns the options used to configure this provider.
*
Expand Down Expand Up @@ -656,7 +666,7 @@ private String normalizeUri(String uri) {
final int port = httpServer.getServerPort();
String server = httpServer.getServerHost();
final String loopbackAddress = InetAddress.getLoopbackAddress().getHostAddress();
if (getOptions().isUseLoopbackIpAddress()) {
if (options.isUseLoopbackIpAddress()) {
server = loopbackAddress;
}
final boolean localAddress = server.equals("localhost") || server.equals(loopbackAddress);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package one.jpro.platform.auth.core.oauth2.provider;

import javafx.stage.Stage;
import one.jpro.platform.auth.core.http.HttpMethod;
import one.jpro.platform.auth.core.oauth2.OAuth2API;
import one.jpro.platform.auth.core.oauth2.OAuth2AuthenticationProvider;
import one.jpro.platform.auth.core.oauth2.OAuth2Flow;
import one.jpro.platform.auth.core.oauth2.OAuth2Options;
import one.jpro.platform.auth.core.utils.AuthUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;

Expand All @@ -27,7 +31,7 @@ public class GoogleAuthenticationProvider extends OpenIDAuthenticationProvider {
* @param options custom OAuth2 options
*/
public GoogleAuthenticationProvider(@Nullable final Stage stage, @NotNull final OAuth2Options options) {
super(stage, options);
super(stage, new GoogleOAuth2API(options));
}

/**
Expand All @@ -40,7 +44,7 @@ public GoogleAuthenticationProvider(@Nullable final Stage stage, @NotNull final
public GoogleAuthenticationProvider(@Nullable final Stage stage,
@NotNull final String clientId,
@NotNull final String clientSecret) {
super(stage, new OAuth2Options()
super(stage, new GoogleOAuth2API(new OAuth2Options()
.setFlow(OAuth2Flow.AUTH_CODE)
.setClientId(clientId)
.setClientSecret(clientSecret)
Expand All @@ -52,7 +56,7 @@ public GoogleAuthenticationProvider(@Nullable final Stage stage,
.setJwkPath("https://www.googleapis.com/oauth2/v3/certs")
.setIntrospectionPath("https://oauth2.googleapis.com/tokeninfo")
.setRevocationPath("https://oauth2.googleapis.com/revoke")
.setUserInfoParams(new JSONObject().put("alt", "json")));
.setUserInfoParams(new JSONObject().put("alt", "json"))));
}

/**
Expand All @@ -74,4 +78,57 @@ public static CompletableFuture<OpenIDAuthenticationProvider> 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<JSONObject> 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);
}
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
Expand All @@ -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;
Expand Down