Spring Security - Using custom DB authorization with Auth0 pre-authentication

Viewed 722

I am currently working on a Spring Boot REST application with Spring Security. My workplace use Auth0 (external third-party service providing user management) for their authentication and have requested me to implement it in this application. Authentication occurs in the front end application written in React. The frontend application shows a login form and sends the username and password to Auth0, Auth0 verifies the credentials and returns a JWT token when the user is validated.

After this, the frontend application will call the REST services from my application passing a JWT token in the Authorize header. Using an Auth0 plugin, Spring Security verifies this token and the request is allowed to execute. I have tested this much to be working as expected. The code is as follows:

import java.util.Arrays;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.auth0.spring.security.api.JwtWebSecurityConfigurer;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
    
    @Value(value = "${auth0.apiAudience}")
    private String apiAudience;
    @Value(value = "${auth0.issuer}")
    private String issuer;
    
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        configuration.setAllowCredentials(true);
        configuration.addAllowedHeader("Authorization");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors();
        JwtWebSecurityConfigurer  //Auth0 provided class performs per-authentication using JWT token
            .forRS256(apiAudience, issuer)
            .configure(http)
            .authorizeRequests()
            .antMatchers(HttpMethod.GET, "/Test/public").permitAll()
            .antMatchers(HttpMethod.GET, "/Test/authenticated").authenticated();
    }
    
}

Now, once this authentication is done, I have observed that the principal in the security context gets updated with user id from Auth0. I have verified this by this code snippet:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String name = authentication.getName(); // Returns the Auth0 user id.

The next step I expect to do is to use this user id to match the user with roles and permissions in my existing database schema. Therefore, I need to implement a custom authorization mechanism that plugs into Spring Security as well. In other words the user's roles must be loaded into the security context shortly after the (pre)authentication is done. How do I implement this? Is there some class that I need to extend or implement some interface?

2 Answers

I think what you are looking for is the AuthenticationProvider Interface. Here are two examples how I handle Authentication:

DaoAuthentication

@Component
public class DaoAdminAuthenticationProvider extends DaoAuthenticationProvider {
private static final Logger LOG = 
LoggerFactory.getLogger(DaoAdminAuthenticationProvider.class);

private final AdminUserRepository adminUserRepository;

public DaoAdminAuthenticationProvider(AdminUserRepository adminUserRepository, DaoAdminUserDetailsService daoAdminUserDetailsService) {
    this.adminUserRepository = adminUserRepository;
    setPasswordEncoder(new BCryptPasswordEncoder(11));
    this.setUserDetailsService(daoAdminUserDetailsService);
}

@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {

    AdminUser adminUser = adminUserRepository.findByEmail(auth.getName());
    if (adminUser == null) {
        LOG.info("Invalid username or password");
        throw new BadCredentialsException("Invalid username or password");
    }

    Authentication result = super.authenticate(auth);
    return new UsernamePasswordAuthenticationToken(adminUser, result.getCredentials(), result.getAuthorities());
}

@Override
public boolean supports(Class<?> authentication) {
    return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

JwtAuthenticationProvider

@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOG = 
LoggerFactory.getLogger(JwtAuthenticationProvider.class);

private static final String EX_TOKEN_INVALID = "jwt.token.invalid";

private final JwtTokenService jwtTokenService;

@SuppressWarnings("unused")
public JwtAuthenticationProvider() {
    this(null);
}

@Autowired
public JwtAuthenticationProvider(JwtTokenService jwtTokenService) {
    this.jwtTokenService = jwtTokenService;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    try {
        String token = (String) authentication.getCredentials();
        String username = jwtTokenService.getUsernameFromToken(token);

        return jwtTokenService.validateToken(token)
                .map(aBoolean -> new JwtAuthenticatedProfile(username))
                .orElseThrow(() -> new TokenException(EX_TOKEN_INVALID));

    } catch (JwtException ex) {
        LOG.error("Invalid JWT Token");
        throw new TokenException(EX_TOKEN_INVALID);
    }
}

@Override
public boolean supports(Class<?> authentication) {
    return JwtAuthentication.class.equals(authentication);
}
}

The other classes like JwtTokenService etc. I implemented as well. But regarding to your question I think the answer is to use the AuthenticationProvider Interface.

Ok, I found a solution though I think it's a bit dirty. Going by the weird way that the official Auth0 classes are structured, what I've done could possibly be described as a hack. Anyway, here goes:

First of all, I a custom user details service by implementing the AuthenticationUserDetailsService interface:

@Service
public class VUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationJsonWebToken> {

@Autowired
UserRepository userRepository;

Logger logger = LoggerFactory.getLogger(VUserDetailsService.class);

@Override
@Transactional(readOnly = true)
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationJsonWebToken token) throws UsernameNotFoundException {
    logger.debug("User id: "+token.getName());
    // Verify whether there is an entry for this id in the database.
    User user = userRepository.findByAuxillaryId(token.getName());
    
    if(user == null)
        throw new UsernameNotFoundException("The user with id "+token.getName()+" not found in database.");
    
    logger.debug("Obtained user details from db: "+user.toString());
    List<GrantedAuthority> authoritiesList = new ArrayList<>();
    // Get user roles
    List<UserRole> userRoles = user.getUserRoles();
    if(userRoles != null) logger.debug("Number of user roles:"+userRoles.size());
    
    for(UserRole userRole : userRoles) {
        logger.debug(userRole.getCompositeKey().getRole());
        authoritiesList.add(new SimpleGrantedAuthority(userRole.getCompositeKey().getRole()));
    }
        
    return new org.springframework.security.core.userdetails.User(token.getName(), "TEMP", authoritiesList);
}
}

Here auxillary id is the user id assigned when a user is created in Auth0. Note that PreAuthenticatedAuthenticationJsonWebToken is a class provided by Auth0 as well.

After this, I created a custom authentication provider extending the Auth0 provided JwtAuthenticationProvider:

public class VAuthenticationProvider extends JwtAuthenticationProvider {
    
    public VAuthenticationProvider(JwkProvider jwkProvider, String issuer, String audience) {
        super(jwkProvider, issuer, audience);
    }

    @Autowired
    VUserDetailsService vUserDetailsService;
    
    Logger logger = LoggerFactory.getLogger(VAuthenticationProvider.class);

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        logger.debug("*** Processing authentication for token: "+authentication.getName());
        logger.debug("*** Current granted authorities: "+authentication.getAuthorities());
        UserDetails userDetails = vUserDetailsService.loadUserDetails((PreAuthenticatedAuthenticationJsonWebToken) authentication);
        authentication = new PreAuthenticatedAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
        return authentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        //com.auth0.spring.security.api.authentication.PreAuthenticatedAuthenticationJsonWebToken
        return authentication.equals(PreAuthenticatedAuthenticationJsonWebToken.class);
    }
}

Then I used this authentication provider in my security configuration class:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Value(value = "${auth0.apiAudience}")
    private String apiAudience;
    @Value(value = "${auth0.issuer}")
    private String issuer;

    @Autowired
    VUserDetailsService vUserDetailsService;

    Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);
    
    @Bean
    public VAuthenticationProvider authProvider() {
        JwkProvider jwkProvider = new JwkProviderBuilder(issuer).build(); //Auth0 provided class
        VAuthenticationProvider vAuthProvider = new VAuthenticationProvider(jwkProvider, issuer, apiAudience);
        return vAuthProvider;
        
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors();
        JwtWebSecurityConfigurer.forRS256(apiAudience, issuer, authProvider())
                .configure(http)
                .authorizeRequests().antMatchers(HttpMethod.GET, "/Test/public").permitAll()
                .antMatchers(HttpMethod.GET, "/Test/authenticated").authenticated()
                .antMatchers(HttpMethod.GET, "/admin/*").hasRole("ADMIN") //Not Auth0 role, defined in my DB.
                .antMatchers(HttpMethod.GET, "/Test/root").hasRole("ROOT"); //Not Auth0 role, defined in my DB.
    }
/* Code ommitted */

Now, all my requests are getting filtered based on the roles in my database. Thus, Auth0 is only being used for authentication and authorization is based on roles in my database.

If anyone thinks this solution could be improved, please let me know.

Related