I've been looking into adding instrumentation to OpenTelemetry to capture the enduser.id, enduser.role, and enduser.scope attributes as part of Server spans for applications using Spring Security. In Spring Security terms, this means retrieving the Authentication principal name, roles, and scopes from the Authentication object obtained for a successful authentication for inbound requests. I'm trying to identify the best location in Spring Security to instrument to capture those attributes from an Authentication object.
I have found is that there is no "single place" to capture the Authentication object that results from successful authentications for inbound requests.
I considered instrumenting SecurityContext or ReactiveSecurityContext to capture the Authentication from when the SecurityContext or ReactiveSecurityContext is written. But, unfortunately, they can be written in places that aren't just for inbound requests. For example, it could be for async processing, or scheduled tasks, or the application could even switch the active authentication during request processing for impersonation. Therefore, this would catch more use cases than desired.
Next, I looked at all the places that perform authentication for inbound requests. Some examples:
Reactive:
* AuthenticationWebFilter.authenticate
* OAuth2AuthorizationCodeGrantWebFilter.authenticate
Servlet:
* AuthenticationFilter.attemptAuthentication
* AbstractPreAuthenticatedProcessingFilter.doAuthenticate
* AbstractAuthenticationProcessingFilter.doFilter
* RememberMeAuthenticationFilter.doFilter
* BasicAuthenticationFilter.doFilterInternal
* BearerTokenAuthenticationFilter.doFilterInternal
* OAuth2AuthorizationCodeGrantFilter.processAuthorizationResponse
So, one option would be to add OpenTelemetry instrumentation to each of these places. While this is possible, it's not ideal because it is very fragile. For example, new filters that authenticate inbound requests won't be covered, and any changes to any of those locations could also break the instrumentation. It also requires different strategies in each case for actually capturing the Authentication object, since not every location encapsulates the Authentication object in the same way.
What I would really love is some kind of hook or listener that the instrumentation could use to listen for successful authentications for inbound requests. Perhaps these locations could fire off a Spring event, and the instrumentation could listen for those events (assuming the callback runs on the same thread, so the instrumentation has access to the active Span).
Would it be possible for spring-security to add a hook/listener that is triggered with authentication attempt results for inbound requests? This would make writing this type of automatic instrumentation easier.
Would you recommend a different approach?
Comment From: marcusdacoregio
Hi, @philsttr.
I believe you would be able to do it by relying on the SecurityContextChangedEventListener. Although that listener will be triggered for any change in the context, not only for HTTP requests, you can use org.springframework.web.context.request.RequestContextHolder#getRequestAttributes to check if there is a RequestAttributes bound to the thread. It would look similar to this:
@Bean
static SecurityContextHolderStrategy securityContextHolderStrategy() {
SecurityContextHolderStrategy original = SecurityContextHolder.getContextHolderStrategy();
SecurityContextChangedListener listener = new MyListener();
SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(original, listener);
SecurityContextHolder.setContextHolderStrategy(strategy);
return strategy;
}
static class MyListener implements SecurityContextChangedListener {
@Override
public void securityContextChanged(SecurityContextChangedEvent event) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
// there is a request bound to the thread
// you can even check if requestAttributes instanceof ServletRequestAttributes
}
}
}
@jzheaux do you have any other suggestions on how to achieve this?
Comment From: philsttr
Thanks for the response @marcusdacoregio.
A couple problems with that approach:
1. RequestContextHolder only works in servlet based apps, and does not work in webflux apps. Is there an equivalent for webflux apps? The listener would also need access to the reactor subscriber context, since that is where the OpenTelemetry span information would reside for webflux apps. (as opposed to a ThreadLocal)
2. It could potentially trigger a second time if an app switched the context after the security filters initially set the context. Although, this situation might be able to be detected by checking to see if securityContextChangedEvent.getOldContext() does not contain an Authentication. Would that work?
Comment From: marcusdacoregio
In Spring Security terms, this means retrieving the Authentication principal name, roles, and scopes from the Authentication object obtained for a successful authentication for inbound requests
What do you mean by successful authentication? Do you want to capture those attributes only when the user performs the authentication? Or, if the session contains an authenticated security context, would you consider that too? Have you considered implementing a filter that captures the attributes for each request?
Comment From: philsttr
Excellent questions. I'll clarify.
I want to capture the attributes for each inbound request that has an associated Authentication (either because authentication was performed earlier in the request processing, or the request is part of an authenticated session).
I have not considered implementing a filter, because I'm not quite sure how the OpenTelemetry Java Agent instrumentation could inject that filter into the "right" spot in the filter chain for all (or at least the vast majority of) apps. The agent is intended to "just work" when enabled for an application, so it is important that it works in a wide variety of apps. The agent can easily inject bytecode at strategic spots that would work across all/majority of apps. It seemed easier to me for the agent to inject some bytecode, rather than manipulate one or more filter chains in an app.
Comment From: philsttr
You got me thinking about filters.
Perhaps the agent could instrument the build() methods of ServerHttpSecurity and HttpSecurity to add a filter before SecurityWebFiltersOrder.LOGOUT. Then the filter could capture the attributes from the current Authentication. Seems pretty straightforward.
Comment From: marcusdacoregio
Sounds like a good place to do it. I think that for servlet apps you should place your filter before the AuthorizationFilter since logout is pretty early in the filter chain. Note that the SwitchUserFilter is placed after the AuthorizationFilter.
Comment From: philsttr
Got it, yeah I was looking at the reactive filter chain. I assumed the servlet was similar, but it's a bit different as you mentioned.
I have a working implementation of the reactive instrumentation (will be contributing it as part of https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/9400). It's pretty clean, and I'm pretty happy with it. So I'm going to close this issue.
Thanks for the brainstorming and the help!
Comment From: philsttr
For reference: https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9777 adds the OpenTelemetry instrumentation described above