diff --git a/docs/src/main/asciidoc/_spans.adoc b/docs/src/main/asciidoc/_spans.adoc
index bca12b758..6088775f8 100644
--- a/docs/src/main/asciidoc/_spans.adoc
+++ b/docs/src/main/asciidoc/_spans.adoc
@@ -532,6 +532,22 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in
|method|Method name that got annotated with @Scheduled.
|===
+=== Security Context Change
+
+> Indicates that a SecurityContextChangedEvent happened during the current span.
+
+**Span name** `Security Context Change`.
+
+Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan`
+
+.Event Values
+|===
+|Name | Description
+|Authentication cleared %s|Event created when an Authentication object is removed from the SecurityContext. (since the name contains `%s` the final value will be resolved at runtime)
+|Authentication replaced %s|Event created when an Authentication object is replaced with a new one in the SecurityContext. (since the name contains `%s` the final value will be resolved at runtime)
+|Authentication set %s|Event created when an Authentication object is added to the SecurityContext. (since the name contains `%s` the final value will be resolved at runtime)
+|===
+
=== Session Create Span
> Span created when a new session has to be created.
diff --git a/pom.xml b/pom.xml
index 31bcb1582..bdac48726 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,8 @@
5.13.2
0.32.0
2.3.4.RELEASE
+ 2.2.0.RELEASE
+ 5.6.0-M2
false
4.9.0
4.8.0
@@ -83,7 +85,6 @@
1.7.1
3.3.0
3.0.1
- 2.2.0.RELEASE
3.9.1
1.7
10.0.6
@@ -267,11 +268,22 @@
import
pom
+
+ org.springframework.security
+ spring-security-bom
+ ${spring-security-version}
+ pom
+ import
+
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
${spring-security-boot-autoconfigure.version}
- true
+
+
+ org.springframework.security.oauth
+ spring-security-oauth2
+ ${spring-security-oauth2.version}
cglib
@@ -289,11 +301,6 @@
mockwebserver
${mockwebserver.version}
-
- org.springframework.security.oauth
- spring-security-oauth2
- ${spring-security-oauth2.version}
-
io.zipkin.aws
brave-propagation-aws
diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/security/TraceSecurityAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/security/TraceSecurityAutoConfiguration.java
new file mode 100644
index 000000000..bb88dbad0
--- /dev/null
+++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/security/TraceSecurityAutoConfiguration.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.sleuth.autoconfig.instrument.security;
+
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cloud.sleuth.Tracer;
+import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
+import org.springframework.cloud.sleuth.instrument.security.TracingSecurityContextChangedListener;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.context.SecurityContextChangedListener;
+
+/**
+ * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
+ * Auto-configuration} that registers instrumentation for Spring Security.
+ *
+ * @author Jonatan Ivanov
+ * @since 3.1.0
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass(SecurityContextChangedListener.class)
+@ConditionalOnProperty(value = "spring.sleuth.security.enabled", matchIfMissing = true)
+@ConditionalOnBean(Tracer.class)
+@AutoConfigureAfter(BraveAutoConfiguration.class)
+public class TraceSecurityAutoConfiguration {
+
+ @Bean
+ public TracingSecurityContextChangedListener tracingSecurityContextChangedListener(Tracer tracer) {
+ return new TracingSecurityContextChangedListener(tracer);
+ }
+
+}
diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories
index fdeb8e939..d931b5211 100644
--- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories
@@ -22,6 +22,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.web.client.feign.TraceFei
org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebAsyncClientAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.scheduling.TraceSchedulingAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.session.TraceSessionAutoConfiguration,\
+org.springframework.cloud.sleuth.autoconfig.instrument.security.TraceSecurityAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceFunctionAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringIntegrationAutoConfiguration,\
diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml
index f5d44ce63..11628a9b7 100644
--- a/spring-cloud-sleuth-instrumentation/pom.xml
+++ b/spring-cloud-sleuth-instrumentation/pom.xml
@@ -212,6 +212,11 @@
spring-session-data-redis
true
+
+ org.springframework.security
+ spring-security-core
+ true
+
io.projectreactor.kotlin
reactor-kotlin-extensions
diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/SleuthSecuritySpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/SleuthSecuritySpan.java
new file mode 100644
index 000000000..1b77df1cb
--- /dev/null
+++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/SleuthSecuritySpan.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.sleuth.instrument.security;
+
+import org.springframework.cloud.sleuth.docs.DocumentedSpan;
+import org.springframework.cloud.sleuth.docs.EventValue;
+
+/**
+ * DocumentedSpan for Spring Security Instrumentation.
+ *
+ * @author Jonatan Ivanov
+ * @since 3.1.0
+ */
+enum SleuthSecuritySpan implements DocumentedSpan {
+
+ /**
+ * Indicates that a SecurityContextChangedEvent happened during the current span.
+ */
+ SECURITY_CONTEXT_CHANGE {
+ @Override
+ public String getName() {
+ return "Security Context Change";
+ }
+
+ @Override
+ public EventValue[] getEvents() {
+ return SleuthSecurityEvent.values();
+ }
+ };
+
+ enum SleuthSecurityEvent implements EventValue {
+
+ /**
+ * Event created when an Authentication object is added to the SecurityContext.
+ */
+ AUTHENTICATION_SET {
+ @Override
+ public String getValue() {
+ return "Authentication set %s";
+ }
+ },
+
+ /**
+ * Event created when an Authentication object is replaced with a new one in the
+ * SecurityContext.
+ */
+ AUTHENTICATION_REPLACED {
+ @Override
+ public String getValue() {
+ return "Authentication replaced %s";
+ }
+ },
+
+ /**
+ * Event created when an Authentication object is removed from the
+ * SecurityContext.
+ */
+ AUTHENTICATION_CLEARED {
+ @Override
+ public String getValue() {
+ return "Authentication cleared %s";
+ }
+ }
+
+ }
+
+}
diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListener.java
new file mode 100644
index 000000000..e9220690a
--- /dev/null
+++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListener.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2021-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.sleuth.instrument.security;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.cloud.sleuth.Span;
+import org.springframework.cloud.sleuth.Tracer;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextChangedEvent;
+import org.springframework.security.core.context.SecurityContextChangedListener;
+
+import static java.lang.String.format;
+import static org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan.SECURITY_CONTEXT_CHANGE;
+import static org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan.SleuthSecurityEvent;
+import static org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan.SleuthSecurityEvent.AUTHENTICATION_CLEARED;
+import static org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan.SleuthSecurityEvent.AUTHENTICATION_REPLACED;
+import static org.springframework.cloud.sleuth.instrument.security.SleuthSecuritySpan.SleuthSecurityEvent.AUTHENTICATION_SET;
+
+/**
+ * {@link SecurityContextChangedListener} that adds tracing support for Spring Security.
+ *
+ * @author Jonatan Ivanov
+ * @since 3.1.0
+ */
+public class TracingSecurityContextChangedListener implements SecurityContextChangedListener {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TracingSecurityContextChangedListener.class);
+
+ private final Tracer tracer;
+
+ public TracingSecurityContextChangedListener(Tracer tracer) {
+ this.tracer = tracer;
+ }
+
+ @Override
+ public void securityContextChanged(SecurityContextChangedEvent securityContextChangedEvent) {
+ SecurityContext previousContext = securityContextChangedEvent.getPreviousContext();
+ SecurityContext currentContext = securityContextChangedEvent.getCurrentContext();
+ Authentication previousAuthentication = previousContext != null ? previousContext.getAuthentication() : null;
+ Authentication currentAuthentication = currentContext != null ? currentContext.getAuthentication() : null;
+
+ if (previousAuthentication != null) {
+ if (currentAuthentication != null) {
+ attachEvent(AUTHENTICATION_REPLACED, toString(previousAuthentication, currentAuthentication));
+ }
+ else {
+ attachEvent(AUTHENTICATION_CLEARED, toString(previousAuthentication));
+ }
+ }
+ else if (currentAuthentication != null) {
+ attachEvent(AUTHENTICATION_SET, toString(currentAuthentication));
+ }
+ // null-null is not handled since we won't create an event for that case
+ }
+
+ private String toString(Authentication previousAuthentication, Authentication currentAuthentication) {
+ return toString(previousAuthentication) + " -> " + toString(currentAuthentication);
+ }
+
+ private String toString(Authentication authentication) {
+ return authentication != null ? authentication.getClass().getSimpleName() + authentication.getAuthorities()
+ : "null";
+ }
+
+ private void attachEvent(SleuthSecurityEvent sleuthSecurityEvent, String... params) {
+ Span span = this.tracer.currentSpan();
+ if (span != null) {
+ String event = format(sleuthSecurityEvent.getValue(), (Object[]) params);
+ LOGGER.info(event);
+ SECURITY_CONTEXT_CHANGE.wrap(span).event(event);
+ }
+ }
+
+}
diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListenerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListenerTests.java
new file mode 100644
index 000000000..28da983ba
--- /dev/null
+++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/security/TracingSecurityContextChangedListenerTests.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2021-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.sleuth.instrument.security;
+
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import org.springframework.cloud.sleuth.Span;
+import org.springframework.cloud.sleuth.Tracer;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextChangedEvent;
+import org.springframework.security.core.context.SecurityContextImpl;
+
+import static org.assertj.core.api.BDDAssertions.then;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Jonatan Ivanov
+ */
+@ExtendWith(MockitoExtension.class)
+class TracingSecurityContextChangedListenerTests {
+
+ @Mock
+ private Tracer tracer;
+
+ @Mock
+ private Span span;
+
+ @Captor
+ private ArgumentCaptor eventCaptor;
+
+ @InjectMocks
+ TracingSecurityContextChangedListener listener;
+
+ @Test
+ void null_authentication_objects_should_result_in_noop() {
+ listener.securityContextChanged(new SecurityContextChangedEvent(null, null));
+ listener.securityContextChanged(new SecurityContextChangedEvent(new SecurityContextImpl(null), null));
+ listener.securityContextChanged(new SecurityContextChangedEvent(null, new SecurityContextImpl(null)));
+ listener.securityContextChanged(
+ new SecurityContextChangedEvent(new SecurityContextImpl(null), new SecurityContextImpl(null)));
+
+ verifyNoInteractions(tracer);
+ }
+
+ @Test
+ void null_span_should_result_in_noop() {
+ when(tracer.currentSpan()).thenReturn(null);
+ listener.securityContextChanged(new SecurityContextChangedEvent(null, fakeSecurityContext()));
+
+ verify(tracer).currentSpan();
+ }
+
+ @Test
+ void set_context_changed_event_should_result_in_new_even_on_the_span() {
+ when(tracer.currentSpan()).thenReturn(span);
+ listener.securityContextChanged(new SecurityContextChangedEvent(null, fakeSecurityContext()));
+
+ verify(tracer).currentSpan();
+ verify(span).event(eventCaptor.capture());
+
+ then(eventCaptor.getValue()).isEqualTo("Authentication set AnonymousAuthenticationToken[ANONYMOUS]");
+ }
+
+ @Test
+ void clear_context_changed_event_should_result_in_new_even_on_the_span() {
+ when(tracer.currentSpan()).thenReturn(span);
+ listener.securityContextChanged(new SecurityContextChangedEvent(fakeSecurityContext(), null));
+
+ verify(tracer).currentSpan();
+ verify(span).event(eventCaptor.capture());
+
+ then(eventCaptor.getValue()).isEqualTo("Authentication cleared AnonymousAuthenticationToken[ANONYMOUS]");
+ }
+
+ @Test
+ void replace_context_changed_event_should_result_in_new_even_on_the_span() {
+ when(tracer.currentSpan()).thenReturn(span);
+ listener.securityContextChanged(new SecurityContextChangedEvent(fakeSecurityContext(), fakeSecurityContext()));
+
+ verify(tracer).currentSpan();
+ verify(span).event(eventCaptor.capture());
+
+ then(eventCaptor.getValue()).isEqualTo(
+ "Authentication replaced AnonymousAuthenticationToken[ANONYMOUS] -> AnonymousAuthenticationToken[ANONYMOUS]");
+ }
+
+ private SecurityContext fakeSecurityContext() {
+ return new SecurityContextImpl(fakeAuthentication());
+ }
+
+ private Authentication fakeAuthentication() {
+ return new AnonymousAuthenticationToken("123", "username",
+ Collections.singletonList(new SimpleGrantedAuthority("ANONYMOUS")));
+ }
+
+}
diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml
index 8171631ef..04ffa5604 100644
--- a/tests/brave/pom.xml
+++ b/tests/brave/pom.xml
@@ -57,6 +57,7 @@
spring-cloud-sleuth-instrumentation-rxjava-tests
spring-cloud-sleuth-instrumentation-r2dbc-tests
spring-cloud-sleuth-instrumentation-scheduling-tests
+ spring-cloud-sleuth-instrumentation-security-tests
spring-cloud-sleuth-instrumentation-task-tests
spring-cloud-sleuth-instrumentation-webflux-tests
spring-cloud-sleuth-instrumentation-rsocket-tests
diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/pom.xml
new file mode 100644
index 000000000..b8e760f20
--- /dev/null
+++ b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/pom.xml
@@ -0,0 +1,81 @@
+
+
+
+
+ 4.0.0
+
+ spring-cloud-sleuth-instrumentation-security-tests
+ jar
+ Spring Cloud Sleuth Brave Security Instrumentation Tests
+ Spring Cloud Sleuth Brave Security Instrumentation Tests
+
+
+ org.springframework.cloud
+ spring-cloud-sleuth-tests-brave
+ 3.1.0-SNAPSHOT
+ ..
+
+
+
+ true
+
+
+
+
+
+
+ maven-deploy-plugin
+
+ true
+
+
+
+
+
+
+
+ ${project.groupId}
+ spring-cloud-sleuth-tests-common
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.cloud
+ spring-cloud-starter-sleuth
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ io.zipkin.brave
+ brave-tests
+
+
+
+
diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/security/TracingSecurityContextChangedListenerIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/security/TracingSecurityContextChangedListenerIntegrationTests.java
new file mode 100644
index 000000000..8cfc639db
--- /dev/null
+++ b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/security/TracingSecurityContextChangedListenerIntegrationTests.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2021-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.sleuth.brave.instrument.security;
+
+import brave.sampler.Sampler;
+
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler;
+import org.springframework.cloud.sleuth.instrument.security.SpringSecurityTests;
+import org.springframework.cloud.sleuth.test.TestSpanHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+
+/**
+ * @author Jonatan Ivanov
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ContextConfiguration(classes = TracingSecurityContextChangedListenerIntegrationTests.Config.class)
+public class TracingSecurityContextChangedListenerIntegrationTests extends SpringSecurityTests {
+
+ @Configuration(proxyBeanMethods = false)
+ static class Config {
+
+ @Bean
+ Sampler alwaysSampler() {
+ return Sampler.ALWAYS_SAMPLE;
+ }
+
+ @Bean
+ TestSpanHandler testSpanHandler(brave.test.TestSpanHandler spanHandler) {
+ return new BraveTestSpanHandler(spanHandler);
+ }
+
+ @Bean
+ brave.test.TestSpanHandler spanHandler() {
+ return new brave.test.TestSpanHandler();
+ }
+
+ }
+
+}
diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/resources/application.yml
new file mode 100644
index 000000000..5ec055055
--- /dev/null
+++ b/tests/brave/spring-cloud-sleuth-instrumentation-security-tests/src/test/resources/application.yml
@@ -0,0 +1,3 @@
+spring.application.name: spring-security-test-app
+
+#logging.level.org.springframework.security: TRACE
diff --git a/tests/common/pom.xml b/tests/common/pom.xml
index d6b158acd..a930ecd33 100644
--- a/tests/common/pom.xml
+++ b/tests/common/pom.xml
@@ -64,6 +64,11 @@
spring-integration-core
true
+
+ org.springframework.security
+ spring-security-core
+ true
+
org.springframework.boot
spring-boot-starter-websocket
diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/security/SpringSecurityTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/security/SpringSecurityTests.java
new file mode 100644
index 000000000..7071db5ac
--- /dev/null
+++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/security/SpringSecurityTests.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2021-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.sleuth.instrument.security;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.cloud.sleuth.exporter.FinishedSpan;
+import org.springframework.cloud.sleuth.test.TestSpanHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.context.SecurityContextChangedListener;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static org.assertj.core.api.BDDAssertions.then;
+import static org.springframework.cloud.sleuth.Span.Kind.SERVER;
+
+/**
+ * @author Jonatan Ivanov
+ */
+@ContextConfiguration(classes = SpringSecurityTests.Config.class)
+public abstract class SpringSecurityTests {
+
+ @Autowired
+ TestSpanHandler testSpanHandler;
+
+ @Autowired
+ TestRestTemplate restTemplate;
+
+ @BeforeEach
+ void setUp() {
+ testSpanHandler.clear();
+ }
+
+ @Test
+ void authenticated_user_should_trigger_events() {
+ long beforeStart = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+ ResponseEntity entity = restTemplate.withBasicAuth("user", "password").getForEntity("/", String.class);
+ long afterStop = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+
+ then(entity.getStatusCode().is2xxSuccessful()).isTrue();
+ then(entity.getBody()).isEqualTo("authenticated");
+
+ List> authEvents = getAuthEvents(testSpanHandler.reportedSpans());
+ then(authEvents).isNotEmpty()
+ .allSatisfy(authEvent -> then(authEvent.getKey()).isStrictlyBetween(beforeStart, afterStop));
+
+ for (int i = 0; i < authEvents.size(); i += 2) {
+ String setEvent = authEvents.get(i).getValue();
+ then(setEvent).isEqualTo("Authentication set UsernamePasswordAuthenticationToken[USER]");
+ String clearEvent = authEvents.get(i + 1).getValue();
+ then(clearEvent).isEqualTo("Authentication cleared UsernamePasswordAuthenticationToken[USER]");
+ }
+ }
+
+ @Test
+ void anonymous_should_trigger_events() {
+ long beforeStart = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+ ResponseEntity entity = restTemplate.getForEntity("/", String.class);
+ long afterStop = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+
+ then(entity.getStatusCode().is2xxSuccessful()).isTrue();
+ then(entity.getBody()).contains("html", "form");
+
+ List> authEvents = getAuthEvents(testSpanHandler.reportedSpans());
+ then(authEvents).isNotEmpty()
+ .allSatisfy(authEvent -> then(authEvent.getKey()).isStrictlyBetween(beforeStart, afterStop));
+
+ for (int i = 0; i < authEvents.size(); i += 2) {
+ String setEvent = authEvents.get(i).getValue();
+ then(setEvent).isEqualTo("Authentication set AnonymousAuthenticationToken[ROLE_ANONYMOUS]");
+ String clearEvent = authEvents.get(i + 1).getValue();
+ then(clearEvent).isEqualTo("Authentication cleared AnonymousAuthenticationToken[ROLE_ANONYMOUS]");
+ }
+ }
+
+ private List> getAuthEvents(List spans) {
+ return spans.stream().filter(span -> span.getKind() == SERVER).map(FinishedSpan::getEvents)
+ .flatMap(Collection::stream).filter(event -> event.getValue().contains("Authentication"))
+ .collect(Collectors.toList());
+ }
+
+ @EnableAutoConfiguration(
+ excludeName = "org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebClientAutoConfiguration")
+ @Configuration(proxyBeanMethods = false)
+ static class Config {
+
+ // TODO: Remove this after Spring Boot auto-configuration is available
+ Config(List listeners) {
+ listeners.forEach(SecurityContextHolder::addListener);
+ }
+
+ @Bean
+ UserDetailsService userDetailsService() {
+ return new InMemoryUserDetailsManager(User.withDefaultPasswordEncoder().username("user")
+ .password("password").authorities("USER").build());
+ }
+
+ @Bean
+ HomeController homeController() {
+ return new HomeController();
+ }
+
+ }
+
+ @RestController
+ static class HomeController {
+
+ @GetMapping
+ String home() {
+ return "authenticated";
+ }
+
+ }
+
+}