Skip to content

Commit

Permalink
Merge pull request quarkusio#39945 from michalvavrik/feature/detect-i…
Browse files Browse the repository at this point in the history
…mplicit-basic-auth-requirement

Detect basic authentication is implicitly required when it can be safely determined and enable the basic auth by default for such scenarios
  • Loading branch information
sberyozkin authored Apr 11, 2024
2 parents b8fa2e4 + 1662bb0 commit d358c64
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 6 deletions.
5 changes: 5 additions & 0 deletions extensions/oidc/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@
<artifactId>quarkus-resteasy-jackson-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file-deployment</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.quarkus.oidc.test;

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

import org.eclipse.microprofile.jwt.JsonWebToken;
import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.oidc.BearerTokenAuthentication;
import io.quarkus.test.QuarkusDevModeTest;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
import io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication;
import io.restassured.RestAssured;

@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
public class ImplicitBasicAuthAndBearerAuthCombinationTest {

@RegisterExtension
static final QuarkusDevModeTest test = new QuarkusDevModeTest()
.withApplicationRoot((jar) -> jar
.addClasses(BasicBearerResource.class)
.addAsResource(
new StringAsset("""
quarkus.security.users.embedded.enabled=true
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.alice=alice
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.credentials.secret=secret
quarkus.http.auth.proactive=false
"""),
"application.properties"));

@Test
public void testBasicEnabledAsSelectedWithAnnotation() {
// endpoint is annotated with 'BasicAuthentication', so basic auth must be enabled
RestAssured.given().auth().oauth2(getAccessToken()).get("/basic-bearer/bearer")
.then().statusCode(200).body(Matchers.is("alice"));
RestAssured.given().auth().basic("alice", "alice").get("/basic-bearer/basic")
.then().statusCode(204);
RestAssured.given().auth().basic("alice", "alice").get("/basic-bearer/bearer")
.then().statusCode(401);
RestAssured.given().auth().oauth2(getAccessToken()).get("/basic-bearer/basic")
.then().statusCode(401);
}

private static String getAccessToken() {
return KeycloakTestResourceLifecycleManager.getAccessToken("alice");
}

@BearerTokenAuthentication
@Path("basic-bearer")
public static class BasicBearerResource {

@Inject
JsonWebToken accessToken;

@GET
@BasicAuthentication
@Path("basic")
public String basic() {
return accessToken.getName();
}

@GET
@Path("bearer")
public String bearer() {
return accessToken.getName();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package io.quarkus.oidc.test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;

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

import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;

import io.quarkus.oidc.IdToken;
import io.quarkus.test.QuarkusDevModeTest;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
import io.restassured.RestAssured;

@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
public class ImplicitBasicAuthAndCodeFlowAuthCombinationTest {

@RegisterExtension
static final QuarkusDevModeTest test = new QuarkusDevModeTest()
.withApplicationRoot((jar) -> jar
.addClasses(BasicCodeFlowResource.class)
.addAsResource(
new StringAsset("""
quarkus.security.users.embedded.enabled=true
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.alice=alice
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus
quarkus.oidc.client-id=quarkus-web-app
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.http.auth.permission.code-flow.paths=/basic-code-flow/code-flow
quarkus.http.auth.permission.code-flow.policy=authenticated
quarkus.http.auth.permission.code-flow.auth-mechanism=code
quarkus.http.auth.permission.basic.paths=/basic-code-flow/basic
quarkus.http.auth.permission.basic.policy=authenticated
quarkus.http.auth.permission.basic.auth-mechanism=basic
"""),
"application.properties"));

@Test
public void testBasicEnabledAsSelectedWithHttpPerm() throws IOException, InterruptedException {
// endpoint is annotated with 'BasicAuthentication', so basic auth must be enabled
RestAssured.given().auth().basic("alice", "alice").get("/basic-code-flow/basic")
.then().statusCode(204);
RestAssured.given().auth().basic("alice", "alice").redirects().follow(false)
.get("/basic-code-flow/code-flow").then().statusCode(302);

try (final WebClient webClient = createWebClient()) {

try {
webClient.getPage("http://localhost:8080/basic-code-flow/basic");
fail("Exception is expected because by the basic auth is required");
} catch (FailingHttpStatusCodeException ex) {
// Reported by Quarkus
assertEquals(401, ex.getStatusCode());
}
HtmlPage page = webClient.getPage("http://localhost:8080/basic-code-flow/code-flow");

assertEquals("Sign in to quarkus", page.getTitleText());

HtmlForm loginForm = page.getForms().get(0);

loginForm.getInputByName("username").setValueAttribute("alice");
loginForm.getInputByName("password").setValueAttribute("alice");

page = loginForm.getInputByName("login").click();

assertEquals("alice", page.getBody().asNormalizedText());

webClient.getCookieManager().clearCookies();
}
}

private WebClient createWebClient() {
WebClient webClient = new WebClient();
webClient.setCssErrorHandler(new SilentCssErrorHandler());
return webClient;
}

private static String getAccessToken() {
return KeycloakTestResourceLifecycleManager.getAccessToken("alice");
}

@Path("basic-code-flow")
public static class BasicCodeFlowResource {

@Inject
@IdToken
JsonWebToken idToken;

@GET
@Path("basic")
public String basic() {
return idToken.getName();
}

@GET
@Path("code-flow")
public String codeFlow() {
return idToken.getName();
}
}

}
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
Loading

0 comments on commit d358c64

Please sign in to comment.