Sometimes we have to deal with situations when we need to obtain information about an authenticated user after the authentication process is completed. We might, for example, need to know the username or the roles/authorities of the user that has accessed an endpoint.

In this topic, you’ll learn about some of the ways to get details about a currently authenticated user. We will start with theory and a few examples and then create a program with two users and two endpoints.

Authentication interface

When someone logs in, Spring Security creates an Authentication object. Using this object, we can get information about a currently logged-in user. Here are some of the methods of the Authentication interface that we can use:

  • String getName() returns the username.
  • Collection<? extends GrantedAuthority> getAuthorities() returns the user’s roles/authorities.

If we need more details about the authenticated user, we can use the following method:

  • Object getPrincipal() returns the current user object. It can be UserDetailsUser that implements the UserDetails, which are provided by Spring Security, or a custom user object. Note that the return type of the method is Object. This makes the framework more flexible. We will need to cast the return object to UserDetailsUser, or our own user object if we have a custom one.

But how can we obtain an Authentication object? Let’s study some examples.

Get user details in a controller

We can get an Authentication object that contains data about the current user in a method of a controller by defining Authentication as a method argument. Here is an example:

@GetMapping("/username")
public void username(Authentication auth) {
    System.out.println(auth.getName());     
}

The code above will print the username of the user that performed the request.

We can also specify Principal as a method argument. It’s an interface that the Authentication interface extends. It has only one method, getName().

Now, let’s see an example of obtaining a UserDetails object that stores more information about the current user:

@GetMapping("/details")
public void details(Authentication auth) {
    UserDetails details = (UserDetails) auth.getPrincipal();

    System.out.println("Username: " + details.getUsername());
    System.out.println("User has authorities/roles: " + details.getAuthorities());
}

The code above will print the username and authorities/roles of the user that performed the request.

We can also use a special annotation to get the current user details.

@AuthenticationPrincipal

Using the @AuthenticationPrincipal annotation, we can get an object of the UserDetails type or a class that implements it in a controller method directly without Authentication. Here is an example:

@GetMapping("/username")
public void username(@AuthenticationPrincipal UserDetails details) {
    System.out.println(details.getUsername());
}

As you can see, this approach is shorter and more readable. With @AuthenticationPrincipal we skip casting, and it limits the security-specific code to the annotation itself.

Note that the abovementioned ways to obtain details about the current user only work in a controller.

Get user details anywhere in an app

Another way to obtain an Authentication object is by using SecurityContext that provides the getAuthentication() method which returns the Authentication object. You can obtain the SecurityContext via a static method named getContext() of the SecurityContextHolder. Here is an example:

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

This approach has one advantage over the other approaches we’ve described: it can be used anywhere in an application, not only in a controller’s handler methods. But there are also some downsides. For example, it is harder to test the program if you choose this approach.

Now let’s see where this information can be used.

Example

To get a better idea of this subject, we’ll create a program that receives and stores items. The program has two hardcoded users and two endpoints.

Let’s assume that we’ve already created a Spring Boot project with web and security dependencies included. In the code below we configure two users in an in-memory user store, turn on HTTP basic auth, specify that all endpoints require authentication, and unblock POST requests:

@EnableWebSecurity
public class WebSecurityConfigurerImpl extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("A").password("pass1").roles()
                .and()
                .withUser("B").password("pass2").roles()
                .and()
                .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
        http.httpBasic();
        http.csrf().disable();
    }
}

Now let’s implement the controller. It has two endpoints:

  • POST /items takes a query parameter item and stores it. Internally items are stored in a Map. The key of the Map is a username and the value is a Set with the items created by the user.
  • GET /items returns items created by the current user.
@RestController
public class ItemsController {
    private final Map<String, Set<String>> items = new ConcurrentHashMap<>();

    @PostMapping("/items")
    public void addItem(@AuthenticationPrincipal UserDetails details, @RequestParam String item) {
        String username = details.getUsername();

        if (items.containsKey(username)) {
            items.get(username).add(item);
        } else {
            items.put(username, new HashSet<>(Set.of(item)));
        }
    }

    @GetMapping("/items")
    public Set<String> getItems(@AuthenticationPrincipal UserDetails details) {
        return items.getOrDefault(details.getUsername(), Set.of());
    }
}

As you can see, we use the @AuthenticationPrincipal annotation to get current user details. When a user creates a new item we obtain their username and check if the Map contains this key. If the username is present, we obtain the associated Set which contains current user items and add the new item. If the username is not present in the Map, we create a new entry. The second endpoint simply returns items created by the current user, or an empty Set if the user hasn’t created any items yet.

It is important to mention that if a user is not authenticated, the @AuthenticationPrincipal will inject null.

Without knowing who the currently logged-in user is we wouldn’t be able to implement this functionality. To help us know who created an item, there should be a link between the item and its author.

Now let’s run and test the program.

Running the app

To test the program we will create 4 items: a vase, a table, a cup, and a chair. User A will create the first two items, and User B the second two items. Here is an example with User A and the vase:

Now let’s try to get all the items created by the first user:

As you can see, the JSON array contains the vase and the table, which is the correct behavior. You can try to get the items created by User B. It will also work! Feel free to experiment with this.

Conclusion

In this topic, you’ve learned that:

  • you can use an Authentication object after the authentication is completed to get details about the current user;
  • an Authentication object (or Principal) can be obtained in a method of a controller by specifying it as a method argument;
  • using the SecurityContextHolder you can get an Authentication object anywhere in your app;
  • the @AuthenticationPrincipal annotation allows injecting a UserDetails object (or its implementation) directly in a method of a controller.

Additionally, we’ve created a program with two users and two endpoints and made sure that a user of the app could only retrieve the items they created and not see items created by other users.

Leave a Reply

Your email address will not be published.