Skip to content

Commit e789088

Browse files
committed
Support OAuth2 token introspection
fixes #157 Signed-off-by: Nageswara Rao Maridu <maridu.nageswararao@gmail.com>
1 parent c1609f7 commit e789088

File tree

7 files changed

+117
-2
lines changed

7 files changed

+117
-2
lines changed

mock/src/main/java/com/tngtech/keycloakmock/impl/UrlConfiguration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ public class UrlConfiguration {
1616
private static final String OPEN_ID_JWKS_PATH = "certs";
1717
private static final String OPEN_ID_AUTHORIZATION_PATH = "auth";
1818
private static final String OPEN_ID_END_SESSION_PATH = "logout";
19+
20+
private static final String TOKEN_INTROSPECTION_PATH = "/introspect";
21+
1922
@Nonnull private final Protocol protocol;
2023
private final int port;
2124
@Nonnull private final String hostname;
@@ -132,4 +135,9 @@ public String getHostname() {
132135
public String getRealm() {
133136
return realm;
134137
}
138+
139+
@Nonnull
140+
public URI getTokenIntrospectionEndPoint() {
141+
return getOpenIdPath(OPEN_ID_TOKEN_PATH + TOKEN_INTROSPECTION_PATH);
142+
}
135143
}

mock/src/main/java/com/tngtech/keycloakmock/impl/dagger/ServerModule.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.tngtech.keycloakmock.impl.handler.OutOfBandLoginRoute;
1515
import com.tngtech.keycloakmock.impl.handler.RequestUrlConfigurationHandler;
1616
import com.tngtech.keycloakmock.impl.handler.ResourceFileHandler;
17+
import com.tngtech.keycloakmock.impl.handler.TokenIntrospectionRoute;
1718
import com.tngtech.keycloakmock.impl.handler.TokenRoute;
1819
import com.tngtech.keycloakmock.impl.handler.WellKnownRoute;
1920
import dagger.Lazy;
@@ -148,7 +149,8 @@ Router provideRouter(
148149
@Nonnull LogoutRoute logoutRoute,
149150
@Nonnull DelegationRoute delegationRoute,
150151
@Nonnull OutOfBandLoginRoute outOfBandLoginRoute,
151-
@Nonnull @Named("keycloakJs") ResourceFileHandler keycloakJsRoute) {
152+
@Nonnull @Named("keycloakJs") ResourceFileHandler keycloakJsRoute,
153+
@Nonnull TokenIntrospectionRoute tokenIntrospectionRoute) {
152154
UrlConfiguration routing = defaultConfiguration.forRequestContext(null, ":realm");
153155
Router router = Router.router(vertx);
154156
router
@@ -180,6 +182,10 @@ Router provideRouter(
180182
router.get(routing.getOpenIdPath("delegated").getPath()).handler(delegationRoute);
181183
router.get(routing.getOutOfBandLoginLoginEndpoint().getPath()).handler(outOfBandLoginRoute);
182184
router.route("/auth/js/keycloak.js").handler(keycloakJsRoute);
185+
router
186+
.post(routing.getTokenIntrospectionEndPoint().getPath())
187+
.handler(BodyHandler.create())
188+
.handler(tokenIntrospectionRoute);
183189
return router;
184190
}
185191

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.tngtech.keycloakmock.impl.handler;
2+
3+
import com.tngtech.keycloakmock.impl.helper.TokenHelper;
4+
import io.jsonwebtoken.Claims;
5+
import io.netty.handler.codec.http.HttpResponseStatus;
6+
import io.vertx.core.Handler;
7+
import io.vertx.core.json.Json;
8+
import io.vertx.ext.web.RoutingContext;
9+
import java.time.Instant;
10+
import java.util.HashMap;
11+
import java.util.Map;
12+
import javax.annotation.Nonnull;
13+
import javax.inject.Inject;
14+
import javax.inject.Singleton;
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
18+
@Singleton
19+
public class TokenIntrospectionRoute implements Handler<RoutingContext> {
20+
21+
private static final Logger LOG = LoggerFactory.getLogger(TokenIntrospectionRoute.class);
22+
23+
private static final String TOKEN_PREFIX = "token=";
24+
private static final String ACTIVE_CLAIM = "active";
25+
26+
@Nonnull private final TokenHelper tokenHelper;
27+
28+
@Inject
29+
public TokenIntrospectionRoute(@Nonnull TokenHelper tokenHelper) {
30+
this.tokenHelper = tokenHelper;
31+
}
32+
33+
@Override
34+
public void handle(RoutingContext routingContext) {
35+
LOG.info(
36+
"Inside TokenIntrospectionRoute. Request body is : {}", routingContext.body().asString());
37+
38+
String body = routingContext.body().asString();
39+
40+
if (!body.startsWith(TOKEN_PREFIX)) {
41+
routingContext
42+
.response()
43+
.putHeader("content-type", "application/json")
44+
.setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
45+
.end("Invalid request body");
46+
}
47+
48+
String token = body.replaceFirst("^" + TOKEN_PREFIX, "");
49+
50+
LOG.debug("Received a request to introspect token : {}", token);
51+
52+
Map<String, Object> claims;
53+
try {
54+
claims = tokenHelper.parseToken(token);
55+
} catch (Exception e) {
56+
// If the token is invalid, initialize an empty claims map
57+
claims = new HashMap<>();
58+
}
59+
60+
// To support various use cases, we are returning the same claims as the input token
61+
Map<String, Object> responseClaims = new HashMap<>(claims);
62+
63+
if (responseClaims.get(Claims.EXPIRATION) != null
64+
&& isExpiryTimeInFuture(responseClaims.get(Claims.EXPIRATION).toString())) {
65+
LOG.debug("Introspected token is valid");
66+
responseClaims.put(ACTIVE_CLAIM, true);
67+
} else {
68+
LOG.debug("Introspected token is invalid");
69+
responseClaims.put(ACTIVE_CLAIM, false);
70+
}
71+
72+
routingContext
73+
.response()
74+
.putHeader("content-type", "application/json")
75+
.end(Json.encode(responseClaims));
76+
}
77+
78+
private boolean isExpiryTimeInFuture(String expiryTime) {
79+
long currentTimeInSec = Instant.now().getEpochSecond();
80+
long expiryTimeInSec = Long.parseLong(expiryTime);
81+
return currentTimeInSec < expiryTimeInSec;
82+
}
83+
}

mock/src/main/java/com/tngtech/keycloakmock/impl/handler/WellKnownRoute.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ private JsonObject getConfiguration(@Nonnull UrlConfiguration requestConfigurati
4444
.put("subject_types_supported", new JsonArray(Collections.singletonList("public")))
4545
.put(
4646
"id_token_signing_alg_values_supported",
47-
new JsonArray(Collections.singletonList("RS256")));
47+
new JsonArray(Collections.singletonList("RS256")))
48+
.put("introspection_endpoint", requestConfiguration.getTokenIntrospectionEndPoint());
4849
return result;
4950
}
5051
}

mock/src/test/java/com/tngtech/keycloakmock/impl/UrlConfigurationTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ void urls_are_correct() {
156156
.hasToString("http://localhost:8000/auth/realms/master/protocol/openid-connect/certs");
157157
assertThat(urlConfiguration.getTokenEndpoint())
158158
.hasToString("http://localhost:8000/auth/realms/master/protocol/openid-connect/token");
159+
assertThat(urlConfiguration.getTokenIntrospectionEndPoint())
160+
.hasToString(
161+
"http://localhost:8000/auth/realms/master/protocol/openid-connect/token/introspect");
159162
}
160163

161164
@Test
@@ -174,6 +177,9 @@ void urls_are_correct_with_no_context_path() {
174177
.hasToString("http://localhost:8000/realms/master/protocol/openid-connect/certs");
175178
assertThat(urlConfiguration.getTokenEndpoint())
176179
.hasToString("http://localhost:8000/realms/master/protocol/openid-connect/token");
180+
assertThat(urlConfiguration.getTokenIntrospectionEndPoint())
181+
.hasToString(
182+
"http://localhost:8000/realms/master/protocol/openid-connect/token/introspect");
177183
}
178184

179185
@Test
@@ -198,6 +204,10 @@ void urls_are_correct_with_custom_context_path() {
198204
assertThat(urlConfiguration.getTokenEndpoint())
199205
.hasToString(
200206
"http://localhost:8000/custom/context/path/realms/master/protocol/openid-connect/token");
207+
208+
assertThat(urlConfiguration.getTokenIntrospectionEndPoint())
209+
.hasToString(
210+
"http://localhost:8000/custom/context/path/realms/master/protocol/openid-connect/token/introspect");
201211
}
202212

203213
@Test

mock/src/test/java/com/tngtech/keycloakmock/impl/handler/WellKnownRouteTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class WellKnownRouteTest extends HandlerTestBase {
2424
private static final String END_SESSION_ENDPOINT = "endSessionEndpoint";
2525
private static final String JWKS_URI = "jwksUri";
2626
private static final String TOKEN_ENDPOINT = "tokenEndpoint";
27+
private static final String TOKEN_INTROSPECTION_ENDPOINT = "tokenIntrospectionEndpoint";
2728

2829
@Mock private UrlConfiguration urlConfiguration;
2930

@@ -37,6 +38,9 @@ void well_known_configuration_is_complete() throws URISyntaxException {
3738
doReturn(new URI(END_SESSION_ENDPOINT)).when(urlConfiguration).getEndSessionEndpoint();
3839
doReturn(new URI(JWKS_URI)).when(urlConfiguration).getJwksUri();
3940
doReturn(new URI(TOKEN_ENDPOINT)).when(urlConfiguration).getTokenEndpoint();
41+
doReturn(new URI(TOKEN_INTROSPECTION_ENDPOINT))
42+
.when(urlConfiguration)
43+
.getTokenIntrospectionEndPoint();
4044

4145
wellKnownRoute.handle(routingContext);
4246

@@ -57,6 +61,7 @@ private ConfigurationResponse getExpectedResponse() {
5761
Arrays.asList("code", "code id_token", "id_token", "token id_token");
5862
response.subject_types_supported = Collections.singletonList("public");
5963
response.id_token_signing_alg_values_supported = Collections.singletonList("RS256");
64+
response.introspection_endpoint = TOKEN_INTROSPECTION_ENDPOINT;
6065
return response;
6166
}
6267
}

mock/src/test/java/com/tngtech/keycloakmock/test/ConfigurationResponse.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ public class ConfigurationResponse {
1111
public List<String> subject_types_supported;
1212
public List<String> id_token_signing_alg_values_supported;
1313
public String end_session_endpoint;
14+
15+
public String introspection_endpoint;
1416
}

0 commit comments

Comments
 (0)