Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a way to observe security events #37472

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/cdi.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -471,11 +471,12 @@
<5> Decorators can inject other beans.
<6> The decorator may invoke any method of the delegate object. And the container invokes either the next decorator in the chain or the business method of the intercepted instance.

NOTE: Instances of decorators are dependent objects of the bean instance they intercept, i.e. a new decorator instance is created for each intercepted bean.

Check failure on line 474 in docs/src/main/asciidoc/cdi.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsErrors] Use 'you' rather than 'i'. Raw Output: {"message": "[Quarkus.TermsErrors] Use 'you' rather than 'i'.", "location": {"path": "docs/src/main/asciidoc/cdi.adoc", "range": {"start": {"line": 474, "column": 90}}}, "severity": "ERROR"}

Check warning on line 474 in docs/src/main/asciidoc/cdi.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'that is' rather than 'i.e.' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'that is' rather than 'i.e.' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/cdi.adoc", "range": {"start": {"line": 474, "column": 90}}}, "severity": "WARNING"}

[[events-and-observers]]
=== Events and Observers

Check warning on line 477 in docs/src/main/asciidoc/cdi.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in '14.4. Events and Observers'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in '14.4. Events and Observers'.", "location": {"path": "docs/src/main/asciidoc/cdi.adoc", "range": {"start": {"line": 477, "column": 1}}}, "severity": "INFO"}

Check warning on line 477 in docs/src/main/asciidoc/cdi.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.HeadingPunctuation] Do not use end punctuation in headings. Raw Output: {"message": "[Quarkus.HeadingPunctuation] Do not use end punctuation in headings.", "location": {"path": "docs/src/main/asciidoc/cdi.adoc", "range": {"start": {"line": 477, "column": 1}}}, "severity": "INFO"}

Beans may also produce and consume events to interact in a completely decoupled fashion.

Check warning on line 479 in docs/src/main/asciidoc/cdi.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/cdi.adoc", "range": {"start": {"line": 479, "column": 7}}}, "severity": "WARNING"}
Any Java object can serve as an event payload.
The optional qualifiers act as topic selectors.

Expand Down
73 changes: 73 additions & 0 deletions docs/src/main/asciidoc/security-customization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,79 @@
to make sure that Quarkus can propagate it. For more information see the
xref:context-propagation.adoc[Context Propagation Guide].

[[observe-security-events]]
== Observe security events

Quarkus beans can use xref:cdi.adoc#events-and-observers[CDI observers] to consume authentication and authorization security events.
michalvavrik marked this conversation as resolved.
Show resolved Hide resolved
The observers can be either synchronous or asynchronous.

.List of supported security events

* `io.quarkus.security.spi.runtime.AuthenticationFailureEvent`
* `io.quarkus.security.spi.runtime.AuthenticationSuccessEvent`
* `io.quarkus.security.spi.runtime.AuthorizationFailureEvent`
* `io.quarkus.security.spi.runtime.AuthorizationSuccessEvent`
* `io.quarkus.oidc.SecurityEvent`

[[TIP]]
For more information about security events specific to the Quarkus OpenID Connect extension, please see
the xref:security-oidc-code-flow-authentication.adoc#listen-to-authentication-events[Listening to important authentication events]
section of the OIDC code flow mechanism for protecting web applications guide.

[source,java]
----
package org.acme.security;

import io.quarkus.security.spi.runtime.AuthenticationFailureEvent;
import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent;
import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
import io.quarkus.security.spi.runtime.SecurityEvent;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.ObservesAsync;
import org.jboss.logging.Logger;

public class SecurityEventObserver {

private static final Logger LOG = Logger.getLogger(SecurityEventObserver.class.getName());

void observeAuthenticationSuccess(@ObservesAsync AuthenticationSuccessEvent event) { <1>
LOG.debugf("User '%s' has authenticated successfully", event.getSecurityIdentity().getPrincipal().getName());
}

void observeAuthenticationFailure(@ObservesAsync AuthenticationFailureEvent event) {
RoutingContext routingContext = (RoutingContext) event.getEventProperties().get(RoutingContext.class.getName());
LOG.debugf("Authentication failed, request path: '%s'", routingContext.request().path());
}

void observeAuthorizationSuccess(@ObservesAsync AuthorizationSuccessEvent event) {
String principalName = getPrincipalName(event);
if (principalName != null) {
LOG.debugf("User '%s' has been authorized successfully", event.getSecurityIdentity().getPrincipal().getName());
}
}

void observeAuthorizationFailure(@Observes AuthorizationFailureEvent event) {
LOG.debugf(event.getAuthorizationFailure(), "User '%s' authorization failed", event.getSecurityIdentity().getPrincipal().getName());
}

private static String getPrincipalName(SecurityEvent event) { <2>
if (event.getSecurityIdentity() != null) {
return event.getSecurityIdentity().getPrincipal().getName();
}
return null;
}

}
michalvavrik marked this conversation as resolved.
Show resolved Hide resolved
----
<1> This observer consumes all the `AuthenticationSuccessEvent` events asynchronously, which means that HTTP request processing will continue regardless on the event processing.

Check warning on line 689 in docs/src/main/asciidoc/security-customization.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than ', that'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than ', that'.", "location": {"path": "docs/src/main/asciidoc/security-customization.adoc", "range": {"start": {"line": 689, "column": 141}}}, "severity": "INFO"}

Check warning on line 689 in docs/src/main/asciidoc/security-customization.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'many' or 'much' rather than 'a lot of' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'many' or 'much' rather than 'a lot of' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/security-customization.adoc", "range": {"start": {"line": 689, "column": 155}}}, "severity": "WARNING"}
Depending on the application, that can be a lot of the `AuthenticationSuccessEvent` events.
For that reason, asynchronous processing can have positive effect on performance.
<2> Common code for all supported security event types is possible because they all implement the `io.quarkus.security.spi.runtime.SecurityEvent` interface.

IMPORTANT: The gRPC extension currently does not support security events.

== References

* xref:security-overview.adoc[Quarkus Security overview]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1214,10 +1214,10 @@
You must ensure that the callback path you enter in the GitHub OAuth application configuration matches the endpoint path where you want the user to be redirected after a successful GitHub authentication and application authorization.
In this case, it has to be set to `http:localhost:8080/github/userinfo`.


[[listen-to-authentication-events]]
=== Listening to important authentication events

You can register the `@ApplicationScoped` bean which will observe important OIDC authentication events.

Check warning on line 1220 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 1220, "column": 36}}}, "severity": "INFO"}
When a user logs in for the first time, re-authenticates, or refreshes the session, the listener is updated.
In the future, more events might be reported.
For example:
Expand All @@ -1243,6 +1243,8 @@
}
----

TIP: You can listen to other security events as described in the xref:security-customization.adoc#observe-security-events[Observe security events] section of the Security Tips and Tricks guide.

=== Propagating tokens to downstream services

For information about Authorization Code Flow access token propagation to downstream services, see the xref:security-openid-connect-client-reference.adoc#token-propagation[Token Propagation] section.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem;
import io.quarkus.arc.deployment.RecorderBeanInitializedBuildItem;
import io.quarkus.arc.deployment.SynthesisFinishedBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.arc.processor.BuiltinScope;
import io.quarkus.arc.processor.ObserverInfo;
import io.quarkus.deployment.ApplicationArchive;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
Expand All @@ -67,6 +69,7 @@
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
import io.quarkus.deployment.builditem.ServiceStartBuildItem;
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
Expand All @@ -90,6 +93,7 @@
import io.quarkus.kubernetes.spi.KubernetesPortBuildItem;
import io.quarkus.netty.deployment.MinNettyAllocatorMaxOrderBuildItem;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.security.spi.runtime.SecurityEvent;
import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem;
import io.quarkus.vertx.deployment.VertxBuildItem;
import io.quarkus.vertx.http.deployment.VertxWebRouterBuildItem;
Expand Down Expand Up @@ -792,4 +796,36 @@ void initGrpcSecurityInterceptor(List<BindableServiceBuildItem> bindables, Capab
}
}

@Record(RUNTIME_INIT)
@Consume(RuntimeConfigSetupCompleteBuildItem.class)
@BuildStep
void validateSecurityEventsNotObserved(SynthesisFinishedBuildItem synthesisFinished,
Capabilities capabilities,
GrpcSecurityRecorder recorder,
BeanArchiveIndexBuildItem indexBuildItem) {
if (!capabilities.isPresent(Capability.SECURITY)) {
return;
}

// collect all SecurityEvent classes
Set<DotName> knownSecurityEventClasses = new HashSet<>();
knownSecurityEventClasses.add(DotName.createSimple(SecurityEvent.class));
indexBuildItem
.getIndex()
.getAllKnownImplementors(SecurityEvent.class)
.stream()
.map(ClassInfo::name)
.forEach(knownSecurityEventClasses::add);

// find at least one CDI observer and validate security events are disabled
knownClasses: for (DotName knownSecurityEventClass : knownSecurityEventClasses) {
for (ObserverInfo observer : synthesisFinished.getObservers()) {
if (observer.getObservedType().name().equals(knownSecurityEventClass)) {
recorder.validateSecurityEventsDisabled(knownSecurityEventClass.toString());
break knownClasses;
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.grpc.auth;

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

import jakarta.enterprise.event.Observes;

import io.quarkus.security.spi.runtime.SecurityEvent;

public class SecurityEventObserver {

private final List<SecurityEvent> storage = new CopyOnWriteArrayList<>();

void observe(@Observes SecurityEvent event) {
storage.add(event);
}

List<SecurityEvent> getStorage() {
return storage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.grpc.auth;

import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.example.security.SecuredService;

import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.test.QuarkusUnitTest;

public class SecurityEventsValidationFailureTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(jar -> jar
.addClass(SecurityEventObserver.class)
.addPackage(SecuredService.class.getPackage()))
.assertException(throwable -> {
assertTrue(throwable instanceof ConfigurationException);
assertTrue(throwable.getMessage().contains("quarkus.security.events.enabled"));
});

@Test
void test() {
// must be here to run test
Assertions.fail();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.microprofile.config.ConfigProvider;

import io.grpc.BindableService;
import io.grpc.ServerMethodDefinition;
import io.quarkus.arc.runtime.BeanContainer;
import io.quarkus.grpc.runtime.GrpcContainer;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.runtime.configuration.ConfigurationException;

@Recorder
public class GrpcSecurityRecorder {
Expand Down Expand Up @@ -43,4 +47,17 @@ public void initGrpcSecurityInterceptor(Map<String, List<String>> serviceClassTo
container.beanInstance(GrpcSecurityInterceptor.class).init(svcToMethods);
}

public void validateSecurityEventsDisabled(String observedSecurityEvent) {
boolean securityEventsEnabled = ConfigProvider
.getConfig()
.getOptionalValue("quarkus.security.events.enabled", boolean.class)
.orElse(Boolean.TRUE);
if (securityEventsEnabled) {
throw new ConfigurationException("""
Found observer method for event type '%s', but the gRPC extension does not support security
events. Either disable security events with the 'quarkus.security.events.enabled'
sberyozkin marked this conversation as resolved.
Show resolved Hide resolved
configuration property, or remove security event CDI observers.""".formatted(observedSecurityEvent),
Set.of("quarkus.security.events.enabled"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem;
import io.quarkus.arc.deployment.QualifierRegistrarBuildItem;
import io.quarkus.arc.deployment.SynthesisFinishedBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.processor.InjectionPointInfo;
import io.quarkus.arc.processor.QualifierRegistrar;
Expand All @@ -37,14 +36,11 @@
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.Tenant;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.TenantIdentityProvider;
Expand Down Expand Up @@ -77,7 +73,6 @@

@BuildSteps(onlyIf = OidcBuildStep.IsEnabled.class)
public class OidcBuildStep {
public static final DotName DOTNAME_SECURITY_EVENT = DotName.createSimple(SecurityEvent.class.getName());
private static final DotName TENANT_NAME = DotName.createSimple(Tenant.class);
private static final DotName TENANT_FEATURE_NAME = DotName.createSimple(TenantFeature.class);
private static final DotName TENANT_IDENTITY_PROVIDER_NAME = DotName.createSimple(TenantIdentityProvider.class);
Expand Down Expand Up @@ -224,18 +219,6 @@ public SyntheticBeanBuildItem setup(
.done();
}

// Note that DefaultTenantConfigResolver injects quarkus.http.proxy.enable-forwarded-prefix
@Consume(RuntimeConfigSetupCompleteBuildItem.class)
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
public void findSecurityEventObservers(
OidcRecorder recorder,
SynthesisFinishedBuildItem synthesisFinished) {
boolean isSecurityEventObserved = synthesisFinished.getObservers().stream()
.anyMatch(observer -> observer.asObserver().getObservedType().name().equals(DOTNAME_SECURITY_EVENT));
recorder.setSecurityEventObserved(isSecurityEventObserved);
}

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
public void produceTenantResolverInterceptors(CombinedIndexBuildItem indexBuildItem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import java.util.Map;

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

/**
* Security event.
*
*/
public class SecurityEvent {
public class SecurityEvent extends AbstractSecurityEvent {
public static final String SESSION_TOKENS_PROPERTY = "session-tokens";

public enum Type {
Expand Down Expand Up @@ -58,30 +59,19 @@ public enum Type {
}

private final Type eventType;
private final SecurityIdentity securityIdentity;
private final Map<String, Object> eventProperties;

public SecurityEvent(Type eventType, SecurityIdentity securityIdentity) {
super(securityIdentity, null);
this.eventType = eventType;
this.securityIdentity = securityIdentity;
this.eventProperties = Map.of();
}

public SecurityEvent(Type eventType, Map<String, Object> eventProperties) {
super(null, eventProperties);
this.eventType = eventType;
this.securityIdentity = null;
this.eventProperties = eventProperties;
}

public Type getEventType() {
return eventType;
}

public SecurityIdentity getSecurityIdentity() {
return securityIdentity;
}

public Map<String, Object> getEventProperties() {
return eventProperties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.SecurityEvent.Type;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpHeaders;
Expand Down Expand Up @@ -97,7 +98,7 @@ public void accept(MultiMap form) {
tokens.addTokenVerification(key, result);

if (resolver.isSecurityEventObserved()) {
resolver.getSecurityEvent().fire(
SecurityEventHelper.fire(resolver.getSecurityEvent(),
new SecurityEvent(Type.OIDC_BACKCHANNEL_LOGOUT_INITIATED,
Map.of(OidcConstants.BACK_CHANNEL_LOGOUT_TOKEN, result)));
}
Expand Down
Loading
Loading