Skip to content

Commit

Permalink
Support OAuth2 token introspection
Browse files Browse the repository at this point in the history
fixes #157

Signed-off-by: Nageswara Rao Maridu <[email protected]
  • Loading branch information
nrmaridu committed Jan 21, 2024
1 parent c1609f7 commit 681d4c6
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public class UrlConfiguration {
private static final String OPEN_ID_JWKS_PATH = "certs";
private static final String OPEN_ID_AUTHORIZATION_PATH = "auth";
private static final String OPEN_ID_END_SESSION_PATH = "logout";

private static final String TOKEN_INTROSPECTION_PATH = "/introspect";

@Nonnull private final Protocol protocol;
private final int port;
@Nonnull private final String hostname;
Expand Down Expand Up @@ -132,4 +135,9 @@ public String getHostname() {
public String getRealm() {
return realm;
}

@Nonnull
public URI getTokenIntrospectionEndPoint() {
return getOpenIdPath(OPEN_ID_TOKEN_PATH + TOKEN_INTROSPECTION_PATH);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.tngtech.keycloakmock.impl.handler.OutOfBandLoginRoute;
import com.tngtech.keycloakmock.impl.handler.RequestUrlConfigurationHandler;
import com.tngtech.keycloakmock.impl.handler.ResourceFileHandler;
import com.tngtech.keycloakmock.impl.handler.TokenIntrospectionRoute;
import com.tngtech.keycloakmock.impl.handler.TokenRoute;
import com.tngtech.keycloakmock.impl.handler.WellKnownRoute;
import dagger.Lazy;
Expand Down Expand Up @@ -148,7 +149,8 @@ Router provideRouter(
@Nonnull LogoutRoute logoutRoute,
@Nonnull DelegationRoute delegationRoute,
@Nonnull OutOfBandLoginRoute outOfBandLoginRoute,
@Nonnull @Named("keycloakJs") ResourceFileHandler keycloakJsRoute) {
@Nonnull @Named("keycloakJs") ResourceFileHandler keycloakJsRoute,
@Nonnull TokenIntrospectionRoute tokenIntrospectionRoute) {
UrlConfiguration routing = defaultConfiguration.forRequestContext(null, ":realm");
Router router = Router.router(vertx);
router
Expand Down Expand Up @@ -180,6 +182,10 @@ Router provideRouter(
router.get(routing.getOpenIdPath("delegated").getPath()).handler(delegationRoute);
router.get(routing.getOutOfBandLoginLoginEndpoint().getPath()).handler(outOfBandLoginRoute);
router.route("/auth/js/keycloak.js").handler(keycloakJsRoute);
router
.post(routing.getTokenIntrospectionEndPoint().getPath())
.handler(BodyHandler.create())
.handler(tokenIntrospectionRoute);
return router;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.tngtech.keycloakmock.impl.handler;

import com.tngtech.keycloakmock.impl.helper.TokenHelper;
import io.jsonwebtoken.Claims;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.Handler;
import io.vertx.core.json.Json;
import io.vertx.ext.web.RoutingContext;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public class TokenIntrospectionRoute implements Handler<RoutingContext> {

private static final Logger LOG = LoggerFactory.getLogger(TokenIntrospectionRoute.class);

private static final String TOKEN_PREFIX = "token=";
private static final String ACTIVE_CLAIM = "active";

@Nonnull private final TokenHelper tokenHelper;

@Inject
public TokenIntrospectionRoute(@Nonnull TokenHelper tokenHelper) {
this.tokenHelper = tokenHelper;
}

@Override
public void handle(RoutingContext routingContext) {
LOG.info(
"Inside TokenIntrospectionRoute. Request body is : {}", routingContext.body().asString());

String body = routingContext.body().asString();

if (!body.startsWith(TOKEN_PREFIX)) {
routingContext
.response()
.putHeader("content-type", "application/json")
.setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
.end("Invalid request body");
}

String token = body.replaceFirst("^" + TOKEN_PREFIX, "");

LOG.debug("Received a request to introspect token : {}", token);

Map<String, Object> claims;
try {
claims = tokenHelper.parseToken(token);
} catch (Exception e) {
// If the token is invalid, initialize an empty claims map
claims = new HashMap<>();
}

// To support various use cases, we are returning the same claims as the input token
Map<String, Object> responseClaims = new HashMap<>(claims);

if (responseClaims.get(Claims.EXPIRATION) != null
&& isExpiryTimeInFuture(responseClaims.get(Claims.EXPIRATION).toString())) {
LOG.debug("Introspected token is valid");
responseClaims.put(ACTIVE_CLAIM, true);
} else {
LOG.debug("Introspected token is invalid");
responseClaims.put(ACTIVE_CLAIM, false);
}

routingContext
.response()
.putHeader("content-type", "application/json")
.end(Json.encode(responseClaims));
}

private boolean isExpiryTimeInFuture(String expiryTime) {
long currentTimeInSec = Instant.now().getEpochSecond();
long expiryTimeInSec = Long.parseLong(expiryTime);
return currentTimeInSec < expiryTimeInSec;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ private JsonObject getConfiguration(@Nonnull UrlConfiguration requestConfigurati
.put("subject_types_supported", new JsonArray(Collections.singletonList("public")))
.put(
"id_token_signing_alg_values_supported",
new JsonArray(Collections.singletonList("RS256")));
new JsonArray(Collections.singletonList("RS256")))
.put("introspection_endpoint", requestConfiguration.getTokenIntrospectionEndPoint());
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ void urls_are_correct() {
.hasToString("http://localhost:8000/auth/realms/master/protocol/openid-connect/certs");
assertThat(urlConfiguration.getTokenEndpoint())
.hasToString("http://localhost:8000/auth/realms/master/protocol/openid-connect/token");
assertThat(urlConfiguration.getTokenIntrospectionEndPoint())
.hasToString(
"http://localhost:8000/auth/realms/master/protocol/openid-connect/token/introspect");
}

@Test
Expand All @@ -174,6 +177,9 @@ void urls_are_correct_with_no_context_path() {
.hasToString("http://localhost:8000/realms/master/protocol/openid-connect/certs");
assertThat(urlConfiguration.getTokenEndpoint())
.hasToString("http://localhost:8000/realms/master/protocol/openid-connect/token");
assertThat(urlConfiguration.getTokenIntrospectionEndPoint())
.hasToString(
"http://localhost:8000/realms/master/protocol/openid-connect/token/introspect");
}

@Test
Expand All @@ -198,6 +204,10 @@ void urls_are_correct_with_custom_context_path() {
assertThat(urlConfiguration.getTokenEndpoint())
.hasToString(
"http://localhost:8000/custom/context/path/realms/master/protocol/openid-connect/token");

assertThat(urlConfiguration.getTokenIntrospectionEndPoint())
.hasToString(
"http://localhost:8000/custom/context/path/realms/master/protocol/openid-connect/token/introspect");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class WellKnownRouteTest extends HandlerTestBase {
private static final String END_SESSION_ENDPOINT = "endSessionEndpoint";
private static final String JWKS_URI = "jwksUri";
private static final String TOKEN_ENDPOINT = "tokenEndpoint";
private static final String TOKEN_INTROSPECTION_ENDPOINT = "tokenIntrospectionEndpoint";

@Mock private UrlConfiguration urlConfiguration;

Expand All @@ -37,6 +38,9 @@ void well_known_configuration_is_complete() throws URISyntaxException {
doReturn(new URI(END_SESSION_ENDPOINT)).when(urlConfiguration).getEndSessionEndpoint();
doReturn(new URI(JWKS_URI)).when(urlConfiguration).getJwksUri();
doReturn(new URI(TOKEN_ENDPOINT)).when(urlConfiguration).getTokenEndpoint();
doReturn(new URI(TOKEN_INTROSPECTION_ENDPOINT))
.when(urlConfiguration)
.getTokenIntrospectionEndPoint();

wellKnownRoute.handle(routingContext);

Expand All @@ -57,6 +61,7 @@ private ConfigurationResponse getExpectedResponse() {
Arrays.asList("code", "code id_token", "id_token", "token id_token");
response.subject_types_supported = Collections.singletonList("public");
response.id_token_signing_alg_values_supported = Collections.singletonList("RS256");
response.introspection_endpoint = TOKEN_INTROSPECTION_ENDPOINT;
return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public class ConfigurationResponse {
public List<String> subject_types_supported;
public List<String> id_token_signing_alg_values_supported;
public String end_session_endpoint;

public String introspection_endpoint;
}

0 comments on commit 681d4c6

Please sign in to comment.