Skip to content

Commit

Permalink
Detect Basic auth implicitly required and enable the mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Apr 8, 2024
1 parent b2c935b commit 0b209bd
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -127,13 +134,46 @@ void setMtlsCertificateRoleProperties(
}
}

@BuildStep(onlyIf = IsApplicationBasicAuthRequired.class)
void detectBasicAuthImplicitlyRequired(HttpBuildTimeConfig buildTimeConfig,
BeanRegistrationPhaseBuildItem beanRegistrationPhaseBuildItem, ApplicationIndexBuildItem applicationIndexBuildItem,
BuildProducer<SystemPropertyBuildItem> systemPropertyProducer,
List<EagerSecurityInterceptorBindingBuildItem> 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<AnnotationsTransformerBuildItem> annotationsTransformerProducer,
BuildProducer<SecurityInformationBuildItem> 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()
Expand All @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean> basic;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)
* <p>
* 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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -377,6 +400,42 @@ public void accept(HttpCredentialTransport t) {
});
}

private static void addBasicAuthMechanismIfImplicitlyRequired(
Instance<HttpAuthenticationMechanism> httpAuthenticationMechanism,
List<HttpAuthenticationMechanism> mechanisms, Instance<IdentityProvider<?>> 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
Expand All @@ -397,8 +456,8 @@ public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
}

@Override
public HttpCredentialTransport getCredentialTransport() {
return null;
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
return Uni.createFrom().nullItem();
}

}
Expand Down

This file was deleted.

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

import java.security.Principal;
import java.util.stream.Collectors;

import jakarta.inject.Inject;
Expand All @@ -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")
Expand All @@ -34,12 +31,6 @@ public class ProtectedResource {
@Inject
SecurityIdentity identity;

@Inject
SecurityIdentityAssociation securityIdentityAssociation;

@Inject
Principal principal;

@Inject
OidcConfigurationMetadata configMetadata;

Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import io.restassured.RestAssured;

@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
@TestHTTPEndpoint(TestSecurityAnnotationResource.class)
public class TestSecurityLazyAuthTest {

@Test
Expand Down
Loading

0 comments on commit 0b209bd

Please sign in to comment.