Skip to content

Commit 0ba379b

Browse files
authored
Support opaque tokens for the first issuer (#75)
1 parent 2621bfe commit 0ba379b

File tree

8 files changed

+180
-23
lines changed

8 files changed

+180
-23
lines changed

src/main/java/org/gridsuite/gateway/GatewayConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class GatewayConfig {
2727
public static final String END_POINT_SERVICE_NAME = "end_point_service_name";
2828

2929
public static final String HEADER_USER_ID = "userId";
30+
public static final String HEADER_CLIENT_ID = "clientId";
3031

3132
@Bean
3233
public RouteLocator myRoutes(RouteLocatorBuilder builder, ApplicationContext context) {

src/main/java/org/gridsuite/gateway/GatewayService.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
package org.gridsuite.gateway;
99

1010
import org.gridsuite.gateway.dto.OpenIdConfiguration;
11+
import org.gridsuite.gateway.dto.TokenIntrospection;
12+
import org.springframework.beans.factory.annotation.Value;
1113
import org.springframework.stereotype.Service;
14+
import org.springframework.web.reactive.function.BodyInserters;
1215
import org.springframework.web.reactive.function.client.WebClient;
1316
import org.springframework.web.util.DefaultUriBuilderFactory;
1417
import org.springframework.web.util.UriComponentsBuilder;
@@ -18,6 +21,12 @@
1821
public class GatewayService {
1922
private WebClient.Builder webClientBuilder;
2023

24+
@Value("${client_id}")
25+
private String clientId;
26+
27+
@Value("${client_secret}")
28+
private String clientSecret;
29+
2130
public GatewayService() {
2231
webClientBuilder = WebClient.builder();
2332
}
@@ -44,4 +53,31 @@ public Mono<String> getJwkSet(String jwkSetUri) {
4453
.bodyToMono(String.class)
4554
.single();
4655
}
56+
57+
public Mono<String> getOpaqueTokenIntrospectionUri(String issBaseUri) {
58+
WebClient webClient = webClientBuilder.uriBuilderFactory(new DefaultUriBuilderFactory(issBaseUri)).build();
59+
60+
String path = UriComponentsBuilder.fromPath("/.well-known/openid-configuration")
61+
.toUriString();
62+
63+
return webClient.get()
64+
.uri(path)
65+
.retrieve()
66+
.bodyToMono(OpenIdConfiguration.class)
67+
.single()
68+
.map(OpenIdConfiguration::getIntrospectionEndpoint);
69+
}
70+
71+
public Mono<TokenIntrospection> getOpaqueTokenIntrospection(String introspectionUri, String token) {
72+
WebClient webClient = webClientBuilder.uriBuilderFactory(new DefaultUriBuilderFactory(introspectionUri)).build();
73+
74+
return webClient.post()
75+
.body(BodyInserters
76+
.fromFormData("client_id", clientId)
77+
.with("client_secret", clientSecret)
78+
.with("token", token))
79+
.retrieve()
80+
.bodyToMono(TokenIntrospection.class)
81+
.single();
82+
}
4783
}

src/main/java/org/gridsuite/gateway/dto/OpenIdConfiguration.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,17 @@ public String getJwksUri() {
2424
public void setJwksUri(String jwksUri) {
2525
this.jwksUri = jwksUri;
2626
}
27+
28+
@JsonAlias("introspection_endpoint")
29+
String introspectionEndpoint;
30+
31+
@JsonGetter("introspectionEndpoint")
32+
public String getIntrospectionEndpoint() {
33+
return introspectionEndpoint;
34+
}
35+
36+
@JsonSetter("introspectionEndpoint")
37+
public void setIntrospectionEndpoint(String introspectionEndpoint) {
38+
this.introspectionEndpoint = introspectionEndpoint;
39+
}
2740
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Copyright (c) 2023, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
package org.gridsuite.gateway.dto;
9+
10+
import com.fasterxml.jackson.annotation.JsonAlias;
11+
import com.fasterxml.jackson.annotation.JsonGetter;
12+
import com.fasterxml.jackson.annotation.JsonSetter;
13+
14+
public class TokenIntrospection {
15+
16+
boolean active;
17+
18+
public boolean getActive() {
19+
return active;
20+
}
21+
22+
public void setActive(boolean active) {
23+
this.active = active;
24+
}
25+
26+
@JsonAlias("client_id")
27+
String clientId;
28+
29+
@JsonGetter("clientId")
30+
public String getClientId() {
31+
return clientId;
32+
}
33+
34+
@JsonSetter("clientId")
35+
public void setClientId(String clientId) {
36+
this.clientId = clientId;
37+
}
38+
}

src/main/java/org/gridsuite/gateway/filters/TokenValidatorGlobalPreFilter.java

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import lombok.AllArgsConstructor;
2020
import lombok.Getter;
2121
import org.gridsuite.gateway.GatewayService;
22+
import org.gridsuite.gateway.dto.TokenIntrospection;
2223
import org.slf4j.Logger;
2324
import org.slf4j.LoggerFactory;
2425
import org.springframework.beans.factory.annotation.Value;
@@ -37,14 +38,16 @@
3738
import java.util.concurrent.ConcurrentHashMap;
3839

3940
import static org.gridsuite.gateway.GatewayConfig.HEADER_USER_ID;
41+
//TODO add client_id
42+
//import static org.gridsuite.gateway.GatewayConfig.HEADER_CLIENT_ID;
4043

4144
/**
4245
* @author Chamseddine Benhamed <chamseddine.benhamed at rte-france.com>
4346
*/
4447
@Component
4548
public class TokenValidatorGlobalPreFilter extends AbstractGlobalPreFilter {
4649

47-
public static final String UNAUTHORIZED_INVALID_PLAIN_JOSE_OBJECT_ENCODING = "{}: 401 Unauthorized, Invalid plain JOSE object encoding";
50+
public static final String UNAUTHORIZED_INVALID_PLAIN_JOSE_OBJECT_ENCODING = "{}: 401 Unauthorized, Invalid plain JOSE object encoding or inactive opaque token";
4851
public static final String PARSING_ERROR = "{}: 500 Internal Server Error, error has been reached unexpectedly while parsing";
4952

5053
private static final Logger LOGGER = LoggerFactory.getLogger(TokenValidatorGlobalPreFilter.class);
@@ -103,33 +106,56 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
103106
try {
104107
jwt = JWTParser.parse(token);
105108
jwtClaimsSet = jwt.getJWTClaimsSet();
109+
110+
LOGGER.debug("checking issuer");
111+
if (allowedIssuers.stream().noneMatch(iss -> jwtClaimsSet.getIssuer().startsWith(iss))) {
112+
LOGGER.info("{}: 401 Unauthorized, {} Issuer is not in the issuers white list", exchange.getRequest().getPath(), jwtClaimsSet.getIssuer());
113+
return completeWithCode(exchange, HttpStatus.UNAUTHORIZED);
114+
}
115+
116+
Issuer iss = new Issuer(jwtClaimsSet.getIssuer());
117+
ClientID clientID = new ClientID(jwtClaimsSet.getAudience().get(0));
118+
119+
JWSAlgorithm jwsAlg = JWSAlgorithm.parse(jwt.getHeader().getAlgorithm().getName());
120+
return proceedFilter(new FilterInfos(exchange, chain, jwt, jwtClaimsSet, iss, clientID, jwsAlg));
106121
} catch (java.text.ParseException e) {
107122
// Invalid plain JOSE object encoding
108-
LOGGER.info(UNAUTHORIZED_INVALID_PLAIN_JOSE_OBJECT_ENCODING, exchange.getRequest().getPath());
109-
return completeWithCode(exchange, HttpStatus.UNAUTHORIZED);
123+
// Don't print the full stacktrace here for less verbose logs,
124+
// we have enough context with just the message
125+
LOGGER.debug("JWTParser.parse ParseException, will attempt to use as opaque token: ({})", e.getMessage());
126+
// TODO try more than just the first issuer here ? get the issuer from the client ?
127+
return validateOpaqueReferenceToken(allowedIssuers.get(0), token, exchange, chain);
110128
}
129+
}
111130

112-
LOGGER.debug("checking issuer");
113-
if (allowedIssuers.stream().noneMatch(iss -> jwtClaimsSet.getIssuer().startsWith(iss))) {
114-
LOGGER.info("{}: 401 Unauthorized, {} Issuer is not in the issuers white list", exchange.getRequest().getPath(), jwtClaimsSet.getIssuer());
115-
return completeWithCode(exchange, HttpStatus.UNAUTHORIZED);
116-
}
117-
118-
Issuer iss = new Issuer(jwtClaimsSet.getIssuer());
119-
ClientID clientID = new ClientID(jwtClaimsSet.getAudience().get(0));
120-
121-
JWSAlgorithm jwsAlg = JWSAlgorithm.parse(jwt.getHeader().getAlgorithm().getName());
122-
return proceedFilter(new FilterInfos(exchange, chain, jwt, jwtClaimsSet, iss, clientID, jwsAlg));
131+
private Mono<Void> validateOpaqueReferenceToken(String issBaseUri, String token, ServerWebExchange exchange,
132+
GatewayFilterChain chain) {
133+
// TODO CACHE the two requests
134+
return gatewayService.getOpaqueTokenIntrospectionUri(issBaseUri)
135+
.flatMap(uri -> gatewayService.getOpaqueTokenIntrospection(uri, token))
136+
.flatMap((TokenIntrospection tokenIntrospection) -> {
137+
// TODO really add the client_id header instead of userid
138+
exchange.getRequest().mutate()
139+
.headers(h -> h.set(HEADER_USER_ID, tokenIntrospection.getClientId()));
140+
if (tokenIntrospection.getActive()) {
141+
LOGGER.debug("Opaque Token verified, it can be trusted");
142+
return chain.filter(exchange);
143+
} else {
144+
LOGGER.info(UNAUTHORIZED_INVALID_PLAIN_JOSE_OBJECT_ENCODING, exchange.getRequest().getPath());
145+
return completeWithCode(exchange, HttpStatus.UNAUTHORIZED);
146+
}
147+
});
123148
}
124149

125150
private Mono<Void> validate(FilterInfos filterInfos, JWKSet jwkset) throws BadJOSEException, JOSEException {
126151

127152
// Create validator for signed ID tokens
153+
// this works with jwt access tokens too (by chance ?) Do we need to modify this ?
128154
IDTokenValidator validator = new IDTokenValidator(filterInfos.getIss(), filterInfos.getClientID(), filterInfos.getJwsAlg(), jwkset);
129155

130156
validator.validate(filterInfos.getJwt(), null);
131157
// we can safely trust the JWT
132-
LOGGER.debug("Token verified, it can be trusted");
158+
LOGGER.debug("JWT Token verified, it can be trusted");
133159

134160
//we add the subject header
135161
filterInfos.getExchange().getRequest()

src/main/java/org/gridsuite/gateway/filters/UserAdminControlGlobalPreFilter.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@
1818
import org.springframework.web.server.ServerWebExchange;
1919
import reactor.core.publisher.Mono;
2020

21-
import java.util.Objects;
21+
import java.util.List;
2222

2323
import static org.gridsuite.gateway.GatewayConfig.HEADER_USER_ID;
24+
import static org.gridsuite.gateway.GatewayConfig.HEADER_CLIENT_ID;
2425

2526
/**
2627
* @author Etienne Homer <etienne.homer at rte-france.com>
@@ -40,9 +41,22 @@ public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull GatewayFi
4041
LOGGER.debug("Filter : {}", getClass().getSimpleName());
4142

4243
HttpHeaders httpHeaders = exchange.getRequest().getHeaders();
43-
String sub = Objects.requireNonNull(httpHeaders.get(HEADER_USER_ID)).get(0);
44+
List<String> maybeSubList = httpHeaders.get(HEADER_USER_ID);
45+
List<String> maybeClientIdList = httpHeaders.get(HEADER_CLIENT_ID);
4446

45-
return userAdminService.userExists(sub).flatMap(userExist -> Boolean.TRUE.equals(userExist) ? chain.filter(exchange) : completeWithCode(exchange, HttpStatus.FORBIDDEN));
47+
if (maybeSubList != null) {
48+
String sub = maybeSubList.get(0);
49+
return userAdminService.userExists(sub).flatMap(userExist -> Boolean.TRUE.equals(userExist) ? chain.filter(exchange) : completeWithCode(exchange, HttpStatus.FORBIDDEN));
50+
}
51+
52+
if (maybeClientIdList != null) {
53+
// String clientId = maybeClientId.get(0);
54+
// TODO do something with clientId
55+
return chain.filter(exchange);
56+
}
57+
58+
// no sub or no clientid, can't control access
59+
return completeWithCode(exchange, HttpStatus.UNAUTHORIZED);
4660
}
4761

4862
@Override

src/test/java/org/gridsuite/gateway/TokenValidationTest.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"gridsuite.services.network-modification-server.base-uri=http://localhost:${wiremock.server.port}",
7171
"gridsuite.services.user-admin-server.base-uri=http://localhost:${wiremock.server.port}",
7272
"gridsuite.services.sensitivity-analysis-server.base-uri=http://localhost:${wiremock.server.port}",
73+
"allowed-issuers=http://localhost:${wiremock.server.port}"
7374
})
7475

7576
@AutoConfigureWireMock(port = 0)
@@ -260,6 +261,12 @@ public void gatewayTest() {
260261
.withHeader("Content-Type", "application/json")
261262
.withBody("{\"id\": \"report1\", \"reports\" :[{\"date\":\"2001:01:01T11:11\", \"report\": \"Lets Rock\" }]}")));
262263

264+
testToken(elementUuid, token);
265+
//TODO are all requests supposed to work with reference tokens from clients ?
266+
testToken(elementUuid, "clientopaquetoken");
267+
}
268+
269+
private void testToken(UUID elementUuid, String token) {
263270
webClient
264271
.get().uri("case/v1/cases")
265272
.header("Authorization", "Bearer " + token)
@@ -388,12 +395,21 @@ private void initStubForJwk() {
388395
stubFor(get(urlEqualTo("/.well-known/openid-configuration"))
389396
.willReturn(aResponse()
390397
.withHeader("Content-Type", "application/json")
391-
.withBody("{\"jwks_uri\": \"http://localhost:" + port + "/jwks\"}")));
398+
.withBody("{"
399+
+ "\"jwks_uri\": \"http://localhost:" + port + "/jwks\","
400+
+ "\"introspection_endpoint\": \"http://localhost:" + port + "/introspection\""
401+
+ "}")));
392402

393403
stubFor(get(urlEqualTo("/jwks"))
394404
.willReturn(aResponse()
395405
.withHeader("Content-Type", "application/json")
396406
.withBody("{\"keys\" : [ " + rsaKey.toJSONString() + " ] }")));
407+
408+
stubFor(post(urlEqualTo("/introspection"))
409+
.withRequestBody(equalTo("client_id=gridsuite&client_secret=secret&token=clientopaquetoken"))
410+
.willReturn(aResponse()
411+
.withHeader("Content-Type", "application/json")
412+
.withBody("{\"active\":true,\"token_type\":\"Bearer\",\"exp\":2673442276,\"client_id\":\"chmits\"}")));
397413
}
398414

399415
@Test
@@ -409,7 +425,10 @@ public void testJwksUpdate() {
409425
stubFor(get(urlEqualTo("/.well-known/openid-configuration"))
410426
.willReturn(aResponse()
411427
.withHeader("Content-Type", "application/json")
412-
.withBody("{\"jwks_uri\": \"http://localhost:" + port + "/jwks\"}")));
428+
.withBody("{"
429+
+ "\"jwks_uri\": \"http://localhost:" + port + "/jwks\","
430+
+ "\"introspection_endpoint\": \"http://localhost:" + port + "/introspection\""
431+
+ "}")));
413432

414433
UUID stubId = UUID.randomUUID();
415434

@@ -474,7 +493,15 @@ public void invalidToken() {
474493
stubFor(get(urlEqualTo("/.well-known/openid-configuration"))
475494
.willReturn(aResponse()
476495
.withHeader("Content-Type", "application/json")
477-
.withBody("{\"jwks_uri\": \"http://localhost:" + port + "/jwks\"}")));
496+
.withBody("{"
497+
+ "\"jwks_uri\": \"http://localhost:" + port + "/jwks\","
498+
+ "\"introspection_endpoint\": \"http://localhost:" + port + "/introspection\""
499+
+ "}")));
500+
501+
stubFor(post(urlEqualTo("/introspection"))
502+
.willReturn(aResponse()
503+
.withHeader("Content-Type", "application/json")
504+
.withBody("{\"active\":false}")));
478505

479506
stubFor(get(urlEqualTo("/jwks"))
480507
.willReturn(aResponse()
@@ -518,7 +545,7 @@ public void invalidToken() {
518545
.exchange()
519546
.expectStatus().isEqualTo(401);
520547

521-
//test with non JSON token
548+
// test with non JSON token, non valid reference token
522549
webClient
523550
.get().uri("case/v1/cases")
524551
.header("Authorization", "Bearer " + "NonValidToken")
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
allowed-issuers: http://localhost
1+
allowed-issuers: http://localhost
2+
client_id: gridsuite
3+
client_secret: secret

0 commit comments

Comments
 (0)