15

I'm new to the Spring framework, so I apologize in advance for any gaping holes in my understanding.

I'm using Auth0 to secure my API, which works perfectly. My setup & config is the same as the suggested setup in the Auth0 documentation:

// SecurityConfig.java
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // auth0 config vars here

    @Override
    protected void configure(HttpSecurity http) {
        JwtWebSecurityConfigurer
                .forRS256(apiAudience, issuer)
                .configure(http)
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/api/public").permitAll()
                .antMatchers(HttpMethod.GET, "/api/private").authenticated();
    }
}

With this setup, the spring security principal is being set to the userId (sub) from the jwt token: auth0|5b2b.... However, instead of just the userId, I want it set to the matching user (from my database). My question is how to do that.

What I've tried

I've tried implementing a custom database-backed UserDetailsService that I copied from this tutorial. However, it's not getting called regardless of how I try to add it to my conf. I've tried adding it several different ways with no effect:

// SecurityConfig.java (changes only)

    // My custom userDetailsService, overriding the loadUserByUsername
    // method from Spring Framework's UserDetailsService.
    @Autowired
    private MyUserDetailsService userDetailsService;

    protected void configure(HttpSecurity http) {
        http.userDetailsService(userDetailsService);  // Option 1
        http.authenticationProvider(authenticationProvider());  // Option 2
        JwtWebSecurityConfigurer
                [...]  // The rest unchanged from above
    }

    @Override  // Option 3 & 4: Override the following method
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider());  // Option 3
        auth.userDetailsService(userDetailsService);  // Option 4
    }

    @Bean  // Needed for Options 2 or 4
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        return authProvider;
    }

Unfortunately none of the similar "userDetails not being called" questions have helped me due to me needing to combine it with Auth0 authentication.

I'm not positive that I'm on the right path with this. It seems strange to me that I can't find any documentation from Auth0 on this extremely common use case, so maybe I'm missing something obvious.

PS: Not sure if relevant, but the following is always logged during init.

Jun 27, 2018 11:25:22 AM com.test.UserRepository initDao
INFO: No authentication manager set. Reauthentication of users when changing passwords will not be performed.

EDIT 1:

Based on Ashish451's answer, I tried copying his CustomUserDetailsService, and added the following to my SecurityConfig:

@Autowired
private CustomUserDetailsService userService;

@Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

@Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
    auth.userDetailsService( userService );
}

Unfortunately with those changes, CustomUserDetailsService is still not being called.

EDIT 2:

Output when adding the logging method suggested by @Norberto Ritzmann:

Jul 04, 2018 3:49:22 PM com.test.repositories.UserRepositoryImpl initDao
INFO: No authentication manager set. Reauthentication of users when changing passwords will not be performed.
Jul 04, 2018 3:49:22 PM com.test.runner.JettyRunner testUserDetailsImpl
INFO: UserDetailsService implementation: com.test.services.CustomUserDetailsService
5
  • Only implementing a custom database-backed UserDetailsService should work. My guess is you need to add @ ComponentScan to Spring find out your @ Service class in your package. Commented Jul 4, 2018 at 13:23
  • I did try that actually (ref @git-flo's answer), but unfortunately to no effect. Since auth0 handles authentication and sets the security principal, I believe that auth0 might be overwriting my attempts at customizing userDetails. I'm not sure how to test that theory, however.
    – Joakim
    Commented Jul 4, 2018 at 13:32
  • All right, that is a good call. Try put this method on your main class and see what it logs: @Autowired public void testUserDetailsImpl(UserDetailsService service) { log.info("UserDetailsService implementation: " + service.getClass().getName()); } Commented Jul 4, 2018 at 13:38
  • I added the ouput in an edit to my question. So it looks like it is being set. I should probably also have mentioned that the constructor of the service is being called. Just none of the methods. Is it still possible that my auth0 implementation overwrites it? Or since I'm not implementing a custom filterChain, maybe it's just never being used by Spring Security? I'm certainly not calling any of the service methods explicitly, so I'm kind of relying on Spring Security deciding that it needs a UserDetailsService and the methods within it.
    – Joakim
    Commented Jul 4, 2018 at 14:11
  • Auth0 probably are overriding the default security behavior and are not using the UserDetailsService. Commented Jul 4, 2018 at 19:27

5 Answers 5

3
+25

Looking at your Adapter code you are generating JWT token in configure itself. Am not sure whats apiAudience, issuer but it generated sub of JWT I guess. Your issue is that you want to change JWT sub as per your Database.

I have recently implemented JWT security in Spring Boot Application.

And I am setting UserName after fetching it from Database.

I have added code with pkg info for clarity.

// My adapter class. Its same as your's except one thing that I have added a Filter to it. In this Filter I am authenticating JWT token. This filter will be called each time a Secured Rest URL is fired.

import java.nio.charset.StandardCharsets;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;

import com.dev.myapp.jwt.model.CustomUserDetailsService;
import com.dev.myapp.security.RestAuthenticationEntryPoint;
import com.dev.myapp.security.TokenAuthenticationFilter;
import com.dev.myapp.security.TokenHelper;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {




    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private CustomUserDetailsService jwtUserDetailsService; // Get UserDetail bu UserName

    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint; // Handle any exception during Authentication

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //  Binds User service for User and Password Query from Database with Password Encryption
    @Autowired
    public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
        auth.userDetailsService( jwtUserDetailsService )
            .passwordEncoder( passwordEncoder() );
    }

    @Autowired
    TokenHelper tokenHelper;  // Contains method for JWT key Generation, Validation and many more...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and()
        .exceptionHandling().authenticationEntryPoint( restAuthenticationEntryPoint ).and()
        .authorizeRequests()
        .antMatchers("/auth/**").permitAll()
        .anyRequest().authenticated().and()
        .addFilterBefore(new TokenAuthenticationFilter(tokenHelper, jwtUserDetailsService), BasicAuthenticationFilter.class);

        http.csrf().disable();
    }


    //  Patterns to ignore from JWT security check
    @Override
    public void configure(WebSecurity web) throws Exception {
        // TokenAuthenticationFilter will ignore below paths
        web.ignoring().antMatchers(
                HttpMethod.POST,
                "/auth/login"
        );
        web.ignoring().antMatchers(
                HttpMethod.GET,
                "/",
                "/assets/**",
                "/*.html",
                "/favicon.ico",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js"
            );

    }
}

// User Service to get User Details

@Transactional
@Repository
public class CustomUserDetailsService implements UserDetailsService {

    protected final Log LOGGER = LogFactory.getLog(getClass());

    @Autowired
    private UserRepo userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User uu = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
        } else {
            return user;
        }
    }

}

// Unauthorized access handler

@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // This is invoked when user tries to access a secured REST resource without supplying any credentials
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}

// Filter Chain for Validating JWT Token

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    protected final Log logger = LogFactory.getLog(getClass());

    private TokenHelper tokenHelper;

    private UserDetailsService userDetailsService;

    public TokenAuthenticationFilter(TokenHelper tokenHelper, UserDetailsService userDetailsService) {
        this.tokenHelper = tokenHelper;
        this.userDetailsService = userDetailsService;
    }


    @Override
    public void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain
    ) throws IOException, ServletException {

        String username;
        String authToken = tokenHelper.getToken(request);

        logger.info("AuthToken: "+authToken);

        if (authToken != null) {
            // get username from token
            username = tokenHelper.getUsernameFromToken(authToken);
            logger.info("UserName: "+username);
            if (username != null) {
                // get user
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (tokenHelper.validateToken(authToken, userDetails)) {
                    // create authentication
                    TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
                    authentication.setToken(authToken);
                    SecurityContextHolder.getContext().setAuthentication(authentication); // Adding Token in Security COntext
                }
            }else{
                logger.error("Something is wrong with Token.");
            }
        }
        chain.doFilter(request, response);
    }
}

// TokenBasedAuthentication class

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;


public class TokenBasedAuthentication extends AbstractAuthenticationToken {

    private static final long serialVersionUID = -8448265604081678951L;
    private String token;
    private final UserDetails principle;

    public TokenBasedAuthentication( UserDetails principle ) {
        super( principle.getAuthorities() );
        this.principle = principle;
    }

    public String getToken() {
        return token;
    }

    public void setToken( String token ) {
        this.token = token;
    }

    @Override
    public boolean isAuthenticated() {
        return true;
    }

    @Override
    public Object getCredentials() {
        return token;
    }

    @Override
    public UserDetails getPrincipal() {
        return principle;
    }

}

// Helper class for JWT generation and Validation Logic

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import com.dev.myapp.common.TimeProvider;
import com.dev.myapp.entity.User;


@Component
public class TokenHelper {

    protected final Log LOGGER = LogFactory.getLog(getClass());

    @Value("${app.name}") // reading details from property file added in Class path
    private String APP_NAME;

    @Value("${jwt.secret}")
    public String SECRET;

    @Value("${jwt.licenseSecret}")
    public String LICENSE_SECRET;

    @Value("${jwt.expires_in}")
    private int EXPIRES_IN;

    @Value("${jwt.mobile_expires_in}")
    private int MOBILE_EXPIRES_IN;

    @Value("${jwt.header}")
    private String AUTH_HEADER;

    @Autowired
    TimeProvider timeProvider;  // return current time. Basically Deployment time.

    private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;


    //  Generate Token based on UserName. You can Customize this 
    public String generateToken(String username) {
        String audience = generateAudience();
        return Jwts.builder()
                .setIssuer( APP_NAME )
                .setSubject(username)
                .setAudience(audience)
                .setIssuedAt(timeProvider.now())
                .setExpiration(generateExpirationDate())
                .signWith( SIGNATURE_ALGORITHM, SECRET )
                .compact();
    }


    public Boolean validateToken(String token, UserDetails userDetails) {
        User user = (User) userDetails;
        final String username = getUsernameFromToken(token);
        final Date created = getIssuedAtDateFromToken(token);
        return (
                username != null &&
                username.equals(userDetails.getUsername())
        );
    }


   //  If Token is valid will extract all claim else throw appropriate error
    private Claims getAllClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.error("Could not get all claims Token from passed token");
            claims = null;
        }
        return claims;
    }


    private Date generateExpirationDate() {
        long expiresIn = EXPIRES_IN;
        return new Date(timeProvider.now().getTime() + expiresIn * 1000);
    }

}

For this Log

No authentication manager set. Reauthentication of users when changing passwords 

Since you have not implemented a methods with Name loadUserByUsername. You are getting this log.

Edit 1:

I am using Filter Chain just to Validate Token and add User in Security Context which will be extracted from Token....

Am using JWT and you are using AuthO, only Implementation is Different. Am added full implementation for a complete work flow.

You focus on implementing authenticationManagerBean and configureGlobal from WebSecurityConfig class to use UserService.

and TokenBasedAuthentication class implementation.

Other things you can skip.

3
  • Thank you for your answer. If my understanding is correct, you are implementing a full filter chain, which allows you to set the custom userDetails in your TokenAuthenticationFilter class. Unfortunately, I'm having some trouble following your example because I'm utilizing Auth0 to handle the token authentication. I'm not seeing any way I can implement your solution without also discarding Auth0 authentication in favor of a custom filterchain. Do you have any suggestions in that regard?
    – Joakim
    Commented Jun 29, 2018 at 15:24
  • just added more details
    – MyTwoCents
    Commented Jun 29, 2018 at 15:52
  • thank you for the update. I implemented most of the changes you suggested (shown in an edit to my question). However, I see that you're only using the TokenBasedAuthentication class in your TokenAuthenticationFilter. How should I go about implementing TokenBasedAuthentication given that I don't have a TokenAuthenticationFilter class?
    – Joakim
    Commented Jun 30, 2018 at 12:36
2

Maybe this is an spring-boot context initialization issue, meaning the @Autowired annotation cannot be resolved during the initialization of the Configuration class.

You could try the @ComponentScan() annotation on top of your Configuration class and load your MyUserDetailsService explicitly. (see: https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-configuration-classes.html#using-boot-importing-configuration). Having done this I would recommend the following in your Configuration class:

@Autowired
private MyUserDetailsService userService;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService);
}

Hope this can help you out.

1
  • Still nothing. The custom userService isn't being called. Due to the amount of solutions I've tried, my current thinking is that the Auth0 implementation overwrites my custom userService. After all, it does set the spring security principal.
    – Joakim
    Commented Jul 3, 2018 at 21:39
2

I ended up asking Auth0 support about this, and they say that it's currently not possible to modify the principal without modifying the library source.

They provide an alternative approach, however, which is to use a JWT validation library (e.g. https://github.com/auth0/java-jwt) instead of their Spring Security API SDK.

My solution will be to modify my code to work with just the token as principal.

1
1

You could extend JwtAuthenticationProvider with overridden authenticate method which will put your user into Authentication object:

  • Using springboot 2.1.7.RELEASE
  • Auth0 deps: com.auth0:auth0:1.14.2, com.auth0:auth0-spring-security-api:1.2.5, com.auth0:jwks-rsa:0.8.3

Note: some errors in the following code snippets might exist as I've transformed kotlin code by hand into java

Configure SecurityConfig as usual but pass modified authentication provider:

@Autowired UserService userService;

...

@Override
protected void configure(HttpSecurity http) {

    // same thing used in usual method `JwtWebSecurityConfigurer.forRS256(String audience, String issuer)`
    JwkProvider jwkProvider = JwkProviderBuilder(issuer).build()

    // provider deduced from existing default one
    Auth0UserAuthenticationProvider authenticationProvider = new Auth0UserAuthenticationProvider(userService, jwkProvider, issuer, audience)

    JwtWebSecurityConfigurer
           .forRS256(apiAudience, issuer, authenticationProvider)
           .configure(http)
           .authorizeRequests()
           .antMatchers(HttpMethod.GET, "/api/public").permitAll()
           .antMatchers(HttpMethod.GET, "/api/private").authenticated();
}

Extend default JwtAuthenticationProvider which is usually used in method JwtWebSecurityConfigurer.forRS256(String audience, String issuer)

public class Auth0UserAuthenticationProvider extends JwtAuthenticationProvider {
    private final UserService userService;
    public (UserService userService, JwkProvider jwkProvider, String issuer, String audience) {
        super(jwkProvider, issuer, audience);
        this.userService = userService;
    }

    /**
     * Intercept Authentication object before it is set in context
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Authentication jwtAuth = super.authenticate(authentication);
        // Use your service and get user details here
        User user = userService.getDetailsOrWhatever(jwtAuth.getPrincipal().toString());
        // TODO implement following class which merges Auth0 provided details with your user
        return new MyAuthentication(jwtAuth, user);
    }
}

Implement your own MyAuthentication.class which will override getDetails() and return the actual user instead of decoded token given by Auth0 library.

Afterwards user will be available in

SecurityContextHolder.getContext().getAuthentication().getDetails();
4
  • The only problem with this approach is JwtAuthenticationProvider is a final class and you cannot extend it. Commented Feb 25, 2020 at 16:29
  • It is not final in com.auth0:auth0-spring-security-api:1.2.5 as seen in source JwtAuthenticationProvider.java
    – Ivar
    Commented Feb 26, 2020 at 9:23
  • what is the package name for JwtAuthenticationProvider? You might be talking one that is from auth0 library. I am talking about org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider Commented Feb 27, 2020 at 18:43
  • It is the one from auth0, as mentioned in previous comment. I've tried to make it clear in the answer as well.
    – Ivar
    Commented Feb 27, 2020 at 22:15
0

This is what I ended up doing if you want to just stick with using spring libraries. You can change Auth0UserDetailsService to query user details from any source.

public class Auth0JwtAuthenticationProvider implements AuthenticationProvider {

@Autowired
ApplicationContext context;
@Autowired
@Qualifier("auth0UserDetailsService")
UserDetailsService auth0UserDetailsService;
private JwtAuthenticationProvider jwtAuthenticationProvider;

public Auth0JwtAuthenticationProvider() {

}

@PostConstruct
public void postConstruct() {
    jwtAuthenticationProvider = new JwtAuthenticationProvider(context.getBean(JwtDecoder.class));
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Authentication token = jwtAuthenticationProvider.authenticate(authentication);
    ((AbstractAuthenticationToken) token).setDetails(auth0UserDetailsService.loadUserByUsername(authentication.getName()));
    return token;
}

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

}

And for UserDetailsService I am getting user details using a service.

public class Auth0UserDetailsService implements UserDetailsService {


@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

    RestTemplate restTemplate = new RestTemplate();

    .....
  ResponseEntity<Auth0UserDetails> responseEntity = restTemplate.exchange(url, HttpMethod.GET, entity, Auth0UserDetails.class);
    return responseEntity.getBody();

}

.....

}

And UserDetails will have all details you need.

public class Auth0UserDetails implements UserDetails {

@JsonProperty("name")
private String userName;

@JsonProperty("created_at")
private LocalDateTime createdAt;

@JsonProperty("email")
private String email;

@JsonProperty("email_verified")
private boolean isEmailVerified;

@JsonProperty("nickname")
private String nickName;

@JsonProperty("picture")
private String pictureURL;

@JsonProperty("updated_at")
private LocalDateTime updatedAt;

@JsonProperty("user_id")
private String userID;

... //Getters and Setter and overridden methods.

}

    @EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public Auth0JwtAuthenticationProvider auth0JwtAuthenticationProvider() {
        return new Auth0JwtAuthenticationProvider();
    }

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity.authenticationProvider(auth0JwtAuthenticationProvider()).authorizeRequests().
....
.oauth2ResourceServer().jwt();

}}

Not the answer you're looking for? Browse other questions tagged or ask your own question.