Skip to content

Commit 4bb4ad0

Browse files
committed
Initial implementation of a more 'pure' OpenID Connection token provider.
1 parent 6c27285 commit 4bb4ad0

14 files changed

Lines changed: 293 additions & 58 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ build
88
**/bin
99
**/.idea/
1010
lib/
11+
.vscode

buildSrc/src/main/groovy/cwms-data-api-client.java-conventions.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ configurations.implementation.extendsFrom(configurations.annotationProcessor)
2222

2323
test {
2424
useJUnitPlatform()
25+
// when passing --tests by default it needs to match all in all subjects
26+
filter {
27+
setFailOnNoMatchingTests(false)
28+
}
2529
}
2630

2731
javadoc {
Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* MIT License
33
*
4-
*
4+
* Copyright (c) 2024 Hydrologic Engineering Center
55
*
66
* Permission is hereby granted, free of charge, to any person obtaining a copy
77
* of this software and associated documentation files (the "Software"), to deal
@@ -27,22 +27,15 @@
2727
import mil.army.usace.hec.cwms.http.client.HttpRequestResponse;
2828
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
2929
import mil.army.usace.hec.cwms.http.client.request.HttpRequestExecutor;
30+
import mil.army.usace.hec.cwms.http.client.request.QueryParameters;
3031

3132
import java.io.IOException;
3233
import java.net.InetSocketAddress;
3334
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;
3835
import java.security.MessageDigest;
3936
import java.security.NoSuchAlgorithmException;
4037
import java.security.SecureRandom;
41-
import java.util.ArrayList;
4238
import java.util.Base64;
43-
import java.util.HashMap;
44-
import java.util.List;
45-
import java.util.Map;
4639
import java.util.concurrent.CompletableFuture;
4740
import java.util.concurrent.atomic.AtomicReference;
4841
import java.awt.Desktop;
@@ -52,7 +45,13 @@
5245
import com.sun.net.httpserver.HttpHandler;
5346
import com.sun.net.httpserver.HttpServer;
5447

55-
public final class AuthCodePkceTokenRequestBuilder extends TokenRequestBuilder {
48+
/**
49+
* Use Authorization Code + PKCE method to retrieve initial token set.
50+
*
51+
* If a desktop is available users's default Browser is opened with the given auth URL
52+
* To complete the additional requirements.
53+
*/
54+
public final class AuthCodePkceTokenRequestBuilder extends TokenRequestBuilder<AuthCodePkceTokenRequestBuilder> {
5655

5756
@Override
5857
OAuth2Token retrieveToken() throws IOException {
@@ -67,7 +66,7 @@ OAuth2Token retrieveToken() throws IOException {
6766

6867
MessageDigest md = MessageDigest.getInstance("SHA-256");
6968
final String challenge = b64encoder.encodeToString(md.digest(verifierBytes));
70-
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
69+
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
7170
int port = server.getAddress().getPort();
7271
String host = server.getAddress().getHostName();
7372
final AtomicReference<String> code = new AtomicReference<>();
@@ -79,33 +78,27 @@ OAuth2Token retrieveToken() throws IOException {
7978
@Override
8079
public void handle(HttpExchange exchange) throws IOException {
8180
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-
81+
final QueryParameters parameters = QueryParameters.parse(query);
82+
9083
code.set(parameters.get("code").get(0));
9184
state.set(parameters.get("state").get(0));
85+
System.out.println("Got code");
86+
server.stop(0);
9287
future.complete(null);
9388
}
9489

9590
});
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);
91+
final String redirectUri = String.format("http://%s:%d", host, port);
92+
final QueryParameters authParameters = QueryParameters.empty()
93+
.set("grant_type", "code")
94+
.set("client_id", getClientId())
95+
.set("scopes", "openid profile")
96+
.set("code_challenge_method", "S256")
97+
.set("code_challenge", challenge)
98+
.set("redirect_uri", redirectUri);
99+
String urlStr= String.format("%s?%s", getAuthUrl().getApiRoot(), authParameters.encode());
108100
// start server to listen
101+
server.start();
109102
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Action.BROWSE)) {
110103
Desktop.getDesktop().browse(URI.create(urlStr));
111104
} else {
@@ -115,11 +108,31 @@ public void handle(HttpExchange exchange) throws IOException {
115108
future.join();
116109
System.out.println("Next steps.");
117110

118-
111+
final UrlEncodedFormData formData = new UrlEncodedFormData();
112+
formData.addClientId(getClientId())
113+
.addGrantType("authorization_code")
114+
.addParameter("code_verifier", verifier)
115+
.addScopes("openid", "profile")
116+
.addParameter("redirect_uri", redirectUri)
117+
.addParameter("session_state", state.get())
118+
.addParameter("code", code.get())
119+
.addParameter("response_mode", "fragment")
120+
.addParameter("response_type", "id_token token");
121+
122+
HttpRequestExecutor executor =
123+
new HttpRequestBuilderImpl(getTokenUrl())
124+
.post()
125+
.withBody(formData.buildEncodedString())
126+
.withMediaType(MEDIA_TYPE);
127+
try (HttpRequestResponse response = executor.execute()) {
128+
String body = response.getBody();
129+
if (body != null) {
130+
retVal = OAuth2ObjectMapper.mapJsonToObject(body, OAuth2Token.class);
131+
}
132+
}
133+
return retVal;
119134
} catch (NoSuchAlgorithmException ex) {
120135
throw new IOException("Unable to retrieve SecureRandom or Message Digest instance to generate verifier", ex);
121136
}
122-
123-
return retVal;
124137
}
125138
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
import java.io.IOException;
3232

33-
public final class DirectGrantX509TokenRequestBuilder extends TokenRequestBuilder {
33+
public final class DirectGrantX509TokenRequestBuilder extends TokenRequestBuilder<DirectGrantX509TokenRequestBuilder> {
3434

3535
@Override
3636
OAuth2Token retrieveToken() throws IOException {

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
1010
import mil.army.usace.hec.cwms.http.client.auth.OAuth2TokenProvider;
1111

12+
/**
13+
* Handle generic OIDC auth based on configuration elements in the .well-known/openid-configuration
14+
* values.
15+
*
16+
* Defaults to using Authorization Code + PKCE.
17+
* Support should be provided to support alternative flows as a user-at-login decision point.
18+
*/
1219
public final class OidcAuthTokenProvider implements OAuth2TokenProvider {
1320

1421
private final String clientId;
@@ -29,12 +36,15 @@ public String retrieveWellKnownEndpoint(ApiConnectionInfo apiConnectionInfo) thr
2936
}
3037

3138
};
32-
ApiConnectionInfo info = new ApiConnectionInfoBuilder(wellKnownUrl).build();/// this is getting really obnoxious to keep typing out.
39+
ApiConnectionInfo info = new ApiConnectionInfoBuilder(wellKnownUrl).build();
40+
String what = "auth";
3341
try {
3442
this.authUrl = controller.retrieveAuthUrl(info, null);
43+
what = "token";
3544
this.tokenUrl = controller.retrieveTokenUrl(info, null);
45+
// TODO: process appropriate extensions to determine things like "kc_idp_hint"
3646
} catch (IOException ex) {
37-
throw new CompletionException("Unable to return auth or token URL", ex);
47+
throw new CompletionException("Unable to return " + what + " URL", ex);
3848
}
3949
}
4050

@@ -73,10 +83,13 @@ public OAuth2Token newToken() throws IOException {
7383
synchronized (this) {
7484
/**
7585
* 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.
86+
* *should* be a user setting. So like additional drop down or something in the gui.
87+
* There are various notes about it in different sections for discussion.
7788
*/
7889
token = new AuthCodePkceTokenRequestBuilder()
79-
.withUrl(authUrl)
90+
.withAuthUrl(authUrl)
91+
.withTokenUrl(tokenUrl)
92+
.buildRequest()
8093
.withClientId(clientId)
8194
.fetchToken();
8295
return token;

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ public final class RefreshTokenRequestBuilder implements RefreshTokenRequestFlue
1919
* @return Builder for http request
2020
*/
2121
@Override
22-
public TokenRequestFluentBuilder withRefreshToken(String refreshToken) {
22+
public <T> TokenRequestFluentBuilder<T> withRefreshToken(String refreshToken) {
2323
this.refreshToken = Objects.requireNonNull(refreshToken, "Missing required refresh token");
24-
return new RefreshTokenRequestExecutor();
24+
// NOTE: The executor clearly extends TokenRequestBuilder which implements TokenRequestFluentBuilder so
25+
// I'm really confused why we need the cast.
26+
return (TokenRequestFluentBuilder<T>) new RefreshTokenRequestExecutor();
2527
}
2628

27-
class RefreshTokenRequestExecutor extends TokenRequestBuilder {
29+
class RefreshTokenRequestExecutor extends TokenRequestBuilder<RefreshTokenRequestExecutor> {
2830

2931
@Override
3032
OAuth2Token retrieveToken() throws IOException {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package hec.army.usace.hec.cwbi.auth.http.client;
22

33
public interface RefreshTokenRequestFluentBuilder {
4-
TokenRequestFluentBuilder withRefreshToken(String refreshToken);
4+
<T> TokenRequestFluentBuilder<T> withRefreshToken(String refreshToken);
55
}

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,42 @@
2828
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
2929
import mil.army.usace.hec.cwms.http.client.auth.OAuth2Token;
3030

31-
abstract class TokenRequestBuilder implements TokenRequestFluentBuilder {
31+
abstract class TokenRequestBuilder<T> implements TokenRequestFluentBuilder<TokenRequestBuilder<T>> {
3232

3333
static final String MEDIA_TYPE = "application/x-www-form-urlencoded";
3434
private ApiConnectionInfo url;
35+
private ApiConnectionInfo authUrl;
36+
private ApiConnectionInfo tokenUrl;
3537
private String clientId;
3638

3739
abstract OAuth2Token retrieveToken() throws IOException;
3840

41+
/**
42+
* Method used method the auth and token URL are the same.
43+
* @return
44+
* @deprecated implementations, even when they are the same, should use the individual auth/token methods.
45+
*/
46+
@Deprecated(forRemoval = true)
3947
ApiConnectionInfo getUrl() {
4048
return url;
4149
}
4250

51+
/**
52+
* Retrieve the specific Auth endpoint URL.
53+
* @return
54+
*/
55+
ApiConnectionInfo getAuthUrl() {
56+
return authUrl;
57+
}
58+
59+
/**
60+
* Retrieve the specific Token endpiont URL.
61+
* @return
62+
*/
63+
ApiConnectionInfo getTokenUrl() {
64+
return tokenUrl;
65+
}
66+
4367
String getClientId() {
4468
return clientId;
4569
}
@@ -50,6 +74,24 @@ public RequestClientId withUrl(ApiConnectionInfo url) {
5074
return new RequestClientIdImpl();
5175
}
5276

77+
@Override
78+
public RequestClientId buildRequest() {
79+
return new RequestClientIdImpl();
80+
}
81+
82+
@Override
83+
public TokenRequestBuilder<T> withAuthUrl(ApiConnectionInfo url) {
84+
this.authUrl = url;
85+
return this;
86+
}
87+
88+
@Override
89+
public TokenRequestBuilder<T> withTokenUrl(ApiConnectionInfo url) {
90+
this.tokenUrl = url;
91+
return this;
92+
}
93+
94+
5395
private class RequestClientIdImpl implements RequestClientId {
5496

5597
@Override

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,36 @@
2525

2626
import mil.army.usace.hec.cwms.http.client.ApiConnectionInfo;
2727

28-
public interface TokenRequestFluentBuilder {
28+
public interface TokenRequestFluentBuilder<T> {
2929

30+
/**
31+
* If given auth method uses a single URL.
32+
* @param url
33+
* @return
34+
* @deprecated even for implementations, like direct grant/resource owner password credentials
35+
* should use the individual endpoints in the appropriate sections to avoid configuration
36+
* details that are too specific but filter up among the usage.
37+
*/
38+
@Deprecated(forRemoval = true)
3039
RequestClientId withUrl(ApiConnectionInfo url);
40+
41+
/**
42+
* Create object for next step in auth.
43+
* @return
44+
*/
45+
RequestClientId buildRequest();
46+
47+
/**
48+
* set specific Auth URL endpoint.
49+
* @param url
50+
* @return
51+
*/
52+
TokenRequestFluentBuilder<T> withAuthUrl(ApiConnectionInfo url);
53+
54+
/**
55+
* set specific Token URL endpoint.
56+
* @param url
57+
* @return
58+
*/
59+
TokenRequestFluentBuilder<T> withTokenUrl(ApiConnectionInfo url);
3160
}

0 commit comments

Comments
 (0)