Skip to content

Commit 9bb41d1

Browse files
Exclude non username and password authentication from user tracking (#6995)
* Exclude non username and password authentication from user tracking * Move log level to debug
1 parent 23683a0 commit 9bb41d1

7 files changed

Lines changed: 167 additions & 4 deletions

File tree

dd-java-agent/instrumentation/spring-security-5/src/main/java17/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22

33
import static datadog.trace.api.UserEventTrackingMode.DISABLED;
44
import static datadog.trace.api.UserEventTrackingMode.EXTENDED;
5+
import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY;
56

67
import datadog.trace.api.Config;
78
import datadog.trace.api.UserEventTrackingMode;
89
import datadog.trace.bootstrap.instrumentation.decorator.AppSecUserEventDecorator;
10+
import java.util.Collections;
911
import java.util.HashMap;
1012
import java.util.Map;
13+
import java.util.Set;
14+
import java.util.concurrent.ConcurrentHashMap;
1115
import java.util.stream.Collectors;
16+
import org.slf4j.Logger;
17+
import org.slf4j.LoggerFactory;
18+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
1219
import org.springframework.security.core.Authentication;
1320
import org.springframework.security.core.userdetails.User;
1421
import org.springframework.security.core.userdetails.UserDetails;
@@ -17,6 +24,13 @@ public class SpringSecurityUserEventDecorator extends AppSecUserEventDecorator {
1724

1825
public static final SpringSecurityUserEventDecorator DECORATE =
1926
new SpringSecurityUserEventDecorator();
27+
private static final String SPRING_SECURITY_PACKAGE = "org.springframework.security";
28+
29+
private static final Logger LOGGER =
30+
LoggerFactory.getLogger(SpringSecurityUserEventDecorator.class);
31+
32+
private static final Set<Class<?>> SKIPPED_AUTHS =
33+
Collections.newSetFromMap(new ConcurrentHashMap<>());
2034

2135
public void onSignup(UserDetails user, Throwable throwable) {
2236
// skip failures while signing up a user, later on, we might want to generate a separate event
@@ -53,8 +67,7 @@ public void onLogin(Authentication authentication, Throwable throwable, Authenti
5367
return;
5468
}
5569

56-
// For now, exclude all OAuth events. See APPSEC-12547.
57-
if (authentication.getClass().getName().contains("OAuth")) {
70+
if (shouldSkipAuthentication(authentication)) {
5871
return;
5972
}
6073

@@ -98,4 +111,26 @@ public void onLogin(Authentication authentication, Throwable throwable, Authenti
98111
}
99112
}
100113
}
114+
115+
private static boolean shouldSkipAuthentication(final Authentication authentication) {
116+
if (authentication instanceof UsernamePasswordAuthenticationToken) {
117+
return false;
118+
}
119+
if (SKIPPED_AUTHS.add(authentication.getClass())) {
120+
final Class<?> authClass = authentication.getClass();
121+
LOGGER.debug(
122+
SEND_TELEMETRY, "Skipped authentication, auth={}", findRootAuthentication(authClass));
123+
}
124+
return true;
125+
}
126+
127+
private static String findRootAuthentication(Class<?> authentication) {
128+
while (authentication != Object.class) {
129+
if (authentication.getName().startsWith(SPRING_SECURITY_PACKAGE)) {
130+
return authentication.getName();
131+
}
132+
authentication = authentication.getSuperclass();
133+
}
134+
return Authentication.class.getName(); // set this a default for really custom impls
135+
}
101136
}

dd-java-agent/instrumentation/spring-security-5/src/test/groovy/datadog/trace/instrumentation/springsecurity5/SecurityConfig.groovy

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,34 @@
11
package datadog.trace.instrumentation.springsecurity5
22

3+
import custom.CustomAuthenticationFilter
4+
import custom.CustomAuthenticationProvider
35
import org.springframework.boot.jdbc.DataSourceBuilder
46
import org.springframework.context.annotation.Bean
57
import org.springframework.context.annotation.Configuration
8+
import org.springframework.security.authentication.AuthenticationManager
69
import org.springframework.security.config.annotation.web.builders.HttpSecurity
710
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
11+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer
812
import org.springframework.security.provisioning.JdbcUserDetailsManager
913
import org.springframework.security.provisioning.UserDetailsManager
1014
import org.springframework.security.web.SecurityFilterChain
15+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
1116

1217
import javax.sql.DataSource
1318

19+
import static datadog.trace.instrumentation.springsecurity5.SecurityConfig.CustomDsl.customDsl
20+
1421
@Configuration
1522
@EnableWebSecurity
1623
class SecurityConfig {
1724

1825
@Bean
1926
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
20-
http.authorizeHttpRequests(
27+
http.apply(customDsl())
28+
http
29+
.authorizeHttpRequests(
2130
(requests) -> requests
22-
.requestMatchers("/", "/success", "/register", "/login").permitAll()
31+
.requestMatchers("/", "/success", "/register", "/login", "/custom").permitAll()
2332
.anyRequest().authenticated())
2433
.csrf().disable()
2534
.formLogin((form) -> form.loginPage("/login").permitAll())
@@ -40,4 +49,17 @@ class SecurityConfig {
4049
UserDetailsManager userDetailsService() {
4150
return new JdbcUserDetailsManager(dataSource)
4251
}
52+
53+
static class CustomDsl extends AbstractHttpConfigurer<CustomDsl, HttpSecurity> {
54+
@Override
55+
void configure(HttpSecurity http) throws Exception {
56+
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager)
57+
http.authenticationProvider(new CustomAuthenticationProvider())
58+
http.addFilterBefore(new CustomAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter)
59+
}
60+
61+
static CustomDsl customDsl() {
62+
return new CustomDsl()
63+
}
64+
}
4365
}

dd-java-agent/instrumentation/spring-security-5/src/test/groovy/datadog/trace/instrumentation/springsecurity5/SpringBootBasedTest.groovy

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package datadog.trace.instrumentation.springsecurity5
22

3+
import ch.qos.logback.classic.Logger
4+
import ch.qos.logback.core.Appender
35
import com.datadog.appsec.AppSecHttpServerTest
46
import datadog.trace.agent.test.base.HttpServer
57
import datadog.trace.core.DDSpan
@@ -14,6 +16,7 @@ import spock.lang.Shared
1416

1517
import static datadog.trace.instrumentation.springsecurity5.TestEndpoint.LOGIN
1618
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
19+
import static datadog.trace.instrumentation.springsecurity5.TestEndpoint.CUSTOM
1720
import static datadog.trace.instrumentation.springsecurity5.TestEndpoint.REGISTER
1821
import static datadog.trace.instrumentation.springsecurity5.TestEndpoint.UNKNOWN
1922
import static datadog.trace.instrumentation.springsecurity5.TestEndpoint.NOT_FOUND
@@ -225,6 +228,33 @@ class SpringBootBasedTest extends AppSecHttpServerTest<ConfigurableApplicationCo
225228
span.getTags().findAll { it.key.startsWith('appsec.events.users.signup') }.isEmpty()
226229
}
227230

231+
void 'test skipped authentication'() {
232+
setup:
233+
final appender = Mock(Appender)
234+
final logger = SpringSecurityUserEventDecorator.LOGGER as Logger
235+
logger.addAppender(appender)
236+
237+
and:
238+
final requestCount = 3
239+
final request = request(CUSTOM, "GET", null).addHeader('X-Custom-User', 'batman').build()
240+
241+
when:
242+
final response = (1..requestCount).collect { client.newCall(request).execute() }.first()
243+
TEST_WRITER.waitForTraces(3)
244+
final span = TEST_WRITER.flatten().first() as DDSpan
245+
logger.detachAppender(appender) // cant add cleanup
246+
247+
then:
248+
response.code() == CUSTOM.status
249+
span.context().resourceName.contains(CUSTOM.path)
250+
span.getTags().findAll { key, value -> key.startsWith('appsec.events.users.login')}.isEmpty()
251+
// single call to the appender
252+
1 * appender.doAppend(_) >> {
253+
assert it[0].toString().contains('Skipped authentication, auth=org.springframework.security.authentication.AbstractAuthenticationToken')
254+
}
255+
0 * appender._
256+
}
257+
228258
@SuppressWarnings('GroovyAssignabilityCheck')
229259
private static String randomString(int length) {
230260
return new Random().with { random ->

dd-java-agent/instrumentation/spring-security-5/src/test/groovy/datadog/trace/instrumentation/springsecurity5/TestEndpoint.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ enum TestEndpoint {
55
REGISTER("register", 200, ""),
66
NOT_FOUND("not-found", 404, "not found"),
77
UNKNOWN("", 451, null), // This needs to have a valid status code
8+
CUSTOM("custom", 302, ""),
89

910
private final String path
1011
private final String rawPath
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package custom;
2+
3+
import jakarta.servlet.ServletException;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import java.io.IOException;
7+
import org.springframework.security.authentication.AuthenticationManager;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.security.core.AuthenticationException;
10+
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
11+
12+
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
13+
14+
private static final String HEADER_NAME = "X-Custom-User";
15+
16+
private final AuthenticationManager authenticationManager;
17+
18+
public CustomAuthenticationFilter(final AuthenticationManager authenticationManager) {
19+
super("/custom");
20+
this.authenticationManager = authenticationManager;
21+
}
22+
23+
@Override
24+
public Authentication attemptAuthentication(
25+
HttpServletRequest request, HttpServletResponse response)
26+
throws AuthenticationException, IOException, ServletException {
27+
final String user = request.getHeader(HEADER_NAME);
28+
if (user == null) {
29+
return null;
30+
}
31+
return authenticationManager.authenticate(new CustomAuthenticationToken(user));
32+
}
33+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package custom;
2+
3+
import org.springframework.security.authentication.AuthenticationProvider;
4+
import org.springframework.security.core.Authentication;
5+
import org.springframework.security.core.AuthenticationException;
6+
7+
public class CustomAuthenticationProvider implements AuthenticationProvider {
8+
9+
@Override
10+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
11+
return authentication;
12+
}
13+
14+
@Override
15+
public boolean supports(Class<?> authentication) {
16+
return CustomAuthenticationToken.class.isAssignableFrom(authentication);
17+
}
18+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package custom;
2+
3+
import java.util.Collections;
4+
import org.springframework.security.authentication.AbstractAuthenticationToken;
5+
6+
public class CustomAuthenticationToken extends AbstractAuthenticationToken {
7+
8+
private final String user;
9+
10+
public CustomAuthenticationToken(String user) {
11+
super(Collections.emptyList());
12+
this.user = user;
13+
}
14+
15+
@Override
16+
public Object getCredentials() {
17+
return user;
18+
}
19+
20+
@Override
21+
public Object getPrincipal() {
22+
return user;
23+
}
24+
}

0 commit comments

Comments
 (0)