From 0b209bd21b787fca85b92159363a657829c8b0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Mon, 8 Apr 2024 13:24:36 +0200 Subject: [PATCH] Detect Basic auth implicitly required and enable the mechanism --- .../deployment/HttpSecurityProcessor.java | 51 ++++++++++++++- .../vertx/http/runtime/AuthConfig.java | 3 +- .../runtime/security/HttpAuthenticator.java | 63 +++++++++++++++++- .../CustomBasicHttpAuthMechanism.java | 28 -------- .../it/keycloak/ProtectedResource.java | 31 --------- .../TestSecurityAnnotationResource.java | 65 +++++++++++++++++++ .../src/main/resources/application.properties | 6 ++ .../it/keycloak/TestSecurityLazyAuthTest.java | 2 +- .../src/main/resources/application.properties | 1 - 9 files changed, 183 insertions(+), 67 deletions(-) delete mode 100644 integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomBasicHttpAuthMechanism.java create mode 100644 integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TestSecurityAnnotationResource.java diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index a79eb04049dbdc..6e86f73d93de00 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -3,6 +3,8 @@ import static io.quarkus.arc.processor.DotNames.APPLICATION_SCOPED; import static io.quarkus.arc.processor.DotNames.DEFAULT_BEAN; import static io.quarkus.arc.processor.DotNames.SINGLETON; +import static io.quarkus.vertx.http.runtime.security.HttpAuthenticator.BASIC_AUTH_ANNOTATION_DETECTED; +import static io.quarkus.vertx.http.runtime.security.HttpAuthenticator.TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED; import static java.util.stream.Collectors.toMap; import java.lang.reflect.Modifier; @@ -32,15 +34,19 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.processor.BeanInfo; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.ApplicationIndexBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem; @@ -71,6 +77,7 @@ public class HttpSecurityProcessor { private static final DotName AUTH_MECHANISM_NAME = DotName.createSimple(HttpAuthenticationMechanism.class); private static final DotName BASIC_AUTH_MECH_NAME = DotName.createSimple(BasicAuthenticationMechanism.class); + private static final DotName BASIC_AUTH_ANNOTATION_NAME = DotName.createSimple(BasicAuthentication.class); @Record(ExecutionTime.STATIC_INIT) @BuildStep @@ -127,13 +134,46 @@ void setMtlsCertificateRoleProperties( } } + @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) + void detectBasicAuthImplicitlyRequired(HttpBuildTimeConfig buildTimeConfig, + BeanRegistrationPhaseBuildItem beanRegistrationPhaseBuildItem, ApplicationIndexBuildItem applicationIndexBuildItem, + BuildProducer systemPropertyProducer, + List eagerSecurityInterceptorBindings) { + if (makeBasicAuthMechDefaultBean(buildTimeConfig)) { + var appIndex = applicationIndexBuildItem.getIndex(); + boolean noCustomAuthMechanismsDetected = beanRegistrationPhaseBuildItem + .getContext() + .beans() + .filter(b -> b.hasType(AUTH_MECHANISM_NAME)) + .filter(BeanInfo::isClassBean) + .filter(b -> appIndex.getClassByName(b.getBeanClass()) != null) + .isEmpty(); + // we can't decide whether custom mechanisms support basic auth or not + if (noCustomAuthMechanismsDetected) { + systemPropertyProducer + .produce(new SystemPropertyBuildItem(TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED, Boolean.TRUE.toString())); + if (!eagerSecurityInterceptorBindings.isEmpty()) { + boolean basicAuthAnnotationUsed = eagerSecurityInterceptorBindings + .stream() + .map(EagerSecurityInterceptorBindingBuildItem::getAnnotationBindings) + .flatMap(Arrays::stream) + .anyMatch(BASIC_AUTH_ANNOTATION_NAME::equals); + // @BasicAuthentication is used, hence the basic authentication is required + if (basicAuthAnnotationUsed) { + systemPropertyProducer + .produce(new SystemPropertyBuildItem(BASIC_AUTH_ANNOTATION_DETECTED, Boolean.TRUE.toString())); + } + } + } + } + } + @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) AdditionalBeanBuildItem initBasicAuth(HttpBuildTimeConfig buildTimeConfig, BuildProducer annotationsTransformerProducer, BuildProducer securityInformationProducer) { - if (!buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig) - && !buildTimeConfig.auth.basic.orElse(false)) { + if (makeBasicAuthMechDefaultBean(buildTimeConfig)) { //if not explicitly enabled we make this a default bean, so it is the fallback if nothing else is defined annotationsTransformerProducer.produce(new AnnotationsTransformerBuildItem(AnnotationsTransformer .appliedToClass() @@ -148,7 +188,12 @@ AdditionalBeanBuildItem initBasicAuth(HttpBuildTimeConfig buildTimeConfig, return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(BasicAuthenticationMechanism.class).build(); } - public static boolean applicationBasicAuthRequired(HttpBuildTimeConfig buildTimeConfig, + private static boolean makeBasicAuthMechDefaultBean(HttpBuildTimeConfig buildTimeConfig) { + return !buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig) + && !buildTimeConfig.auth.basic.orElse(false); + } + + private static boolean applicationBasicAuthRequired(HttpBuildTimeConfig buildTimeConfig, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { //basic auth explicitly disabled if (buildTimeConfig.auth.basic.isPresent() && !buildTimeConfig.auth.basic.get()) { 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 e4ff3dec29715a..79bccaec08a5cd 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 @@ -14,7 +14,8 @@ public class AuthConfig { /** * If basic auth should be enabled. If both basic and form auth is enabled then basic auth will be enabled in silent mode. * - * If no authentication mechanisms are configured basic auth is the default. + * The basic auth is enabled by default if no authentication mechanisms are configured or Quarkus can safely + * determine that basic authentication is required. */ @ConfigItem public Optional basic; 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 84eb2f49368a4f..87b39a7cdcd3b3 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 @@ -23,16 +23,20 @@ import org.jboss.logging.Logger; import io.netty.handler.codec.http.HttpResponseStatus; +import io.quarkus.arc.Arc; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; import io.quarkus.security.spi.runtime.AuthenticationFailureEvent; import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent; import io.quarkus.security.spi.runtime.SecurityEventHelper; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @@ -41,6 +45,24 @@ */ @Singleton public class HttpAuthenticator { + /** + * Special handling for the basic authentication mechanism, for user convenience, we add the mechanism when: + * - not explicitly disabled or enabled + * - is default bean and not programmatically looked up because there are other authentication mechanisms + * - no custom auth mechanism is defined because then, we can't tell if user didn't provide custom impl. + * - there is a provider that supports it (if not, we inform user via the log) + *

+ * Presence of this system property means that we need to test whether: + * - there are HTTP Permissions using explicitly this mechanism + * - or {@link io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication} + */ + public static final String TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED = "io.quarkus.security.http.test-if-basic-auth-implicitly-required"; + /** + * Whether {@link io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication} has been detected, + * which means that user needs to use basic authentication. + * Only set when detected and {@link HttpAuthenticator#TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED} is true. + */ + public static final String BASIC_AUTH_ANNOTATION_DETECTED = "io.quarkus.security.http.basic-authentication-annotation-detected"; private static final Logger log = Logger.getLogger(HttpAuthenticator.class); /** * Added to a {@link RoutingContext} as selected authentication mechanism. @@ -106,6 +128,7 @@ public HttpAuthenticator(IdentityProviderManager identityProviderManager, """.formatted(mechanism.getClass().getName(), mechanism.getCredentialTypes())); } } + addBasicAuthMechanismIfImplicitlyRequired(httpAuthenticationMechanism, mechanisms, providers); if (mechanisms.isEmpty()) { this.mechanisms = new HttpAuthenticationMechanism[] { new NoAuthenticationMechanism() }; } else { @@ -377,6 +400,42 @@ public void accept(HttpCredentialTransport t) { }); } + private static void addBasicAuthMechanismIfImplicitlyRequired( + Instance httpAuthenticationMechanism, + List mechanisms, Instance> providers) { + if (!Boolean.getBoolean(TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED) || isBasicAuthNotRequired()) { + return; + } + + var basicAuthMechInstance = httpAuthenticationMechanism.select(BasicAuthenticationMechanism.class); + if (basicAuthMechInstance.isResolvable() && !mechanisms.contains(basicAuthMechInstance.get())) { + for (IdentityProvider i : providers) { + if (UsernamePasswordAuthenticationRequest.class.equals(i.getRequestType())) { + mechanisms.add(basicAuthMechInstance.get()); + return; + } + } + log.debug(""" + BasicAuthenticationMechanism has been enabled because no custom authentication mechanism has been detected + and basic authentication is required either by the HTTP Security Policy or '@BasicAuthentication', but + there is no IdentityProvider based on username and password. Please use one of supported extensions. + For more information, go to the https://quarkus.io/guides/security-basic-authentication-howto. + """); + } + } + + private static boolean isBasicAuthNotRequired() { + if (Boolean.getBoolean(BASIC_AUTH_ANNOTATION_DETECTED)) { + return false; + } + for (var policy : Arc.container().instance(HttpConfiguration.class).get().auth.permissions.values()) { + if (BasicAuthentication.AUTH_MECHANISM_SCHEME.equals(policy.authMechanism.orElse(null))) { + return false; + } + } + return true; + } + static class NoAuthenticationMechanism implements HttpAuthenticationMechanism { @Override @@ -397,8 +456,8 @@ public Set> getCredentialTypes() { } @Override - public HttpCredentialTransport getCredentialTransport() { - return null; + public Uni getCredentialTransport(RoutingContext context) { + return Uni.createFrom().nullItem(); } } diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomBasicHttpAuthMechanism.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomBasicHttpAuthMechanism.java deleted file mode 100644 index 691628206ad613..00000000000000 --- a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomBasicHttpAuthMechanism.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.quarkus.it.keycloak; - -import jakarta.enterprise.context.ApplicationScoped; - -import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.ChallengeData; -import io.smallrye.mutiny.Uni; -import io.vertx.ext.web.RoutingContext; - -/** - * Just like {@link BasicAuthenticationMechanism}, but only challenge on demand, for when - * {@link io.quarkus.oidc.runtime.CodeAuthenticationMechanism} is not selected explicitly, it can happen - * that challenge is send to {@link BasicAuthenticationMechanism}. - */ -@ApplicationScoped -public class CustomBasicHttpAuthMechanism extends BasicAuthenticationMechanism { - public CustomBasicHttpAuthMechanism() { - super(null, false); - } - - @Override - public Uni getChallenge(RoutingContext context) { - if (context.request().getHeader("custom") != null) { - return super.getChallenge(context); - } - return Uni.createFrom().nullItem(); - } -} diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java index 87ac6edf3bb4df..5f6c6f16f06f38 100644 --- a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -1,6 +1,5 @@ package io.quarkus.it.keycloak; -import java.security.Principal; import java.util.stream.Collectors; import jakarta.inject.Inject; @@ -20,11 +19,9 @@ import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.oidc.RefreshToken; -import io.quarkus.oidc.UserInfo; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; -import io.quarkus.security.runtime.SecurityIdentityAssociation; import io.vertx.ext.web.RoutingContext; @Path("/web-app") @@ -34,12 +31,6 @@ public class ProtectedResource { @Inject SecurityIdentity identity; - @Inject - SecurityIdentityAssociation securityIdentityAssociation; - - @Inject - Principal principal; - @Inject OidcConfigurationMetadata configMetadata; @@ -59,34 +50,12 @@ public class ProtectedResource { @Inject RefreshToken refreshToken; - @Inject - UserInfo userInfo; - @Context SecurityContext securityContext; @Inject RoutingContext routingContext; - @GET - @Path("test-security") - public String testSecurity() { - return securityContext.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":" - + principal.getName() + ":" - + securityIdentityAssociation.getDeferredIdentity().await().indefinitely().getPrincipal().getName(); - } - - @GET - @Path("test-security-oidc") - public String testSecurityJwt() { - return idToken.getName() + ":" + identity.getPrincipal().getName() + ":" + principal.getName() - + ":" + securityIdentityAssociation.getDeferredIdentity().await().indefinitely().getPrincipal().getName() - + ":" + idToken.getGroups().iterator().next() - + ":" + idToken.getClaim("email") - + ":" + userInfo.getString("sub") - + ":" + configMetadata.get("audience"); - } - @GET @Path("configMetadataIssuer") public String configMetadataIssuer() { diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TestSecurityAnnotationResource.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TestSecurityAnnotationResource.java new file mode 100644 index 00000000000000..ec298f2e7f7b27 --- /dev/null +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TestSecurityAnnotationResource.java @@ -0,0 +1,65 @@ +package io.quarkus.it.keycloak; + +import java.security.Principal; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.oidc.UserInfo; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.SecurityIdentityAssociation; + +@Path("test-security-annotation") +@Authenticated +public class TestSecurityAnnotationResource { + + @Inject + SecurityIdentity identity; + + @Inject + SecurityIdentityAssociation securityIdentityAssociation; + + @Inject + Principal principal; + + @Context + SecurityContext securityContext; + + @Inject + @IdToken + JsonWebToken idToken; + + @Inject + OidcConfigurationMetadata configMetadata; + + @Inject + UserInfo userInfo; + + @GET + @Path("test-security") + public String testSecurity() { + return securityContext.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":" + + principal.getName() + ":" + + securityIdentityAssociation.getDeferredIdentity().await().indefinitely().getPrincipal().getName(); + } + + @GET + @Path("test-security-oidc") + public String testSecurityJwt() { + return idToken.getName() + ":" + identity.getPrincipal().getName() + ":" + principal.getName() + + ":" + securityIdentityAssociation.getDeferredIdentity().await().indefinitely().getPrincipal().getName() + + ":" + idToken.getGroups().iterator().next() + + ":" + idToken.getClaim("email") + + ":" + userInfo.getString("sub") + + ":" + configMetadata.get("audience"); + } + +} diff --git a/integration-tests/oidc-code-flow/src/main/resources/application.properties b/integration-tests/oidc-code-flow/src/main/resources/application.properties index a493ffe608e113..44b5a9b1765b79 100644 --- a/integration-tests/oidc-code-flow/src/main/resources/application.properties +++ b/integration-tests/oidc-code-flow/src/main/resources/application.properties @@ -204,3 +204,9 @@ quarkus.log.category."io.quarkus.vertx.http.runtime.security.HttpAuthenticator". quarkus.log.category."io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder".level=DEBUG quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL + +# make code flow default for all paths expect for 'test-security-annotation' path to test annotations +quarkus.http.auth.permission.use-code-flow-by-default.paths=/web-app*,/web-app2*,/web-app3*,/tenant-autorefresh*,/tenant-https*,/tenant-logout*,/tenant-nonce*,/tenant-refresh*,/public-web-app*,/index.html,/,/tenant-cookie-path-header,/tenant-javascript +quarkus.http.auth.permission.use-code-flow-by-default.policy=permit +quarkus.http.auth.permission.use-code-flow-by-default.shared=true +quarkus.http.auth.permission.use-code-flow-by-default.auth-mechanism=code \ No newline at end of file diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java index 40732c2d319cce..e401b515328b76 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -19,7 +19,7 @@ import io.restassured.RestAssured; @QuarkusTest -@TestHTTPEndpoint(ProtectedResource.class) +@TestHTTPEndpoint(TestSecurityAnnotationResource.class) public class TestSecurityLazyAuthTest { @Test diff --git a/integration-tests/oidc/src/main/resources/application.properties b/integration-tests/oidc/src/main/resources/application.properties index c9e1a6d9f3511d..c36960d8c794e1 100644 --- a/integration-tests/oidc/src/main/resources/application.properties +++ b/integration-tests/oidc/src/main/resources/application.properties @@ -14,7 +14,6 @@ quarkus.native.additional-build-args=-H:IncludeResources=.*\\.jks quarkus.http.cors=true quarkus.http.cors.origins=* -quarkus.http.auth.basic=true quarkus.security.users.embedded.enabled=true quarkus.security.users.embedded.plain-text=true quarkus.security.users.embedded.users.alice=password