Skip to content

Commit

Permalink
Merge pull request quarkusio#39493 from michalvavrik/feature/add-form…
Browse files Browse the repository at this point in the history
…-auth-login-event

Fire SecurityEvent on Form authentication login success
  • Loading branch information
sberyozkin authored Mar 19, 2024
2 parents b1029b8 + e019cbb commit bc6157b
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/security-customization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ The observers can be either synchronous or asynchronous.
* `io.quarkus.security.spi.runtime.AuthorizationFailureEvent`
* `io.quarkus.security.spi.runtime.AuthorizationSuccessEvent`
* `io.quarkus.oidc.SecurityEvent`
* `io.quarkus.vertx.http.runtime.security.FormAuthenticationEvent`

[[TIP]]
For more information about security events specific to the Quarkus OpenID Connect extension, please see
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,26 @@
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Supplier;

import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.ObservesAsync;

import org.awaitility.Awaitility;
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.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.vertx.http.runtime.security.FormAuthenticationEvent;
import io.restassured.RestAssured;
import io.restassured.filter.cookie.CookieFilter;

Expand All @@ -39,7 +47,7 @@ public class FormBasicAuthHttpRootTestCase {
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(TestIdentityProvider.class, TestTrustedIdentityProvider.class, TestIdentityController.class,
PathHandler.class)
PathHandler.class, FormAuthEventObserver.class)
.addAsResource(new StringAsset(APP_PROPS), "application.properties");
}
});
Expand All @@ -52,6 +60,9 @@ public static void setup() {

@Test
public void testFormBasedAuthSuccess() {
Assertions.assertEquals(0, FormAuthEventObserver.syncEvents.size());
Assertions.assertEquals(0, FormAuthEventObserver.asyncEvents.size());

CookieFilter cookies = new CookieFilter();
RestAssured
.given()
Expand All @@ -66,6 +77,9 @@ public void testFormBasedAuthSuccess() {
.cookie("quarkus-redirect-location",
detailedCookie().value(containsString("/root/admin")).path(equalTo("/root")));

Assertions.assertEquals(0, FormAuthEventObserver.syncEvents.size());
Assertions.assertEquals(0, FormAuthEventObserver.asyncEvents.size());

RestAssured
.given()
.filter(cookies)
Expand All @@ -81,6 +95,15 @@ public void testFormBasedAuthSuccess() {
.cookie("quarkus-credential",
detailedCookie().value(notNullValue()).path(equalTo("/root")));

Assertions.assertEquals(1, FormAuthEventObserver.syncEvents.size());
var event = FormAuthEventObserver.syncEvents.get(0);
Assertions.assertNotNull(event.getSecurityIdentity());
Assertions.assertEquals("admin", event.getSecurityIdentity().getPrincipal().getName());
String eventType = (String) event.getEventProperties().get(FormAuthenticationEvent.FORM_CONTEXT);
Assertions.assertNotNull(eventType);
Assertions.assertEquals(FormAuthenticationEvent.FormEventType.FORM_LOGIN.toString(), eventType);
Awaitility.await().untilAsserted(() -> Assertions.assertEquals(1, FormAuthEventObserver.asyncEvents.size()));

RestAssured
.given()
.filter(cookies)
Expand All @@ -92,5 +115,20 @@ public void testFormBasedAuthSuccess() {
.statusCode(200)
.body(equalTo("admin:/root/admin"));

Assertions.assertEquals(1, FormAuthEventObserver.syncEvents.size());
Assertions.assertEquals(1, FormAuthEventObserver.asyncEvents.size());
}

public static class FormAuthEventObserver {
private static final List<FormAuthenticationEvent> syncEvents = new CopyOnWriteArrayList<>();
private static final List<FormAuthenticationEvent> asyncEvents = new CopyOnWriteArrayList<>();

void observe(@Observes FormAuthenticationEvent event) {
syncEvents.add(event);
}

void observeAsync(@ObservesAsync FormAuthenticationEvent event) {
asyncEvents.add(event);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.vertx.http.runtime.security;

import java.util.Map;

import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.spi.runtime.AbstractSecurityEvent;

public final class FormAuthenticationEvent extends AbstractSecurityEvent {

public static final String FORM_CONTEXT = "io.quarkus.vertx.http.runtime.security.FormAuthenticationEvent#CONTEXT";

public enum FormEventType {
/**
* Event fired when a user was successfully authenticated with a call to the Form mechanism POST location.
*/
FORM_LOGIN
}

private FormAuthenticationEvent(SecurityIdentity securityIdentity, Map<String, Object> eventProperties) {
super(securityIdentity, eventProperties);
}

static FormAuthenticationEvent createLoginEvent(SecurityIdentity identity) {
return new FormAuthenticationEvent(identity, Map.of(FORM_CONTEXT, FormEventType.FORM_LOGIN.toString()));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.quarkus.vertx.http.runtime.security;

import static io.quarkus.security.spi.runtime.SecurityEventHelper.fire;
import static io.quarkus.vertx.http.runtime.security.FormAuthenticationEvent.createLoginEvent;

import java.net.URI;
import java.security.SecureRandom;
import java.util.Arrays;
Expand All @@ -9,8 +12,11 @@
import java.util.Set;
import java.util.function.Consumer;

import jakarta.enterprise.event.Event;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.inject.Inject;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

import io.netty.handler.codec.http.HttpHeaderNames;
Expand All @@ -22,6 +28,7 @@
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.security.identity.request.TrustedAuthenticationRequest;
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.vertx.http.runtime.FormAuthConfig;
import io.quarkus.vertx.http.runtime.FormAuthRuntimeConfig;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
Expand Down Expand Up @@ -52,16 +59,19 @@ public class FormAuthenticationMechanism implements HttpAuthenticationMechanism
private final boolean redirectToLoginPage;
private final CookieSameSite cookieSameSite;
private final String cookiePath;

private final boolean isFormAuthEventObserver;
private final PersistentLoginManager loginManager;
private final Event<FormAuthenticationEvent> formAuthEvent;

//the temp encryption key, persistent across dev mode restarts
static volatile String encryptionKey;

@Inject
FormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) {
FormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig,
Event<FormAuthenticationEvent> formAuthEvent, BeanManager beanManager,
@ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled) {
String key;
if (!httpConfiguration.encryptionKey.isPresent()) {
if (httpConfiguration.encryptionKey.isEmpty()) {
if (encryptionKey != null) {
//persist across dev mode restarts
key = encryptionKey;
Expand Down Expand Up @@ -92,6 +102,9 @@ public class FormAuthenticationMechanism implements HttpAuthenticationMechanism
this.redirectToLoginPage = loginPage != null;
this.redirectToErrorPage = errorPage != null;
this.cookieSameSite = CookieSameSite.valueOf(runtimeForm.cookieSameSite.name());
this.isFormAuthEventObserver = SecurityEventHelper.isEventObserved(createLoginEvent(null), beanManager,
securityEventsEnabled);
this.formAuthEvent = this.isFormAuthEventObserver ? formAuthEvent : null;
}

public FormAuthenticationMechanism(String loginPage, String postLocation,
Expand All @@ -111,6 +124,8 @@ public FormAuthenticationMechanism(String loginPage, String postLocation,
this.cookieSameSite = CookieSameSite.valueOf(cookieSameSite);
this.cookiePath = cookiePath;
this.loginManager = loginManager;
this.isFormAuthEventObserver = false;
this.formAuthEvent = null;
}

public Uni<SecurityIdentity> runFormAuth(final RoutingContext exchange,
Expand Down Expand Up @@ -141,6 +156,10 @@ public void handle(Void event) {
.subscribe().with(new Consumer<SecurityIdentity>() {
@Override
public void accept(SecurityIdentity identity) {
if (isFormAuthEventObserver) {
fire(formAuthEvent, createLoginEvent(identity));
}

try {
loginManager.save(identity, exchange, null, exchange.request().isSSL());
if (redirectToLandingPage
Expand Down

0 comments on commit bc6157b

Please sign in to comment.