Home > Back-end >  How to set a custom principal object during or after authentication?
How to set a custom principal object during or after authentication?

Time:02-24

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?

CodePudding user response:

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.

CodePudding user response:

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);
    }

}
  • Related