31

I've been trying to get @AuthenticationPrincipal to work properly with a custom User class. Unfortunately, the user is always null. Here's the code:

Controller

@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(@AuthenticationPrincipal User user) {
    ModelAndView mav= new ModelAndView("/web/index");
    mav.addObject("user", user);
    return mav;
}

Security Config

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CustomUserDetailsService customUserDetailsService;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
    }

}

CustomUserDetailsService

@Component
public class CustomUserDetailsService implements UserDetailsService {

@Autowired
UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // Spring Data findByXY function
    return userRepository.findByUsername(username);
}

User Entity

public class User implements UserDetails{
    private String username;
    private String password;
    private Collection<Authority> authorities;

    // Getters and Setters

}

Authority Entity

public class Authority implements GrantedAuthority{
    private User user;
    private String role;

    // Getters and Setters

    @Override
    public String getAuthority() {
        return this.getRole();
    }
}

I've tried various solutions to this I found online, e.g. converting my custom user object like this:

return new org.springframework.security.core.userdetails.User(user.getLogin(), user.getPassword(), true, true, true, true,  authorities);

The other ways to get the active users are working without a problem, but I find the @AuthenticationProvider CustomUserObject to be the cleanest way, which is why I would like to get this to work. Any help is greatly appreciated.

5
  • Well.. you can always obtain Principal object, get principal's name and find User by username. If Spring sec. authentication is being implemented correctly of course. Commented Nov 9, 2015 at 19:15
  • 1
    Yeah, that's what I'm doing at the moment ... Authentication auth = SecurityContextHolder.getContext().getAuthentication(); User principal = uRep.findByUsername(auth.getName()); .. but as I said, I'd like to have that clean solution when it's available Commented Nov 9, 2015 at 19:24
  • 2
    I am facing exactly the same issue as you. @ AuthenticationPrincipal is always giving me a default user (The User class just initlalized with default constructor), whereas ContextSecurityHolder and HttpServletRequest#getUserPrincipal() give me the expected (authenticated) user. By the way, should you not annotate your SecurityConfig with @ EnableWebSecurity instead of @ EnableWebMvcSecurity (deprecated in Spring Security 4+)? Commented Nov 10, 2015 at 13:55
  • Thanks for pointing that out, completely missed that :). If you figure it out please don't hesitate to post here. Commented Nov 10, 2015 at 14:00
  • I have filed a JIRA at Spring: jira.spring.io/browse/SEC-3145 Commented Nov 10, 2015 at 15:55

13 Answers 13

10

In my case, I get a String back (the username) and not the UserDetails object, i.e. you should define the method signature as

public ModelAndView index(@AuthenticationPrincipal String username)

This is not strange, since @AuthenticationPrincipal in effect returns Authentication.getPrincipal() and according to the documentation:

In the case of an authentication request with username and password, this would be the username. Callers are expected to populate the principal for an authentication request.

The AuthenticationManager implementation will often return an Authentication containing richer information as the principal for use by the application. Many of the authentication providers will create a UserDetails object as the principal. See: https://docs.spring.io/spring-security/site/docs/5.0.0.RELEASE/api/

So, I am assuming that your AuthenticationManager implementation is returning just a username

Sign up to request clarification or add additional context in comments.

Comments

8

Instead of using @AuthenticationPrincipal you can directly specify your dependency for authenticated user in method argument. something as given below

@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(Principal user) {
    ModelAndView mav= new ModelAndView("/web/index");
    mav.addObject("user", user);
    return mav;
} 

This Principal object will be actual object that got authenticated through spring security. Spring will inject this for you when the method will get invoked.

3 Comments

As I said, those methods all work just fine. But according to the documentation, I should be able to get my custom user object via the AuthenticationPrincipal annotation as long as my user object implements UserDetails and I enable WebMvcSecurity (I'm using Spring Security 4.0.2). Any idea what I could be missing?
Surprisingly, the approach with @AuthenticationPrincipal User did not work for me, but using Principal direclty worked. Waiting for an answer from Spring 3145 ticket on this point...
@RémiDoolaeghe - this is a late answer, but you need to be getting an instance of UserDetails instead of just User.
4

From documentation:

Class AuthenticationPrincipalArgumentResolver will resolve the CustomUser argument using Authentication.getPrincipal() from the SecurityContextHolder. If the Authentication or Authentication.getPrincipal() is null, it will return null. If the types do not match, null will be returned unless AuthenticationPrincipal.errorOnInvalidType() is true in which case a ClassCastException will be thrown.

This simple code works for me:

@RestController
public class ApiController {

  @RequestMapping(method = RequestMethod.GET, path = "/api/public/{id}")
  public ResponseEntity<ApiResponse> getPublicResource(
      @PathVariable Integer id,
      @AuthenticationPrincipal String principal
  ) {

    if (id % 2 == 0) {
      SecurityContext context = SecurityContextHolder.getContext();
      Authentication authentication = context.getAuthentication();
      return ResponseEntity.ok(new ApiResponse(
          authentication.getPrincipal().toString(),
          "this method scrapes principal directly from SpringSecurityContext"));
    }

    return ResponseEntity.ok(new ApiResponse(
        principal.toString(),
        "this method retrieves principal from method argument"));

  }

}

Just check the type of the principal in Authentication object.

Comments

3

You need to check if you are using the correct version of @AuthenticationPrincipal annotation and the correct version of AuthenticationPrincipalArgumentResolver class together.

Prior to 4.0 version of Spring Security you had to use classes:

  • org.springframework.security.web.bind.annotation.AuthenticationPrincipal

  • org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver

From version 4.0 onwards, you must use:

  • org.springframework.security.core.annotation.AuthenticationPrincipal

  • org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver

Look at @AuthenticationPrincipal official docs for configuration examples.

Comments

2

I found another solution, even this is not the canonical one.

In your controller

@RequestMapping(value = "/login", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<UserDTO> login(@RequestBody UserDTO user){
    try {
        usersService.authenticate(user.getUsername(), user.getPassword());
        return new ResponseEntity<UserDTO>(user, HttpStatus.OK);
    }
    catch (BadCredentialsException e){
        return new ResponseEntity<UserDTO>(HttpStatus.UNAUTHORIZED);
    }
}

Where the UserDTO os just a form to contains a username and a password

In your CustomUserDetailsService

public void authenticate(String username, String password){
    try{
        User user = new User(username, password);
        Authentication request = new UsernamePasswordAuthenticationToken(user, password, Arrays.asList(WebSecurityConfiguration.USER));
        Authentication result = authenticationManager.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);       
    } catch (InternalAuthenticationServiceException e){
        // treat as a bad credential
    }
}

Implement your own AuthenticationManager

@Component
class DefaultAuthenticationManager implements AuthenticationManager {

@Autowired
private CustomUserDetailsService usersService;

public Authentication authenticate(Authentication auth) throws AuthenticationException {
    UserDetails user = usersService.loadUserByUsername(((User)auth.getPrincipal()).getUsername());
    if (user != null) {
        return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
    }
// Handle bad credentials here  
}
}

What is fundamental is that the Principal in the CustomUserDetailsService#authenticate is an object, not the name of your authenticated user, so that the framework can handle it and then inject through @AuthenticationPrincipal mechanism. This worked for me.

Comments

1
@RequestMapping(method= RequestMethod.GET,value="/authenticate", produces = MediaType.APPLICATION_JSON_VALUE)
public Object authenticate(@AuthenticationPrincipal Object obj) {
    return obj;
}

I have the same problem, I was able to make this work. You could use it with a mapper

@RequestMapping(method = RequestMethod.GET, value = "/authenticate2", produces = MediaType.APPLICATION_JSON_VALUE)
public User authenticate2(@AuthenticationPrincipal Object obj) throws IOException {
    return mapper.readValue(mapper.writeValueAsString(obj), User.class);
}

These worked for me, I hope it would work for anyone in the future

Comments

1

Defining (expression = "userDetails") for the @AuthenticationPrincipal works for me

public ResponseEntity<UserDetails> index(@AuthenticationPrincipal(expression = "userDetails") UserDetails userDetails) {
     return ResponseEntity.ok(userDetails);
}

Comments

1
@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(@AuthenticationPrincipal(expression = "user") User user) {
    ModelAndView mav= new ModelAndView("/web/index");
    mav.addObject("user", user);
    return mav;
}

Just put expression = "user" and you are good to go. (If your User entity class name is different, update this accordingly.)

Comments

1

The return type of @AuthenticationPrincipal can be different for different auth methods (Username/password vs Oauth2 for example).

To find out the return type of @AuthenticationPrincipal. See https://stackoverflow.com/a/76435763/5303092

Comments

0

I'm achieving this with an authentication provider that extends AbstractUserDetailsAuthenticationProvider .

In my provider I'm overriding the following method:

protected UserDetails retrieveUser( String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {}

and in this method I'm creating my UserDetails implementation and return it. Then I'm able to get it in the controller with the AuthenticationPrincipal annotation.

1 Comment

I'm implementing the interface and not overriding the the Abstract class. The AuthenticatedPrincipal Argument is getting created with new instance of the our userDetails object that implements the spring version but that's not helpful as all the properties are null.
0

Even though documentation mentioned about a customUser, I don't think it's possible. Use @AuthenticationPrincipal with java.lang.Object or org.springframework.security.core.userdetails.User as spring controller method parameter type.

If the types do not match, null will be returned unless AuthenticationPrincipal.errorOnInvalidType() is true in which case a ClassCastException will be thrown. Spring API Documentation

Comments

0

You have to ensure that the class which you want to get is implemented UserDetails because the annotation returns this.

Comments

-1

I am facing exactly the same issue as you. for some reason, spring security's @EnableWebSecurity not add argumentResolver automatically, you need to add it manual:

<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver"/>
    </mvc:argument-resolvers>
</mvc:annotation-driven>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.