diff --git a/docs/src/main/asciidoc/security-openid-connect.adoc b/docs/src/main/asciidoc/security-openid-connect.adoc index 64f4088865f86..b1e4c54ea5d5a 100644 --- a/docs/src/main/asciidoc/security-openid-connect.adoc +++ b/docs/src/main/asciidoc/security-openid-connect.adoc @@ -336,6 +336,18 @@ Set `quarkus.oidc.user-info-required=true` if a UserInfo JSON object from the OI A request will be sent to the OpenId Provider UserInfo endpoint and an `io.quarkus.oidc.UserInfo` (a simple `javax.json.JsonObject` wrapper) object will be created. `io.quarkus.oidc.UserInfo` can be either injected or accessed as a SecurityIdentity `userinfo` attribute. +[[token-introspection]] +== Token Introspection + +An opaque token has to be introspected by sending it to the OpenId Provider token introspection endpoint. + +If the opaque token is active then a token introspection `username` and `scope` properties will be used to build a `Securityidentity`. Additionally, an `io.quarkus.oidc.TokenIntrospection` (a simple `javax.json.JsonObject` wrapper) object will be created and can be either injected or accessed as a SecurityIdentity `introspection` attribute. + +Signed JWT tokens can also be introspected when no local matching `JsonWebKey` is available. + +If you only work with JWT tokens then it is recommended to disable the opaque token introspection with `quarkus.oidc.token.allow-opaque-token-introspection=false`. +Additionally, disabling the introspection of signed JWT tokens is also advised with `quarkus.oidc.token.allow-jwt-introspection=false` if you expect that a local `JsonWebKey` will always be available since a 7`JsonWebKeySet` containing the public verification keys is periodically refreshed when the token has no matching `JsonWebKey`. + [[config-metadata]] == Configuration Metadata @@ -695,6 +707,7 @@ import io.quarkus.test.security.TestSecurity; import io.quarkus.test.security.oidc.Claim; import io.quarkus.test.security.oidc.ConfigMetadata; import io.quarkus.test.security.oidc.OidcSecurity; +import io.quarkus.test.security.oidc.OidcConfigurationMetadata; import io.quarkus.test.security.oidc.UserInfo; import io.restassured.RestAssured; @@ -730,7 +743,11 @@ where `ProtectedResource` class may look like this: [source, java] ---- -@Path("/web-app") +import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.oidc.UserInfo; +import org.eclipse.microprofile.jwt.JsonWebToken; + +@Path("/service") @Authenticated public class ProtectedResource { @@ -762,6 +779,67 @@ Note that `@TestSecurity` annotation must always be used and its `user` property `@OidcSecurity` annotation is optional and can be used to set the additional token claims, as well as `UserInfo` and `OidcConfigurationMetadata` properties. Additionally, if `quarkus.oidc.token.issuer` property is configured then it will be used as an `OidcConfigurationMetadata` `issuer` property value. +If you work with the opaque tokens then you can test them as follows: + +[source, java] +---- +import static org.hamcrest.Matchers.is; +import org.junit.jupiter.api.Test; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.oidc.OidcSecurity; +import io.quarkus.test.security.oidc.TokenIntrospection; +import io.restassured.RestAssured; + +@QuarkusTest +@TestHTTPEndpoint(ProtectedResource.class) +public class TestSecurityAuthTest { + + @Test + @TestSecurity(user = "userOidc", roles = "viewer") + @OidcSecurity(introspectionRequired = true, + introspection = { + @TokenIntrospection(key = "email", value = "user@gmail.com") + } + ) + public void testOidcWithClaimsUserInfoAndMetadata() { + RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then() + .body(is("userOidc:viewer:userOidc:viewer")); + } + +} +---- + +where `ProtectedResource` class may look like this: + +[source, java] +---- +import io.quarkus.oidc.TokenIntrospection; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/service") +@Authenticated +public class ProtectedResource { + + @Inject + SecurityIdentity securityIdentity; + @Inject + TokenIntrospection introspection; + + @GET + @Path("test-security-oidc-opaque-token") + public String testSecurityOidcOpaqueToken() { + return securityIdentity.getPrincipal().getName() + ":" + securityIdentity.getRoles().iterator().next() + + ":" + introspection.getString("username") + + ":" + introspection.getString("scope") + + ":" + introspection.getString("email"); + } +} +---- + +Note that `@TestSecurity` `user` and `roles` attributes are availabe as `TokenIntrospection` `username` and `scope` properties and you can use `io.quarkus.test.security.oidc.TokenIntrospection` to add the additional introspection response properties such as an `email`, etc. + == How to check the errors in the logs == Please enable `io.quarkus.oidc.runtime.OidcProvider` `TRACE` level logging to see more details about the token verification errors: diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java new file mode 100644 index 0000000000000..f955ab69d0071 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java @@ -0,0 +1,23 @@ +package io.quarkus.oidc; + +import javax.json.JsonObject; + +import io.quarkus.oidc.runtime.AbstractJsonObjectResponse; + +/** + * Represents a token introspection result + * + */ +public class TokenIntrospection extends AbstractJsonObjectResponse { + + public TokenIntrospection() { + } + + public TokenIntrospection(String introspectionJson) { + super(introspectionJson); + } + + public TokenIntrospection(JsonObject json) { + super(json); + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java index 5b5a851462e4d..44952cc5b6d5a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java @@ -1,62 +1,19 @@ package io.quarkus.oidc; -import java.io.StringReader; -import java.util.Collections; -import java.util.Map; -import java.util.Set; - -import javax.json.Json; -import javax.json.JsonArray; import javax.json.JsonObject; -import javax.json.JsonReader; -import javax.json.JsonValue; -public class UserInfo { +import io.quarkus.oidc.runtime.AbstractJsonObjectResponse; - private JsonObject json; +public class UserInfo extends AbstractJsonObjectResponse { public UserInfo() { } public UserInfo(String userInfoJson) { - json = toJsonObject(userInfoJson); + super(userInfoJson); } public UserInfo(JsonObject json) { - this.json = json; - } - - public String getString(String name) { - return json.getString(name); - } - - public JsonArray getArray(String name) { - return json.getJsonArray(name); - } - - public JsonObject getObject(String name) { - return json.getJsonObject(name); - } - - public Object get(String name) { - return json.get(name); - } - - public boolean contains(String propertyName) { - return json.containsKey(propertyName); - } - - public Set getPropertyNames() { - return Collections.unmodifiableSet(json.keySet()); - } - - public Set> getAllProperties() { - return Collections.unmodifiableSet(json.entrySet()); - } - - private static JsonObject toJsonObject(String userInfoJson) { - try (JsonReader jsonReader = Json.createReader(new StringReader(userInfoJson))) { - return jsonReader.readObject(); - } + super(json); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java new file mode 100644 index 0000000000000..3ae4e5b39ce98 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java @@ -0,0 +1,71 @@ +package io.quarkus.oidc.runtime; + +import java.io.StringReader; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonNumber; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.json.JsonValue; + +public class AbstractJsonObjectResponse { + private JsonObject json; + + public AbstractJsonObjectResponse() { + } + + public AbstractJsonObjectResponse(String introspectionJson) { + this(toJsonObject(introspectionJson)); + } + + public AbstractJsonObjectResponse(JsonObject json) { + this.json = json; + } + + public String getString(String name) { + return json.getString(name); + } + + public Boolean getBoolean(String name) { + return json.getBoolean(name); + } + + public Long getLong(String name) { + JsonNumber number = json.getJsonNumber(name); + return number != null ? number.longValue() : null; + } + + public JsonArray getArray(String name) { + return json.getJsonArray(name); + } + + public JsonObject getObject(String name) { + return json.getJsonObject(name); + } + + public Object get(String name) { + return json.get(name); + } + + public boolean contains(String propertyName) { + return json.containsKey(propertyName); + } + + public Set getPropertyNames() { + return Collections.unmodifiableSet(json.keySet()); + } + + public Set> getAllProperties() { + return Collections.unmodifiableSet(json.entrySet()); + } + + private static JsonObject toJsonObject(String userInfoJson) { + try (JsonReader jsonReader = Json.createReader(new StringReader(userInfoJson))) { + return jsonReader.readObject(); + } + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index 1454338a9e2e0..824bfd79d34b8 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -168,11 +168,12 @@ public Uni apply(TokenVerificationResult result, Throwable t) QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(); builder.addCredential(tokenCred); OidcUtils.setSecurityIdentityUserInfo(builder, userInfo); + OidcUtils.setSecurityIdentityIntrospecton(builder, result.introspectionResult); OidcUtils.setSecurityIdentityConfigMetadata(builder, resolvedContext); String principalMember = ""; - if (result.introspectionResult.containsKey(OidcConstants.INTROSPECTION_TOKEN_USERNAME)) { + if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_USERNAME)) { principalMember = OidcConstants.INTROSPECTION_TOKEN_USERNAME; - } else if (result.introspectionResult.containsKey(OidcConstants.INTROSPECTION_TOKEN_SUB)) { + } else if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_SUB)) { // fallback to "sub", if "username" is not present principalMember = OidcConstants.INTROSPECTION_TOKEN_SUB; } @@ -184,7 +185,7 @@ public String getName() { return userName; } }); - if (result.introspectionResult.containsKey(OidcConstants.TOKEN_SCOPE)) { + if (result.introspectionResult.contains(OidcConstants.TOKEN_SCOPE)) { for (String role : result.introspectionResult.getString(OidcConstants.TOKEN_SCOPE).split(" ")) { builder.addRole(role.trim()); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index b92382ade854c..a0300aa3c3ad3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java @@ -21,6 +21,7 @@ import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.request.TokenAuthenticationRequest; @@ -144,17 +145,17 @@ public Uni apply(Void v) { public Uni introspectToken(String token) { return client.introspectToken(token).onItemOrFailure() - .transform(new BiFunction() { + .transform(new BiFunction() { @Override - public TokenVerificationResult apply(JsonObject jsonObject, Throwable t) { + public TokenVerificationResult apply(TokenIntrospection introspectionResult, Throwable t) { if (t != null) { throw new AuthenticationFailedException(t); } - if (!Boolean.TRUE.equals(jsonObject.getBoolean(OidcConstants.INTROSPECTION_TOKEN_ACTIVE))) { + if (!Boolean.TRUE.equals(introspectionResult.getBoolean(OidcConstants.INTROSPECTION_TOKEN_ACTIVE))) { throw new AuthenticationFailedException(); } - Long exp = jsonObject.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP); + Long exp = introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP); if (exp != null) { final int lifespanGrace = client.getOidcConfig().token.lifespanGrace.isPresent() ? client.getOidcConfig().token.lifespanGrace.getAsInt() @@ -164,7 +165,7 @@ public TokenVerificationResult apply(JsonObject jsonObject, Throwable t) { } } - return new TokenVerificationResult(null, jsonObject); + return new TokenVerificationResult(null, introspectionResult); } }); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index f6746b6023be7..664d19709252f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -10,6 +10,7 @@ import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.smallrye.mutiny.Uni; @@ -58,7 +59,7 @@ public Uni getUserInfo(String token) { .send().onItem().transform(resp -> getUserInfo(resp)); } - public Uni introspectToken(String token) { + public Uni introspectToken(String token) { MultiMap introspectionParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); introspectionParams.add(OidcConstants.INTROSPECTION_TOKEN, token); introspectionParams.add(OidcConstants.INTROSPECTION_TOKEN_TYPE_HINT, OidcConstants.ACCESS_TOKEN_VALUE); @@ -127,17 +128,29 @@ private JsonObject getUserInfo(HttpResponse resp) { return getJsonObject(resp); } - private JsonObject getTokenIntrospection(HttpResponse resp) { - return getJsonObject(resp); + private TokenIntrospection getTokenIntrospection(HttpResponse resp) { + return new TokenIntrospection(getString(resp)); } - private JsonObject getJsonObject(HttpResponse resp) { + private static JsonObject getJsonObject(HttpResponse resp) { if (resp.statusCode() == 200) { return resp.bodyAsJsonObject(); } else { - String errorMessage = resp.bodyAsString(); - LOG.debugf("Request has failed: status: %d, error message: %s", resp.statusCode(), errorMessage); - throw new OIDCException(errorMessage); + throw responseException(resp); + } + } + + private static String getString(HttpResponse resp) { + if (resp.statusCode() == 200) { + return resp.bodyAsString(); + } else { + throw responseException(resp); } } + + private static OIDCException responseException(HttpResponse resp) { + String errorMessage = resp.bodyAsString(); + LOG.debugf("Request has failed: status: %d, error message: %s", resp.statusCode(), errorMessage); + throw new OIDCException(errorMessage); + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTokenCredentialProducer.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTokenCredentialProducer.java index d6bb9d611ff49..ee88ac297ab3f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTokenCredentialProducer.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTokenCredentialProducer.java @@ -9,8 +9,8 @@ import io.quarkus.arc.AlternativePriority; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.IdTokenCredential; -import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.RefreshToken; +import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; import io.quarkus.security.identity.SecurityIdentity; @@ -69,8 +69,25 @@ RefreshToken currentRefreshToken() { UserInfo currentUserInfo() { UserInfo userInfo = (UserInfo) identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE); if (userInfo == null) { - throw new OIDCException("UserInfo can not be injected"); + LOG.trace("UserInfo is null"); + userInfo = new UserInfo(); } return userInfo; } + + /** + * The producer method for the current UserInfo + * + * @return the user info + */ + @Produces + @RequestScoped + TokenIntrospection currentTokenIntrospection() { + TokenIntrospection introspection = (TokenIntrospection) identity.getAttribute(OidcUtils.INTROSPECTION_ATTRIBUTE); + if (introspection == null) { + LOG.trace("TokenIntrospection is null"); + introspection = new TokenIntrospection(); + } + return introspection; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index db3ac78f9d98e..4a566720c051a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -17,12 +17,14 @@ import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.ForbiddenException; import io.quarkus.security.credential.TokenCredential; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity.Builder; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; @@ -30,6 +32,7 @@ public final class OidcUtils { public static final String CONFIG_METADATA_ATTRIBUTE = "configuration-metadata"; public static final String USER_INFO_ATTRIBUTE = "userinfo"; + public static final String INTROSPECTION_ATTRIBUTE = "introspection"; public static final String TENANT_ID_ATTRIBUTE = "tenant-id"; /** * This pattern uses a positive lookahead to split an expression around the forward slashes @@ -189,6 +192,10 @@ public static void setSecurityIdentityUserInfo(QuarkusSecurityIdentity.Builder b } } + public static void setSecurityIdentityIntrospecton(Builder builder, TokenIntrospection introspectionResult) { + builder.addAttribute(INTROSPECTION_ATTRIBUTE, introspectionResult); + } + public static void setSecurityIdentityConfigMetadata(QuarkusSecurityIdentity.Builder builder, TenantConfigContext resolvedContext) { if (resolvedContext.provider.client != null) { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenVerificationResult.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenVerificationResult.java index 87fea597d4f3f..c73435e2714fc 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenVerificationResult.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenVerificationResult.java @@ -1,13 +1,14 @@ package io.quarkus.oidc.runtime; +import io.quarkus.oidc.TokenIntrospection; import io.vertx.core.json.JsonObject; public class TokenVerificationResult { - public TokenVerificationResult(JsonObject localVerificationResult, JsonObject introspectionResult) { + JsonObject localVerificationResult; + TokenIntrospection introspectionResult; + + public TokenVerificationResult(JsonObject localVerificationResult, TokenIntrospection introspectionResult) { this.localVerificationResult = localVerificationResult; this.introspectionResult = introspectionResult; } - - JsonObject localVerificationResult; - JsonObject introspectionResult; } diff --git a/integration-tests/oidc-tenancy/pom.xml b/integration-tests/oidc-tenancy/pom.xml index 46040878b0837..4d457da24da89 100644 --- a/integration-tests/oidc-tenancy/pom.xml +++ b/integration-tests/oidc-tenancy/pom.xml @@ -60,7 +60,11 @@ awaitility test - + + io.quarkus + quarkus-test-security-oidc + test + io.quarkus diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java index 0d4b3e0b2639c..1337c00db6e4d 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java @@ -96,6 +96,7 @@ public String introspect() { return "{" + " \"active\": " + introspection + "," + " \"scope\": \"user\"," + + " \"email\": \"user@gmail.com\"," + " \"username\": \"alice\"" + " }"; } diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantOpaqueResource.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantOpaqueResource.java index 81fba96f66ea2..f219556fd8c03 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantOpaqueResource.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantOpaqueResource.java @@ -7,6 +7,7 @@ import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.OIDCException; +import io.quarkus.oidc.TokenIntrospection; import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; @@ -19,6 +20,16 @@ public class TenantOpaqueResource { @Inject AccessTokenCredential accessToken; + @Inject + TokenIntrospection tokenIntrospection; + + @GET + @RolesAllowed("user") + @Path("tenant-oidc/api/testsecurity") + public String testSecurity() { + return "tenant-oidc-opaque:" + identity.getPrincipal().getName(); + } + @GET @RolesAllowed("user") @Path("tenant-oidc/api/user") @@ -26,7 +37,9 @@ public String userName() { if (!identity.getCredential(AccessTokenCredential.class).isOpaque()) { throw new OIDCException("Opaque token is expected"); } - return "tenant-oidc-opaque:" + identity.getPrincipal().getName(); + return "tenant-oidc-opaque:" + identity.getPrincipal().getName() + + ":" + tokenIntrospection.getString("scope") + + ":" + tokenIntrospection.getString("email"); } @GET diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index 65db6f79cf469..b797f9f56f42b 100644 --- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -297,7 +297,7 @@ public void testSimpleOidcJwtWithJwkRefresh() { .when().get("/tenant-opaque/tenant-oidc/api/user") .then() .statusCode(200) - .body(equalTo("tenant-oidc-opaque:alice")); + .body(equalTo("tenant-oidc-opaque:alice:user:user@gmail.com")); // OIDC JWK endpoint must've been called only twice, once as part of the Quarkus OIDC initialization // and once during the 1st request with a token kid '2', follow up requests must've been blocked due to the interval diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java new file mode 100644 index 0000000000000..2b91486077699 --- /dev/null +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -0,0 +1,35 @@ +package io.quarkus.it.keycloak; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.oidc.OidcSecurity; +import io.quarkus.test.security.oidc.TokenIntrospection; +import io.restassured.RestAssured; + +@QuarkusTest +@TestHTTPEndpoint(TenantOpaqueResource.class) +public class TestSecurityLazyAuthTest { + + @Test + @TestSecurity(user = "alice", roles = "user") + public void testWithDummyUser() { + RestAssured.when().get("tenant-oidc/api/testsecurity").then() + .body(is("tenant-oidc-opaque:alice")); + } + + @Test + @TestSecurity(user = "alice", roles = "user") + @OidcSecurity(introspectionRequired = true, introspection = { + @TokenIntrospection(key = "email", value = "user@gmail.com") + }) + public void testOpaqueTokenWithDummyUser() { + RestAssured.when().get("tenant-oidc/api/user").then() + .body(is("tenant-oidc-opaque:alice:user:user@gmail.com")); + } + +} diff --git a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcSecurity.java b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcSecurity.java index 1f1d4ac973312..15adc5959c36c 100644 --- a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcSecurity.java +++ b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcSecurity.java @@ -11,6 +11,10 @@ Claim[] claims() default {}; + boolean introspectionRequired() default false; + + TokenIntrospection[] introspection() default {}; + UserInfo[] userinfo() default {}; ConfigMetadata[] config() default {}; diff --git a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorProducer.java b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorProducer.java index 7295011dc1f8f..3136dabffb23a 100644 --- a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorProducer.java +++ b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorProducer.java @@ -2,6 +2,7 @@ import java.lang.annotation.Annotation; import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; @@ -19,6 +20,7 @@ import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.IdTokenCredential; import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.oidc.runtime.OidcJwtCallerPrincipal; import io.quarkus.oidc.runtime.OidcUtils; import io.quarkus.security.identity.SecurityIdentity; @@ -48,23 +50,44 @@ public SecurityIdentity augment(final SecurityIdentity identity, final Annotatio QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); final OidcSecurity oidcSecurity = findOidcSecurity(annotations); - // JsonWebToken - JwtClaims claims = new JwtClaims(); - claims.setClaim(Claims.preferred_username.name(), identity.getPrincipal().getName()); - claims.setClaim(Claims.groups.name(), identity.getRoles().stream().collect(Collectors.toList())); - if (oidcSecurity != null && oidcSecurity.claims() != null) { - for (Claim claim : oidcSecurity.claims()) { - claims.setClaim(claim.key(), claim.value()); + + final boolean introspectionRequired = oidcSecurity != null && oidcSecurity.introspectionRequired(); + + if (!introspectionRequired) { + // JsonWebToken + JwtClaims claims = new JwtClaims(); + claims.setClaim(Claims.preferred_username.name(), identity.getPrincipal().getName()); + claims.setClaim(Claims.groups.name(), identity.getRoles().stream().collect(Collectors.toList())); + if (oidcSecurity != null && oidcSecurity.claims() != null) { + for (Claim claim : oidcSecurity.claims()) { + claims.setClaim(claim.key(), claim.value()); + } + } + String jwt = generateToken(claims); + IdTokenCredential idToken = new IdTokenCredential(jwt, null); + AccessTokenCredential accessToken = new AccessTokenCredential(jwt, null); + + JsonWebToken principal = new OidcJwtCallerPrincipal(claims, idToken); + builder.setPrincipal(principal); + builder.addCredential(idToken); + builder.addCredential(accessToken); + } else { + JsonObjectBuilder introspectionBuilder = Json.createObjectBuilder(); + introspectionBuilder.add(OidcConstants.INTROSPECTION_TOKEN_ACTIVE, true); + introspectionBuilder.add(OidcConstants.INTROSPECTION_TOKEN_USERNAME, identity.getPrincipal().getName()); + introspectionBuilder.add(OidcConstants.TOKEN_SCOPE, + identity.getRoles().stream().collect(Collectors.joining(" "))); + + if (oidcSecurity != null && oidcSecurity.introspection() != null) { + for (TokenIntrospection introspection : oidcSecurity.introspection()) { + introspectionBuilder.add(introspection.key(), introspection.value()); + } } + + builder.addAttribute(OidcUtils.INTROSPECTION_ATTRIBUTE, + new io.quarkus.oidc.TokenIntrospection(introspectionBuilder.build())); + builder.addCredential(new AccessTokenCredential(UUID.randomUUID().toString(), null)); } - String jwt = generateToken(claims); - IdTokenCredential idToken = new IdTokenCredential(jwt, null); - AccessTokenCredential accessToken = new AccessTokenCredential(jwt, null); - - JsonWebToken principal = new OidcJwtCallerPrincipal(claims, idToken); - builder.setPrincipal(principal); - builder.addCredential(idToken); - builder.addCredential(accessToken); // UserInfo if (oidcSecurity != null && oidcSecurity.userinfo() != null) { diff --git a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/TokenIntrospection.java b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/TokenIntrospection.java new file mode 100644 index 0000000000000..ab24f3b5e374d --- /dev/null +++ b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/TokenIntrospection.java @@ -0,0 +1,13 @@ +package io.quarkus.test.security.oidc; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({}) +public @interface TokenIntrospection { + String key(); + + String value(); +}