Issue
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
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);
}
}
Answered By - Stefan Falk
Answer Checked By - Terry (JavaFixing Volunteer)