From f4c3c381fbc189d0c9edb2502128dc58b60011fb Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 19 Mar 2020 14:29:09 +0100 Subject: [PATCH 1/2] Make it possible to initialize a synthetic bean during RUNTIME_INIT - follows up on https://github.com/quarkusio/quarkus/pull/5938 --- .../deployment/SyntheticBeanBuildItem.java | 19 +++++++ .../deployment/SyntheticBeansProcessor.java | 50 ++++++++++++++----- .../io/quarkus/arc/runtime/ArcRecorder.java | 6 ++- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeanBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeanBuildItem.java index f9f283c8c1148..e9a9df065be05 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeanBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeanBuildItem.java @@ -9,6 +9,7 @@ import io.quarkus.arc.processor.BeanConfigurator; import io.quarkus.arc.processor.BeanConfiguratorBase; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.runtime.RuntimeValue; /** @@ -36,6 +37,10 @@ public ExtendedBeanConfigurator configurator() { return configurator; } + public boolean isStaticInit() { + return configurator.staticInit; + } + /** * This construct is not thread-safe and should not be reused. */ @@ -43,9 +48,11 @@ public static class ExtendedBeanConfigurator extends BeanConfiguratorBase supplier; RuntimeValue runtimeValue; + boolean staticInit; ExtendedBeanConfigurator(DotName implClazz) { super(implClazz); + this.staticInit = true; } /** @@ -73,6 +80,18 @@ public ExtendedBeanConfigurator runtimeValue(RuntimeValue runtimeValue) { return this; } + /** + * By default, synthetic beans are initialized during {@link ExecutionTime#STATIC_INIT}. It is possible to mark a + * synthetic bean to be initialized during {@link ExecutionTime#RUNTIME_INIT}. However, in such case a client that + * attempts to obtain such bean during {@link ExecutionTime#STATIC_INIT} will receive an exception. + * + * @return self + */ + public ExtendedBeanConfigurator setRuntimeInit() { + this.staticInit = false; + return this; + } + public DotName getImplClazz() { return implClazz; } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeansProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeansProcessor.java index df549b2e76ac0..0312463c5f5d2 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeansProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeansProcessor.java @@ -8,6 +8,8 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import javax.enterprise.inject.CreationException; + import org.jboss.jandex.DotName; import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem; @@ -71,21 +73,42 @@ void build(ArcRecorder recorder, List runtimeBeans, List syntheticBeans, + BeanRegistrationPhaseBuildItem beanRegistration, BuildProducer configurators) { + + Map> suppliersMap = new HashMap<>(); + + for (SyntheticBeanBuildItem bean : syntheticBeans) { + if (!bean.isStaticInit()) { + initSyntheticBean(recorder, suppliersMap, beanRegistration, bean); + } + } + recorder.initRuntimeSupplierBeans(suppliersMap); + } + + private void initSyntheticBean(ArcRecorder recorder, Map> suppliersMap, + BeanRegistrationPhaseBuildItem beanRegistration, SyntheticBeanBuildItem bean) { + DotName implClazz = bean.configurator().getImplClazz(); + String name = createName(implClazz.toString(), bean.configurator().getQualifiers().toString()); + if (bean.configurator().runtimeValue != null) { + suppliersMap.put(name, recorder.createSupplier(bean.configurator().runtimeValue)); + } else { + suppliersMap.put(name, bean.configurator().supplier); + } + beanRegistration.getContext().configure(implClazz) + .read(bean.configurator()) + .creator(creator(name)) + .done(); } private String createName(String beanClass, String qualifiers) { @@ -102,6 +125,9 @@ public void accept(MethodCreator m) { ResultHandle supplier = m.invokeInterfaceMethod( MethodDescriptor.ofMethod(Map.class, "get", Object.class, Object.class), staticMap, m.load(name)); + // Throw an exception if no supplier is found + m.ifNull(supplier).trueBranch().throwException(CreationException.class, + "Synthetic bean instance not initialized yet: " + name); ResultHandle result = m.invokeInterfaceMethod( MethodDescriptor.ofMethod(Supplier.class, "get", Object.class), supplier); diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java index 0d4ac9109fa37..3126bfba11b05 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java @@ -46,10 +46,14 @@ public void initExecutor(ExecutorService executor) { Arc.setExecutor(executor); } - public void initSupplierBeans(Map> beans) { + public void initStaticSupplierBeans(Map> beans) { supplierMap = new ConcurrentHashMap<>(beans); } + public void initRuntimeSupplierBeans(Map> beans) { + supplierMap.putAll(beans); + } + public BeanContainer initBeanContainer(ArcContainer container, List listeners, Collection removedBeanTypes) throws Exception { From 53b3b483a1ac127a39e5563d8275818e0b5554fd Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Fri, 20 Mar 2020 12:13:32 +1100 Subject: [PATCH 2/2] Multiple authentication fixes - Use synthetic beans to configure form and basic auth - Allow multiple authentication mechanisms - Better default behaviour based on what is configured Fixes #7768 Fixes #5284 --- .../runtime/auth/OAuth2AuthMechanism.java | 14 ++ .../io/quarkus/security/test/CustomAuth.java | 17 +- .../runtime/OidcAuthenticationMechanism.java | 16 ++ .../jwt/runtime/auth/JWTAuthMechanism.java | 28 +++ .../deployment/HttpSecurityProcessor.java | 39 ++++- .../CombinedFormBasicAuthTestCase.java | 160 ++++++++++++++++++ .../vertx/http/runtime/AuthConfig.java | 5 +- .../BasicAuthenticationMechanism.java | 12 ++ .../security/FormAuthenticationMechanism.java | 73 ++++---- .../security/HttpAuthenticationMechanism.java | 15 ++ .../runtime/security/HttpAuthenticator.java | 126 +++++++++++--- .../security/HttpCredentialTransport.java | 73 ++++++++ .../security/HttpSecurityRecorder.java | 50 +++++- 13 files changed, 547 insertions(+), 81 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CombinedFormBasicAuthTestCase.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java diff --git a/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java b/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java index 1989ee7e74582..966be355c0104 100644 --- a/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java +++ b/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java @@ -1,5 +1,7 @@ package io.quarkus.elytron.security.oauth2.runtime.auth; +import java.util.Collections; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -10,9 +12,11 @@ import io.quarkus.security.credential.TokenCredential; 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.TokenAuthenticationRequest; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; import io.vertx.ext.web.RoutingContext; /** @@ -53,4 +57,14 @@ public CompletionStage getChallenge(RoutingContext context) { "Bearer {token}"); return CompletableFuture.completedFuture(result); } + + @Override + public Set> getCredentialTypes() { + return Collections.singleton(TokenAuthenticationRequest.class); + } + + @Override + public HttpCredentialTransport getCredentialTransport() { + return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, "bearer"); + } } diff --git a/extensions/elytron-security-properties-file/deployment/src/test/java/io/quarkus/security/test/CustomAuth.java b/extensions/elytron-security-properties-file/deployment/src/test/java/io/quarkus/security/test/CustomAuth.java index 5ef716d28a275..2a7eafeb2d92e 100644 --- a/extensions/elytron-security-properties-file/deployment/src/test/java/io/quarkus/security/test/CustomAuth.java +++ b/extensions/elytron-security-properties-file/deployment/src/test/java/io/quarkus/security/test/CustomAuth.java @@ -4,8 +4,10 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -19,10 +21,11 @@ import io.quarkus.security.credential.PasswordCredential; 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.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; -import io.undertow.security.idm.IdentityManager; +import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; import io.vertx.ext.web.RoutingContext; /** @@ -36,8 +39,6 @@ public class CustomAuth implements HttpAuthenticationMechanism { private static final int PREFIX_LENGTH = BASIC_PREFIX.length(); private static final String COLON = ":"; - private IdentityManager identityManager; - @Override public CompletionStage authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { @@ -83,4 +84,14 @@ public CompletionStage getChallenge(RoutingContext context) { "BASIC realm=CUSTOM"); return CompletableFuture.completedFuture(result); } + + @Override + public Set> getCredentialTypes() { + return Collections.singleton(UsernamePasswordAuthenticationRequest.class); + } + + @Override + public HttpCredentialTransport getCredentialTransport() { + return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, "basic"); + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java index d4a095de1d1f8..75e6268963ce9 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java @@ -1,5 +1,7 @@ package io.quarkus.oidc.runtime; +import java.util.Collections; +import java.util.Set; import java.util.concurrent.CompletionStage; import javax.enterprise.context.ApplicationScoped; @@ -8,8 +10,11 @@ import io.quarkus.oidc.OIDCException; 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.TokenAuthenticationRequest; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; import io.vertx.ext.web.RoutingContext; @ApplicationScoped @@ -41,4 +46,15 @@ private boolean isWebApp(RoutingContext context) { return OidcTenantConfig.ApplicationType.WEB_APP == tenantContext.oidcConfig.applicationType; } + @Override + public Set> getCredentialTypes() { + return Collections.singleton(TokenAuthenticationRequest.class); + } + + @Override + public HttpCredentialTransport getCredentialTransport() { + //not 100% correct, but enough for now + //if OIDC is present we don't really want another bearer mechanism + return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, "bearer"); + } } diff --git a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java index ab8793afbdede..44d2c8faf0cb6 100644 --- a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java +++ b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java @@ -2,6 +2,7 @@ import static io.vertx.core.http.HttpHeaders.COOKIE; +import java.util.Collections; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -18,9 +19,11 @@ import io.quarkus.security.credential.TokenCredential; 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.TokenAuthenticationRequest; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; import io.smallrye.jwt.auth.AbstractBearerTokenExtractor; import io.smallrye.jwt.auth.cdi.PrincipalProducer; import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; @@ -32,6 +35,9 @@ */ @ApplicationScoped public class JWTAuthMechanism implements HttpAuthenticationMechanism { + protected static final String COOKIE_HEADER = "Cookie"; + protected static final String AUTHORIZATION_HEADER = "Authorization"; + protected static final String BEARER = "Bearer"; @Inject private JWTAuthContextInfo authContextInfo; @@ -90,4 +96,26 @@ protected String getCookieValue(String cookieName) { return cookie != null ? cookie.getValue() : null; } } + + @Override + public Set> getCredentialTypes() { + return Collections.singleton(TokenAuthenticationRequest.class); + } + + @Override + public HttpCredentialTransport getCredentialTransport() { + final String tokenHeaderName = authContextInfo.getTokenHeader(); + if (COOKIE_HEADER.equals(tokenHeaderName)) { + String tokenCookieName = authContextInfo.getTokenCookie(); + + if (tokenCookieName == null) { + tokenCookieName = BEARER; + } + return new HttpCredentialTransport(HttpCredentialTransport.Type.COOKIE, tokenCookieName); + } else if (AUTHORIZATION_HEADER.equals(tokenHeaderName)) { + return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BEARER); + } else { + return new HttpCredentialTransport(HttpCredentialTransport.Type.OTHER_HEADER, tokenHeaderName); + } + } } 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 46765a3fce59f..56d8839eda1db 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 @@ -5,9 +5,11 @@ import java.util.Map; import java.util.function.Supplier; +import javax.inject.Singleton; + import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -20,6 +22,7 @@ import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.DenySecurityPolicy; import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; import io.quarkus.vertx.http.runtime.security.HttpAuthorizer; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; @@ -50,14 +53,41 @@ public void builtins(BuildProducer producer, HttpBu @BuildStep @Record(ExecutionTime.RUNTIME_INIT) - void initFormAuth( - BeanContainerBuildItem beanContainerBuildItem, + SyntheticBeanBuildItem initFormAuth( HttpSecurityRecorder recorder, HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration) { if (buildTimeConfig.auth.form.enabled) { - recorder.setupFormAuth(beanContainerBuildItem.getValue(), httpConfiguration, buildTimeConfig); + return SyntheticBeanBuildItem.configure(FormAuthenticationMechanism.class) + .types(HttpAuthenticationMechanism.class) + .setRuntimeInit() + .scope(Singleton.class) + .supplier(recorder.setupFormAuth(httpConfiguration, buildTimeConfig)).done(); } + return null; + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + SyntheticBeanBuildItem initBasicAuth( + HttpSecurityRecorder recorder, + HttpBuildTimeConfig buildTimeConfig) { + if (buildTimeConfig.auth.form.enabled && !buildTimeConfig.auth.basic) { + //if form auth is enabled and we are not then we don't install + return null; + } + SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem + .configure(BasicAuthenticationMechanism.class) + .types(HttpAuthenticationMechanism.class) + .setRuntimeInit() + .scope(Singleton.class) + .supplier(recorder.setupBasicAuth(buildTimeConfig)); + if (!buildTimeConfig.auth.form.enabled && !buildTimeConfig.auth.basic) { + //if not explicitly enabled we make this a default bean, so it is the fallback if nothing else is defined + configurator.defaultBean(); + } + + return configurator.done(); } @BuildStep @@ -79,7 +109,6 @@ void setupAuthenticationMechanisms( } if (buildTimeConfig.auth.form.enabled) { - beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(FormAuthenticationMechanism.class)); } else if (buildTimeConfig.auth.basic) { beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(BasicAuthenticationMechanism.class)); } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CombinedFormBasicAuthTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CombinedFormBasicAuthTestCase.java new file mode 100644 index 0000000000000..177d9e16676a9 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CombinedFormBasicAuthTestCase.java @@ -0,0 +1,160 @@ +package io.quarkus.vertx.http.security; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +import java.util.function.Supplier; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; + +public class CombinedFormBasicAuthTestCase { + + private static final String APP_PROPS = "" + + "quarkus.http.auth.basic=true\n" + + "quarkus.http.auth.form.enabled=true\n" + + "quarkus.http.auth.form.login-page=login\n" + + "quarkus.http.auth.form.error-page=error\n" + + "quarkus.http.auth.form.landing-page=landing\n" + + "quarkus.http.auth.policy.r1.roles-allowed=admin\n" + + "quarkus.http.auth.permission.roles1.paths=/admin\n" + + "quarkus.http.auth.permission.roles1.policy=r1\n"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestTrustedIdentityProvider.class, TestIdentityController.class, + PathHandler.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin"); + } + + @Test + public void testFormBasedAuthSuccess() { + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/login")) + .cookie("quarkus-redirect-location", containsString("/admin")); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "admin") + .formParam("j_password", "admin") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/admin")) + .cookie("quarkus-credential", notNullValue()); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(equalTo("admin:/admin")); + + } + + @Test + public void testFormBasedAuthSuccessLandingPage() { + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "admin") + .formParam("j_password", "admin") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/landing")) + .cookie("quarkus-credential", notNullValue()); + + } + + @Test + public void testFormAuthFailure() { + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "admin") + .formParam("j_password", "wrongpassword") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/error")); + + } + + @Test + public void testBasicBasedAuthSuccess() { + RestAssured + .given() + .auth().preemptive().basic("admin", "admin") + .redirects().follow(false) + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(equalTo("admin:/admin")); + + } + + @Test + public void testBasicAuthFailure() { + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .auth().basic("admin", "wrongpassword") + .filter(cookies) + .redirects().follow(false) + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/login")) + .cookie("quarkus-redirect-location", containsString("/admin")); + + } +} 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 e10291c08d6c1..a4d1d7f417e68 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,10 +14,7 @@ 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, unless an - * {@link io.quarkus.security.identity.IdentityProvider} - * is present that supports {@link io.quarkus.security.identity.request.TokenAuthenticationRequest} in which case - * form auth will be the default. + * If no authentication mechanisms are configured basic auth is the default. */ @ConfigItem public boolean basic; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java index e2785fa061048..260057f316bb0 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.regex.Pattern; @@ -39,6 +40,7 @@ import io.quarkus.security.credential.PasswordCredential; 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.vertx.ext.web.RoutingContext; @@ -177,4 +179,14 @@ public CompletionStage getChallenge(RoutingContext context) { challenge); return CompletableFuture.completedFuture(result); } + + @Override + public Set> getCredentialTypes() { + return Collections.singleton(UsernamePasswordAuthenticationRequest.class); + } + + @Override + public HttpCredentialTransport getCredentialTransport() { + return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BASIC); + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java index dd0c5e8dfefa9..c26e108280d34 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java @@ -1,73 +1,49 @@ package io.quarkus.vertx.http.runtime.security; -import java.security.SecureRandom; -import java.util.Base64; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.BiFunction; import java.util.function.Function; -import javax.inject.Singleton; - import org.jboss.logging.Logger; import io.netty.handler.codec.http.HttpHeaderNames; import io.quarkus.security.credential.PasswordCredential; 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.vertx.http.runtime.FormAuthConfig; -import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.vertx.core.Handler; import io.vertx.core.MultiMap; import io.vertx.core.http.Cookie; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; -@Singleton public class FormAuthenticationMechanism implements HttpAuthenticationMechanism { private static final Logger log = Logger.getLogger(FormAuthenticationMechanism.class); public static final String DEFAULT_POST_LOCATION = "/j_security_check"; - private volatile String loginPage; - private volatile String errorPage; - private volatile String postLocation = DEFAULT_POST_LOCATION; - private volatile String locationCookie = "quarkus-redirect-location"; - private volatile String landingPage = "/index.html"; - private volatile boolean redirectAfterLogin; - - private volatile PersistentLoginManager loginManager; - - private static String encryptionKey; - - public FormAuthenticationMechanism() { - } - - public void init(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { - String key; - if (!httpConfiguration.encryptionKey.isPresent()) { - if (encryptionKey != null) { - //persist across dev mode restarts - key = encryptionKey; - } else { - byte[] data = new byte[32]; - new SecureRandom().nextBytes(data); - key = encryptionKey = Base64.getEncoder().encodeToString(data); - log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key); - } - } else { - key = httpConfiguration.encryptionKey.get(); - } - FormAuthConfig form = buildTimeConfig.auth.form; - loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), - form.newCookieInterval.toMillis()); - loginPage = form.loginPage.startsWith("/") ? form.loginPage : "/" + form.loginPage; - errorPage = form.errorPage.startsWith("/") ? form.errorPage : "/" + form.errorPage; - landingPage = form.landingPage.startsWith("/") ? form.landingPage : "/" + form.landingPage; - redirectAfterLogin = form.redirectAfterLogin; + private final String loginPage; + private final String errorPage; + private final String postLocation = DEFAULT_POST_LOCATION; + private final String locationCookie = "quarkus-redirect-location"; + private final String landingPage; + private final boolean redirectAfterLogin; + + private final PersistentLoginManager loginManager; + + public FormAuthenticationMechanism(String loginPage, String errorPage, String landingPage, boolean redirectAfterLogin, + PersistentLoginManager loginManager) { + this.loginPage = loginPage; + this.errorPage = errorPage; + this.landingPage = landingPage; + this.redirectAfterLogin = redirectAfterLogin; + this.loginManager = loginManager; } public CompletionStage runFormAuth(final RoutingContext exchange, @@ -194,4 +170,13 @@ public CompletionStage getChallenge(RoutingContext context) { } } + @Override + public Set> getCredentialTypes() { + return new HashSet<>(Arrays.asList(UsernamePasswordAuthenticationRequest.class, TrustedAuthenticationRequest.class)); + } + + @Override + public HttpCredentialTransport getCredentialTransport() { + return new HttpCredentialTransport(HttpCredentialTransport.Type.POST, postLocation); + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java index fccef05e13d1d..af83311adbdee 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java @@ -1,10 +1,12 @@ package io.quarkus.vertx.http.runtime.security; +import java.util.Set; import java.util.concurrent.CompletionStage; import java.util.function.Function; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; import io.vertx.ext.web.RoutingContext; /** @@ -16,10 +18,23 @@ public interface HttpAuthenticationMechanism { CompletionStage getChallenge(RoutingContext context); + /** + * Returns the required credential types. If there are no identity managers installed that support the + * listed types then this mechanism will not be enabled. + */ + Set> getCredentialTypes(); + default CompletionStage sendChallenge(RoutingContext context) { return getChallenge(context).thenApply(new ChallengeSender(context)); } + /** + * The credential transport, used to make sure multiple incompatible mechanisms are not installed + * + * May be null if this mechanism cannot interfere with other mechanisms + */ + HttpCredentialTransport getCredentialTransport(); + class ChallengeSender implements Function { private final RoutingContext context; 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 84209d9c482a1..6d4a6d48738af 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 @@ -1,8 +1,14 @@ package io.quarkus.vertx.http.runtime.security; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.stream.Collectors; +import java.util.function.Function; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.inject.Instance; @@ -12,7 +18,8 @@ import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; -import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; +import io.quarkus.security.identity.request.AuthenticationRequest; import io.vertx.ext.web.RoutingContext; /** @@ -24,33 +31,55 @@ public class HttpAuthenticator { @Inject IdentityProviderManager identityProviderManager; - final HttpAuthenticationMechanism mechanism; + final HttpAuthenticationMechanism[] mechanisms; public HttpAuthenticator() { - mechanism = null; + mechanisms = null; } @Inject public HttpAuthenticator(Instance instance, - Instance> usernamePassword) { - if (instance.isResolvable()) { - if (instance.isAmbiguous()) { - throw new IllegalStateException("Multiple HTTP authentication mechanisms are not implemented yet, discovered " - + instance.stream().collect(Collectors.toList())); + Instance> providers) { + List mechanisms = new ArrayList<>(); + for (HttpAuthenticationMechanism mechanism : instance) { + boolean notFound = false; + for (Class mechType : mechanism.getCredentialTypes()) { + boolean found = false; + for (IdentityProvider i : providers) { + if (i.getRequestType().equals(mechType)) { + found = true; + break; + } + } + if (!found) { + notFound = true; + break; + } } - mechanism = instance.get(); - } else { - if (!usernamePassword.isUnsatisfied()) { - //TODO: config - mechanism = new BasicAuthenticationMechanism("Quarkus"); - } else { - mechanism = new NoAuthenticationMechanism(); + if (!notFound) { + mechanisms.add(mechanism); } } - } + if (mechanisms.isEmpty()) { + this.mechanisms = new HttpAuthenticationMechanism[] { new NoAuthenticationMechanism() }; + } else { + this.mechanisms = mechanisms.toArray(new HttpAuthenticationMechanism[mechanisms.size()]); + //validate that we don't have multiple incompatible mechanisms + Map map = new HashMap<>(); + for (HttpAuthenticationMechanism i : mechanisms) { + HttpCredentialTransport credentialTransport = i.getCredentialTransport(); + if (credentialTransport == null) { + continue; + } + HttpAuthenticationMechanism existing = map.get(credentialTransport); + if (existing != null) { + throw new RuntimeException("Multiple mechanisms present that use the same credential transport " + + credentialTransport + ". Mechanisms are " + i + " and " + existing); + } + map.put(credentialTransport, i); + } - public HttpAuthenticator(HttpAuthenticationMechanism mechanism) { - this.mechanism = mechanism == null ? new NoAuthenticationMechanism() : mechanism; + } } /** @@ -63,7 +92,22 @@ public HttpAuthenticator(HttpAuthenticationMechanism mechanism) { * If no credentials are present it will resolve to null. */ public CompletionStage attemptAuthentication(RoutingContext routingContext) { - return mechanism.authenticate(routingContext, identityProviderManager); + + CompletionStage result = mechanisms[0].authenticate(routingContext, identityProviderManager); + for (int i = 1; i < mechanisms.length; ++i) { + HttpAuthenticationMechanism mech = mechanisms[i]; + result = result.thenCompose(new Function>() { + @Override + public CompletionStage apply(SecurityIdentity data) { + if (data != null) { + return CompletableFuture.completedFuture(data); + } + return mech.authenticate(routingContext, identityProviderManager); + } + }); + } + + return result; } /** @@ -75,11 +119,37 @@ public CompletionStage sendChallenge(RoutingContext routingContext, Runnab if (closeTask == null) { closeTask = NoopCloseTask.INSTANCE; } - return mechanism.sendChallenge(routingContext).thenRun(closeTask); + CompletionStage result = mechanisms[0].sendChallenge(routingContext); + for (int i = 1; i < mechanisms.length; ++i) { + HttpAuthenticationMechanism mech = mechanisms[i]; + result = result.thenCompose(new Function>() { + @Override + public CompletionStage apply(Boolean aBoolean) { + if (aBoolean) { + return CompletableFuture.completedFuture(true); + } + return mech.sendChallenge(routingContext); + } + }); + } + return result.thenRun(closeTask); } - public CompletionStage getChallenge(RoutingContext context) { - return mechanism.getChallenge(context); + public CompletionStage getChallenge(RoutingContext routingContext) { + CompletionStage result = mechanisms[0].getChallenge(routingContext); + for (int i = 1; i < mechanisms.length; ++i) { + HttpAuthenticationMechanism mech = mechanisms[i]; + result = result.thenCompose(new Function>() { + @Override + public CompletionStage apply(ChallengeData data) { + if (data != null) { + return CompletableFuture.completedFuture(data); + } + return mech.getChallenge(routingContext); + } + }); + } + return result; } static class NoAuthenticationMechanism implements HttpAuthenticationMechanism { @@ -96,6 +166,16 @@ public CompletionStage getChallenge(RoutingContext context) { return CompletableFuture.completedFuture(challengeData); } + @Override + public Set> getCredentialTypes() { + return Collections.singleton(AnonymousAuthenticationRequest.class); + } + + @Override + public HttpCredentialTransport getCredentialTransport() { + return null; + } + } static class NoopCloseTask implements Runnable { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java new file mode 100644 index 0000000000000..78b89a527f72e --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java @@ -0,0 +1,73 @@ +package io.quarkus.vertx.http.runtime.security; + +import java.util.Objects; + +/** + * A representation of HTTP credential transport. In particular this includes things such as: + * + * Cookies + * Authorization header + * POST + * + * It is not permitted for multiple HTTP authentication mechanisms to use the same credential + * transport type, as they will not be able to figure out which mechanisms should process which + * request. + */ +public class HttpCredentialTransport { + + private final Type transportType; + private final String typeTarget; + + public HttpCredentialTransport(Type transportType, String typeTarget) { + this.transportType = Objects.requireNonNull(transportType); + this.typeTarget = Objects.requireNonNull(typeTarget).toLowerCase(); + } + + public enum Type { + /** + * A cookie. The type target is the cookie name + */ + COOKIE, + /** + * Auth header, type target is the auth type (basic, bearer etc) + */ + AUTHORIZATION, + /** + * A different header, type target is the header name + */ + OTHER_HEADER, + /** + * A post request, target is the POST URI + */ + POST + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + HttpCredentialTransport that = (HttpCredentialTransport) o; + + if (transportType != that.transportType) + return false; + return typeTarget != null ? typeTarget.equals(that.typeTarget) : that.typeTarget == null; + } + + @Override + public int hashCode() { + int result = transportType != null ? transportType.hashCode() : 0; + result = 31 * result + (typeTarget != null ? typeTarget.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "HttpCredentialTransport{" + + "transportType=" + transportType + + ", typeTarget='" + typeTarget + '\'' + + '}'; + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 1654fbd8113ec..4a548a67a3b56 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -1,5 +1,7 @@ package io.quarkus.vertx.http.runtime.security; +import java.security.SecureRandom; +import java.util.Base64; import java.util.Map; import java.util.concurrent.CompletionException; import java.util.function.BiFunction; @@ -8,11 +10,14 @@ import javax.enterprise.inject.spi.CDI; +import org.jboss.logging.Logger; + import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.arc.runtime.BeanContainerListener; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.FormAuthConfig; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.vertx.core.Handler; @@ -22,6 +27,11 @@ @Recorder public class HttpSecurityRecorder { + private static final Logger log = Logger.getLogger(HttpSecurityRecorder.class); + + //the temp encryption key, persistent across dev mode restarts + static volatile String encryptionKey; + public Handler authenticationMechanismHandler() { return new Handler() { @@ -106,8 +116,44 @@ public void created(BeanContainer container) { }; } - public void setupFormAuth(BeanContainer container, HttpConfiguration httpConfiguration, + public Supplier setupFormAuth(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { - container.instance(FormAuthenticationMechanism.class).init(httpConfiguration, buildTimeConfig); + + return new Supplier() { + @Override + public FormAuthenticationMechanism get() { + String key; + if (!httpConfiguration.encryptionKey.isPresent()) { + if (encryptionKey != null) { + //persist across dev mode restarts + key = encryptionKey; + } else { + byte[] data = new byte[32]; + new SecureRandom().nextBytes(data); + key = encryptionKey = Base64.getEncoder().encodeToString(data); + log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key); + } + } else { + key = httpConfiguration.encryptionKey.get(); + } + FormAuthConfig form = buildTimeConfig.auth.form; + PersistentLoginManager loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), + form.newCookieInterval.toMillis()); + String loginPage = form.loginPage.startsWith("/") ? form.loginPage : "/" + form.loginPage; + String errorPage = form.errorPage.startsWith("/") ? form.errorPage : "/" + form.errorPage; + String landingPage = form.landingPage.startsWith("/") ? form.landingPage : "/" + form.landingPage; + boolean redirectAfterLogin = form.redirectAfterLogin; + return new FormAuthenticationMechanism(loginPage, errorPage, landingPage, redirectAfterLogin, loginManager); + } + }; + } + + public Supplier setupBasicAuth(HttpBuildTimeConfig buildTimeConfig) { + return new Supplier() { + @Override + public BasicAuthenticationMechanism get() { + return new BasicAuthenticationMechanism(buildTimeConfig.auth.realm, "BASIC", buildTimeConfig.auth.form.enabled); + } + }; } }