Skip to content

Commit

Permalink
Add form based authentication
Browse files Browse the repository at this point in the history
Fixes #4348
  • Loading branch information
stuartwdouglas committed Oct 24, 2019
1 parent 7e35142 commit c7ed0c5
Show file tree
Hide file tree
Showing 15 changed files with 697 additions and 21 deletions.
28 changes: 28 additions & 0 deletions docs/src/main/asciidoc/security-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,34 @@ very much a work in progress, so this list will be expanded over the coming week

Please see the linked documents above for details on how to setup the various extensions.

### Authenticating Via HTTP

Quarkus has two built in authentication mechanisms for HTTP based FORM and BASIC auth. This mechanism is pluggable
however so extensions can add additional mechanisms (most notably OpenID Connect for Keycloak based auth).

#### Basic Authentication

To enable basic authentication set `quarkus.http.auth.basic=true`. You must also have at least one extension installed
that provides a username/password based `IdentityProvider`, such as link:security-jdbc-guide.html[Elytron JDBC].

#### Form Based Authentication

Quarkus provides form based authentication that works in a similar manner to traditional Servlet form based auth. Unlike
traditional form authentication the authenticated user is not stored in a HTTP session, as Quarkus does not provide
clustered HTTP session support. Instead the authentication information is stored in an encrypted cookie, which can
be read by all members of the cluster (provided they all share the same encryption key).

The encryption key can be set using the `auth.session.encryption-key` property, and it must be at least 16 characters
long. This key is hashed using SHA-256 and the resulting digest is used as a key for AES-256 encryption of the cookie
value. This cookie contains a expiry time as part of the encrypted value, so all nodes in the cluster must have their
clocks synchronised. At one minute intervals a new cookie will be generated with an updated expiry time if the session
is in use.

The following properties can be used to configure form based auth:

include::{generated-dir}/config/quarkus-http-auth-form.adoc[opts=optional, leveloffset=+1]


### Securing Web Endpoints

Quarkus has an integrated plugable web security layer. If security is enabled all HTTP requests will have a permission
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.quarkus.elytron.security.runtime.ElytronRecorder;
import io.quarkus.elytron.security.runtime.ElytronSecurityDomainManager;
import io.quarkus.elytron.security.runtime.ElytronTokenIdentityProvider;
import io.quarkus.elytron.security.runtime.ElytronTrustedIdentityProvider;
import io.quarkus.runtime.RuntimeValue;

/**
Expand Down Expand Up @@ -56,12 +57,12 @@ void addBeans(BuildProducer<AdditionalBeanBuildItem> beans, List<ElytronPassword
List<ElytronTokenMarkerBuildItem> token) {
beans.produce(AdditionalBeanBuildItem.unremovableOf(ElytronSecurityDomainManager.class));
beans.produce(AdditionalBeanBuildItem.unremovableOf(DefaultRoleDecoder.class));

if (!token.isEmpty()) {
beans.produce(AdditionalBeanBuildItem.unremovableOf(ElytronTokenIdentityProvider.class));
}
if (!pw.isEmpty()) {
beans.produce(AdditionalBeanBuildItem.unremovableOf(ElytronPasswordIdentityProvider.class));
beans.produce(AdditionalBeanBuildItem.unremovableOf(ElytronTrustedIdentityProvider.class));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.quarkus.elytron.security.runtime;

import java.util.concurrent.CompletionStage;
import java.util.function.Supplier;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import org.jboss.logging.Logger;
import org.wildfly.security.auth.server.RealmIdentity;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.auth.server.SecurityDomain;
import org.wildfly.security.auth.server.ServerAuthenticationContext;
import org.wildfly.security.credential.PasswordCredential;

import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.vertx.http.runtime.security.TrustedAuthenticationRequest;

/**
*
*
*/
@ApplicationScoped
public class ElytronTrustedIdentityProvider implements IdentityProvider<TrustedAuthenticationRequest> {

private static Logger log = Logger.getLogger(ElytronTrustedIdentityProvider.class);

@Inject
SecurityDomain domain;

@Override
public Class<TrustedAuthenticationRequest> getRequestType() {
return TrustedAuthenticationRequest.class;
}

@Override
public CompletionStage<SecurityIdentity> authenticate(TrustedAuthenticationRequest request,
AuthenticationRequestContext context) {
return context.runBlocking(new Supplier<SecurityIdentity>() {
@Override
public SecurityIdentity get() {
org.wildfly.security.auth.server.SecurityIdentity result;
try {
RealmIdentity id = domain.getIdentity(request.getPrincipal());
if (!id.exists()) {
return null;
}
PasswordCredential cred = id.getCredential(PasswordCredential.class);
ServerAuthenticationContext ac = domain.createNewAuthenticationContext();
ac.setAuthenticationName(request.getPrincipal());
ac.addPrivateCredential(cred);
ac.authorize();
result = ac.getAuthorizedIdentity();

if (result == null) {
throw new AuthenticationFailedException();
}
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
builder.setPrincipal(result.getPrincipal());
for (String i : result.getRoles()) {
builder.addRole(i);
}
return builder.build();
} catch (RealmUnavailableException e) {
throw new RuntimeException(e);
} catch (SecurityException e) {
log.debug("Authentication failed", e);
throw new AuthenticationFailedException();
}
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,23 @@ public Response toResponse(UnauthorizedException exception) {
}
}
}
HttpAuthenticator authenticator = identity.getAttribute(HttpAuthenticator.class.getName());
RoutingContext context = ResteasyContext.getContextData(RoutingContext.class);
if (authenticator != null && context != null) {
try {
ChallengeData challengeData = authenticator.getChallenge(context)
.toCompletableFuture()
.get();
return Response.status(challengeData.status)
.header(challengeData.headerName.toString(), challengeData.headerContent)
.build();
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to read challenge data for unauthorized response", e);
return Response.status(401).entity("Not authorized").build();
if (context != null) {
HttpAuthenticator authenticator = context.get(HttpAuthenticator.class.getName());
if (authenticator != null) {
try {
ChallengeData challengeData = authenticator.getChallenge(context)
.toCompletableFuture()
.get();
return Response.status(challengeData.status)
.header(challengeData.headerName.toString(), challengeData.headerContent)
.build();
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to read challenge data for unauthorized response", e);
return Response.status(401).entity("Not authorized").build();
}
}
} else {
return Response.status(401).entity("Not authorized").build();
}
return Response.status(401).entity("Not authorized").build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@
import java.util.function.Supplier;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.BeanContainerListenerBuildItem;
import io.quarkus.deployment.Capabilities;
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.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.HttpConfiguration;
import io.quarkus.vertx.http.runtime.PolicyConfig;
import io.quarkus.vertx.http.runtime.security.AuthenticatedHttpSecurityPolicy;
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.HttpAuthenticator;
import io.quarkus.vertx.http.runtime.security.HttpAuthorizer;
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
Expand All @@ -40,6 +43,18 @@ public void builtins(BuildProducer<HttpSecurityPolicyBuildItem> producer, HttpBu
}
}

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void initFormAuth(
BeanContainerBuildItem beanContainerBuildItem,
HttpSecurityRecorder recorder,
HttpBuildTimeConfig buildTimeConfig,
HttpConfiguration httpConfiguration) {
if (buildTimeConfig.auth.form.enabled) {
recorder.setupFormAuth(beanContainerBuildItem.getValue(), httpConfiguration, buildTimeConfig);
}
}

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void setupAuthenticationMechanisms(
Expand All @@ -58,7 +73,9 @@ void setupAuthenticationMechanisms(
policyMap.put(e.getName(), e.policySupplier);
}

if (buildTimeConfig.auth.basic) {
if (buildTimeConfig.auth.form.enabled) {
beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(FormAuthenticationMechanism.class));
} else if (buildTimeConfig.auth.basic) {
beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(BasicAuthenticationMechanism.class));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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 FormAuthTestCase {

private static final String APP_PROPS = "" +
"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<JavaArchive>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(TestIdentityProvider.class, TestTrustedIdentityProvider.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"));

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkus.vertx.http.security;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

import javax.inject.Singleton;

import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.vertx.http.runtime.security.TrustedAuthenticationRequest;

@Singleton
public class TestTrustedIdentityProvider implements IdentityProvider<TrustedAuthenticationRequest> {
@Override
public Class<TrustedAuthenticationRequest> getRequestType() {
return TrustedAuthenticationRequest.class;
}

@Override
public CompletionStage<SecurityIdentity> authenticate(TrustedAuthenticationRequest request,
AuthenticationRequestContext context) {
TestIdentityController.TestIdentity ident = TestIdentityController.idenitities.get(request.getPrincipal());
if (ident == null) {
return CompletableFuture.completedFuture(null);
}
return CompletableFuture
.completedFuture(QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(request.getPrincipal()))
.addRoles(ident.roles).build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ public class AuthConfig {
public boolean basic;

/**
* If form auth should be enabled.
* Form Auth config
*/
@ConfigItem
public boolean form;
public FormAuthConfig form;

/**
* The authentication realm
Expand Down
Loading

0 comments on commit c7ed0c5

Please sign in to comment.