Skip to content

Commit

Permalink
Apply the required claims restriction to OIDC introspections
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Oct 29, 2024
1 parent 4b6211c commit c2587b8
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,27 @@ public TokenIntrospection apply(TokenIntrospection introspectionResult, Throwabl
throw new AuthenticationFailedException(ex);
}

if (requiredClaims != null && !requiredClaims.isEmpty()) {
for (Map.Entry<String, String> requiredClaim : requiredClaims.entrySet()) {
String introspectionClaimValue = null;
try {
introspectionClaimValue = introspectionResult.getString(requiredClaim.getKey());
} catch (ClassCastException ex) {
LOG.debugf("Introspection claim %s is not String", requiredClaim.getKey());
throw new AuthenticationFailedException();
}
if (introspectionClaimValue == null) {
LOG.debugf("Introspection claim %s is missing", requiredClaim.getKey());
throw new AuthenticationFailedException();
}
if (!introspectionClaimValue.equals(requiredClaim.getValue())) {
LOG.debugf("Value of the introspection claim %s does not match required value of %s",
requiredClaim.getKey(), requiredClaim.getValue());
throw new AuthenticationFailedException();
}
}
}

return introspectionResult;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.it.keycloak;

import java.time.Duration;
import java.util.Map;
import java.util.function.Supplier;

import jakarta.enterprise.context.ApplicationScoped;
Expand Down Expand Up @@ -35,6 +36,7 @@ public OidcTenantConfig get() {

String path = context.request().path();
String tenantId = path.split("/")[2];

if ("tenant-d".equals(tenantId)) {
OidcTenantConfig config = new OidcTenantConfig();
config.setTenantId("tenant-c");
Expand Down Expand Up @@ -70,7 +72,20 @@ public OidcTenantConfig get() {
config.setJwksPath("jwks");
// try the absolute URI
config.setIntrospectionPath(authServerUri + "/introspect");
return config;
} else if ("tenant-introspection-required-claims".equals(tenantId)) {

OidcTenantConfig config = new OidcTenantConfig();
config.setTenantId("tenant-introspection-required-claims");
config.token.setRequiredClaims(Map.of("required_claim", "1"));
String uri = context.request().absoluteURI();
String authServerUri = uri.replace("/tenant-introspection/tenant-introspection-required-claims",
"/oidc");
config.setAuthServerUrl(authServerUri);
config.setDiscoveryEnabled(false);
config.setClientId("client");
config.setIntrospectionPath(authServerUri + "/introspect");
config.setAllowTokenIntrospectionCache(false);
return config;
} else if ("tenant-oidc-no-discovery".equals(tenantId)) {
OidcTenantConfig config = new OidcTenantConfig();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class OidcResource {
private volatile boolean rotate;
private volatile int jwkEndpointCallCount;
private volatile int introspectionEndpointCallCount;
private volatile int opaqueToken2UsageCount;
private volatile int revokeEndpointCallCount;
private volatile int userInfoEndpointCallCount;
private volatile boolean enableDiscovery = true;
Expand Down Expand Up @@ -112,6 +113,13 @@ public int resetIntrospectionEndpointCallCount() {
return introspectionEndpointCallCount;
}

@POST
@Path("opaque-token-call-count")
public int resetOpaqueTokenCallCount() {
opaqueToken2UsageCount = 0;
return opaqueToken2UsageCount;
}

@POST
@Produces("application/json")
@Path("introspect")
Expand All @@ -120,7 +128,12 @@ public String introspect(@FormParam("client_id") String clientId, @FormParam("cl
introspectionEndpointCallCount++;

boolean activeStatus = introspection && !token.endsWith("-invalid");

boolean requiredClaim = true;
if (token.endsWith("_2") && ++opaqueToken2UsageCount == 2) {
// This is to confirm that the same opaque token_2 works well when its introspection response
// includes `required_claim` with value "1" but fails when the required claim is not included
requiredClaim = false;
}
String introspectionClientId = "none";
String introspectionClientSecret = "none";
if (clientSecret != null) {
Expand All @@ -146,6 +159,7 @@ public String introspect(@FormParam("client_id") String clientId, @FormParam("cl
" \"scope\": \"user\"," +
" \"email\": \"[email protected]\"," +
" \"username\": \"alice\"," +
(requiredClaim ? "\"required_claim\": \"1\"," : "") +
" \"introspection_client_id\": \"" + introspectionClientId + "\"," +
" \"introspection_client_secret\": \"" + introspectionClientSecret + "\"," +
" \"client_id\": \"" + clientId + "\"" +
Expand Down Expand Up @@ -251,13 +265,23 @@ public String testAccessTokenWithEmptyScope(@QueryParam("kid") String kid, @Quer
@POST
@Path("opaque-token")
@Produces("application/json")
public String testOpaqueToken(@QueryParam("kid") String kid) {
public String testOpaqueToken() {
return "{\"access_token\": \"987654321\"," +
" \"token_type\": \"Bearer\"," +
" \"refresh_token\": \"123456789\"," +
" \"expires_in\": 300 }";
}

@POST
@Path("opaque-token2")
@Produces("application/json")
public String testOpaqueToken2() {
return "{\"access_token\": \"987654321_2\"," +
" \"token_type\": \"Bearer\"," +
" \"refresh_token\": \"123456789\"," +
" \"expires_in\": 300 }";
}

@POST
@Path("enable-introspection")
public boolean setIntrospection() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkus.it.keycloak;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.security.Authenticated;

@Path("/tenant-introspection")
@Authenticated
public class TenantIntrospectionRequiredClaimsResource {

@Inject
TokenIntrospection token;

@GET
@Path("tenant-introspection-required-claims")
public String userPermission() {
return token.getUsername() + ", required_claim:" + token.getString("required_claim");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,28 @@ public void testOpaqueTokenScopePermission() {
.statusCode(403);
}

@Test
public void testTokenIntrospectionRequiredClaims() {
RestAssured.when().post("/oidc/enable-introspection").then().body(equalTo("true"));
RestAssured.when().post("/oidc/opaque-token-call-count").then().body(equalTo("0"));

// Successful request with opaque token 2
String opaqueToken2 = getOpaqueAccessToken2FromSimpleOidc();
RestAssured.given().auth().oauth2(opaqueToken2)
.when().get("/tenant-introspection/tenant-introspection-required-claims")
.then()
.statusCode(200)
.body(equalTo("alice, required_claim:1"));

// Expected to fail now because its introspection does not include the expected required claim
RestAssured.given().auth().oauth2(opaqueToken2)
.when().get("/tenant-introspection/tenant-introspection-required-claims")
.then()
.statusCode(401);

RestAssured.when().post("/oidc/opaque-token-call-count").then().body(equalTo("0"));
}

@Test
public void testResolveStaticTenantsByPathPatterns() {
// default tenant path pattern is more specific, therefore it wins over tenant-b pattern that is also matched
Expand Down Expand Up @@ -900,6 +922,15 @@ private String getOpaqueAccessTokenFromSimpleOidc() {
return object.getString("access_token");
}

private String getOpaqueAccessToken2FromSimpleOidc() {
String json = RestAssured
.when()
.post("/oidc/opaque-token2")
.body().asString();
JsonObject object = new JsonObject(json);
return object.getString("access_token");
}

static WebClient createWebClient() {
WebClient webClient = new WebClient();
webClient.setCssErrorHandler(new SilentCssErrorHandler());
Expand Down

0 comments on commit c2587b8

Please sign in to comment.