Skip to content

Commit 6c27285

Browse files
committed
WIP, need to request use of dispatcher vs enqueue in test.
1 parent 81f770e commit 6c27285

9 files changed

Lines changed: 539 additions & 11 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* MIT License
3+
*
4+
*
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
package hec.army.usace.hec.cwbi.auth.http.client;
25+
26+
import mil.army.usace.hec.cwms.http.client.HttpRequestBuilderImpl;
27+
import mil.army.usace.hec.cwms.http.client.HttpRequestResponse;
28+
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
29+
import mil.army.usace.hec.cwms.http.client.request.HttpRequestExecutor;
30+
31+
import java.io.IOException;
32+
import java.net.InetSocketAddress;
33+
import java.net.URI;
34+
import java.net.URISyntaxException;
35+
import java.net.URL;
36+
import java.net.URLDecoder;
37+
import java.nio.charset.StandardCharsets;
38+
import java.security.MessageDigest;
39+
import java.security.NoSuchAlgorithmException;
40+
import java.security.SecureRandom;
41+
import java.util.ArrayList;
42+
import java.util.Base64;
43+
import java.util.HashMap;
44+
import java.util.List;
45+
import java.util.Map;
46+
import java.util.concurrent.CompletableFuture;
47+
import java.util.concurrent.atomic.AtomicReference;
48+
import java.awt.Desktop;
49+
import java.awt.Desktop.Action;
50+
51+
import com.sun.net.httpserver.HttpExchange;
52+
import com.sun.net.httpserver.HttpHandler;
53+
import com.sun.net.httpserver.HttpServer;
54+
55+
public final class AuthCodePkceTokenRequestBuilder extends TokenRequestBuilder {
56+
57+
@Override
58+
OAuth2Token retrieveToken() throws IOException {
59+
60+
OAuth2Token retVal = null;
61+
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
62+
try {
63+
byte[] verifierBytes = new byte[128];
64+
SecureRandom.getInstanceStrong().nextBytes(verifierBytes);
65+
Base64.Encoder b64encoder = Base64.getUrlEncoder();
66+
final String verifier = b64encoder.encodeToString(verifierBytes);
67+
68+
MessageDigest md = MessageDigest.getInstance("SHA-256");
69+
final String challenge = b64encoder.encodeToString(md.digest(verifierBytes));
70+
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
71+
int port = server.getAddress().getPort();
72+
String host = server.getAddress().getHostName();
73+
final AtomicReference<String> code = new AtomicReference<>();
74+
final AtomicReference<String> state = new AtomicReference<>();
75+
final CompletableFuture<Void> future = new CompletableFuture<>();
76+
77+
server.createContext("/", new HttpHandler() {
78+
79+
@Override
80+
public void handle(HttpExchange exchange) throws IOException {
81+
final String query = exchange.getRequestURI().getQuery();
82+
Map<String, List<String>> parameters = new HashMap<>();
83+
for (String pair: query.split("&")) {
84+
String[] kv = pair.split("=");
85+
String parameter = URLDecoder.decode(kv[0], StandardCharsets.UTF_8);
86+
String value = kv.length > 1 ? URLDecoder.decode(kv[1], StandardCharsets.UTF_8) : null;
87+
parameters.computeIfAbsent(parameter, p -> new ArrayList<>()).add(value);
88+
}
89+
90+
code.set(parameters.get("code").get(0));
91+
state.set(parameters.get("state").get(0));
92+
future.complete(null);
93+
}
94+
95+
});
96+
97+
String formBody = new UrlEncodedFormData()
98+
.addPassword("")
99+
.addGrantType("code")
100+
.addScopes("openid", "profile")
101+
.addClientId(getClientId())
102+
.addUsername("")
103+
.addParameter("code_challenge_method", "S256")
104+
.addParameter("code_challenge", challenge)
105+
.addParameter("redirect_uri", String.format("http://%s:%d", host, port))
106+
.buildEncodedString();
107+
String urlStr= String.format("%s/%s", getUrl().getApiRoot(), formBody);
108+
// start server to listen
109+
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Action.BROWSE)) {
110+
Desktop.getDesktop().browse(URI.create(urlStr));
111+
} else {
112+
System.out.println(String.format("Paste the following into a browser to continue login: %s", urlStr));
113+
}
114+
115+
future.join();
116+
System.out.println("Next steps.");
117+
118+
119+
} catch (NoSuchAlgorithmException ex) {
120+
throw new IOException("Unable to retrieve SecureRandom or Message Digest instance to generate verifier", ex);
121+
}
122+
123+
return retVal;
124+
}
125+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package hec.army.usace.hec.cwbi.auth.http.client;
2+
3+
import java.io.IOException;
4+
import java.util.Objects;
5+
import java.util.concurrent.CompletionException;
6+
7+
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
8+
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfoBuilder;
9+
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
10+
import mil.army.usace.hec.cwms.http.client.auth.OAuth2TokenProvider;
11+
12+
public final class OidcAuthTokenProvider implements OAuth2TokenProvider {
13+
14+
private final String clientId;
15+
private final String wellKnownUrl;
16+
private final ApiConnectionInfo tokenUrl;
17+
private final ApiConnectionInfo authUrl;
18+
private OAuth2Token token = null;
19+
20+
public OidcAuthTokenProvider(String clientId, String wellKnownUrl) {
21+
this.clientId = Objects.requireNonNull(clientId, "Missing required client id.");
22+
this.wellKnownUrl = Objects.requireNonNull(clientId, "Missing required well known Url.");
23+
24+
OpenIdTokenController controller = new OpenIdTokenController() {
25+
26+
@Override
27+
public String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) throws IOException {
28+
return wellKnownUrl; // we already have it.
29+
}
30+
31+
};
32+
ApiConnectionInfo info = new ApiConnectionInfoBuilder(wellKnownUrl).build();/// this is getting really obnoxious to keep typing out.
33+
try {
34+
this.authUrl = controller.retrieveAuthUrl(info, null);
35+
this.tokenUrl = controller.retrieveTokenUrl(info, null);
36+
} catch (IOException ex) {
37+
throw new CompletionException("Unable to return auth or token URL", ex);
38+
}
39+
}
40+
41+
@Override
42+
public void clear() {
43+
synchronized (this) {
44+
this.token = null;
45+
}
46+
}
47+
48+
@Override
49+
public OAuth2Token getToken() throws IOException {
50+
synchronized(this) {
51+
if (token == null) {
52+
token = newToken();
53+
}
54+
return token;
55+
}
56+
}
57+
58+
@Override
59+
public OAuth2Token refreshToken() throws IOException {
60+
synchronized (this) {
61+
OAuth2Token newToken = new RefreshTokenRequestBuilder()
62+
.withRefreshToken(token.getRefreshToken())
63+
.withUrl(tokenUrl)
64+
.withClientId(clientId)
65+
.fetchToken();
66+
token = newToken;
67+
return token;
68+
}
69+
}
70+
71+
@Override
72+
public OAuth2Token newToken() throws IOException {
73+
synchronized (this) {
74+
/**
75+
* It may make sense to allow something to override this usage, however that
76+
* *should* be a user setting. So like additional drop down or something.
77+
*/
78+
token = new AuthCodePkceTokenRequestBuilder()
79+
.withUrl(authUrl)
80+
.withClientId(clientId)
81+
.fetchToken();
82+
return token;
83+
}
84+
85+
}
86+
87+
}

cwbi-auth-http-client/src/main/java/hec/army/usace/hec/cwbi/auth/http/client/OpenIdTokenController.java

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,63 @@ public abstract class OpenIdTokenController {
1212

1313
static final String ACCEPT_HEADER = "application/json";
1414
private static final String TOKEN_ENDPOINT_KEY = "token_endpoint";
15-
protected abstract String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) throws IOException;
15+
private static final String AUTH_ENDPOINT_KEY = "authorization_endpoint";
16+
17+
18+
private String authEndpoint = null;
19+
private String tokenEndpoint = null;
20+
21+
/**
22+
* Retrieve json text of the .wellknown/openid-configuration
23+
* @param apiConnectionInfo
24+
* @return
25+
* @throws IOException
26+
*/
27+
public abstract String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) throws IOException;
28+
1629
public final ApiConnectionInfo retrieveTokenUrl(ApiConnectionInfo apiConnectionInfo, SslSocketData sslSocketData) throws IOException {
17-
String wellKnownEndpoint = retrieveWellKnownEndpoint(apiConnectionInfo);
18-
ApiConnectionInfo wellKnownApiConnectionInfo = new ApiConnectionInfoBuilder(wellKnownEndpoint)
30+
if (tokenEndpoint == null) {
31+
String wellKnownEndpoint = retrieveWellKnownEndpoint(apiConnectionInfo)
32+
// stop gap until source of information is corrected
33+
.replace("identityc","identity");
34+
35+
ApiConnectionInfo wellKnownApiConnectionInfo = new ApiConnectionInfoBuilder(wellKnownEndpoint)
36+
.withSslSocketData(sslSocketData)
37+
.build();
38+
HttpRequestExecutor executor = new HttpRequestBuilderImpl(wellKnownApiConnectionInfo)
39+
.get()
40+
.withMediaType(ACCEPT_HEADER);
41+
try (HttpRequestResponse response = executor.execute()) {
42+
tokenEndpoint = OAuth2ObjectMapper.getValueForKey(response.getBody(), TOKEN_ENDPOINT_KEY)
43+
// stop gap until source of information is corrected
44+
.replace("identityc","identity");
45+
}
46+
}
47+
return new ApiConnectionInfoBuilder(tokenEndpoint)
1948
.withSslSocketData(sslSocketData)
2049
.build();
21-
HttpRequestExecutor executor = new HttpRequestBuilderImpl(wellKnownApiConnectionInfo)
22-
.get()
23-
.withMediaType(ACCEPT_HEADER);
24-
try (HttpRequestResponse response = executor.execute()) {
25-
String tokenEndpoint = OAuth2ObjectMapper.getValueForKey(response.getBody(), TOKEN_ENDPOINT_KEY);
26-
return new ApiConnectionInfoBuilder(tokenEndpoint)
50+
}
51+
52+
public final ApiConnectionInfo retrieveAuthUrl(ApiConnectionInfo apiConnectionInfo, SslSocketData sslSocketData) throws IOException {
53+
54+
if (authEndpoint == null) {
55+
String wellKnownEndpoint = retrieveWellKnownEndpoint(apiConnectionInfo)
56+
// stop gap until source of information is corrected
57+
.replace("identityc","identity");
58+
ApiConnectionInfo wellKnownApiConnectionInfo = new ApiConnectionInfoBuilder(wellKnownEndpoint)
2759
.withSslSocketData(sslSocketData)
2860
.build();
61+
HttpRequestExecutor executor = new HttpRequestBuilderImpl(wellKnownApiConnectionInfo)
62+
.get()
63+
.withMediaType(ACCEPT_HEADER);
64+
try (HttpRequestResponse response = executor.execute()) {
65+
authEndpoint = OAuth2ObjectMapper.getValueForKey(response.getBody(), AUTH_ENDPOINT_KEY)
66+
// stop gap until source of information is corrected
67+
.replace("identityc","identity");
68+
}
2969
}
70+
return new ApiConnectionInfoBuilder(authEndpoint)
71+
.withSslSocketData(sslSocketData)
72+
.build();
3073
}
3174
}

0 commit comments

Comments
 (0)