Summary
When using anonymous user in security and trying to inject the Authentication or Principal to a method in a RestController they are null. If a real user is authenticated then it will work.
Actual Behavior
Getting null in Authentication and Principal when anonymous user is logged and working with real user. When using SecurityContextHolder.getContext().getAuthentication() we are getting the right user (anonymous).
Expected Behavior
The Principal and the Authentication should be the same as returned from:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();.
Configuration
@Override
protected void configure(HttpSecurity http) throws Exception
{
if (customSecurityConfig != null){
customSecurityConfig.configure(http);
}
http.authorizeRequests()
.antMatchers("/manage/health","/manage/info").permitAll()
.antMatchers("/manage/**", "/debug/**").hasRole(managementServerProperties.getSecurity().getRole())
.and()
.csrf().disable().logout().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(new SpnegoEntryPoint())
.and()
.addFilter(getBasicAuthenticationFilter(authenticationManagerBean()))
.addFilterBefore(
getSpnegoAuthenticationProcessingFilter(authenticationManagerBean()),
BasicAuthenticationFilter.class);
}
Version
Spring boot 1.3.7
Sample
Working code:
@RestController
class UserController
{
@Timed
@RequestMapping(value = "/api/user", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "getUserInfo", response = UserDTO.class, notes = "get user info",
produces = MediaType.APPLICATION_JSON_VALUE)
public UserDTO getUser()
{
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return new UserDTO(authentication.getName(),
Lists.newArrayList(AuthorityUtils.authorityListToSet(authentication.getAuthorities())));
}
}
Not working code (getting null)
@RestController
class UserController
{
@Timed
@RequestMapping(value = "/api/user", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "getUserInfo", response = UserDTO.class, notes = "get user info",
produces = MediaType.APPLICATION_JSON_VALUE)
public UserDTO getUser(Authentication authentication)
{
return new UserDTO(authentication.getName(),
Lists.newArrayList(AuthorityUtils.authorityListToSet(authentication.getAuthorities())));
}
}
Comment From: ezraroi
any update on this?
Comment From: renjithgr
Is this an expected behaviour? I am also facing the same "problem". I have a page which I would like to show to both Authenticated and Anonymous users in different way. I have specified "permitAll()" for that url. I was expecting to differentiate between the two in the controller/template. But since Authentication is coming as null, I am not able to. Following is my current configuration.
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/content").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").permitAll()
.and()
.logout().permitAll();
}
Here I am having problems with the URL ending in "/content".
Comment From: ezraroi
@renjithgr you can use
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
in your controller
Comment From: gregb
I found this issue searching for solutions to the same problem, which seemed like a bug to me as well. I eventually traced all the way into SecurityContextHolderAwareRequestWrapper and found this:
/**
* Obtain the current active <code>Authentication</code>
*
* @return the authentication object or <code>null</code>
*/
private Authentication getAuthentication() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!trustResolver.isAnonymous(auth)) {
return auth;
}
return null;
}
So I guess it's intentional, though the docs don't seem to give much in the way of reasoning. On a hunch I threw a custom trust resolver into the context:
@Bean
public AuthenticationTrustResolver trustResolver() {
return new AuthenticationTrustResolver() {
@Override
public boolean isRememberMe(final Authentication authentication) {
return false;
}
@Override
public boolean isAnonymous(final Authentication authentication) {
return false;
}
};
}
This worked, and now I can get my AnonymousAuthenticationToken successfully injected into an Authorization parameter on my controller method. Although I haven't noticed any other issues with my application, returning false all the time feels like a poor way to handle this. If anyone could explain why the AnonymousAuthenticationToken couldn't just be returned as-is, I'd prefer to remove this from my code.
Thanks!
Comment From: kivimango
i experienced this issue too
Comment From: opncow
For everybody landing here, because they can't inject Authentication at all: Try registering a SecurityContextHolderAwareRequestFilter manually. After registering it, e.g. as shown below, I can inject Authentication again as usual in my controllers:
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityContextHolderAwareRequestFilter securityContextHolderAwareRequestFilter() {
return new SecurityContextHolderAwareRequestFilter();
}
}
For me the problem began when I switched to a custom AuthenticationManager. This seems to turn off some default configurations (Spring Boot 2.0.3).
Comment From: bob1923
( How does the triage process work? )
As gregb pointed out 2 years ago, and from the javadoc, this is deliberate. Why does the SecurityContextHolder contain an anonymous Authentication instance, but it's not injected into the controller parameter?
Yes, there's a workaround, but it's ugly.
Would love to have it fixed - springboot 2.1.9? Pleeeease....
Comment From: vojtapol
Solution for injecting Principal is to use the @AuthenticationPrincipal annotation on the parameter.
Comment From: jzheaux
The Principal and the Authentication should be the same as returned from: Authentication authentication = SecurityContextHolder.getContext().getAuthentication();.
This is addressed in https://github.com/spring-projects/spring-framework/issues/25981. In Spring Web 5.3.1, using @AuthenticationPrincipal will retrieve the Principal directly from the SecurityContextHolder instead of from the HttpServletRequest.
Note also that you can get the SecurityContext using @CurrentSecurityContext as of Spring Security 5.2.
As for the question about the deliberate exclusion of anonymous users in the wrapper class, you can see Luke Taylor and Rob Winch's answers in two older issues: https://github.com/spring-projects/spring-security/issues/1333 and https://github.com/spring-projects/spring-security/issues/2232.
Comment From: minogin
But this does not address the initial issue of resolving anonymous Authentication in controller. What if I don't need a Principal but an Authentication object, how do I get one as a controller parameter? As Authentication class extends Principal I can't even create my custom resolver as it will be overridden by default spring principal argument resolver. I know I can use SecurityContextHolder.getContext().authentication but this is ugly.
Comment From: jzheaux
Hi, @minogin. I'd recommend using @CurrentSecurityContext if you want the Authentication.
You can do:
public String myMethod(@CurrentSecurityContext SecurityContext context) {
Authentication authentication = context.getAuthentication();
// ...
}
Comment From: minogin
Unfortunately this does not work for me as ServletRequestMethodArgumentResolver resolves any arguments which are subclasses of Principal to authentication.principal which is null in case of anonymous auth.
See ServletRequestMethodArgumentResolver:resolveArgument(...):
else if (Principal.class.isAssignableFrom(paramType)) {
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null && !paramType.isInstance(userPrincipal)) {
throw new IllegalStateException(
"Current user principal is not of type [" + paramType.getName() + "]: " + userPrincipal);
}
return userPrincipal;
}
While custom argument resolvers are added after ServletRequestMethodArgumentResolver. See getDefaultArgumentResolvers.
So I wonder how it works for you. Do I miss something? Is there a way to disable this standard behaviour?
Comment From: jzheaux
You're right, @minogin. I removed my other suggestions in my earlier comment to alleviate any confusion.
Comment From: minogin
A workaround (maybe not the best one) is to make CurrentSecurityContextArgumentResolver the first one in the list.
Kotlin code:
@EventListener
fun initEvaluators(event: ContextRefreshedEvent) {
val handler = event.applicationContext.getBean(RequestMappingHandlerAdapter::class.java)
val resolvers = handler.argumentResolvers!!
handler.argumentResolvers = buildList {
add(resolvers.first { it is CurrentSecurityContextArgumentResolver })
addAll(resolvers.filter { it !is CurrentSecurityContextArgumentResolver })
}
}