From d3e2e0feb9f7c046233c02d97a1af4573e0cf4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Mon, 2 Sep 2024 22:55:41 +0200 Subject: [PATCH] Support for two or more authentications for a single request --- .../security-authentication-mechanisms.adoc | 64 ++++++++ .../OidcConfigurationMetadataProducer.java | 4 +- .../runtime/OidcJsonWebTokenProducer.java | 2 +- .../runtime/OidcTokenCredentialProducer.java | 14 +- .../io/quarkus/oidc/runtime/OidcUtils.java | 32 ++++ .../security/InclusiveAuthValidationTest.java | 95 +++++++++++ .../vertx/http/runtime/AuthConfig.java | 23 +++ .../runtime/security/HttpAuthenticator.java | 62 ++++++- .../runtime/security/HttpSecurityUtils.java | 31 ++++ .../security/MtlsAuthenticationMechanism.java | 6 + .../RoutingContextAwareSecurityIdentity.java | 87 ++++++++++ integration-tests/oidc-mtls/pom.xml | 154 ++++++++++++++++++ .../io/quarkus/it/oidc/OidcMtlsEndpoint.java | 28 ++++ .../src/main/resources/application.properties | 11 ++ .../src/main/resources/server-keystore.jks | Bin 0 -> 2423 bytes .../src/main/resources/server-truststore.jks | Bin 0 -> 925 bytes .../java/io/quarkus/it/oidc/OidcMtlsIT.java | 7 + .../java/io/quarkus/it/oidc/OidcMtlsTest.java | 109 +++++++++++++ .../src/test/resources/client-keystore.jks | Bin 0 -> 2214 bytes .../src/test/resources/client-truststore.jks | Bin 0 -> 2423 bytes integration-tests/pom.xml | 1 + 21 files changed, 718 insertions(+), 12 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/InclusiveAuthValidationTest.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RoutingContextAwareSecurityIdentity.java create mode 100644 integration-tests/oidc-mtls/pom.xml create mode 100644 integration-tests/oidc-mtls/src/main/java/io/quarkus/it/oidc/OidcMtlsEndpoint.java create mode 100644 integration-tests/oidc-mtls/src/main/resources/application.properties create mode 100644 integration-tests/oidc-mtls/src/main/resources/server-keystore.jks create mode 100644 integration-tests/oidc-mtls/src/main/resources/server-truststore.jks create mode 100644 integration-tests/oidc-mtls/src/test/java/io/quarkus/it/oidc/OidcMtlsIT.java create mode 100644 integration-tests/oidc-mtls/src/test/java/io/quarkus/it/oidc/OidcMtlsTest.java create mode 100644 integration-tests/oidc-mtls/src/test/resources/client-keystore.jks create mode 100644 integration-tests/oidc-mtls/src/test/resources/client-truststore.jks diff --git a/docs/src/main/asciidoc/security-authentication-mechanisms.adoc b/docs/src/main/asciidoc/security-authentication-mechanisms.adoc index 4ff538208f223..73dc068a08d6a 100644 --- a/docs/src/main/asciidoc/security-authentication-mechanisms.adoc +++ b/docs/src/main/asciidoc/security-authentication-mechanisms.adoc @@ -584,6 +584,70 @@ ifndef::no-quarkus-elytron-security-oauth2[ ^|Yes] If different sources provide the user credentials, you can combine authentication mechanisms. For example, you can combine the built-in Basic and the Quarkus `quarkus-oidc` Bearer token authentication mechanisms. +The authentication process completes as soon as the first `SecurityIdentity` is produced by one of the authentication mechanisms. + +In some cases it can be required that all registered authentication mechanisms create their `SecurityIdentity`. +It can be required when the credentials such as tokens have to be passed over <>, +for example, when users are authenticating via `Virtual Private Network`, or when the current token has to be bound +to the client certificate for the token verification to succeed, guaranteeing that the token was issued exactly +to the same client which is currently passing this token to Quarkus alongside its client certificate. + +In Quarkus such authentication is called `inclusive` and you can enable it like this: + +[source,properties] +---- +quarkus.http.auth.inclusive=true +---- + +If the authentication is inclusive then `SecurityIdentity` created by the first authentication mechanism can be +injected into the application code. +For example, if both <> and basic authentication mechanism authentications are required, +the <> authentication mechanism will create `SecurityIdentity` first. + +Additional `SecurityIdentity` instances can be accessed as a `quarkus.security.identities` attribute on the first +`SecurityIdentity`, however, accessing these extra identities directly may not be necessary, for example, +when both <> and xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer authentication] mechanisms +have been combined and done their authentications, the authenticated bearer token can be injected as a token +credential alongside `SecurityIdentity` created by <>. This is exemplified below: + +[source,java] +---- +package org.acme.security; + +import io.quarkus.oidc.AccessTokenCredential; +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class InclusiveAuthExampleBean { + + @Inject + SecurityIdentity mtlsIdentity; <1> + + @Inject + AccessTokenCredential accessTokenCredential; + + private AccessTokenCredential getAccessTokenCredential() { + if (doItHardWay()) { + var securityIdentities = HttpSecurityUtils.getSecurityIdentities(mtlsIdentity); <2> + if (securityIdentities != null) { + SecurityIdentity bearerIdentity = securityIdentities.get(OidcConstants.BEARER_SCHEME); + if (bearerIdentity != null) { + return bearerIdentity.getCredential(AccessTokenCredential.class); + } + } + } + return accessTokenCredential; + } + +} +---- +<1> This is the `SecurityIdentity` created by applicable authentication mechanism with the highest priority. +<2> Other applicable authentication mechanisms performed authentication and are available on the `SecurityIdentity`. + [IMPORTANT] ==== You cannot combine the Quarkus `quarkus-oidc` Bearer token and `smallrye-jwt` authentication mechanisms because both mechanisms attempt to verify the token extracted from the HTTP Bearer token authentication scheme. diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java index c3f7f3b22f1b9..85a92a7b2c90f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java @@ -18,9 +18,7 @@ public class OidcConfigurationMetadataProducer { @Produces @RequestScoped OidcConfigurationMetadata produce() { - OidcConfigurationMetadata configMetadata = null; - - configMetadata = (OidcConfigurationMetadata) identity.getAttribute(OidcUtils.CONFIG_METADATA_ATTRIBUTE); + OidcConfigurationMetadata configMetadata = OidcUtils.getAttribute(identity, OidcUtils.CONFIG_METADATA_ATTRIBUTE); if (configMetadata == null && tenantConfig.getDefaultTenant().oidcConfig.tenantEnabled) { configMetadata = tenantConfig.getDefaultTenant().provider.getMetadata(); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java index c9a66cf76f5d9..12ea233cf004b 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java @@ -61,7 +61,7 @@ private JsonWebToken getTokenCredential(Class type) { && ((OidcJwtCallerPrincipal) identity.getPrincipal()).getCredential().getClass() == type) { return (JsonWebToken) identity.getPrincipal(); } - TokenCredential credential = identity.getCredential(type); + TokenCredential credential = OidcUtils.getTokenCredential(identity, type); if (credential != null && credential.getToken() != null) { if (credential instanceof AccessTokenCredential && ((AccessTokenCredential) credential).isOpaque()) { throw new OIDCException("Opaque access token can not be converted to JsonWebToken"); 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 fff629c7bffb5..d738578f8846b 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 @@ -30,7 +30,7 @@ public class OidcTokenCredentialProducer { @Produces @RequestScoped IdTokenCredential currentIdToken() { - IdTokenCredential cred = identity.getCredential(IdTokenCredential.class); + IdTokenCredential cred = OidcUtils.getTokenCredential(identity, IdTokenCredential.class); if (cred == null || cred.getToken() == null) { LOG.trace("IdToken is null"); cred = new IdTokenCredential(); @@ -43,7 +43,7 @@ IdTokenCredential currentIdToken() { @Alternative @Priority(1) AccessTokenCredential currentAccessToken() { - AccessTokenCredential cred = identity.getCredential(AccessTokenCredential.class); + AccessTokenCredential cred = OidcUtils.getTokenCredential(identity, AccessTokenCredential.class); if (cred == null || cred.getToken() == null) { LOG.trace("AccessToken is null"); cred = new AccessTokenCredential(); @@ -54,7 +54,7 @@ AccessTokenCredential currentAccessToken() { @Produces @RequestScoped RefreshToken currentRefreshToken() { - RefreshToken cred = identity.getCredential(RefreshToken.class); + RefreshToken cred = OidcUtils.getTokenCredential(identity, RefreshToken.class); if (cred == null) { LOG.trace("RefreshToken is null"); cred = new RefreshToken(); @@ -70,7 +70,7 @@ RefreshToken currentRefreshToken() { @Produces @RequestScoped UserInfo currentUserInfo() { - UserInfo userInfo = (UserInfo) identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE); + UserInfo userInfo = OidcUtils.getAttribute(identity, OidcUtils.USER_INFO_ATTRIBUTE); if (userInfo == null) { LOG.trace("UserInfo is null"); userInfo = new UserInfo(); @@ -106,8 +106,8 @@ TokenIntrospection idTokenIntrospection() { @Produces @RequestScoped TokenIntrospection tokenIntrospection() { - TokenVerificationResult codeFlowAccessTokenResult = (TokenVerificationResult) identity - .getAttribute(OidcUtils.CODE_ACCESS_TOKEN_RESULT); + TokenVerificationResult codeFlowAccessTokenResult = OidcUtils.getAttribute(identity, + OidcUtils.CODE_ACCESS_TOKEN_RESULT); if (codeFlowAccessTokenResult == null) { return tokenIntrospectionFromIdentityAttribute(); } else { @@ -116,7 +116,7 @@ TokenIntrospection tokenIntrospection() { } TokenIntrospection tokenIntrospectionFromIdentityAttribute() { - TokenIntrospection introspection = (TokenIntrospection) identity.getAttribute(OidcUtils.INTROSPECTION_ATTRIBUTE); + TokenIntrospection introspection = OidcUtils.getAttribute(identity, OidcUtils.INTROSPECTION_ATTRIBUTE); if (introspection == null) { LOG.trace("TokenIntrospection is null"); introspection = new TokenIntrospection(); 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 6773643e284e8..2f169837cface 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 @@ -53,9 +53,11 @@ import io.quarkus.security.StringPermission; import io.quarkus.security.credential.TokenCredential; import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.TokenAuthenticationRequest; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.security.runtime.QuarkusSecurityIdentity.Builder; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; import io.smallrye.jwt.algorithm.ContentEncryptionAlgorithm; import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm; import io.smallrye.mutiny.Uni; @@ -814,4 +816,34 @@ public static SecretKey createSecretKeyFromDigest(byte[] secretBytes) { throw new OIDCException(ex); } } + + public static T getTokenCredential(SecurityIdentity identity, Class type) { + T credential = identity.getCredential(type); + if (credential == null) { + Map identities = HttpSecurityUtils.getSecurityIdentities(identity); + if (identities != null) { + for (String scheme : identities.keySet()) { + if (scheme.equalsIgnoreCase(OidcConstants.BEARER_SCHEME)) { + return identities.get(scheme).getCredential(type); + } + } + } + } + return credential; + } + + public static T getAttribute(SecurityIdentity identity, String name) { + T attribute = identity.getAttribute(name); + if (attribute == null) { + Map identities = HttpSecurityUtils.getSecurityIdentities(identity); + if (identities != null) { + for (String scheme : identities.keySet()) { + if (scheme.equalsIgnoreCase(OidcConstants.BEARER_SCHEME)) { + return identities.get(scheme).getAttribute(name); + } + } + } + } + return attribute; + } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/InclusiveAuthValidationTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/InclusiveAuthValidationTest.java new file mode 100644 index 0000000000000..83285ca98d6ef --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/InclusiveAuthValidationTest.java @@ -0,0 +1,95 @@ +package io.quarkus.vertx.http.security; + +import java.io.File; +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.StartupEvent; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; +import io.quarkus.vertx.http.runtime.security.MtlsAuthenticationMechanism; +import io.smallrye.certs.Format; +import io.smallrye.certs.junit5.Certificate; +import io.smallrye.certs.junit5.Certificates; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "mtls-test", password = "secret", formats = { + Format.JKS }, client = true)) +public class InclusiveAuthValidationTest { + + private static final String configuration = """ + quarkus.http.auth.inclusive=true + quarkus.http.ssl.certificate.key-store-file=server-keystore.jks + quarkus.http.ssl.certificate.key-store-password=secret + quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks + quarkus.http.ssl.certificate.trust-store-password=secret + quarkus.http.ssl.client-auth=REQUEST + quarkus.http.auth.basic=true + quarkus.http.auth.proactive=true + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TopPriorityAuthMechanism.class, StartupObserver.class) + .addClasses(TestIdentityProvider.class, TestTrustedIdentityProvider.class, TestIdentityController.class) + .addAsResource(new StringAsset(configuration), "application.properties") + .addAsResource(new File("target/certs/mtls-test-keystore.jks"), "server-keystore.jks") + .addAsResource(new File("target/certs/mtls-test-server-truststore.jks"), "server-truststore.jks")) + .assertException(throwable -> { + var errMsg = throwable.getMessage(); + Assertions.assertTrue(errMsg.contains("Inclusive authentication is enabled")); + Assertions.assertTrue(errMsg.contains("TopPriorityAuthMechanism")); + }); + + @Test + public void test() { + Assertions.fail(); + } + + public static class StartupObserver { + void observe(@Observes StartupEvent startupEvent, HttpAuthenticator authenticator) { + // authenticator is only initialized when required + } + } + + @ApplicationScoped + public static class TopPriorityAuthMechanism implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().nullItem(); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().nullItem(); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(UsernamePasswordAuthenticationRequest.class); + } + + @Override + public int getPriority() { + return MtlsAuthenticationMechanism.PRIORITY + 1; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java index 79bccaec08a5c..08107d606038f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java @@ -35,4 +35,27 @@ public class AuthConfig { */ @ConfigItem(defaultValue = "true") public boolean proactive; + + /** + * Require that all registered HTTP authentication mechanisms must complete the authentication. + *

+ * Typically, this property has to be true when the credentials are carried over mTLS, when both mTLS and another + * authentication, for example, OIDC bearer token authentication, must succeed. + * In such cases, `SecurityIdentity` created by the first mechanism, mTLS, can be injected, identities created + * by other mechanisms will be available on `SecurityIdentity`. + * The identities can be retrieved using utility method as in the example below: + * + *

+     * {@code
+     * io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getSecurityIdentities(securityIdentity)
+     * }
+     * 
+ *

+ * This property is false by default which means that the authentication process is complete as soon as the first + * `SecurityIdentity` is created. + *

+ * This property will be ignored if the path specific authentication is enabled. + */ + @ConfigItem(defaultValue = "false") + public boolean inclusive; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java index a4cfd9425edd2..362b60fbd1c8d 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java @@ -2,12 +2,16 @@ import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHENTICATION_FAILURE; import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHENTICATION_SUCCESS; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.SECURITY_IDENTITIES_ATTRIBUTE; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getSecurityIdentities; import static io.quarkus.vertx.http.runtime.security.RolesMapping.ROLES_MAPPING_KEY; +import static io.quarkus.vertx.http.runtime.security.RoutingContextAwareSecurityIdentity.addRoutingCtxToIdentityIfMissing; import static java.lang.Boolean.TRUE; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -25,6 +29,7 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.quarkus.arc.Arc; +import io.quarkus.arc.ClientProxy; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.IdentityProviderManager; @@ -77,8 +82,9 @@ public class HttpAuthenticator { private final IdentityProviderManager identityProviderManager; private final HttpAuthenticationMechanism[] mechanisms; private final SecurityEventHelper securityEventHelper; + private final boolean inclusiveAuth; - public HttpAuthenticator(IdentityProviderManager identityProviderManager, + HttpAuthenticator(IdentityProviderManager identityProviderManager, Event authFailureEvent, Event authSuccessEvent, BeanManager beanManager, HttpBuildTimeConfig httpBuildTimeConfig, @@ -88,6 +94,7 @@ public HttpAuthenticator(IdentityProviderManager identityProviderManager, this.securityEventHelper = new SecurityEventHelper<>(authSuccessEvent, authFailureEvent, AUTHENTICATION_SUCCESS, AUTHENTICATION_FAILURE, beanManager, securityEventsEnabled); this.identityProviderManager = identityProviderManager; + this.inclusiveAuth = httpBuildTimeConfig.auth.inclusive; List mechanisms = new ArrayList<>(); for (HttpAuthenticationMechanism mechanism : httpAuthenticationMechanism) { if (mechanism.getCredentialTypes().isEmpty()) { @@ -141,6 +148,21 @@ public int compare(HttpAuthenticationMechanism mech1, HttpAuthenticationMechanis } }); this.mechanisms = mechanisms.toArray(new HttpAuthenticationMechanism[mechanisms.size()]); + + // if inclusive auth and mTLS are enabled, the mTLS must have the highest priority + if (inclusiveAuth && Arc.container().instance(MtlsAuthenticationMechanism.class).isAvailable()) { + var topMechanism = ClientProxy.unwrap(this.mechanisms[0]); + boolean isMutualTls = topMechanism instanceof MtlsAuthenticationMechanism; + if (!isMutualTls) { + throw new IllegalStateException( + """ + Inclusive authentication is enabled and '%s' does not have + the highest priority. Please lower priority of the '%s' authentication mechanism under '%s'. + """.formatted(MtlsAuthenticationMechanism.class.getName(), + topMechanism.getClass().getName(), + MtlsAuthenticationMechanism.PRIORITY)); + } + } } } @@ -225,6 +247,9 @@ private Uni createSecurityIdentity(RoutingContext routingConte @Override public Uni apply(SecurityIdentity identity) { if (identity != null) { + if (inclusiveAuth) { + return authenticateWithAllMechanisms(identity, i, routingContext); + } if (selectAuthMechanismWithAnnotation && !isAuthMechanismSelected(routingContext)) { return rememberAuthMechScheme(mechanisms[i], routingContext).replaceWith(identity); } @@ -308,6 +333,41 @@ public Uni apply(ChallengeData data) { return result; } + private Uni authenticateWithAllMechanisms(SecurityIdentity identity, int i, + RoutingContext routingContext) { + return getCredentialTransport(mechanisms[i], routingContext) + .onItem().transformToUni(new Function>() { + @Override + public Uni apply(HttpCredentialTransport httpCredentialTransport) { + if (httpCredentialTransport == null || httpCredentialTransport.getAuthenticationScheme() == null) { + log.error(""" + Illegal state - HttpAuthenticationMechanism '%s' authentication scheme is not available. + The authentication scheme is required when inclusive authentication is enabled. + """.formatted(ClientProxy.unwrap(mechanisms[i]).getClass().getName())); + return Uni.createFrom().failure(new AuthenticationFailedException()); + } + var authMechanism = httpCredentialTransport.getAuthenticationScheme(); + + // add current identity to the RoutingContext + var authMechToIdentity = getSecurityIdentities(routingContext); + boolean isFirstIdentity = authMechToIdentity == null; + if (isFirstIdentity) { + authMechToIdentity = new HashMap<>(); + routingContext.put(SECURITY_IDENTITIES_ATTRIBUTE, authMechToIdentity); + } + authMechToIdentity.putIfAbsent(authMechanism, identity); + + // authenticate with remaining mechanisms + if (isFirstIdentity) { + return createSecurityIdentity(routingContext, i + 1) + .replaceWith(addRoutingCtxToIdentityIfMissing(identity, routingContext)); + } else { + return createSecurityIdentity(routingContext, i + 1); + } + } + }); + } + private Uni findBestCandidateMechanism(RoutingContext routingContext, String pathSpecificMechanism, int i) { if (i == mechanisms.length) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java index 92479ac4e8725..c310d2efa0022 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java @@ -7,17 +7,44 @@ import javax.naming.ldap.Rdn; import javax.security.auth.x500.X500Principal; +import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AuthenticationRequest; import io.vertx.ext.web.RoutingContext; public final class HttpSecurityUtils { public final static String ROUTING_CONTEXT_ATTRIBUTE = "quarkus.http.routing.context"; + static final String SECURITY_IDENTITIES_ATTRIBUTE = "io.quarkus.security.identities"; static final String COMMON_NAME = "CN"; private HttpSecurityUtils() { } + /** + * Provides all the {@link SecurityIdentity} created by the inclusive authentication. + * + * @return null if {@link RoutingContext} is not available or {@link #getSecurityIdentities(RoutingContext)} + * @see #getSecurityIdentities(RoutingContext) + */ + public static Map getSecurityIdentities(SecurityIdentity identity) { + var routingContext = getRoutingContextAttribute(identity); + if (routingContext == null) { + return null; + } + return getSecurityIdentities(routingContext); + } + + /** + * When inclusive authentication is enabled, we allow all authentication mechanisms to produce identity. + * However, only the first identity (provided by applicable mechanism with the highest priority) is stored + * in the CDI container. Therefore, we put all the identities into the RoutingContext. + * + * @return null if no identities were found or map with authentication mechanism key and security identity value + */ + public static Map getSecurityIdentities(RoutingContext routingContext) { + return routingContext.get(SECURITY_IDENTITIES_ATTRIBUTE); + } + public static AuthenticationRequest setRoutingContextAttribute(AuthenticationRequest request, RoutingContext context) { request.setAttribute(ROUTING_CONTEXT_ATTRIBUTE, context); return request; @@ -27,6 +54,10 @@ public static RoutingContext getRoutingContextAttribute(AuthenticationRequest re return request.getAttribute(ROUTING_CONTEXT_ATTRIBUTE); } + public static RoutingContext getRoutingContextAttribute(SecurityIdentity identity) { + return identity.getAttribute(RoutingContext.class.getName()); + } + public static RoutingContext getRoutingContextAttribute(Map authenticationRequestAttributes) { return (RoutingContext) authenticationRequestAttributes.get(ROUTING_CONTEXT_ATTRIBUTE); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java index 9fb0da9f63b0a..e651aead7c94a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java @@ -39,6 +39,7 @@ * The authentication handler responsible for mTLS client authentication */ public class MtlsAuthenticationMechanism implements HttpAuthenticationMechanism { + public static final int PRIORITY = 3000; private static final String ROLES_MAPPER_ATTRIBUTE = "roles_mapper"; private Function> certificateToRoles = null; @@ -83,6 +84,11 @@ public Uni getCredentialTransport(RoutingContext contex return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.X509, "X509")); } + @Override + public int getPriority() { + return PRIORITY; + } + void setCertificateToRolesMapper(Function> certificateToRoles) { this.certificateToRoles = certificateToRoles; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RoutingContextAwareSecurityIdentity.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RoutingContextAwareSecurityIdentity.java new file mode 100644 index 0000000000000..c7d5850b72bb1 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RoutingContextAwareSecurityIdentity.java @@ -0,0 +1,87 @@ +package io.quarkus.vertx.http.runtime.security; + +import java.security.Permission; +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import io.quarkus.security.credential.Credential; +import io.quarkus.security.identity.SecurityIdentity; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +final class RoutingContextAwareSecurityIdentity implements SecurityIdentity { + + private static final String ROUTING_CONTEXT_KEY = RoutingContext.class.getName(); + private final SecurityIdentity delegate; + private final RoutingContext routingContext; + + private RoutingContextAwareSecurityIdentity(SecurityIdentity delegate, RoutingContext routingContext) { + this.delegate = delegate; + this.routingContext = routingContext; + } + + static SecurityIdentity addRoutingCtxToIdentityIfMissing(SecurityIdentity delegate, RoutingContext routingContext) { + if (delegate != null && delegate.getAttribute(ROUTING_CONTEXT_KEY) == null) { + return new RoutingContextAwareSecurityIdentity(delegate, routingContext); + } + return delegate; + } + + @Override + public Principal getPrincipal() { + return delegate.getPrincipal(); + } + + @Override + public boolean isAnonymous() { + return delegate.isAnonymous(); + } + + @Override + public Set getRoles() { + return delegate.getRoles(); + } + + @Override + public boolean hasRole(String s) { + return delegate.hasRole(s); + } + + @Override + public T getCredential(Class aClass) { + return delegate.getCredential(aClass); + } + + @Override + public Set getCredentials() { + return delegate.getCredentials(); + } + + @SuppressWarnings("unchecked") + @Override + public T getAttribute(String s) { + if (ROUTING_CONTEXT_KEY.equals(s)) { + return (T) routingContext; + } + return delegate.getAttribute(s); + } + + @Override + public Map getAttributes() { + // we always recreate the map as it could have changed in the delegate + var delegateAttributes = delegate.getAttributes(); + if (delegateAttributes == null || delegateAttributes.isEmpty()) { + return Map.of(ROUTING_CONTEXT_KEY, routingContext); + } + var result = new HashMap<>(delegateAttributes); + result.put(ROUTING_CONTEXT_KEY, routingContext); + return result; + } + + @Override + public Uni checkPermission(Permission permission) { + return delegate.checkPermission(permission); + } +} diff --git a/integration-tests/oidc-mtls/pom.xml b/integration-tests/oidc-mtls/pom.xml new file mode 100644 index 0000000000000..2edad5c91ad20 --- /dev/null +++ b/integration-tests/oidc-mtls/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-oidc-mtls + Quarkus - Integration Tests - OIDC and mTLS authentication + OIDC and mTLS authentication integration tests module + + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-oidc + + + io.quarkus + quarkus-tls-registry + + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-test-keycloak-server + test + + + io.rest-assured + rest-assured + test + + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-oidc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-tls-registry-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + + + maven-failsafe-plugin + + false + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + diff --git a/integration-tests/oidc-mtls/src/main/java/io/quarkus/it/oidc/OidcMtlsEndpoint.java b/integration-tests/oidc-mtls/src/main/java/io/quarkus/it/oidc/OidcMtlsEndpoint.java new file mode 100644 index 0000000000000..53319e396b3f8 --- /dev/null +++ b/integration-tests/oidc-mtls/src/main/java/io/quarkus/it/oidc/OidcMtlsEndpoint.java @@ -0,0 +1,28 @@ +package io.quarkus.it.oidc; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.security.credential.CertificateCredential; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/service") +public class OidcMtlsEndpoint { + + @Inject + SecurityIdentity identity; + + @Inject + JsonWebToken accessToken; + + @GET + @Path("name") + public String getName() { + var cred = identity.getCredential(CertificateCredential.class).getCertificate(); + return "Identities: " + cred.getSubjectX500Principal().getName().split(",")[0] + ", " + + accessToken.getName(); + } +} diff --git a/integration-tests/oidc-mtls/src/main/resources/application.properties b/integration-tests/oidc-mtls/src/main/resources/application.properties new file mode 100644 index 0000000000000..69d52fd93aa24 --- /dev/null +++ b/integration-tests/oidc-mtls/src/main/resources/application.properties @@ -0,0 +1,11 @@ +quarkus.http.tls-configuration-name=oidc-mtls +quarkus.tls.oidc-mtls.key-store.jks.path=server-keystore.jks +quarkus.tls.oidc-mtls.key-store.jks.password=secret +quarkus.tls.oidc-mtls.trust-store.jks.path=server-truststore.jks +quarkus.tls.oidc-mtls.trust-store.jks.password=password + +quarkus.http.auth.inclusive=true + +quarkus.http.ssl.client-auth=REQUIRED +quarkus.http.insecure-requests=DISABLED +quarkus.native.additional-build-args=-H:IncludeResources=.*\\.jks diff --git a/integration-tests/oidc-mtls/src/main/resources/server-keystore.jks b/integration-tests/oidc-mtls/src/main/resources/server-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..da33e8e7a16683d421c7a541bf0013521efb605e GIT binary patch literal 2423 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 aH~3Ca%|*I@e88To>+D>~J`lt~rv3-)J!2gJ literal 0 HcmV?d00001 diff --git a/integration-tests/oidc-mtls/src/main/resources/server-truststore.jks b/integration-tests/oidc-mtls/src/main/resources/server-truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..8ec8e126507b61e0e602b71d9f67b3d7e3c7cae3 GIT binary patch literal 925 zcmezO_TO6u1_mY|W(3o$xs}0Ue&dE&8D>0B0 z=QXr6G&C?YG&M3ZGLI7HHL^4`0AdK2=Jq!+Dj|EBk(GhDiIJbdpox)-sfm%1VUF3s zo}9*=J}DCyv6d_>miqhX+0BER7vzr=weLB0SI~-EN4Tx;+oAX3sZu+AreC;W+rDov z-#5*V51%D8KX(20xaE+MMC5UqlCZ}&*8kY~jW=TIFRK(mcF!wJ^TT7)vd&p~1Uy)$ z`pW;@Uk~HZlKvCjci1|P$j&@8Uwi5_z2paTO5a=hS@rqWe0&n}OLbn_nKBuLad&4PoF%w@U!d89 z+LCrv&mDH_wWWUYJ`36D%^-U7h-9RXpo^<=ZFc0HX`&2UWMlL9lv%uVR}CpmJg{;8 zx_~1U?x&fU85tNCD;mff$O0oymXAe@MTGhL(rFKW9uTVSSw3Zn;O1#zMYmj$0}+^R zfPu)!z}8XzuI^>`BBZaSb80!?+r7pc#`09np_Lozt4zJ}{d+W?|#aPK1E8H9tc&p84 z9$uW3|Ks-G6^q-888qdu-^`O)?fLIsY>e9J3v8=pk6rP))@~NVdhf7^xr%i0hj$v? zKacmOG&aO636*{EblS9x!(U?iHAU{$t+Riy#c;{YELD{*se2PT}_AU?Y+Xxvefqh07{ov?f?J) literal 0 HcmV?d00001 diff --git a/integration-tests/oidc-mtls/src/test/java/io/quarkus/it/oidc/OidcMtlsIT.java b/integration-tests/oidc-mtls/src/test/java/io/quarkus/it/oidc/OidcMtlsIT.java new file mode 100644 index 0000000000000..c7030f1942f36 --- /dev/null +++ b/integration-tests/oidc-mtls/src/test/java/io/quarkus/it/oidc/OidcMtlsIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.oidc; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class OidcMtlsIT extends OidcMtlsTest { +} \ No newline at end of file diff --git a/integration-tests/oidc-mtls/src/test/java/io/quarkus/it/oidc/OidcMtlsTest.java b/integration-tests/oidc-mtls/src/test/java/io/quarkus/it/oidc/OidcMtlsTest.java new file mode 100644 index 0000000000000..ce4b2cd482cad --- /dev/null +++ b/integration-tests/oidc-mtls/src/test/java/io/quarkus/it/oidc/OidcMtlsTest.java @@ -0,0 +1,109 @@ +package io.quarkus.it.oidc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.runtime.util.ClassPathUtils; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.keycloak.client.KeycloakTestClient; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.KeyStoreOptions; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.mutiny.ext.web.client.HttpResponse; +import io.vertx.mutiny.ext.web.client.WebClient; + +@QuarkusTest +public class OidcMtlsTest { + + @TestHTTPResource(ssl = true) + URL url; + + KeycloakTestClient keycloakClient = new KeycloakTestClient(); + + @Test + public void testGetIdentityNames() throws Exception { + Vertx vertx = Vertx.vertx(); + try { + WebClientOptions options = createWebClientOptions(); + WebClient webClient = WebClient.create(new io.vertx.mutiny.core.Vertx(vertx), options); + + // HTTP 200 + HttpResponse resp = webClient.get("/service/name") + .putHeader("Authorization", OidcConstants.BEARER_SCHEME + " " + keycloakClient.getAccessToken("alice")) + .send().await() + .indefinitely(); + assertEquals(200, resp.statusCode()); + String name = resp.bodyAsString(); + assertEquals("Identities: CN=client, alice", name); + + // HTTP 401, invalid token + resp = webClient.get("/service/name") + .putHeader("Authorization", OidcConstants.BEARER_SCHEME + " " + "123") + .send().await() + .indefinitely(); + assertEquals(401, resp.statusCode()); + } finally { + vertx.close(); + } + } + + private WebClientOptions createWebClientOptions() throws Exception { + WebClientOptions webClientOptions = new WebClientOptions().setDefaultHost(url.getHost()) + .setDefaultPort(url.getPort()).setSsl(true).setVerifyHost(false); + + byte[] keyStoreData = getFileContent(Paths.get("client-keystore.jks")); + KeyStoreOptions keyStoreOptions = new KeyStoreOptions() + .setPassword("password") + .setValue(Buffer.buffer(keyStoreData)) + .setType("JKS"); + webClientOptions.setKeyCertOptions(keyStoreOptions); + + byte[] trustStoreData = getFileContent(Paths.get("client-truststore.jks")); + KeyStoreOptions trustStoreOptions = new KeyStoreOptions() + .setPassword("secret") + .setValue(Buffer.buffer(trustStoreData)) + .setType("JKS"); + webClientOptions.setTrustOptions(trustStoreOptions); + + return webClientOptions; + } + + private static byte[] getFileContent(Path path) throws IOException { + byte[] data; + final InputStream resource = Thread.currentThread().getContextClassLoader() + .getResourceAsStream(ClassPathUtils.toResourceName(path)); + if (resource != null) { + try (InputStream is = resource) { + data = doRead(is); + } + } else { + try (InputStream is = Files.newInputStream(path)) { + data = doRead(is); + } + } + return data; + } + + private static byte[] doRead(InputStream is) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int r; + while ((r = is.read(buf)) > 0) { + out.write(buf, 0, r); + } + return out.toByteArray(); + } + +} diff --git a/integration-tests/oidc-mtls/src/test/resources/client-keystore.jks b/integration-tests/oidc-mtls/src/test/resources/client-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..cf6d6ba454864d18322799afac37f520673193d6 GIT binary patch literal 2214 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 aH~3Ca%|*I@e88To>+D>~J`lt~rv3-)J!2gJ literal 0 HcmV?d00001 diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index f77304827c2ca..1f821ced8132e 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -256,6 +256,7 @@ oidc-client-registration oidc-client-reactive oidc-client-wiremock + oidc-mtls oidc-token-propagation oidc-token-propagation-reactive openapi