Context: I have a Spring Boot REST API secured with oauth2 JWT, this part works properly.
Problem: In my RestController I want to access the currently authenticated user's ID with @AuthenticationPrincipal User userDetails but it returns null.
Security Filter Chain:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception
{
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth ->
{
auth.requestMatchers(new AntPathRequestMatcher("/auth/**")).permitAll();
auth.requestMatchers(new AntPathRequestMatcher("/admin/**")).hasAnyRole("ADMIN");
auth.requestMatchers(new AntPathRequestMatcher("/user/**")).hasAnyRole("USER", "ADMIN");
auth.anyRequest().authenticated();
})
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter()))
.authenticationEntryPoint(invalidTokenAuthenticationEntryPoint)
)
.exceptionHandling(exception ->
exception.authenticationEntryPoint(authRequiredAuthenticationEntryPoint))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build();
}
User Entity:
@Entity
@Table(name = "user")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User extends AbstractTimestampEntity implements UserDetails
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long userId;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String email;
@Column(nullable = false)
@JsonIgnore
private String password;
@ManyToMany
@JoinTable(
name = "user_role_junction",
joinColumns = {@JoinColumn(name = "user_id")},
inverseJoinColumns = {@JoinColumn(name = "role_id")}
)
private Set<Role> authorities;
// Constructor for manual user creation
public User(String username, String email, String password, Set<Role> authorities)
{
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
@Override
public boolean isAccountNonExpired()
{
return true;
}
@Override
public boolean isAccountNonLocked()
{
return true;
}
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
@Override
public boolean isEnabled()
{
return true;
}
}
User Details Service implementation
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
private UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository)
{
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
return userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User does not exists!"));
}
}
User Controller
package com.jez.blogapi.controller;
import com.jez.blogapi.model.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController
{
@GetMapping("/test")
public ResponseEntity<String> getUserById(@AuthenticationPrincipal User userDetails)
{
System.out.println(userDetails);
return new ResponseEntity<>("test response message", HttpStatus.OK);
}
}
Resource server build the security-context out of the access token, either by decoding it (if it's a JWT), or introspecting it (whatever the format).
In case of JWT, there is a chapter in the doc explaining how it works, and you should read it.
If you stick with the default Authentication implementation with JWT decoders (JwtAuthenticationToken), then @AuthenticationPrincipal should return a org.springframework.security.oauth2.jwt.Jwt from which you can read claims (provided that the request was authorised with a valid Bearer JWT).
So, yes, as you suggest in the comments to your question, enriching access tokens (when creating it on the authorization server) with all of the user data you need on your resource server(s), seems a better idea than looping to the database for each and every REST API call.
If there are claims you need to access frequently, you could configure the jwtAuthenticationConverter to return your own extension of AbstractAuthenticationToken (instead of JwtAuthenticationToken). It is rather simple and allows to return typed values from named accessors (and de-couple your application code from your authorization server claims structure).
OAuth2 resource servers are queried by OAuth2 clients but mobile and Javascript based applications (Angular, Vue, React, etc) make unsafe OAuth2 clients.
The current recommendation from Spring Security team is to use "confidential" clients on the server.
For mobile and Javascript based applications, I use spring-cloud-gateway (with spring-boot-starter-oauth2-client and TokenRelay= filter) in front of my stateless resource servers. In this architecture, requests are authorized with:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With