'How to set a custom principal object during or after authentication?
I've changed the way a user is authenticated in my backend. From now on I am receiving JWT tokens from Firebase which are then validated on my Spring Boot server.
This is working fine so far but there's one change which I am not too happy about and it's that the principal-object is now a org.springframework.security.oauth2.jwt.Jwt
and not a AppUserEntity
, the user-model, like before.
// Note: "authentication" is a JwtAuthenticationToken
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Jwt jwt = (Jwt) authentication.getPrincipal();
So, after some reading and debugging I found that the BearerTokenAuthenticationFilter
essentially sets the Authentication
object like so:
// BearerTokenAuthenticationFilter.java
AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
// Note: authenticationResult is our JwtAuthenticationToken
Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(context);
and as we can see, this on the other hand comes from the authenticationManager
which is a org.springframework.security.authentication.ProviderManager
and so on. The rabbit hole goes deep.
I didn't find anything that would allow me to somehow replace the Authentication
.
So what's the plan?
Since Firebase is now taking care of user authentication, a user can be created without my backend knowing about it yet. I don't know if this is the best way to do it but I intend to simply create a user record in my database once I discover a valid JWT-token of a user which does not exist yet.
Further, a lot of my business logic currently relies on the principal being a user-entity business object. I could change this code but it's tedious work and who doesn't want to look back on a few lines of legacy code?
Solution 1:[1]
I did it a bit different than Julian Echkard.
In my WebSecurityConfigurerAdapter
I am setting a Customizer
like so:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2ResourceServer()
.jwt(new JwtResourceServerCustomizer(this.customAuthenticationProvider));
}
The customAuthenticationProvider
is a JwtResourceServerCustomizer
which I implemented like this:
public class JwtResourceServerCustomizer implements Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer> {
private final JwtAuthenticationProvider customAuthenticationProvider;
public JwtResourceServerCustomizer(JwtAuthenticationProvider customAuthenticationProvider) {
this.customAuthenticationProvider = customAuthenticationProvider;
}
@Override
public void customize(OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer jwtConfigurer) {
String key = UUID.randomUUID().toString();
AnonymousAuthenticationProvider anonymousAuthenticationProvider = new AnonymousAuthenticationProvider(key);
ProviderManager providerManager = new ProviderManager(this.customAuthenticationProvider, anonymousAuthenticationProvider);
jwtConfigurer.authenticationManager(providerManager);
}
}
I'm configuring the NimbusJwtDecoder
like so:
@Component
public class JwtConfig {
@Bean
public JwtDecoder jwtDecoder() {
String jwkUri = "https://www.googleapis.com/service_accounts/v1/jwk/[email protected]";
return NimbusJwtDecoder.withJwkSetUri(jwkUri)
.build();
}
}
And finally, we need a custom AuthenticationProvider
which will return the Authentication
object we desire:
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtDecoder jwtDecoder;
@Autowired
public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
BearerTokenAuthenticationToken token = (BearerTokenAuthenticationToken) authentication;
Jwt jwt;
try {
jwt = this.jwtDecoder.decode(token.getToken());
} catch (JwtValidationException ex) {
return null;
}
List<GrantedAuthority> authorities = new ArrayList<>();
if (jwt.hasClaim("roles")) {
List<String> rolesClaim = jwt.getClaim("roles");
List<RoleEntity.RoleType> collect = rolesClaim
.stream()
.map(RoleEntity.RoleType::valueOf)
.collect(Collectors.toList());
for (RoleEntity.RoleType role : collect) {
authorities.add(new SimpleGrantedAuthority(role.toString()));
}
}
return new JwtAuthenticationToken(jwt, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(BearerTokenAuthenticationToken.class);
}
}
Solution 2:[2]
This is working fine so far but there's one change which I am not too happy about and it's that the principal-object is now a org.springframework.security.oauth2.jwt.Jwt and not a AppUserEntity, the user-model, like before.
In my application I have circumvented this by rolling my own JwtAuthenticationFilter
instead of using BearerTokenAuthenticationFilter
, which then sets my User
Entity as the principal
in the Authentication
object. However, in my case this constructs a User
barely from the JWT claims, which might be bad practice: SonarLint prompts to use a DTO instead to mitigate the risk of somebody injecting arbitrary data into his user record using a compromised JWT token. I don't know if that is a big deal - if you can't trust your JWTs, you have other problems, IMHO.
I don't know if this is the best way to do it but I intend to simply create a user record in my database once I discover a valid JWT-token of a user which does not exist yet.
Keep in mind that JWTs should be verified by your application in a stateless manner, solely by verifying their signature. You shouldn't hit the database every time you verify them. Therefor it would be better if you create a user record using a method call like
void foo(@AuthenticationPrincipal final Jwt jwt) {
// only invoke next line if reading JWT claims is not enough
final User user = userService.findOrCreateByJwt(jwt);
// TODO method logic
}
once you need to persist changes to the database that involve this user.
Solution 3:[3]
Since
SecurityContextHolder.setContext(context);
won't work for
request.getUserPrincipal();
you may create a custom class extending HttpServletRequestWrapper
import java.security.Principal;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class UserPrincipalHttpServletRequest extends HttpServletRequestWrapper {
private final Principal principal;
public UserPrincipalHttpServletRequest(HttpServletRequest request, Principal principal) {
super(request);
this.principal = principal;
}
@Override
public Principal getUserPrincipal() {
return principal;
}
}
then in your filter do something like this:
protected void doFilterInternal(HttpServletRequest request){
. . .
// create user details, roles are required
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority("SOME ROLE"));
UserDetails userDetails = new User("SOME USERNAME", "SOME PASSWORD", authorities);
// Create an authentication token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// follow the filter chain, using the new wrapped UserPrincipalHtppServletRequest
chain.doFilter(new UserPrincipalHttpServletRequest(request, usernamePasswordAuthenticationToken), response);
// all filters coming up, will be able to run request.getUserPrincipal()
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Stefan Falk |
Solution 2 | Julian Eckhardt |
Solution 3 | ozzi- |