diff --git a/.github/coveragereport/badge_branchcoverage.svg b/.github/coveragereport/badge_branchcoverage.svg
index 8d2dba49..cbae7016 100644
--- a/.github/coveragereport/badge_branchcoverage.svg
+++ b/.github/coveragereport/badge_branchcoverage.svg
@@ -101,7 +101,7 @@
Coverage
Coverage
- 80%80%
+ 76.9%76.9%
diff --git a/.github/coveragereport/badge_linecoverage.svg b/.github/coveragereport/badge_linecoverage.svg
index 22764e15..84e25ed7 100644
--- a/.github/coveragereport/badge_linecoverage.svg
+++ b/.github/coveragereport/badge_linecoverage.svg
@@ -100,7 +100,7 @@
Coverage
Coverage
- 95.1%95.1%
+ 94.5%94.5%
diff --git a/.github/coveragereport/badge_methodcoverage.svg b/.github/coveragereport/badge_methodcoverage.svg
index 0f8b6dad..d449fc9f 100644
--- a/.github/coveragereport/badge_methodcoverage.svg
+++ b/.github/coveragereport/badge_methodcoverage.svg
@@ -102,7 +102,7 @@
Coverage
- 93.6%93.6%
+ 93.1%93.1%
diff --git a/.gitignore b/.gitignore
index b227d28a..dad901df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ build
**/bin
**/.idea/
lib/
+.vscode
diff --git a/build.gradle b/build.gradle
index 17f7b11c..ce8634be 100644
--- a/build.gradle
+++ b/build.gradle
@@ -32,7 +32,7 @@ subprojects {
}
jacoco {
- toolVersion = "0.8.7"
+ toolVersion = "0.8.11"
}
jacocoTestReport {
diff --git a/buildSrc/src/main/groovy/cwms-data-api-client.java-conventions.gradle b/buildSrc/src/main/groovy/cwms-data-api-client.java-conventions.gradle
index 7f6d82f6..5c89a058 100644
--- a/buildSrc/src/main/groovy/cwms-data-api-client.java-conventions.gradle
+++ b/buildSrc/src/main/groovy/cwms-data-api-client.java-conventions.gradle
@@ -22,6 +22,10 @@ configurations.implementation.extendsFrom(configurations.annotationProcessor)
test {
useJUnitPlatform()
+ // when passing --tests by default it needs to match all in all subjects
+ filter {
+ setFailOnNoMatchingTests(false)
+ }
}
javadoc {
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/AuthCodePkceTokenRequestBuilder.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/AuthCodePkceTokenRequestBuilder.java
new file mode 100644
index 00000000..cbe0adbd
--- /dev/null
+++ b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/AuthCodePkceTokenRequestBuilder.java
@@ -0,0 +1,191 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2024 Hydrologic Engineering Center
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package hec.army.usace.hec.cwbi.auth.http.client;
+
+import mil.army.usace.hec.cwms.http.client.HttpRequestBuilderImpl;
+import mil.army.usace.hec.cwms.http.client.HttpRequestResponse;
+import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
+import mil.army.usace.hec.cwms.http.client.request.HttpRequestExecutor;
+import mil.army.usace.hec.cwms.http.client.request.QueryParameters;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+/**
+ * Use Authorization Code + PKCE method to retrieve initial token set.
+ *
+ * If a desktop is available users's default Browser is opened with the given auth URL
+ * To complete the additional requirements.
+ */
+public final class AuthCodePkceTokenRequestBuilder extends TokenRequestBuilder {
+ private static final Logger LOGGER = Logger.getLogger(AuthCodePkceTokenRequestBuilder.class.getName());
+ @Override
+ OAuth2Token retrieveToken() throws IOException {
+
+ OAuth2Token retVal = null;
+ HttpServer server = null;
+ // https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
+ try {
+ byte[] verifierBytes = new byte[128];
+ SecureRandom.getInstanceStrong().nextBytes(verifierBytes);
+ Base64.Encoder b64encoder = Base64.getUrlEncoder().withoutPadding();
+ final String verifier = b64encoder.encodeToString(verifierBytes);
+ final String originalState = UUID.randomUUID().toString();
+
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ final String challenge = b64encoder.encodeToString(md.digest(verifier.getBytes(StandardCharsets.US_ASCII)));
+ server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+ int port = server.getAddress().getPort();
+ String host = server.getAddress().getHostName();
+
+ final CompletableFuture future = new CompletableFuture<>();
+
+ server.createContext("/", new HttpHandler() {
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ Result ret = null;
+
+ final String query = exchange.getRequestURI().getQuery();
+ LOGGER.fine("Got auth server response." + query);
+ final QueryParameters parameters = QueryParameters.parse(query);
+ if (!parameters.get("error").isEmpty()) {
+ String error = parameters.get("error").get(0);
+ String errorDescription = parameters.get("error_description").get(0);
+ ret = Result.failure(error, errorDescription);
+ } else {
+ String code = parameters.get("code").get(0);
+ String state = parameters.get("state").get(0);
+ String session_state = parameters.get("session_state").get(0);
+ ret = Result.success(code ,state, session_state);
+ }
+ LOGGER.fine("Returning result back to thread.");
+ exchange.sendResponseHeaders(204, 0);
+
+ future.complete(ret);
+ }
+
+ });
+ final String redirectUri = String.format("http://%s:%d", host, port);
+ final QueryParameters authParameters = QueryParameters.empty()
+ .set("grant_type", "code")
+ .set("client_id", getClientId())
+ .set("scope", "openid profile")
+ .set("response_type", "code")
+ .set("code_challenge_method", "S256")
+ .set("code_challenge", challenge)
+ .set("redirect_uri", redirectUri)
+ .set("state", originalState);
+ String urlStr= String.format("%s?%s", getAuthUrl().getApiRoot(), authParameters.encode());
+ // start server to listen
+ server.start();
+ LOGGER.info("Handling Auth Request");
+ LOGGER.finer("Auth Request URL: " + urlStr);
+ this.authCallBack.accept(URI.create(urlStr));
+
+ Result result = future.get(3, TimeUnit.MINUTES); // The user is now required to perform manual operations.
+ LOGGER.info("Retrieving Token.");
+ if (result.error != null) {
+ throw new IOException(String.format("Unable to login. %s : %s", result.error, result.errorDescription));
+ }
+ if (!result.state.equals(originalState)) {
+ throw new IOException("Unable to continue login sequence, incorrect state value returned.");
+ }
+ final UrlEncodedFormData formData = new UrlEncodedFormData();
+ formData.addClientId(getClientId())
+ .addGrantType("authorization_code")
+ .addParameter("code_verifier", verifier)
+ .addScopes("openid", "profile")
+ .addParameter("redirect_uri", redirectUri)
+ .addParameter("state", result.state)
+ .addParameter("session_state", result.session_state)
+ .addParameter("code", result.code)
+ .addParameter("response_mode", "fragment")
+ .addParameter("response_type", "id_token token");
+
+ HttpRequestExecutor executor =
+ new HttpRequestBuilderImpl(getTokenUrl())
+ .post()
+ .withBody(formData.buildEncodedString())
+ .withMediaType(MEDIA_TYPE);
+ try (HttpRequestResponse response = executor.execute()) {
+ String body = response.getBody();
+ if (body != null) {
+ retVal = OAuth2ObjectMapper.mapJsonToObject(body, OAuth2Token.class);
+ }
+ }
+ return retVal;
+ } catch (NoSuchAlgorithmException ex) {
+ throw new IOException("Unable to retrieve SecureRandom or Message Digest instance to generate verifier", ex);
+ } catch (InterruptedException | ExecutionException | TimeoutException ex) {
+ throw new IOException("Unable to form login sequence.", ex);
+ }
+ finally {
+ if (server != null) {
+ server.stop(0);
+ }
+ }
+ }
+
+ private static class Result {
+ public final String code;
+ public final String state;
+ public final String session_state;
+
+ public final String error;
+ public final String errorDescription;
+
+ private Result(String code, String state, String session_state, String error, String errorDescription) {
+ this.code = code;
+ this.state = state;
+ this.session_state = session_state;
+ this.error = error;
+ this.errorDescription = errorDescription;
+ }
+
+ public static Result success(String code, String state, String session_state) {
+ return new Result(code, state, session_state, null, null);
+ }
+
+ public static Result failure(String error, String errorDescription) {
+ return new Result(null, null, null, error, errorDescription);
+ }
+ };
+}
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/CwbiAuthTokenProvider.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/CwbiAuthTokenProvider.java
index 36e6a52b..33847549 100644
--- a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/CwbiAuthTokenProvider.java
+++ b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/CwbiAuthTokenProvider.java
@@ -24,35 +24,55 @@
package hec.army.usace.hec.cwbi.auth.http.client;
import hec.army.usace.hec.cwbi.auth.http.client.trustmanagers.CwbiAuthTrustManager;
+
+import java.io.IOException;
import java.util.Objects;
+
import javax.net.ssl.SSLSocketFactory;
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfoBuilder;
import mil.army.usace.hec.cwms.http.client.SslSocketData;
+import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
+/**
+ * Suitable only for CWBI Keycloaks direct grant setup.
+ */
public final class CwbiAuthTokenProvider extends CwbiAuthTokenProviderBase {
private final SSLSocketFactory sslSocketFactory;
- private final String url;
/**
* Provider for OAuth2Tokens.
*
- * @param tokenUrl - URL we are fetching token from
+ * @param wellKnownUrl - URL we are retrieving configuration from
* @param clientId - client name
* @param sslSocketFactory - ssl socket factory
*/
- public CwbiAuthTokenProvider(String tokenUrl, String clientId, SSLSocketFactory sslSocketFactory) {
- super(clientId);
+ public CwbiAuthTokenProvider(String wellKnownUrl, String clientId, SSLSocketFactory sslSocketFactory) {
+ super(clientId, wellKnownUrl);
this.sslSocketFactory = Objects.requireNonNull(sslSocketFactory, "Missing required sslSocketFactory");
- this.url = Objects.requireNonNull(tokenUrl, "Missing required tokenUrl");
}
@Override
ApiConnectionInfo getUrl() {
- return new ApiConnectionInfoBuilder(url)
+ return new ApiConnectionInfoBuilder(this.wellKnownUrl)
+ .withSslSocketData(new SslSocketData(sslSocketFactory, CwbiAuthTrustManager.getTrustManager()))
+ .build();
+ }
+
+ @Override
+ public ApiConnectionInfo getAuthUrl() {
+ // This is specific to CWBI Direct Grant so this replacement as-is is fine
+ return new ApiConnectionInfoBuilder(this.tokenUrl.getApiRoot().replace("identity", "identityc"))
.withSslSocketData(new SslSocketData(sslSocketFactory, CwbiAuthTrustManager.getTrustManager()))
.build();
}
+ @Override
+ public OAuth2Token newToken() throws IOException {
+ return new DirectGrantX509TokenRequestBuilder()
+ .withTokenUrl(getAuthUrl())
+ .buildRequest().withClientId(clientId)
+ .fetchToken();
+ }
}
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/CwbiAuthTokenProviderBase.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/CwbiAuthTokenProviderBase.java
index 9a093dea..7e1b5c55 100644
--- a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/CwbiAuthTokenProviderBase.java
+++ b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/CwbiAuthTokenProviderBase.java
@@ -24,50 +24,35 @@
package hec.army.usace.hec.cwbi.auth.http.client;
import java.io.IOException;
-import java.util.Objects;
+
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
-import mil.army.usace.hec.cwms.http.client.auth.OAuth2TokenProvider;
-abstract class CwbiAuthTokenProviderBase implements OAuth2TokenProvider {
- protected OAuth2Token oauth2Token;
- protected final String clientId;
+abstract class CwbiAuthTokenProviderBase extends OidcAuthTokenProvider {
- protected CwbiAuthTokenProviderBase(String clientId) {
- this.clientId = Objects.requireNonNull(clientId, "Missing required clientId");
+ protected CwbiAuthTokenProviderBase(String clientId, String wellKnownUrl) {
+ super(clientId, wellKnownUrl);
}
abstract ApiConnectionInfo getUrl() throws IOException;
- @Override
- public synchronized void clear() {
- oauth2Token = null;
- }
@Override
public synchronized OAuth2Token getToken() throws IOException {
- if (oauth2Token == null) {
- oauth2Token = newToken();
+ if (token == null) {
+ token = newToken();
}
- return oauth2Token;
- }
-
- @Override
- public OAuth2Token newToken() throws IOException {
- return new DirectGrantX509TokenRequestBuilder()
- .withUrl(getUrl())
- .withClientId(clientId)
- .fetchToken();
+ return token;
}
@Override
public synchronized OAuth2Token refreshToken() throws IOException {
- OAuth2Token token = new RefreshTokenRequestBuilder()
- .withRefreshToken(oauth2Token.getRefreshToken())
- .withUrl(getUrl())
+ OAuth2Token newToken = new RefreshTokenRequestBuilder()
+ .withRefreshToken(token.getRefreshToken())
+ .withUrl(tokenUrl)
.withClientId(clientId)
.fetchToken();
- oauth2Token = token;
+ token = newToken;
return token;
}
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/DirectGrantX509TokenRequestBuilder.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/DirectGrantX509TokenRequestBuilder.java
index faface92..74224222 100644
--- a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/DirectGrantX509TokenRequestBuilder.java
+++ b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/DirectGrantX509TokenRequestBuilder.java
@@ -23,6 +23,7 @@
*/
package hec.army.usace.hec.cwbi.auth.http.client;
+import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
import mil.army.usace.hec.cwms.http.client.HttpRequestBuilderImpl;
import mil.army.usace.hec.cwms.http.client.HttpRequestResponse;
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
@@ -30,7 +31,7 @@
import java.io.IOException;
-public final class DirectGrantX509TokenRequestBuilder extends TokenRequestBuilder {
+public final class DirectGrantX509TokenRequestBuilder extends TokenRequestBuilder {
@Override
OAuth2Token retrieveToken() throws IOException {
@@ -43,7 +44,7 @@ OAuth2Token retrieveToken() throws IOException {
.addUsername("")
.buildEncodedString();
HttpRequestExecutor executor =
- new HttpRequestBuilderImpl(getUrl())
+ new HttpRequestBuilderImpl(getTokenUrl())
.post()
.withBody(formBody)
.withMediaType(MEDIA_TYPE);
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/DiscoveredCwbiAuthTokenProvider.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/DiscoveredCwbiAuthTokenProvider.java
deleted file mode 100644
index 7c51385a..00000000
--- a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/DiscoveredCwbiAuthTokenProvider.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025 Hydrologic Engineering Center
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package hec.army.usace.hec.cwbi.auth.http.client;
-
-import java.io.IOException;
-import java.util.Objects;
-import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
-
-public final class DiscoveredCwbiAuthTokenProvider extends CwbiAuthTokenProviderBase
-{
- private final TokenUrlDiscoveryService tokenUrlDiscoveryService;
- private ApiConnectionInfo url;
-
- public DiscoveredCwbiAuthTokenProvider(String clientId, TokenUrlDiscoveryService tokenUrlDiscoveryService)
- {
- super(clientId);
- this.tokenUrlDiscoveryService = Objects.requireNonNull(tokenUrlDiscoveryService, "Missing required tokenUrlDiscoveryService");
- }
-
- @Override
- synchronized ApiConnectionInfo getUrl() throws IOException {
- if(url == null)
- {
- url = tokenUrlDiscoveryService.discoverTokenUrl();
- }
- return url;
- }
-}
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/OidcAuthTokenProvider.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/OidcAuthTokenProvider.java
new file mode 100644
index 00000000..fc6eb352
--- /dev/null
+++ b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/OidcAuthTokenProvider.java
@@ -0,0 +1,126 @@
+package hec.army.usace.hec.cwbi.auth.http.client;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Objects;
+import java.util.concurrent.CompletionException;
+import java.util.function.Consumer;
+
+import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
+import mil.army.usace.hec.cwms.http.client.ApiConnectionInfoBuilder;
+import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
+import mil.army.usace.hec.cwms.http.client.auth.OAuth2TokenProvider;
+
+/**
+ * Handle generic OIDC auth based on configuration elements in the .well-known/openid-configuration
+ * values.
+ *
+ * Defaults to using Authorization Code + PKCE.
+ * Support should be provided to support alternative flows as a user-at-login decision point.
+ */
+public class OidcAuthTokenProvider implements OAuth2TokenProvider {
+
+ protected final String clientId;
+ protected final String wellKnownUrl;
+ protected final ApiConnectionInfo tokenUrl;
+ protected final ApiConnectionInfo authUrl;
+ protected OAuth2Token token = null;
+ // Default to open browser or print to console for usage, but allow overriding for testing and
+ // other usages.
+ private Consumer authCallback = TokenRequestBuilder.BROWSER_OR_CONSOLE_AUTH_CALLBACK;
+
+ public OidcAuthTokenProvider(String clientId, String wellKnownUrl) {
+ this.clientId = Objects.requireNonNull(clientId, "Missing required client id.");
+ this.wellKnownUrl = Objects.requireNonNull(wellKnownUrl, "Missing required well known Url.");
+
+ OpenIdTokenController controller = new OpenIdTokenController() {
+
+ @Override
+ public String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) throws IOException {
+ return wellKnownUrl; // we already have it.
+ }
+
+ };
+ ApiConnectionInfo info = new ApiConnectionInfoBuilder(wellKnownUrl).build();
+ String what = "auth";
+ try {
+ this.authUrl = controller.retrieveAuthUrl(info, null);
+ what = "token";
+ this.tokenUrl = controller.retrieveTokenUrl(info, null);
+ // TODO: process appropriate extensions to determine things like "kc_idp_hint"
+ } catch (IOException ex) {
+ throw new CompletionException("Unable to return " + what + " URL", ex);
+ }
+ }
+
+ @Override
+ public void clear() {
+ synchronized (this) {
+ this.token = null;
+ }
+ }
+
+ @Override
+ public Consumer getAuthCallback() {
+ return authCallback;
+ }
+
+ @Override
+ public void setAuthCallback(Consumer authCallback) {
+ this.authCallback = authCallback;
+ }
+
+ @Override
+ public OAuth2Token getToken() throws IOException {
+ synchronized(this) {
+ if (token == null) {
+ token = newToken();
+ }
+ return token;
+ }
+ }
+
+ @Override
+ public OAuth2Token refreshToken() throws IOException {
+ synchronized (this) {
+ OAuth2Token newToken = new RefreshTokenRequestBuilder()
+ .withRefreshToken(token.getRefreshToken())
+ .withUrl(tokenUrl)
+ .withClientId(clientId)
+ .fetchToken();
+ token = newToken;
+ return token;
+ }
+ }
+
+ @Override
+ public OAuth2Token newToken() throws IOException {
+ synchronized (this) {
+ /**
+ * It may make sense to allow something to override this usage, however that
+ * *should* be a user setting. So like additional drop down or something in the gui.
+ * There are various notes about it in different sections for discussion.
+ */
+ token = new AuthCodePkceTokenRequestBuilder()
+ .withAuthUrl(authUrl)
+ .withTokenUrl(tokenUrl)
+ .withAuthCallback(authCallback)
+ .buildRequest()
+ .withClientId(clientId)
+ .fetchToken();
+ return token;
+ }
+
+ }
+
+ @Override
+ public ApiConnectionInfo getAuthUrl() {
+ return authUrl;
+ }
+
+ @Override
+ public ApiConnectionInfo getTokenUrl() {
+ return tokenUrl;
+ }
+
+}
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/OpenIdTokenController.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/OpenIdTokenController.java
index de40897c..2dd54ebe 100644
--- a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/OpenIdTokenController.java
+++ b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/OpenIdTokenController.java
@@ -12,20 +12,55 @@ public abstract class OpenIdTokenController {
static final String ACCEPT_HEADER = "application/json";
private static final String TOKEN_ENDPOINT_KEY = "token_endpoint";
- protected abstract String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) throws IOException;
+ private static final String AUTH_ENDPOINT_KEY = "authorization_endpoint";
+
+
+ private String authEndpoint = null;
+ private String tokenEndpoint = null;
+
+ /**
+ * Retrieve json text of the .wellknown/openid-configuration
+ * @param apiConnectionInfo
+ * @return
+ * @throws IOException
+ */
+ public abstract String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) throws IOException;
+
public final ApiConnectionInfo retrieveTokenUrl(ApiConnectionInfo apiConnectionInfo, SslSocketData sslSocketData) throws IOException {
- String wellKnownEndpoint = retrieveWellKnownEndpoint(apiConnectionInfo);
- ApiConnectionInfo wellKnownApiConnectionInfo = new ApiConnectionInfoBuilder(wellKnownEndpoint)
+ if (tokenEndpoint == null) {
+ String wellKnownEndpoint = retrieveWellKnownEndpoint(apiConnectionInfo);
+
+ ApiConnectionInfo wellKnownApiConnectionInfo = new ApiConnectionInfoBuilder(wellKnownEndpoint)
+ .withSslSocketData(sslSocketData)
+ .build();
+ HttpRequestExecutor executor = new HttpRequestBuilderImpl(wellKnownApiConnectionInfo)
+ .get()
+ .withMediaType(ACCEPT_HEADER);
+ try (HttpRequestResponse response = executor.execute()) {
+ tokenEndpoint = OAuth2ObjectMapper.getValueForKey(response.getBody(), TOKEN_ENDPOINT_KEY);
+ }
+ }
+ return new ApiConnectionInfoBuilder(tokenEndpoint)
.withSslSocketData(sslSocketData)
.build();
- HttpRequestExecutor executor = new HttpRequestBuilderImpl(wellKnownApiConnectionInfo)
- .get()
- .withMediaType(ACCEPT_HEADER);
- try (HttpRequestResponse response = executor.execute()) {
- String tokenEndpoint = OAuth2ObjectMapper.getValueForKey(response.getBody(), TOKEN_ENDPOINT_KEY);
- return new ApiConnectionInfoBuilder(tokenEndpoint)
+ }
+
+ public final ApiConnectionInfo retrieveAuthUrl(ApiConnectionInfo apiConnectionInfo, SslSocketData sslSocketData) throws IOException {
+
+ if (authEndpoint == null) {
+ String wellKnownEndpoint = retrieveWellKnownEndpoint(apiConnectionInfo);
+ ApiConnectionInfo wellKnownApiConnectionInfo = new ApiConnectionInfoBuilder(wellKnownEndpoint)
.withSslSocketData(sslSocketData)
.build();
+ HttpRequestExecutor executor = new HttpRequestBuilderImpl(wellKnownApiConnectionInfo)
+ .get()
+ .withMediaType(ACCEPT_HEADER);
+ try (HttpRequestResponse response = executor.execute()) {
+ authEndpoint = OAuth2ObjectMapper.getValueForKey(response.getBody(), AUTH_ENDPOINT_KEY);
+ }
}
+ return new ApiConnectionInfoBuilder(authEndpoint)
+ .withSslSocketData(sslSocketData)
+ .build();
}
}
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/RefreshTokenRequestBuilder.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/RefreshTokenRequestBuilder.java
index 6e319f6a..009d93a0 100644
--- a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/RefreshTokenRequestBuilder.java
+++ b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/RefreshTokenRequestBuilder.java
@@ -1,6 +1,5 @@
package hec.army.usace.hec.cwbi.auth.http.client;
-import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
import mil.army.usace.hec.cwms.http.client.HttpRequestBuilderImpl;
import mil.army.usace.hec.cwms.http.client.HttpRequestResponse;
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
@@ -19,12 +18,12 @@ public final class RefreshTokenRequestBuilder implements RefreshTokenRequestFlue
* @return Builder for http request
*/
@Override
- public TokenRequestFluentBuilder withRefreshToken(String refreshToken) {
+ public TokenRequestFluentBuilder extends TokenRequestFluentBuilder>> withRefreshToken(String refreshToken) {
this.refreshToken = Objects.requireNonNull(refreshToken, "Missing required refresh token");
return new RefreshTokenRequestExecutor();
}
- class RefreshTokenRequestExecutor extends TokenRequestBuilder {
+ class RefreshTokenRequestExecutor extends TokenRequestBuilder {
@Override
OAuth2Token retrieveToken() throws IOException {
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/RefreshTokenRequestFluentBuilder.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/RefreshTokenRequestFluentBuilder.java
index 18976a64..c38a06c2 100644
--- a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/RefreshTokenRequestFluentBuilder.java
+++ b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/RefreshTokenRequestFluentBuilder.java
@@ -1,5 +1,5 @@
package hec.army.usace.hec.cwbi.auth.http.client;
public interface RefreshTokenRequestFluentBuilder {
- TokenRequestFluentBuilder withRefreshToken(String refreshToken);
+ TokenRequestFluentBuilder extends TokenRequestFluentBuilder>> withRefreshToken(String refreshToken);
}
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/TokenRequestBuilder.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/TokenRequestBuilder.java
index 14989734..b6923c64 100644
--- a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/TokenRequestBuilder.java
+++ b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/TokenRequestBuilder.java
@@ -23,23 +23,70 @@
*/
package hec.army.usace.hec.cwbi.auth.http.client;
+import java.awt.Desktop;
+import java.awt.Desktop.Action;
import java.io.IOException;
+import java.net.URI;
import java.util.Objects;
+import java.util.function.Consumer;
+
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
-abstract class TokenRequestBuilder implements TokenRequestFluentBuilder {
+abstract class TokenRequestBuilder implements TokenRequestFluentBuilder> {
+ public static final Consumer BROWSER_OR_CONSOLE_AUTH_CALLBACK = u -> {
+ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Action.BROWSE)) {
+ try {
+ Desktop.getDesktop().browse(u);
+ } catch (IOException ex) {
+ throw new RuntimeException("Unable to open browser", ex);
+ }
+ } else {
+ System.out.println(String.format("Paste the following into a browser to continue login: %s", u.toString()));
+ }
+ };
static final String MEDIA_TYPE = "application/x-www-form-urlencoded";
private ApiConnectionInfo url;
+ private ApiConnectionInfo authUrl;
+ private ApiConnectionInfo tokenUrl;
private String clientId;
+ protected Consumer authCallBack = (u) -> {}; // by default do nothing.
abstract OAuth2Token retrieveToken() throws IOException;
+ /**
+ * Method used method the auth and token URL are the same.
+ * @return
+ * @deprecated implementations, even when they are the same, should use the individual auth/token methods.
+ */
+ @Deprecated(forRemoval = true)
ApiConnectionInfo getUrl() {
return url;
}
+ /**
+ * Retrieve the specific Auth endpoint URL.
+ * @return
+ */
+ ApiConnectionInfo getAuthUrl() {
+ return authUrl;
+ }
+
+ /**
+ * Retrieve the specific Token endpiont URL.
+ * @return
+ */
+ ApiConnectionInfo getTokenUrl() {
+ return tokenUrl;
+ }
+
+ @Override
+ public TokenRequestBuilder withAuthCallback(Consumer authCallback) {
+ this.authCallBack = authCallback;
+ return this;
+ }
+
String getClientId() {
return clientId;
}
@@ -50,6 +97,24 @@ public RequestClientId withUrl(ApiConnectionInfo url) {
return new RequestClientIdImpl();
}
+ @Override
+ public RequestClientId buildRequest() {
+ return new RequestClientIdImpl();
+ }
+
+ @Override
+ public TokenRequestBuilder withAuthUrl(ApiConnectionInfo url) {
+ this.authUrl = url;
+ return this;
+ }
+
+ @Override
+ public TokenRequestBuilder withTokenUrl(ApiConnectionInfo url) {
+ this.tokenUrl = url;
+ return this;
+ }
+
+
private class RequestClientIdImpl implements RequestClientId {
@Override
diff --git a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/TokenRequestFluentBuilder.java b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/TokenRequestFluentBuilder.java
index 53555786..b6eb52b9 100644
--- a/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/TokenRequestFluentBuilder.java
+++ b/cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/TokenRequestFluentBuilder.java
@@ -23,9 +23,49 @@
*/
package hec.army.usace.hec.cwbi.auth.http.client;
+import java.net.URI;
+import java.util.function.Consumer;
+
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
-public interface TokenRequestFluentBuilder {
+public interface TokenRequestFluentBuilder> {
+ /**
+ * If given auth method uses a single URL.
+ * @param url
+ * @return
+ * @deprecated even for implementations, like direct grant/resource owner password credentials
+ * should use the individual endpoints in the appropriate sections to avoid configuration
+ * details that are too specific but filter up among the usage.
+ */
+ @Deprecated(forRemoval = true)
RequestClientId withUrl(ApiConnectionInfo url);
+
+ /**
+ * Create object for next step in auth.
+ * @return
+ */
+ RequestClientId buildRequest();
+
+ /**
+ * set specific Auth URL endpoint.
+ * @param url
+ * @return
+ */
+ TokenRequestFluentBuilder withAuthUrl(ApiConnectionInfo url);
+
+ /**
+ * set specific Token URL endpoint.
+ * @param url
+ * @return
+ */
+ TokenRequestFluentBuilder withTokenUrl(ApiConnectionInfo url);
+
+ /**
+ * For methods where an external step is required to finish authentication
+ * pass in desired operation
+ * @param authCallback
+ * @return
+ */
+ TokenRequestFluentBuilder withAuthCallback(Consumer authCallback);
}
diff --git a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/MockCwbiAuthTokenProvider.java b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/MockCwbiAuthTokenProvider.java
index 7687d325..6a53a9bd 100644
--- a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/MockCwbiAuthTokenProvider.java
+++ b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/MockCwbiAuthTokenProvider.java
@@ -44,14 +44,14 @@ public class MockCwbiAuthTokenProvider extends CwbiAuthTokenProviderBase {
* @param sslSocketFactory - ssl socket factory
*/
public MockCwbiAuthTokenProvider(String url, String clientId, SSLSocketFactory sslSocketFactory) {
- super(clientId);
+ super(clientId, url);
this.sslSocketFactory = Objects.requireNonNull(sslSocketFactory, "Missing required sslSocketFactory");
this.url = url;
}
//used to manually set token for testing
void setOAuth2Token(OAuth2Token token) {
- oauth2Token = token;
+ this.token = token;
}
@Override
diff --git a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/MockDiscoveredCwbiAuthTokenProvider.java b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/MockDiscoveredCwbiAuthTokenProvider.java
deleted file mode 100644
index 18f04482..00000000
--- a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/MockDiscoveredCwbiAuthTokenProvider.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025 Hydrologic Engineering Center
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package hec.army.usace.hec.cwbi.auth.http.client;
-
-import java.io.IOException;
-import java.util.Objects;
-import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
-import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
-
-public class MockDiscoveredCwbiAuthTokenProvider extends CwbiAuthTokenProviderBase {
-
- private final TokenUrlDiscoveryService tokenUrlDiscoveryService;
- private ApiConnectionInfo url;
-
- /**
- * Provider for OAuth2Tokens.
- *
- * @param clientId - client name
- * @param tokenUrlDiscoveryService - service to discover the token URL
- */
- public MockDiscoveredCwbiAuthTokenProvider(String clientId, TokenUrlDiscoveryService tokenUrlDiscoveryService) {
- super(clientId);
- this.tokenUrlDiscoveryService = Objects.requireNonNull(tokenUrlDiscoveryService, "Missing required tokenUrlDiscoveryService");
- }
-
- //used to manually set token for testing
- void setOAuth2Token(OAuth2Token token) {
- oauth2Token = token;
- }
-
- //package scoped for testing
- @Override
- synchronized ApiConnectionInfo getUrl() throws IOException {
- if(url == null)
- {
- url = tokenUrlDiscoveryService.discoverTokenUrl();
- }
- return url;
- }
-
- //package scoped for testing
- TokenUrlDiscoveryService getDiscoveryService() {
- return tokenUrlDiscoveryService;
- }
-}
diff --git a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestCwbiTokenProvider.java b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestCwbiTokenProvider.java
index 9cfdf837..c00d516e 100644
--- a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestCwbiTokenProvider.java
+++ b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestCwbiTokenProvider.java
@@ -28,6 +28,7 @@
import java.util.Collections;
import javax.net.ssl.KeyManager;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.File;
import java.io.IOException;
@@ -38,15 +39,25 @@
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
import javax.net.ssl.SSLSocketFactory;
import mil.army.usace.hec.cwms.http.client.MockHttpServer;
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfoBuilder;
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
+import mil.army.usace.hec.cwms.http.client.request.QueryParameters;
+import okhttp3.HttpUrl;
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.RecordedRequest;
+
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -73,34 +84,70 @@ void tearDown() throws IOException {
}
ApiConnectionInfo buildConnectionInfo() {
- String baseUrl = String.format("http://localhost:%s", mockHttpServer.getPort());
+ String baseUrl = String.format("http://localhost:%s/openid-configuration", mockHttpServer.getPort());
return new ApiConnectionInfoBuilder(baseUrl).build();
}
- protected void launchMockServerWithResource(String resource) throws IOException {
+ private String getResource(String resource) throws IOException {
URL resourceUrl = getClass().getClassLoader().getResource(resource);
if (resourceUrl == null) {
throw new IOException("Failed to get resource: " + resource);
}
Path path = new File(resourceUrl.getFile()).toPath();
String collect = String.join("\n", Files.readAllLines(path));
- mockHttpServer.enqueue(collect);
+ return collect;
+ }
+
+ protected void launchMockServerWithResource(String resource) throws IOException {
+ mockHttpServer.getMockServer().setDispatcher(new Dispatcher() {
+ private final Logger LOGGER = Logger.getLogger(TestOidcTokenProvider.class.getName());
+ @Override
+ public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ final HttpUrl url = request.getRequestUrl();
+ final String path = url.encodedPath();
+ LOGGER.fine("Request for: " + url.toString());
+ LOGGER.fine("Path: " + path);
+
+ try {
+ if (path.endsWith("openid-configuration")) {
+ return new MockResponse().setBody(getResource("openIdConfig2.json")
+ .replace("PORT", ""+mockHttpServer.getPort()));
+ }
+ else if (path.endsWith("/auth")) {
+ throw new IOException("Endpoint should not be called. Request was " + url.toString());
+ //fail("CwbiTokenProvider uses direct grant and should not call the /auth endpoint.");
+ }
+ else if (path.endsWith("/token")) {
+ return new MockResponse().setBody(getResource("oauth2token.json"));
+ }
+ } catch (IOException ex) {
+ fail("Couldn't process mocked request", ex);
+ }
+ return new MockResponse().setResponseCode(404).setBody("Request not mocked.");
+ }
+ });
mockHttpServer.start();
}
@Test
void testBuildTokenProvider() throws IOException {
+ String resource = "oauth2token.json";
+ launchMockServerWithResource(resource);
SSLSocketFactory sslSocketFactory = CwbiAuthSslSocketFactory.buildSSLSocketFactory(
Collections.singletonList(getTestKeyManager()));
- CwbiAuthTokenProvider tokenProvider = new CwbiAuthTokenProvider(TOKEN_URL, "cumulus", sslSocketFactory);
- assertEquals(TOKEN_URL, tokenProvider.getUrl().getApiRoot());
+ String url = buildConnectionInfo().getApiRoot();
+ CwbiAuthTokenProvider tokenProvider = new CwbiAuthTokenProvider(url, "cumulus", sslSocketFactory);
+ assertEquals(url, tokenProvider.getUrl().getApiRoot());
assertEquals("cumulus", tokenProvider.getClientId());
}
@Test
- void testNulls() {
- assertThrows(NullPointerException.class, () -> new CwbiAuthTokenProvider(TOKEN_TEST_URL, "cumulus", null));
- assertThrows(NullPointerException.class, () -> new CwbiAuthTokenProvider(TOKEN_TEST_URL, null, getTestSslSocketFactory()));
+ void testNulls() throws IOException {
+ String resource = "oauth2token.json";
+ launchMockServerWithResource(resource);
+ String url = buildConnectionInfo().getApiRoot();
+ assertThrows(NullPointerException.class, () -> new CwbiAuthTokenProvider(url, "cumulus", null));
+ assertThrows(NullPointerException.class, () -> new CwbiAuthTokenProvider(url, null, getTestSslSocketFactory()));
assertThrows(NullPointerException.class, () -> new CwbiAuthTokenProvider(null, "cumulus", getTestSslSocketFactory()));
}
@@ -128,13 +175,7 @@ void testClear() throws IOException {
String resource = "oauth2token.json";
launchMockServerWithResource(resource);
String url = buildConnectionInfo().getApiRoot();
- MockCwbiAuthTokenProvider tokenProvider = new MockCwbiAuthTokenProvider(url, "cumulus", getTestSslSocketFactory());
- OAuth2Token token = new OAuth2Token();
- token.setAccessToken("abc123");
- token.setTokenType("Bearer");
- token.setExpiresIn(3600);
- token.setRefreshToken("123abc");
- tokenProvider.setOAuth2Token(token);
+ CwbiAuthTokenProvider tokenProvider = new CwbiAuthTokenProvider(url, "cumulus", getTestSslSocketFactory());
OAuth2Token token1 = tokenProvider.getToken();
OAuth2Token token2 = tokenProvider.getToken();
assertSame(token1, token2);
@@ -147,13 +188,9 @@ void testRefreshToken() throws IOException {
String resource = "oauth2token.json";
launchMockServerWithResource(resource);
String url = buildConnectionInfo().getApiRoot();
- MockCwbiAuthTokenProvider tokenProvider = new MockCwbiAuthTokenProvider(url, "cumulus", getTestSslSocketFactory());
- OAuth2Token token = new OAuth2Token();
- token.setAccessToken("abc123");
- token.setTokenType("Bearer");
- token.setExpiresIn(3600);
- token.setRefreshToken("123abc");
- tokenProvider.setOAuth2Token(token);
+ CwbiAuthTokenProvider tokenProvider = new CwbiAuthTokenProvider(url, "cumulus", getTestSslSocketFactory());
+ OAuth2Token token = tokenProvider.getToken();
+ assertNotNull(token, "Failed to retrieve initial token.");
OAuth2Token refreshedToken = tokenProvider.refreshToken();
assertEquals("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", refreshedToken.getAccessToken());
@@ -164,10 +201,13 @@ void testRefreshToken() throws IOException {
}
@Test
- void testConstructor() {
+ void testConstructor() throws IOException {
+ String resource = "oauth2token.json";
+ launchMockServerWithResource(resource);
+ String url = buildConnectionInfo().getApiRoot();
SSLSocketFactory sslSocketFactory = getTestSslSocketFactory();
- MockCwbiAuthTokenProvider tokenProvider = new MockCwbiAuthTokenProvider("test.com", "clientId", sslSocketFactory);
- assertEquals("test.com", tokenProvider.getUrl().getApiRoot());
+ MockCwbiAuthTokenProvider tokenProvider = new MockCwbiAuthTokenProvider(url, "clientId", sslSocketFactory);
+ assertEquals(url, tokenProvider.getUrl().getApiRoot());
assertEquals("clientId", tokenProvider.getClientId());
assertEquals(sslSocketFactory, tokenProvider.getSslSocketFactory());
}
diff --git a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestDirectGrantX509TokenRequestBuilder.java b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestDirectGrantX509TokenRequestBuilder.java
index 7bedfbda..f874609b 100644
--- a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestDirectGrantX509TokenRequestBuilder.java
+++ b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestDirectGrantX509TokenRequestBuilder.java
@@ -30,6 +30,7 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
import java.io.File;
import java.io.IOException;
@@ -38,10 +39,16 @@
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.logging.Logger;
+
import javax.net.ssl.SSLSocketFactory;
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
+import okhttp3.HttpUrl;
+import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+
import org.junit.jupiter.api.Test;
class TestDirectGrantX509TokenRequestBuilder {
@@ -52,7 +59,7 @@ void testRetrieveTokenMissingParams() {
SslSocketData sslSocketData = new SslSocketData(getTestSslSocketFactory(), CwbiAuthTrustManager.getTrustManager());
assertThrows(NullPointerException.class, () -> {
OAuth2Token token = new DirectGrantX509TokenRequestBuilder()
- .withUrl(new ApiConnectionInfoBuilder("https://test.com")
+ .withUrl(new ApiConnectionInfoBuilder("https://test.com/openid-configuration")
.withSslSocketData(sslSocketData)
.build())
.withClientId(null)
@@ -72,13 +79,36 @@ void testDirectGrantX509TokenRequestBuilder() throws IOException {
try (MockWebServer mockWebServer = new MockWebServer()) {
SslSocketData sslSocketData = new SslSocketData(getTestSslSocketFactory(), CwbiAuthTrustManager.getTrustManager());
String body = readJsonFile();
- mockWebServer.enqueue(new MockResponse().setBody(body).setResponseCode(200));
+ mockWebServer.setDispatcher(new Dispatcher() {
+ private final Logger LOGGER = Logger.getLogger(TestOidcTokenProvider.class.getName());
+ @Override
+ public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ final HttpUrl url = request.getRequestUrl();
+ final String path = url.encodedPath();
+ LOGGER.fine("Request for: " + url.toString());
+ LOGGER.fine("Path: " + path);
+
+
+ if (path.endsWith("openid-configuration")) {
+ fail("Using Direct Grant Request builder directly should not invoke the configuration request");
+ }
+ else if (path.endsWith("/auth")) {
+ fail("CwbiTokenProvider uses direct grant and should not call the /auth endpoint.");
+ }
+ else if (path.endsWith("/token")) {
+ return new MockResponse().setBody(body);
+ }
+
+ return new MockResponse().setResponseCode(404).setBody("Request not mocked.");
+ }
+ });
mockWebServer.start();
- String baseUrl = String.format("http://localhost:%s", mockWebServer.getPort());
+ String baseUrl = String.format("http://localhost:%s/token", mockWebServer.getPort());
OAuth2Token token = new DirectGrantX509TokenRequestBuilder()
- .withUrl(new ApiConnectionInfoBuilder(baseUrl)
+ .withTokenUrl(new ApiConnectionInfoBuilder(baseUrl)
.withSslSocketData(sslSocketData)
.build())
+ .buildRequest()
.withClientId("cumulus")
.fetchToken();
assertNotNull(token);
diff --git a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestDiscoveredCwbiTokenProvider.java b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestDiscoveredCwbiTokenProvider.java
deleted file mode 100644
index 2f03869d..00000000
--- a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestDiscoveredCwbiTokenProvider.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025 Hydrologic Engineering Center
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package hec.army.usace.hec.cwbi.auth.http.client;
-
-import hec.army.usace.hec.cwbi.auth.http.client.trustmanagers.CwbiAuthTrustManager;
-import java.io.File;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import javax.net.ssl.SSLSocketFactory;
-import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
-import mil.army.usace.hec.cwms.http.client.ApiConnectionInfoBuilder;
-import mil.army.usace.hec.cwms.http.client.MockHttpServer;
-import mil.army.usace.hec.cwms.http.client.SslSocketData;
-import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
-import org.junit.jupiter.api.AfterEach;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotSame;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-class TestDiscoveredCwbiTokenProvider {
-
- static MockHttpServer mockHttpServer;
-
- static ExecutorService executorService;
-
- @BeforeAll
- static void setUpExecutorService() {
- executorService = Executors.newFixedThreadPool(1);
- }
-
- @BeforeEach
- void setUp() throws IOException {
- mockHttpServer = MockHttpServer.create();
- }
-
- @AfterEach
- void tearDown() throws IOException {
- mockHttpServer.shutdown();
- }
-
- ApiConnectionInfo buildConnectionInfo() {
- SSLSocketFactory sslSocketFactory = getTestSslSocketFactory();
- SslSocketData sslSocketData = new SslSocketData(sslSocketFactory, CwbiAuthTrustManager.getTrustManager());
- String baseUrl = String.format("http://localhost:%s", mockHttpServer.getPort());
- return new ApiConnectionInfoBuilder(baseUrl)
- .withSslSocketData(sslSocketData)
- .build();
- }
-
- protected void launchMockServerWithResource(String resource) throws IOException {
- URL resourceUrl = getClass().getClassLoader().getResource(resource);
- if (resourceUrl == null) {
- throw new IOException("Failed to get resource: " + resource);
- }
- Path path = new File(resourceUrl.getFile()).toPath();
- String collect = String.join("\n", Files.readAllLines(path));
- mockHttpServer.enqueue(collect);
- mockHttpServer.start();
- }
-
- @Test
- void testBuildTokenProvider() throws IOException {
- ApiConnectionInfo tokenUrl = buildConnectionInfo();
- MockTokenUrlDiscoveryService tokenUrlDiscoveryService = new MockTokenUrlDiscoveryService(tokenUrl);
- DiscoveredCwbiAuthTokenProvider tokenProvider = new DiscoveredCwbiAuthTokenProvider("cumulus", tokenUrlDiscoveryService);
- assertEquals(tokenUrl.getApiRoot(), tokenProvider.getUrl().getApiRoot());
- assertEquals("cumulus", tokenProvider.getClientId());
- }
-
- @Test
- void testNulls() {
- assertThrows(NullPointerException.class, () -> new DiscoveredCwbiAuthTokenProvider("cumulus", null));
- assertThrows(NullPointerException.class, () -> new DiscoveredCwbiAuthTokenProvider(null, new MockTokenUrlDiscoveryService(new ApiConnectionInfoBuilder("").build())));
- }
-
- @Test
- void testGetToken() throws IOException {
- String resource = "oauth2token.json";
- launchMockServerWithResource(resource);
- ApiConnectionInfo tokenUrl = buildConnectionInfo();
- MockTokenUrlDiscoveryService tokenUrlDiscoveryService = new MockTokenUrlDiscoveryService(tokenUrl);
- DiscoveredCwbiAuthTokenProvider tokenProvider = new DiscoveredCwbiAuthTokenProvider("cumulus", tokenUrlDiscoveryService);
- OAuth2Token token = tokenProvider.getToken();
- assertEquals("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", token.getAccessToken());
- assertEquals("Bearer", token.getTokenType());
- assertEquals(3600, token.getExpiresIn());
- assertEquals("IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk", token.getRefreshToken());
- assertEquals("create", token.getScope());
- }
-
- @Test
- void testClear() throws IOException {
- String resource = "oauth2token.json";
- launchMockServerWithResource(resource);
- ApiConnectionInfo tokenUrl = buildConnectionInfo();
- MockTokenUrlDiscoveryService tokenUrlDiscoveryService = new MockTokenUrlDiscoveryService(tokenUrl);
- MockDiscoveredCwbiAuthTokenProvider tokenProvider = new MockDiscoveredCwbiAuthTokenProvider("cumulus", tokenUrlDiscoveryService);
- OAuth2Token token = new OAuth2Token();
- token.setAccessToken("abc123");
- token.setTokenType("Bearer");
- token.setExpiresIn(3600);
- token.setRefreshToken("123abc");
- tokenProvider.setOAuth2Token(token);
- OAuth2Token token1 = tokenProvider.getToken();
- OAuth2Token token2 = tokenProvider.getToken();
- assertSame(token1, token2);
- tokenProvider.clear();
- assertNotSame(token1, tokenProvider.getToken());
- }
-
- @Test
- void testRefreshToken() throws IOException {
- String resource = "oauth2token.json";
- launchMockServerWithResource(resource);
- ApiConnectionInfo tokenUrl = buildConnectionInfo();
- MockTokenUrlDiscoveryService tokenUrlDiscoveryService = new MockTokenUrlDiscoveryService(tokenUrl);
- MockDiscoveredCwbiAuthTokenProvider tokenProvider = new MockDiscoveredCwbiAuthTokenProvider("cumulus", tokenUrlDiscoveryService);
- OAuth2Token token = new OAuth2Token();
- token.setAccessToken("abc123");
- token.setTokenType("Bearer");
- token.setExpiresIn(3600);
- token.setRefreshToken("123abc");
- tokenProvider.setOAuth2Token(token);
-
- OAuth2Token refreshedToken = tokenProvider.refreshToken();
- assertEquals("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", refreshedToken.getAccessToken());
- assertEquals("Bearer", refreshedToken.getTokenType());
- assertEquals(3600, refreshedToken.getExpiresIn());
- assertEquals("IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk", refreshedToken.getRefreshToken());
- assertEquals("create", refreshedToken.getScope());
- }
-
- @Test
- void testConstructor() throws IOException {
- SSLSocketFactory sslSocketFactory = getTestSslSocketFactory();
- ApiConnectionInfo tokenUrl = new ApiConnectionInfoBuilder("test.com")
- .withSslSocketData(new SslSocketData(sslSocketFactory, CwbiAuthTrustManager.getTrustManager()))
- .build();
- MockTokenUrlDiscoveryService tokenUrlDiscoveryService = new MockTokenUrlDiscoveryService(tokenUrl);
- MockDiscoveredCwbiAuthTokenProvider tokenProvider = new MockDiscoveredCwbiAuthTokenProvider("clientId", tokenUrlDiscoveryService);
- assertEquals("test.com", tokenProvider.getUrl().getApiRoot());
- assertEquals("clientId", tokenProvider.getClientId());
- assertSame(tokenUrlDiscoveryService, tokenProvider.getDiscoveryService());
- }
-
- private SSLSocketFactory getTestSslSocketFactory() {
- return new SSLSocketFactory() {
- @Override
- public String[] getDefaultCipherSuites() {
- return new String[0];
- }
-
- @Override
- public String[] getSupportedCipherSuites() {
- return new String[0];
- }
-
- @Override
- public Socket createSocket(Socket socket, String s, int i, boolean b) {
- return null;
- }
-
- @Override
- public Socket createSocket(String s, int i) {
- return null;
- }
-
- @Override
- public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) {
- return null;
- }
-
- @Override
- public Socket createSocket(InetAddress inetAddress, int i) {
- return null;
- }
-
- @Override
- public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) {
- return null;
- }
- };
- }
-
- private static class MockTokenUrlDiscoveryService implements TokenUrlDiscoveryService {
- private final ApiConnectionInfo tokenUrl;
-
- private MockTokenUrlDiscoveryService(ApiConnectionInfo tokenUrl) {
- this.tokenUrl = tokenUrl;
- }
-
- @Override
- public ApiConnectionInfo discoverTokenUrl() {
- return tokenUrl;
- }
- }
-}
diff --git a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestOidcTokenProvider.java b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestOidcTokenProvider.java
new file mode 100644
index 00000000..7660d9cb
--- /dev/null
+++ b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestOidcTokenProvider.java
@@ -0,0 +1,166 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2024 Hydrologic Engineering Center
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package hec.army.usace.hec.cwbi.auth.http.client;
+
+import static hec.army.usace.hec.cwbi.auth.http.client.trustmanagers.CwbiAuthTrustManager.TOKEN_TEST_URL;
+import static hec.army.usace.hec.cwbi.auth.http.client.trustmanagers.CwbiAuthTrustManager.TOKEN_URL;
+import java.util.Collections;
+import javax.net.ssl.KeyManager;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.awt.Desktop;
+import java.awt.Desktop.Action;
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import javax.net.ssl.SSLSocketFactory;
+import mil.army.usace.hec.cwms.http.client.MockHttpServer;
+import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
+import mil.army.usace.hec.cwms.http.client.ApiConnectionInfoBuilder;
+import mil.army.usace.hec.cwms.http.client.HttpRequestBuilderImpl;
+import mil.army.usace.hec.cwms.http.client.HttpRequestResponse;
+import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
+import mil.army.usace.hec.cwms.http.client.request.HttpRequestExecutor;
+import mil.army.usace.hec.cwms.http.client.request.QueryParameters;
+import okhttp3.HttpUrl;
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.RecordedRequest;
+
+import org.junit.jupiter.api.AfterEach;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestOidcTokenProvider {
+ private static final Logger LOGGER = Logger.getLogger(TestOidcTokenProvider.class.getName());
+ static MockHttpServer mockCdaServer;
+ static MockHttpServer mockAuthServer;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ mockCdaServer = MockHttpServer.create();
+ mockAuthServer = MockHttpServer.create();
+ mockCdaServer.start();
+ mockAuthServer.start();
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ mockCdaServer.shutdown();
+ mockAuthServer.shutdown();
+ }
+
+ ApiConnectionInfo buildCdaInfo() {
+ String baseUrl = String.format("http://localhost:%s", mockCdaServer.getPort());
+ return new ApiConnectionInfoBuilder(baseUrl).build();
+ }
+
+ ApiConnectionInfo buildAuthInfo() {
+ String baseUrl = String.format("http://localhost:%s", mockAuthServer.getPort());
+ return new ApiConnectionInfoBuilder(baseUrl).build();
+ }
+
+ protected String getResource(String resource) throws IOException {
+ URL resourceUrl = getClass().getClassLoader().getResource(resource);
+ if (resourceUrl == null) {
+ throw new IOException("Failed to get resource: " + resource);
+ }
+ Path path = new File(resourceUrl.getFile()).toPath();
+ return String.join("\n", Files.readAllLines(path));
+ }
+
+ @Test
+ void testBuildTokenProvider() throws Exception {
+ mockAuthServer.getMockServer().setDispatcher(new Dispatcher() {
+ private final Logger LOGGER = Logger.getLogger(TestOidcTokenProvider.class.getName()+"_dispatcher");
+ @Override
+ public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ final HttpUrl url = request.getRequestUrl();
+ final String path = url.encodedPath();
+ LOGGER.info("Request for: " + url.toString());
+ LOGGER.fine("Path: " + path);
+
+ try {
+ if (path.endsWith("openid-configuration")) {
+ return new MockResponse().setBody(getResource("openIdConfig2.json")
+ .replace("PORT", ""+mockAuthServer.getPort()));
+ }
+ else if (path.endsWith("/auth")) {
+ final String query = request.getRequestUrl().query();
+ final QueryParameters parameters = QueryParameters.parse(query);
+ String redirect = parameters.get("redirect_uri").get(0);
+ String state = parameters.get("state").get(0);
+ final String loc = String.format("%s?code=test&state=%s&session_state=zzz", redirect, state);
+ MockResponse response = new MockResponse().setResponseCode(302).setHeader("Location", loc).setBody("hello");
+ LOGGER.info(response.toString());
+ return response;
+ }
+ else if (path.endsWith("/token")) {
+ return new MockResponse().setBody(getResource("oauth2token.json"));
+ }
+ } catch (IOException ex) {
+ fail("Couldn't process mocked request", ex);
+ }
+ return new MockResponse().setResponseCode(404).setBody("Request not mocked.");
+ }
+ });
+
+
+ String wellKnown = "http://localhost:"+mockAuthServer.getPort()+"/auth/realms/cwbi/.well-known/openid-configuration";
+ OidcAuthTokenProvider tokenProvider = new OidcAuthTokenProvider("test", wellKnown);
+ tokenProvider.setAuthCallback(u -> {
+ try {
+ LOGGER.info("Sending " + u.toString());
+ HttpRequestExecutor executor =
+ new HttpRequestBuilderImpl(new ApiConnectionInfoBuilder(u.toString()).build())
+ .get()
+ .withMediaType("text/plain");
+
+ try (HttpRequestResponse response = executor.execute()) {
+ // redirect should be automatically followed. If changes
+ // made that fail this section either enable redirect or handle it.
+ }
+
+ } catch (IOException ex) {
+ fail("Unable to perform client side of authorization procedure.", ex);
+ }
+
+ });
+ final OAuth2Token token = tokenProvider.getToken();
+ assertNotNull(token);
+ }
+}
diff --git a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestOpenIdTokenController.java b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestOpenIdTokenController.java
index 3abbd67c..b0f46296 100644
--- a/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestOpenIdTokenController.java
+++ b/cwbi-auth-http-client/src/test/java/hec/army/usace/hec/cwbi/auth/http/client/TestOpenIdTokenController.java
@@ -51,7 +51,7 @@ void testRetrieveTokenUrl() throws Exception {
ApiConnectionInfo tokenUrl = new OpenIdTokenController(){
@Override
- protected String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) {
+ public String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) {
return wellKnownEndpoint;
}
}.retrieveTokenUrl(new ApiConnectionInfoBuilder(baseUrl).build(), sslSocketData);
diff --git a/cwbi-auth-http-client/src/test/resources/openIdConfig2.json b/cwbi-auth-http-client/src/test/resources/openIdConfig2.json
new file mode 100644
index 00000000..73690146
--- /dev/null
+++ b/cwbi-auth-http-client/src/test/resources/openIdConfig2.json
@@ -0,0 +1,121 @@
+{
+ "issuer": "http://localhost:PORT/auth/realms/cwbi",
+ "authorization_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/auth",
+ "token_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/token",
+ "introspection_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/token/introspect",
+ "userinfo_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/userinfo",
+ "end_session_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/logout",
+ "frontchannel_logout_session_supported": true,
+ "frontchannel_logout_supported": true,
+ "jwks_uri": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/certs",
+ "check_session_iframe": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/login-status-iframe.html",
+ "grant_types_supported": [
+ "authorization_code", "implicit", "refresh_token", "password", "client_credentials",
+ "urn:ietf:params:oauth:grant-type:device_code", "urn:openid:params:grant-type:ciba"
+ ],
+ "acr_values_supported": ["0", "1"],
+ "response_types_supported": [
+ "code", "none", "id_token", "token", "id_token token",
+ "code id_token", "code token", "code id_token token"
+ ],
+ "subject_types_supported": ["public", "pairwise"],
+ "id_token_signing_alg_values_supported": [
+ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256",
+ "HS384", "ES512", "PS256", "PS512", "RS512"
+ ],
+ "id_token_encryption_alg_values_supported": ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"],
+ "id_token_encryption_enc_values_supported": [
+ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512"
+ ],
+ "userinfo_signing_alg_values_supported": [
+ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256",
+ "HS384", "ES512", "PS256", "PS512", "RS512", "none"
+ ],
+ "userinfo_encryption_alg_values_supported": ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"],
+ "userinfo_encryption_enc_values_supported": [
+ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512"
+ ],
+ "request_object_signing_alg_values_supported": [
+ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256",
+ "HS384", "ES512", "PS256", "PS512", "RS512", "none"
+ ],
+ "request_object_encryption_alg_values_supported": ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"],
+ "request_object_encryption_enc_values_supported": [
+ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512"
+ ],
+ "response_modes_supported": [
+ "query", "fragment", "form_post", "query.jwt", "fragment.jwt", "form_post.jwt", "jwt"
+ ],
+ "registration_endpoint": "http://localhost:PORT/auth/realms/cwbi/clients-registrations/openid-connect",
+ "token_endpoint_auth_methods_supported": [
+ "private_key_jwt", "client_secret_basic", "client_secret_post",
+ "tls_client_auth", "client_secret_jwt"
+ ],
+ "token_endpoint_auth_signing_alg_values_supported": [
+ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256",
+ "HS384", "ES512", "PS256", "PS512", "RS512"
+ ],
+ "introspection_endpoint_auth_methods_supported": [
+ "private_key_jwt", "client_secret_basic", "client_secret_post",
+ "tls_client_auth", "client_secret_jwt"
+ ],
+ "introspection_endpoint_auth_signing_alg_values_supported": [
+ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256",
+ "HS384", "ES512", "PS256", "PS512", "RS512"
+ ],
+ "authorization_signing_alg_values_supported": [
+ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256",
+ "HS384", "ES512", "PS256", "PS512", "RS512"
+ ],
+ "authorization_encryption_alg_values_supported": ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"],
+ "authorization_encryption_enc_values_supported": [
+ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512"
+ ],
+ "claims_supported": [
+ "aud", "sub", "iss", "auth_time", "name", "given_name", "family_name",
+ "preferred_username", "email", "acr"
+ ],
+ "claim_types_supported": ["normal"],
+ "claims_parameter_supported": true,
+ "scopes_supported": [
+ "openid", "stig-manager:collection:read", "stig-manager:op", "x509_presented",
+ "stig-manager:op:read", "email", "stig-manager:collection", "microprofile-jwt",
+ "address", "acr", "profile", "groups", "stig-manager:stig", "roles",
+ "web-origins", "stig-manager:stig:read", "offline_access", "cacUID",
+ "preferred_username", "stig-manager:user:read", "phone", "subjectDN", "stig-manager:user"
+ ],
+ "request_parameter_supported": true,
+ "request_uri_parameter_supported": true,
+ "require_request_uri_registration": true,
+ "code_challenge_methods_supported": ["plain", "S256"],
+ "tls_client_certificate_bound_access_tokens": true,
+ "revocation_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/revoke",
+ "revocation_endpoint_auth_methods_supported": [
+ "private_key_jwt", "client_secret_basic", "client_secret_post",
+ "tls_client_auth", "client_secret_jwt"
+ ],
+ "revocation_endpoint_auth_signing_alg_values_supported": [
+ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256",
+ "HS384", "ES512", "PS256", "PS512", "RS512"
+ ],
+ "backchannel_logout_supported": true,
+ "backchannel_logout_session_supported": true,
+ "device_authorization_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/auth/device",
+ "backchannel_token_delivery_modes_supported": ["poll", "ping"],
+ "backchannel_authentication_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/ext/ciba/auth",
+ "backchannel_authentication_request_signing_alg_values_supported": [
+ "PS384", "ES384", "RS384", "ES256", "RS256", "ES512", "PS256", "PS512", "RS512"
+ ],
+ "require_pushed_authorization_requests": false,
+ "pushed_authorization_request_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/ext/par/request",
+ "mtls_endpoint_aliases": {
+ "token_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/token",
+ "revocation_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/revoke",
+ "introspection_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/token/introspect",
+ "device_authorization_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/auth/device",
+ "registration_endpoint": "http://localhost:PORT/auth/realms/cwbi/clients-registrations/openid-connect",
+ "userinfo_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/userinfo",
+ "pushed_authorization_request_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/ext/par/request",
+ "backchannel_authentication_endpoint": "http://localhost:PORT/auth/realms/cwbi/protocol/openid-connect/ext/ciba/auth"
+ }
+}
diff --git a/cwms-aaa-client/build.gradle b/cwms-aaa-client/build.gradle
index 352ba583..23d66ac1 100644
--- a/cwms-aaa-client/build.gradle
+++ b/cwms-aaa-client/build.gradle
@@ -11,6 +11,7 @@ dependencies {
testImplementation(testFixtures(project(":cwms-http-client")))
testImplementation(libs.junit.api)
testRuntimeOnly(libs.junit.engine)
+ testImplementation(libs.okhttp.mockwebserver)
}
publishing {
diff --git a/cwms-data-api-client/build.gradle b/cwms-data-api-client/build.gradle
index b76a59d7..0badad95 100644
--- a/cwms-data-api-client/build.gradle
+++ b/cwms-data-api-client/build.gradle
@@ -40,6 +40,7 @@ dependencies {
testImplementation(libs.mockito.core)
testImplementation(libs.mockito.junit.jupiter)
testImplementation(libs.jackson.yaml)
+ testImplementation(libs.okhttp.mockwebserver)
}
diff --git a/cwms-data-api-client/src/main/java/mil/army/usace/hec/cwms/data/api/client/auth/CdaTokenProviderFactory.java b/cwms-data-api-client/src/main/java/mil/army/usace/hec/cwms/data/api/client/auth/CdaTokenProviderFactory.java
deleted file mode 100644
index 1a5d4d9f..00000000
--- a/cwms-data-api-client/src/main/java/mil/army/usace/hec/cwms/data/api/client/auth/CdaTokenProviderFactory.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025 Hydrologic Engineering Center
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package mil.army.usace.hec.cwms.data.api.client.auth;
-
-import hec.army.usace.hec.cwbi.auth.http.client.CwbiAuthSslSocketFactory;
-import hec.army.usace.hec.cwbi.auth.http.client.DiscoveredCwbiAuthTokenProvider;
-import hec.army.usace.hec.cwbi.auth.http.client.trustmanagers.CwbiAuthTrustManager;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Objects;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.SSLSocketFactory;
-import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
-import mil.army.usace.hec.cwms.http.client.ApiConnectionInfoBuilder;
-import mil.army.usace.hec.cwms.http.client.SslSocketData;
-import mil.army.usace.hec.cwms.http.client.auth.OAuth2TokenProvider;
-
-public final class CdaTokenProviderFactory {
-
- private CdaTokenProviderFactory() {
- throw new AssertionError("This class should not be instantiated");
- }
-
- public static OAuth2TokenProvider createTokenProvider(String url, String clientId, KeyManager keyManager) throws IOException {
- SSLSocketFactory sslSocketFactory = CwbiAuthSslSocketFactory.buildSSLSocketFactory(Collections.singletonList(Objects.requireNonNull(keyManager, "Missing required KeyManager")));
- SslSocketData sslSocketData = new SslSocketData(Objects.requireNonNull(sslSocketFactory, "Missing required SSLSocketFactory"),
- CwbiAuthTrustManager.getTrustManager());
- ApiConnectionInfo apiConnectionInfo = new ApiConnectionInfoBuilder(Objects.requireNonNull(url, "Missing required url")).build();
- CdaTokenUrlDiscoveryService tokenUrlDiscoveryService = new CdaTokenUrlDiscoveryService(apiConnectionInfo, sslSocketData);
- return new DiscoveredCwbiAuthTokenProvider(Objects.requireNonNull(clientId, "Missing required clientId"), tokenUrlDiscoveryService);
- }
-}
diff --git a/cwms-data-api-client/src/main/java/mil/army/usace/hec/cwms/data/api/client/auth/CdaTokenUrlDiscoveryService.java b/cwms-data-api-client/src/main/java/mil/army/usace/hec/cwms/data/api/client/auth/CdaTokenUrlDiscoveryService.java
deleted file mode 100644
index d5e23226..00000000
--- a/cwms-data-api-client/src/main/java/mil/army/usace/hec/cwms/data/api/client/auth/CdaTokenUrlDiscoveryService.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025 Hydrologic Engineering Center
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package mil.army.usace.hec.cwms.data.api.client.auth;
-
-import hec.army.usace.hec.cwbi.auth.http.client.TokenUrlDiscoveryService;
-import java.io.IOException;
-import java.util.Objects;
-import mil.army.usace.hec.cwms.data.api.client.controllers.CdaOpenIdTokenController;
-import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
-import mil.army.usace.hec.cwms.http.client.SslSocketData;
-
-final class CdaTokenUrlDiscoveryService implements TokenUrlDiscoveryService {
-
- private final ApiConnectionInfo apiConnectionInfo;
- private final SslSocketData sslSocketData;
-
- CdaTokenUrlDiscoveryService(ApiConnectionInfo apiConnectionInfo, SslSocketData sslSocketData) {
- this.apiConnectionInfo = Objects.requireNonNull(apiConnectionInfo, "apiConnectionInfo is required");
- this.sslSocketData = Objects.requireNonNull(sslSocketData, "sslSocketData is required");
- }
- @Override
- public ApiConnectionInfo discoverTokenUrl() throws IOException {
- return new CdaOpenIdTokenController()
- .retrieveTokenUrl(apiConnectionInfo, sslSocketData);
- }
-}
diff --git a/cwms-data-api-client/src/main/java/mil/army/usace/hec/cwms/data/api/client/controllers/CdaOpenIdTokenController.java b/cwms-data-api-client/src/main/java/mil/army/usace/hec/cwms/data/api/client/controllers/CdaOpenIdTokenController.java
index 8fe7770c..97b2e38b 100644
--- a/cwms-data-api-client/src/main/java/mil/army/usace/hec/cwms/data/api/client/controllers/CdaOpenIdTokenController.java
+++ b/cwms-data-api-client/src/main/java/mil/army/usace/hec/cwms/data/api/client/controllers/CdaOpenIdTokenController.java
@@ -34,7 +34,7 @@ public final class CdaOpenIdTokenController extends OpenIdTokenController {
private static final String SWAGGER_DOC_ENDPOINT = "swagger-docs";
@Override
- protected String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) throws IOException {
+ public String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) throws IOException {
String url = apiConnectionInfo.getApiRoot() + "/" + SWAGGER_DOC_ENDPOINT;
OpenAPI openAPI = new OpenAPIV3Parser().read(url);
if(openAPI == null) {
diff --git a/cwms-data-api-client/src/test/java/mil/army/usace/hec/cwms/data/api/client/auth/TestCdaTokenProviderFactory.java b/cwms-data-api-client/src/test/java/mil/army/usace/hec/cwms/data/api/client/auth/TestCdaTokenProviderFactory.java
deleted file mode 100644
index 9ef4cd7e..00000000
--- a/cwms-data-api-client/src/test/java/mil/army/usace/hec/cwms/data/api/client/auth/TestCdaTokenProviderFactory.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025 Hydrologic Engineering Center
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package mil.army.usace.hec.cwms.data.api.client.auth;
-
-import java.io.IOException;
-import javax.net.ssl.KeyManager;
-import mil.army.usace.hec.cwms.data.api.client.controllers.TestController;
-import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
-import mil.army.usace.hec.cwms.http.client.auth.OAuth2TokenProvider;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import org.junit.jupiter.api.Test;
-
-final class TestCdaTokenProviderFactory extends TestController {
-
- @Test
- void testNotNull() throws IOException {
- String resource = "radar/v1/json/openIdConfig.json";
- String collect = readJsonFile(resource);
- mockHttpServer.enqueue(collect);
- ApiConnectionInfo webServiceUrl = buildConnectionInfo();
- OAuth2TokenProvider tokenProvider = CdaTokenProviderFactory.createTokenProvider(webServiceUrl.getApiRoot(),"cwms", new KeyManager() {});
- assertNotNull(tokenProvider);
- }
-
- @Test
- void testNulls() {
- assertThrows(NullPointerException.class, () -> CdaTokenProviderFactory.createTokenProvider("test", "cwms", null));
- assertThrows(NullPointerException.class, () -> CdaTokenProviderFactory.createTokenProvider("test", null, new KeyManager() {}));
- assertThrows(NullPointerException.class, () -> CdaTokenProviderFactory.createTokenProvider(null, "cwms", new KeyManager() {}));
- }
-}
diff --git a/cwms-data-api-client/src/test/java/mil/army/usace/hec/cwms/data/api/client/auth/TestCdaTokenUrlDiscoveryService.java b/cwms-data-api-client/src/test/java/mil/army/usace/hec/cwms/data/api/client/auth/TestCdaTokenUrlDiscoveryService.java
deleted file mode 100644
index 35600d82..00000000
--- a/cwms-data-api-client/src/test/java/mil/army/usace/hec/cwms/data/api/client/auth/TestCdaTokenUrlDiscoveryService.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025 Hydrologic Engineering Center
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package mil.army.usace.hec.cwms.data.api.client.auth;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import hec.army.usace.hec.cwbi.auth.http.client.trustmanagers.CwbiAuthTrustManager;
-import java.io.IOException;
-import javax.net.ssl.SSLSocketFactory;
-import mil.army.usace.hec.cwms.data.api.client.controllers.TestController;
-import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
-import mil.army.usace.hec.cwms.http.client.SslSocketData;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-final class TestCdaTokenUrlDiscoveryService extends TestController {
-
- @Test
- void testCdaTokenUrlDiscoveryService() throws IOException {
- SSLSocketFactory mockSslSocketFactory = Mockito.mock(SSLSocketFactory.class);
- String resource = readSwaggerYamlAsJson();
- String openIdConfig = "radar/v1/json/openIdConfig.json";
- ObjectMapper mapper = new ObjectMapper();
- ObjectNode node = (ObjectNode) mapper.readTree(resource);
- ApiConnectionInfo webServiceUrl = buildConnectionInfo();
- ObjectNode components = node.with("components");
- ObjectNode securitySchemes = components.with("securitySchemes");
- ObjectNode openIdConnect = securitySchemes.with("OpenIDConnect");
- openIdConnect.put("openIdConnectUrl", webServiceUrl.getApiRoot() + "/.well-known/openid-configuration");
- String updatedIdpConfig = mapper.writeValueAsString(node);
- mockHttpServer.enqueue(updatedIdpConfig);
- mockHttpServer.enqueue(readJsonFile(openIdConfig));
- SslSocketData sslSocketData = new SslSocketData(mockSslSocketFactory, CwbiAuthTrustManager.getTrustManager());
- CdaTokenUrlDiscoveryService discoveryService = new CdaTokenUrlDiscoveryService(webServiceUrl, sslSocketData);
- assertEquals("https://api.example.com/auth/realms/cwbi/protocol/openid-connect/token", discoveryService.discoverTokenUrl().getApiRoot());
- }
-
- @Test
- void testNulls() {
- SSLSocketFactory mockSslSocketFactory = Mockito.mock(SSLSocketFactory.class);
- SslSocketData sslSocketData = new SslSocketData(mockSslSocketFactory, CwbiAuthTrustManager.getTrustManager());
- ApiConnectionInfo webServiceUrl = buildConnectionInfo();
- assertThrows(NullPointerException.class, () -> new CdaTokenUrlDiscoveryService(null, sslSocketData));
- assertThrows(NullPointerException.class, () -> new CdaTokenUrlDiscoveryService(webServiceUrl, null));
- }
-}
diff --git a/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/SslSocketData.java b/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/SslSocketData.java
index 46b96cf7..9450679e 100644
--- a/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/SslSocketData.java
+++ b/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/SslSocketData.java
@@ -38,11 +38,11 @@ public SslSocketData(SSLSocketFactory sslSocketFactory, X509TrustManager x509Tru
this.x509TrustManager = Objects.requireNonNull(x509TrustManager, "Missing required X509TrustManager");
}
- SSLSocketFactory getSslSocketFactory() {
+ public SSLSocketFactory getSslSocketFactory() {
return sslSocketFactory;
}
- X509TrustManager getX509TrustManager() {
+ public X509TrustManager getX509TrustManager() {
return x509TrustManager;
}
}
diff --git a/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/auth/OAuth2TokenProvider.java b/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/auth/OAuth2TokenProvider.java
index adb6d66c..c709c6bb 100644
--- a/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/auth/OAuth2TokenProvider.java
+++ b/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/auth/OAuth2TokenProvider.java
@@ -25,7 +25,16 @@
package mil.army.usace.hec.cwms.http.client.auth;
import java.io.IOException;
+import java.net.URI;
+import java.util.function.Consumer;
+import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
+
+/**
+ *
+ * TODO: needs additional support for alternative flows. deciding if attempting to
+ * do in this PR or just create issue and do in followup PR.
+ */
public interface OAuth2TokenProvider {
void clear();
@@ -36,4 +45,19 @@ public interface OAuth2TokenProvider {
OAuth2Token newToken() throws IOException;
+ /**
+ * Return auth callback that will be used for this provider.
+ * By default do nothing.
+ * @return
+ */
+ default Consumer getAuthCallback() {
+ return u -> {};
+ }
+
+ default void setAuthCallback(Consumer authCallback) {
+ /** default do nothing... for now */
+ }
+
+ ApiConnectionInfo getAuthUrl();
+ ApiConnectionInfo getTokenUrl();
}
diff --git a/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/request/QueryParameters.java b/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/request/QueryParameters.java
new file mode 100644
index 00000000..7f61402f
--- /dev/null
+++ b/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/request/QueryParameters.java
@@ -0,0 +1,104 @@
+package mil.army.usace.hec.cwms.http.client.request;
+
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper class to support creation and query of URL query parameters
+ */
+public class QueryParameters {
+
+ private Map> parameters = new HashMap<>();
+
+
+
+ private QueryParameters() {
+
+ }
+
+ /**
+ * Retrieve Parameter list
+ * @param parameter
+ * @return List of parameters. If no values are set for the parameter an empty list is returned.
+ */
+ public List get(String parameter) {
+ List values = parameters.computeIfAbsent(parameter, p -> new ArrayList<>());
+ return values;
+ }
+
+ /**
+ * Add an additional value to the parameter
+ * @param parameter
+ * @param value
+ * @return this instance to allow for fluent syntax operations
+ */
+ public QueryParameters add(String parameter, String value) {
+ List values = parameters.computeIfAbsent(parameter, p -> new ArrayList<>());
+ values.add(value);
+ return this;
+ }
+
+ /**
+ * Set given parameter to only the given value. Any previously stored values are lost.
+ * @param parameter
+ * @param value
+ * @return this instance to allow for fluent syntax operations
+ */
+ public QueryParameters set(String parameter, String value) {
+ List values = new ArrayList<>();
+ values.add(value);
+ parameters.put(parameter, values);
+ return this;
+ }
+
+ /**
+ * URL encoded query parameters suitable for appending to query. Does not include the initial ?
+ * @return
+ */
+ public String encode() {
+ final ArrayList sets = new ArrayList<>();
+ parameters.forEach((k,v) -> {
+ ArrayList pairs = new ArrayList<>();
+ for (String value: v) {
+ pairs.add(String.format("%s=%s",
+ URLEncoder.encode(k, StandardCharsets.UTF_8),
+ URLEncoder.encode(value, StandardCharsets.UTF_8)));
+ }
+ sets.add(String.join("&", pairs));
+ });
+
+ return String.join("&", sets);
+ }
+
+ /**
+ * Given a URL query string, create a QueryParameters object for additional processing.
+ * @param query
+ * @return
+ */
+ public static QueryParameters parse(String query) {
+ QueryParameters parameters = new QueryParameters();
+ for (String pair: query.split("&")) {
+ String[] kv = pair.split("=", 2); // parameters are *always* seperated by &, but may have embedded =
+ if (kv[0].trim().isEmpty()) {
+ continue;
+ }
+ String parameter = URLDecoder.decode(kv[0], StandardCharsets.UTF_8);
+ String value = kv.length > 1 ? URLDecoder.decode(kv[1], StandardCharsets.UTF_8) : null;
+ parameters.parameters.computeIfAbsent(parameter, p -> new ArrayList<>()).add(value);
+ }
+ return parameters;
+ }
+
+ /**
+ * Create a new QueryParameters object.
+ * @return
+ */
+ public static QueryParameters empty() {
+ return parse("");
+ }
+}
diff --git a/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestApiConnectionInfo.java b/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestApiConnectionInfo.java
index ec4b0849..2bfbb95c 100644
--- a/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestApiConnectionInfo.java
+++ b/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestApiConnectionInfo.java
@@ -233,6 +233,18 @@ public OAuth2Token newToken() {
token.setScope("create");
return token;
}
+
+ @Override
+ public ApiConnectionInfo getAuthUrl() {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getAuthUrl'");
+ }
+
+ @Override
+ public ApiConnectionInfo getTokenUrl() {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getTokenUrl'");
+ }
};
}
@@ -278,6 +290,18 @@ public OAuth2Token refreshToken() {
public OAuth2Token newToken() {
return null;
}
+
+ @Override
+ public ApiConnectionInfo getAuthUrl() {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getAuthUrl'");
+ }
+
+ @Override
+ public ApiConnectionInfo getTokenUrl() {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getTokenUrl'");
+ }
};
}
diff --git a/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestOAuth2TokenAuthenticator.java b/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestOAuth2TokenAuthenticator.java
index 50bc4028..28f07622 100644
--- a/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestOAuth2TokenAuthenticator.java
+++ b/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestOAuth2TokenAuthenticator.java
@@ -261,5 +261,17 @@ public OAuth2Token newToken() {
token.setRefreshToken(ACCESS_TOKEN);
return token;
}
+
+ @Override
+ public ApiConnectionInfo getAuthUrl() {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getAuthUrl'");
+ }
+
+ @Override
+ public ApiConnectionInfo getTokenUrl() {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getTokenUrl'");
+ }
}
}
diff --git a/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestOAuth2TokenInterceptor.java b/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestOAuth2TokenInterceptor.java
index 7675fa81..533a3a7d 100644
--- a/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestOAuth2TokenInterceptor.java
+++ b/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TestOAuth2TokenInterceptor.java
@@ -104,6 +104,18 @@ public OAuth2Token newToken() {
return token;
}
+ @Override
+ public ApiConnectionInfo getAuthUrl() {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getAuthUrl'");
+ }
+
+ @Override
+ public ApiConnectionInfo getTokenUrl() {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getTokenUrl'");
+ }
+
};
}
}
diff --git a/cwms-http-client/src/testFixtures/java/mil/army/usace/hec/cwms/http/client/MockHttpServer.java b/cwms-http-client/src/testFixtures/java/mil/army/usace/hec/cwms/http/client/MockHttpServer.java
index 6e6cfc07..463b568f 100644
--- a/cwms-http-client/src/testFixtures/java/mil/army/usace/hec/cwms/http/client/MockHttpServer.java
+++ b/cwms-http-client/src/testFixtures/java/mil/army/usace/hec/cwms/http/client/MockHttpServer.java
@@ -52,6 +52,10 @@ public void enqueue(int responseCode, String body) {
mockWebServer.enqueue(new MockResponse().setResponseCode(responseCode).setBody(body));
}
+ public void enqueue(MockResponse response) {
+ mockWebServer.enqueue(response);
+ }
+
public void enqueue(String body, List cookies) {
MockResponse mockResponse = new MockResponse().setBody(body);
for (String cookie : cookies) {
@@ -85,6 +89,10 @@ public void close() throws Exception {
mockWebServer.close();
}
+ public MockWebServer getMockServer() {
+ return mockWebServer;
+ }
+
public RequestWrapper takeRequest() throws Exception {
return new RequestWrapper(mockWebServer.takeRequest());
}
diff --git a/hec-build-ext.gradle b/hec-build-ext.gradle
new file mode 100644
index 00000000..af15166d
--- /dev/null
+++ b/hec-build-ext.gradle
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2024
+ * United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC)
+ * All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL.
+ * Source may not be released without written approval from HEC
+ */
+
+includeBuild("$gradle.ext.externalLibDir") {
+ dependencySubstitution {
+ substitute(module("mil.army.usace.hec:cwms-http-client")).using(project(":cwms-http-client"))
+ substitute(module("mil.army.usace.hec:cwms-data-api-client")).using(project(":cwms-data-api-client"))
+ substitute(module("mil.army.usace.hec:cwms-data-api-model")).using(project(":cwms-data-api-model"))
+ substitute(module("mil.army.usace.hec:cwms-aaa-client")).using(project(":cwms-aaa-client"))
+ substitute(module("mil.army.usace.hec:cwbi-auth-http-client")).using(project(":cwbi-auth-http-client"))
+
+ }
+}