Skip to content

Authentication and Principal arguments in controller functions are null with anonymous principal #4011

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

Closed
ezraroi opened this issue Aug 7, 2016 · 14 comments
Assignees
Labels
in: web An issue in web modules (web, webmvc) status: declined A suggestion or change that we don't feel we should currently apply

Comments

@ezraroi
Copy link

ezraroi commented Aug 7, 2016

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())));
    }

}
@ezraroi
Copy link
Author

ezraroi commented Aug 21, 2016

any update on this?

@renjithgr
Copy link

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".

@ezraroi
Copy link
Author

ezraroi commented Aug 22, 2016

@renjithgr you can use

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

in your controller

@gregb
Copy link

gregb commented Jan 24, 2017

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!

@kivimango
Copy link

i experienced this issue too

@opncow
Copy link

opncow commented Aug 9, 2018

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).

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 7, 2019
@bob1923
Copy link

bob1923 commented Oct 7, 2019

( 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....

@vojtapol
Copy link

vojtapol commented Nov 8, 2020

Solution for injecting Principal is to use the @AuthenticationPrincipal annotation on the parameter.

@jzheaux
Copy link
Contributor

jzheaux commented Nov 11, 2020

The Principal and the Authentication should be the same as returned from:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();.

This is addressed in spring-projects/spring-framework#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: #1333 and #2232.

@jzheaux jzheaux closed this as completed Nov 11, 2020
@jzheaux jzheaux self-assigned this Nov 11, 2020
@jzheaux jzheaux added status: declined A suggestion or change that we don't feel we should currently apply in: web An issue in web modules (web, webmvc) and removed status: waiting-for-triage An issue we've not yet triaged labels Nov 11, 2020
@minogin
Copy link

minogin commented Mar 22, 2021

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.

@jzheaux
Copy link
Contributor

jzheaux commented Mar 22, 2021

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();
    // ...
}

@minogin
Copy link

minogin commented Mar 22, 2021

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?

@jzheaux
Copy link
Contributor

jzheaux commented Mar 22, 2021

You're right, @minogin. I removed my other suggestions in my earlier comment to alleviate any confusion.

@minogin
Copy link

minogin commented Mar 23, 2021

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 })
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web An issue in web modules (web, webmvc) status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

10 participants