Skip to content

Commit b8389f3

Browse files
rraystValentin Brückelchristiangoerdes
authored
OAuth2 client refactoring (membrane#1681)
* refactor: oauth2 client * refactor: oauth2 client * Extracted <input> generation * Chained if to pattern-matching switch * Extract authSubject parameter Extract username extraction to method * remove intermediate reference to session.content * replaced deprecated JSR310Module * removed unthrown exceptions * formatting of constant inits * post-merge cleanup --------- Co-authored-by: Valentin Brückel <brueckel@predic8.de> Co-authored-by: Christian Gördes <118011644+christiangoerdes@users.noreply.github.com>
1 parent accfc95 commit b8389f3

23 files changed

Lines changed: 453 additions & 305 deletions

core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2AnswerParameters.java

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,20 @@
1515

1616
import com.fasterxml.jackson.core.JsonProcessingException;
1717
import com.fasterxml.jackson.databind.ObjectMapper;
18-
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
18+
import com.fasterxml.jackson.databind.SerializationFeature;
19+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
20+
import com.predic8.membrane.core.interceptor.oauth2client.rf.OAuth2TokenResponseBody;
21+
import org.jetbrains.annotations.NotNull;
1922

2023
import java.io.IOException;
21-
import java.io.UnsupportedEncodingException;
2224
import java.time.LocalDateTime;
2325
import java.util.HashMap;
2426
import java.util.Map;
2527

2628
public class OAuth2AnswerParameters {
2729

30+
private final static ObjectMapper om = createObjectMapper();
31+
2832
private String accessToken;
2933
private String tokenType;
3034
private String idToken;
@@ -33,6 +37,12 @@ public class OAuth2AnswerParameters {
3337
private LocalDateTime receivedAt;
3438
private String refreshToken;
3539

40+
public static OAuth2AnswerParameters createFrom(OAuth2TokenResponseBody tokenResponse) {
41+
var r = new OAuth2AnswerParameters();
42+
r.readFrom(tokenResponse);
43+
return r;
44+
}
45+
3646
public String getAccessToken() {
3747
return accessToken;
3848
}
@@ -65,17 +75,18 @@ public String getTokenType() {
6575
return tokenType;
6676
}
6777

68-
public String serialize() throws JsonProcessingException, UnsupportedEncodingException {
69-
return OAuth2Util.urlencode(getObjectMapper().writeValueAsString(this));
78+
public String serialize() throws JsonProcessingException {
79+
return OAuth2Util.urlencode(om.writeValueAsString(this));
7080
}
7181

7282
public static OAuth2AnswerParameters deserialize(String oauth2answer) throws IOException {
73-
return getObjectMapper().readValue(OAuth2Util.urldecode(oauth2answer),OAuth2AnswerParameters.class);
83+
return om.readValue(OAuth2Util.urldecode(oauth2answer),OAuth2AnswerParameters.class);
7484
}
7585

76-
private static ObjectMapper getObjectMapper() {
86+
private static ObjectMapper createObjectMapper() {
7787
ObjectMapper mapper = new ObjectMapper();
78-
mapper.registerModule(new JSR310Module());
88+
mapper.registerModule(new JavaTimeModule());
89+
mapper.enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
7990
return mapper;
8091
}
8192

@@ -106,9 +117,35 @@ public void setRefreshToken(String refreshToken) {
106117
@Override
107118
public String toString() {
108119
try {
109-
return getObjectMapper().writeValueAsString(this);
120+
return om.writeValueAsString(this);
110121
} catch (JsonProcessingException e) {
111122
return "";
112123
}
113124
}
125+
126+
public void updateReceivedAt() {
127+
setReceivedAt(computeReceivedAt());
128+
}
129+
130+
private static @NotNull LocalDateTime computeReceivedAt() {
131+
LocalDateTime now = LocalDateTime.now();
132+
LocalDateTime receivedAt = now.withSecond(floorTo30ies(now)).withNano(0);
133+
return receivedAt;
134+
}
135+
136+
private static int floorTo30ies(LocalDateTime now) {
137+
return now.getSecond() / 30 * 30;
138+
}
139+
140+
public void readFrom(OAuth2TokenResponseBody tokenResponse) {
141+
updateReceivedAt();
142+
143+
setAccessToken(tokenResponse.getAccessToken());
144+
setTokenType(tokenResponse.getTokenType());
145+
setRefreshToken(tokenResponse.getRefreshToken());
146+
// TODO: "refresh_token_expires_in":1209600
147+
setExpiration(tokenResponse.getExpiresIn());
148+
if (tokenResponse.getIdToken() != null)
149+
setIdToken(tokenResponse.getVerifiedIdToken());
150+
}
114151
}

core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2AuthorizationServerInterceptor.java

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.predic8.membrane.core.interceptor.oauth2.tokengenerators.*;
2222
import com.predic8.membrane.core.proxies.*;
2323
import com.predic8.membrane.core.util.*;
24+
import org.jetbrains.annotations.NotNull;
2425
import org.jose4j.lang.*;
2526
import org.slf4j.*;
2627

@@ -31,6 +32,7 @@
3132
@MCElement(name = "oauth2authserver")
3233
public class OAuth2AuthorizationServerInterceptor extends AbstractInterceptor {
3334
private static final Logger log = LoggerFactory.getLogger(OAuth2AuthorizationServerInterceptor.class.getName());
35+
public static final Set<@NotNull String> SUPPORTED_AUTHORIZATION_GRANTS = Set.of("code", "token", "id_token token");
3436

3537
private String issuer;
3638
private String location;
@@ -130,23 +132,24 @@ public void init() {
130132
}
131133

132134
private void addDefaultProcessors() {
133-
getProcessors()
134-
.add(new InvalidMethodProcessor(this))
135-
.add(new FaviconEndpointProcessor(this))
136-
.add(new AuthEndpointProcessor(this))
137-
.add(new TokenEndpointProcessor(this))
138-
.add(new UserinfoEndpointProcessor(this))
139-
.add(new RevocationEndpointProcessor(this))
140-
.add(new LoginDialogEndpointProcessor(this))
141-
.add(new WellknownEndpointProcessor(this))
142-
.add(new CertsEndpointProcessor(this))
143-
.add(new EmptyEndpointProcessor(this))
144-
.add(new DefaultEndpointProcessor(this));
135+
List.of(
136+
new InvalidMethodProcessor(this),
137+
new FaviconEndpointProcessor(this),
138+
new AuthEndpointProcessor(this),
139+
new TokenEndpointProcessor(this),
140+
new UserinfoEndpointProcessor(this),
141+
new RevocationEndpointProcessor(this),
142+
new LoginDialogEndpointProcessor(this),
143+
new WellknownEndpointProcessor(this),
144+
new CertsEndpointProcessor(this),
145+
new EmptyEndpointProcessor(this),
146+
new DefaultEndpointProcessor(this)
147+
).forEach(processors::add);
145148
}
146149

147150
@Override
148151
public Outcome handleRequest(Exchange exc) {
149-
Outcome outcome = getProcessors().runProcessors(exc);
152+
Outcome outcome = processors.runProcessors(exc);
150153
if (outcome != Outcome.CONTINUE)
151154
sessionManager.postProcess(exc);
152155
return outcome;
@@ -327,9 +330,7 @@ public String getShortDescription() {
327330
}
328331

329332
public void addSupportedAuthorizationGrants() {
330-
getSupportedAuthorizationGrants().add("code");
331-
getSupportedAuthorizationGrants().add("token");
332-
getSupportedAuthorizationGrants().add("id_token token");
333+
getSupportedAuthorizationGrants().addAll(SUPPORTED_AUTHORIZATION_GRANTS);
333334
}
334335

335336
public OAuth2Statistics getStatistics() {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.predic8.membrane.core.interceptor.oauth2;
2+
3+
import com.predic8.membrane.core.interceptor.oauth2client.rf.FormPostGenerator;
4+
5+
import java.util.function.Function;
6+
7+
import static java.net.URLEncoder.encode;
8+
import static java.nio.charset.StandardCharsets.UTF_8;
9+
10+
public class OAuth2TokenBody {
11+
private String code;
12+
private String grantType;
13+
private String refreshToken;
14+
private String scope;
15+
private String redirectUri;
16+
17+
public static OAuth2TokenBody refreshTokenBodyBuilder(String refreshToken) {
18+
OAuth2TokenBody r = new OAuth2TokenBody();
19+
r.grantType = "refresh_token";
20+
r.refreshToken = refreshToken;
21+
return r;
22+
}
23+
24+
public static OAuth2TokenBody authorizationCodeBodyBuilder(String code) {
25+
OAuth2TokenBody r = new OAuth2TokenBody();
26+
r.code = code;
27+
r.grantType = "authorization_code";
28+
return r;
29+
}
30+
31+
public OAuth2TokenBody scope(String scope) {
32+
this.scope = scope;
33+
return this;
34+
}
35+
36+
public String build() {
37+
StringBuilder r = new StringBuilder("grant_type=" + grantType);
38+
appendParam(r, "refresh_token", refreshToken);
39+
appendParam(r, "code", code);
40+
appendParam(r, "redirect_uri", redirectUri);
41+
appendParam(r, "scope", scope, e -> encode(e, UTF_8));
42+
return r.toString();
43+
}
44+
45+
private void appendParam(StringBuilder sb, String paramName, String paramValue) {
46+
appendParam(sb, paramName, paramValue, e -> e);
47+
}
48+
49+
private void appendParam(StringBuilder sb, String paramName, String paramValue, Function<String, String> encoder) {
50+
if (paramValue == null)
51+
return;
52+
sb.append("&").append(paramName).append("=").append(encoder.apply(paramValue));
53+
}
54+
55+
public OAuth2TokenBody redirectUri(String redirectUri) {
56+
this.redirectUri = redirectUri;
57+
return this;
58+
}
59+
}

core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2Util.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,17 @@ public static Response createParameterizedJsonErrorResponse(String... params) th
7070
}
7171

7272
public static @NotNull String getPublicURL(Exchange exc) {
73-
return (isHTTPS(exc.getProxy(), getxForwardedProto(exc)) ? "https://" : "http://") + exc.getOriginalHostHeader();
73+
return getProtocol(exc.getProxy(), getxForwardedProto(exc))+ "://" + exc.getOriginalHostHeader();
7474
}
7575

76-
private static boolean isHTTPS(Proxy proxy, String xForwardedProto) {
76+
private static String getProtocol(Proxy proxy, String xForwardedProto) {
7777
if (!(proxy instanceof SSLableProxy sp)) {
78-
return false;
78+
return "http";
79+
}
80+
if (xForwardedProto != null) {
81+
return "https".equals(xForwardedProto) ? "https" : "http";
7982
}
80-
return xForwardedProto != null ? "https".equals(xForwardedProto) : sp.isInboundSSL();
83+
return sp.getProtocol() ;
8184
}
8285

8386
private static String getxForwardedProto(Exchange exc) {

core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/WellknownFile.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private String baseOauth2Url(){
8080
return resolver.combine(getOauth2Issuer() + "/","oauth2/");
8181
}
8282

83-
private void getValuesFromOasi() throws UnsupportedEncodingException {
83+
private void getValuesFromOasi() {
8484
if(oasi == null)
8585
return;
8686

core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/AuthorizationService.java

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.predic8.membrane.core.interceptor.oauth2.OAuth2AnswerParameters;
2424
import com.predic8.membrane.core.interceptor.oauth2.tokengenerators.JwtGenerator;
2525
import com.predic8.membrane.core.interceptor.oauth2client.rf.LogHelper;
26+
import com.predic8.membrane.core.interceptor.oauth2client.rf.OAuth2TokenResponseBody;
2627
import com.predic8.membrane.core.interceptor.oauth2client.rf.token.JWSSigner;
2728
import com.predic8.membrane.core.interceptor.session.Session;
2829
import com.predic8.membrane.core.resolver.ResolverMap;
@@ -31,6 +32,7 @@
3132
import com.predic8.membrane.core.transport.ssl.PEMSupport;
3233
import com.predic8.membrane.core.transport.ssl.SSLContext;
3334
import com.predic8.membrane.core.transport.ssl.StaticSSLContext;
35+
import jakarta.mail.internet.ParseException;
3436
import org.apache.commons.codec.binary.Base64;
3537
import org.jose4j.jwt.JwtClaims;
3638
import org.jose4j.jwt.MalformedClaimException;
@@ -40,16 +42,20 @@
4042
import org.slf4j.LoggerFactory;
4143

4244
import javax.annotation.concurrent.GuardedBy;
45+
import java.io.IOException;
4346
import java.io.InputStream;
44-
import java.net.URLEncoder;
47+
import java.net.URISyntaxException;
4548
import java.util.List;
4649
import java.util.UUID;
4750

4851
import static com.predic8.membrane.core.Constants.USERAGENT;
4952
import static com.predic8.membrane.core.http.Header.*;
5053
import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON;
5154
import static com.predic8.membrane.core.http.MimeType.APPLICATION_X_WWW_FORM_URLENCODED;
52-
import static java.nio.charset.StandardCharsets.UTF_8;
55+
import static com.predic8.membrane.core.interceptor.oauth2.OAuth2TokenBody.authorizationCodeBodyBuilder;
56+
import static com.predic8.membrane.core.interceptor.oauth2.OAuth2TokenBody.refreshTokenBodyBuilder;
57+
import static com.predic8.membrane.core.interceptor.oauth2client.rf.JsonUtils.isJson;
58+
import static java.net.URLEncoder.encode;
5359

5460
public abstract class AuthorizationService {
5561
protected Logger log;
@@ -189,9 +195,12 @@ public void setHttpClient(HttpClient httpClient) {
189195
}
190196

191197
public Response doRequest(Exchange e) throws Exception {
198+
logHelper.handleRequest(e);
192199
if (sslContext != null)
193200
e.setProperty(Exchange.SSL_CONTEXT, sslContext);
194-
return getHttpClient().call(e).getResponse();
201+
Response response = getHttpClient().call(e).getResponse();
202+
logHelper.handleResponse(e);
203+
return response;
195204
}
196205

197206
public SSLParser getSslParser() {
@@ -229,31 +238,20 @@ public Request.Builder applyAuth(Request.Builder requestBuilder, String body) {
229238
return requestBuilder;
230239
}
231240

232-
public Response refreshTokenRequest(Session session, OAuth2AnswerParameters params, String wantedScope) throws Exception {
241+
242+
243+
public String getTokenEndpoint(Session session) {
233244
String tokenEndpoint = getTokenEndpoint();
234245
if (session.get("defaultFlow") != null) {
235246
tokenEndpoint = tokenEndpoint.replaceAll(session.get("defaultFlow"), session.get("triggerFlow"));
236247
}
237-
238-
Exchange e = applyAuth(
239-
new Request.Builder().post(tokenEndpoint)
240-
.contentType(APPLICATION_X_WWW_FORM_URLENCODED)
241-
.header(ACCEPT, APPLICATION_JSON)
242-
.header(USER_AGENT, USERAGENT),
243-
"grant_type=refresh_token" + "&refresh_token=" + params.getRefreshToken() +
244-
(wantedScope != null ? "&scope=" + URLEncoder.encode(wantedScope, UTF_8) : ""))
245-
.buildExchange();
246-
247-
logHelper.handleRequest(e);
248-
Response response = doRequest(e);
249-
logHelper.handleResponse(e);
250-
return response;
248+
return tokenEndpoint;
251249
}
252250

253-
public Response requestUserEndpoint(OAuth2AnswerParameters params) throws Exception {
251+
public Response requestUserEndpoint(String tokenType, String token) throws Exception {
254252
return doRequest(new Request.Builder()
255253
.get(getUserInfoEndpoint())
256-
.header("Authorization", params.getTokenType() + " " + params.getAccessToken())
254+
.header("Authorization", tokenType + " " + token)
257255
.header("User-Agent", USERAGENT)
258256
.header(ACCEPT, APPLICATION_JSON)
259257
.buildExchange());
@@ -286,7 +284,8 @@ private String createClientToken() {
286284
jwtClaims.setExpirationTime(expiration);
287285
jwtClaims.setNotBeforeMinutesInThePast(2f);
288286

289-
return JWSSigner.signToCompactSerialization(jwtClaims.toJson());
287+
String payload = jwtClaims.toJson();
288+
return JWSSigner.generateSignedJWS(payload);
290289
} catch (JoseException | MalformedClaimException e) {
291290
throw new RuntimeException(e);
292291
}
@@ -299,4 +298,42 @@ public InputStream resolve(ResolverMap rm, String baseLocation, String url) thro
299298
return httpClient.call(Request.get(url).buildExchange()).getResponse().getBodyAsStreamDecoded();
300299
return rm.resolve(url);
301300
}
301+
302+
public OAuth2TokenResponseBody refreshTokenRequest(Session session, String wantedScope, String refreshToken) throws Exception {
303+
return parseTokenResponse(checkTokenResponse(doRequest(applyAuth(
304+
new Request.Builder().post(getTokenEndpoint(session))
305+
.contentType(APPLICATION_X_WWW_FORM_URLENCODED)
306+
.header(ACCEPT, APPLICATION_JSON)
307+
.header(USER_AGENT, USERAGENT),
308+
refreshTokenBodyBuilder(refreshToken).scope(wantedScope).build())
309+
.buildExchange())));
310+
}
311+
312+
public OAuth2TokenResponseBody codeTokenRequest(String redirectUri, String code) throws Exception {
313+
return parseTokenResponse(checkTokenResponse(doRequest(applyAuth(
314+
new Request.Builder()
315+
.post(getTokenEndpoint())
316+
.contentType(APPLICATION_X_WWW_FORM_URLENCODED)
317+
.header(ACCEPT, APPLICATION_JSON)
318+
.header(USER_AGENT, USERAGENT),
319+
authorizationCodeBodyBuilder(code).redirectUri(redirectUri).build()).buildExchange())));
320+
}
321+
322+
private OAuth2TokenResponseBody parseTokenResponse(Response response) throws IOException {
323+
return OAuth2TokenResponseBody.parse(this, response.getBodyAsStreamDecoded());
324+
}
325+
326+
private Response checkTokenResponse(Response response) throws IOException, ParseException {
327+
if (response.getStatusCode() != 200) {
328+
response.getBody().read();
329+
throw new RuntimeException("Authorization server returned " + response.getStatusCode() + ".");
330+
}
331+
332+
if (!isJson(response)) {
333+
response.getBody().read();
334+
throw new RuntimeException("Token response is no JSON.");
335+
}
336+
return response;
337+
}
338+
302339
}

0 commit comments

Comments
 (0)