Spring Boot bearer token authentication giving 401

11,023

As looking your requirement, you really don't need multiple http security configurations until you are using multiple authentications for multiple paths (like for some path you would like to have JWT and for some you would like to have basic auth or auth2).

So remove SecurityCredentialsConfig and update WebSecurity to below and you will be good.

@Configuration
@EnableWebSecurity(debug = true)    // Enable security config. This annotation denotes config for spring security.
public class WebSecurity extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtConfig jwtConfig;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                // make sure we use stateless session; session won't be used to store user's state.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // authorization requests config
                .authorizeRequests()
                // allow all who are accessing "auth" service
                .antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll()
                // must be an admin if trying to access admin area (authentication is also required here)
                .antMatchers("/v1/cooks/**").hasAuthority("ADMIN")
                //for other uris
                //   .antMatchers(HttpMethod.GET, "/v1/**").hasRole("USER")
                // Any other request must be authenticated
                .anyRequest().authenticated()
                .and()
                // handle an authorized attempts
                .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                // Add a filter to validate the tokens with every request
                .addFilterAfter(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig));
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
Share:
11,023
kk1957
Author by

kk1957

Updated on June 04, 2022

Comments

  • kk1957
    kk1957 almost 2 years

    I am new to Spring boot so please help me. I have got it working to the point where I am able to generate a Bearer Token with an unauthenticated request. Next I want to use this token to use with an endpoint so that my request is authenticated - this is where my trouble is coming in. I am always getting a 401, looks like something is wrong with my config. Here is my code

    public class ApplicationUser {
    
    private String username;
    private String password;
    private String role;
    
    public ApplicationUser(String username, String password, String role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }
    
    public String getUsername() {
        return username;
    }
    
    public String getPassword() {
        return password;
    }
    
    public String getRole() {
        return role;
        }
    }
    

    JwtConfig Class:

    @Component("jwtConfig")
    public class JwtConfig {
    @Value("${security.jwt.uri:/auth/**}")
    private String Uri;
    
    @Value("${security.jwt.header:Authorization}")
    private String header;
    
    @Value("${security.jwt.prefix:Bearer }")
    private String prefix;
    
    @Value("${security.jwt.expiration:#{24*60*60}}")
    private int expiration;
    
    @Value("${security.jwt.secret:JwtSecretKey}")
    private String secret;
    
    public String getUri() {
        return Uri;
    }
    
    public String getHeader() {
        return header;
    }
    
    public String getPrefix() {
        return prefix;
    }
    
    public int getExpiration() {
        return expiration;
    }
    
    public String getSecret() {
        return secret;
    }
    }
    

    JwtTokenAuthenticationFilter

    public class JwtTokenAuthenticationFilter extends  OncePerRequestFilter {
    
    private final JwtConfig jwtConfig;
    
    public JwtTokenAuthenticationFilter(JwtConfig jwtConfig) {
        this.jwtConfig = jwtConfig;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
    
        // 1. get the authentication header. Tokens are supposed to be passed in the authentication header
        String header = request.getHeader(jwtConfig.getHeader());
    
        // 2. validate the header and check the prefix
        if(header == null || !header.startsWith(jwtConfig.getPrefix())) {
            chain.doFilter(request, response);          // If not valid, go to the next filter.
            return;
        }
    
        // If there is no token provided and hence the user won't be authenticated.
        // It's Ok. Maybe the user accessing a public path or asking for a token.
    
        // All secured paths that needs a token are already defined and secured in config class.
        // And If user tried to access without access token, then he won't be authenticated and an exception will be thrown.
    
        // 3. Get the token
        String token = header.replace(jwtConfig.getPrefix(), "");
    
        try {   // exceptions might be thrown in creating the claims if for example the token is expired
    
            // 4. Validate the token
            Claims claims = Jwts.parser()
                    .setSigningKey(jwtConfig.getSecret().getBytes())
                    .parseClaimsJws(token)
                    .getBody();
    
            String username = claims.getSubject();
            if(username != null) {
                @SuppressWarnings("unchecked")
                List<String> authorities = (List<String>) claims.get("authorities");
    
                // 5. Create auth object
                // UsernamePasswordAuthenticationToken: A built-in object, used by spring to represent the current authenticated / being authenticated user.
                // It needs a list of authorities, which has type of GrantedAuthority interface, where SimpleGrantedAuthority is an implementation of that interface
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                        username, null, authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
    
                // 6. Authenticate the user
                // Now, user is authenticated
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
    
        } catch (Exception e) {
            // In case of failure. Make sure it's clear; so guarantee user won't be authenticated
            SecurityContextHolder.clearContext();
        }
    
        // go to the next filter in the filter chain
        chain.doFilter(request, response);
    }
    
    }
    

    JwtUsernameAndPasswordAuthenticationFilter

    public class JwtUsernameAndPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter   {
    
    // We use auth manager to validate the user credentials
    private AuthenticationManager authManager;
    
    private final JwtConfig jwtConfig;
    
    public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authManager, JwtConfig jwtConfig) {
        this.authManager = authManager;
        this.jwtConfig = jwtConfig;
    
        // By default, UsernamePasswordAuthenticationFilter listens to "/login" path.
        // In our case, we use "/auth". So, we need to override the defaults.
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(jwtConfig.getUri(), "POST"));
    }
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
    
        try {
    
            // 1. Get credentials from request
            UserCredentials creds = new ObjectMapper().readValue(request.getInputStream(), UserCredentials.class);
    
            // 2. Create auth object (contains credentials) which will be used by auth manager
            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                    creds.getUsername(), creds.getPassword(), Collections.emptyList());
    
            // 3. Authentication manager authenticate the user, and use UserDetialsServiceImpl::loadUserByUsername() method to load the user.
            return authManager.authenticate(authToken);
    
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    // Upon successful authentication, generate a token.
    // The 'auth' passed to successfulAuthentication() is the current authenticated user.
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
    
        Long now = System.currentTimeMillis();
        String token = Jwts.builder()
                .setSubject(auth.getName())
                // Convert to list of strings.
                // This is important because it affects the way we get them back in the Gateway.
                .claim("authorities", auth.getAuthorities().stream()
                        .map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
                .setIssuedAt(new Date(now))
                .setExpiration(new Date(now + jwtConfig.getExpiration() * 1000))  // in milliseconds
                .signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret().getBytes())
                .compact();
    
        // Add token to header
        response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
    }
    
    // A (temporary) class just to represent the user credentials
    private static class UserCredentials {
        private String username, password;
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    }
    }
    

    SecurityCredentialsConfig

    @Configuration
    @EnableWebSecurity(debug=true)
    public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    
    @Autowired
    private JwtConfig jwtConfig;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                // make sure we use stateless session; session won't be used to store user's state.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // handle an authorized attempts
                .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                // Add a filter to validate user credentials and add token in the response header
    
                // What's the authenticationManager()?
                // An object provided by WebSecurityConfigurerAdapter, used to authenticate the user passing user's credentials
                // The filter needs this auth manager to authenticate the user.
                .addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig))
                .authorizeRequests()
                // allow all POST requests
                .antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll()
                // any other requests must be authenticated
              //  .antMatchers(HttpMethod.GET, "/v1/**").hasRole("USER")
                .anyRequest().authenticated();
    }
    
    // Spring has UserDetailsService interface, which can be overriden to provide our implementation for fetching user from database (or any other source).
    // The UserDetailsService object is used by the auth manager to load the user from database.
    // In addition, we need to define the password encoder also. So, auth manager can compare and verify passwords.
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    }
    

    UserDetailsServiceImpl

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
    @Autowired
    private BCryptPasswordEncoder encoder;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        final List<ApplicationUser> users = Arrays.asList(
                new ApplicationUser("omar",encoder.encode("12345"), "USER"),
                new ApplicationUser("admin", encoder.encode("12345"), "ADMIN")
        );
    
        for(ApplicationUser appUser: users) {
            if(appUser.getUsername().equals(username)) {
                List<GrantedAuthority> grantedAuthorities = AuthorityUtils
                        .commaSeparatedStringToAuthorityList( appUser.getRole());
    
                return new User(appUser.getUsername(), appUser.getPassword(), grantedAuthorities);
            }
        }
    
        // If user not found. Throw this exception.
        throw new UsernameNotFoundException("Username: " + username + " not found");
    
    }
    }
    

    WebSecurity

    @EnableWebSecurity(debug=true)
    @Order(1000)
    public class WebSecurity extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtConfig jwtConfig;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                // make sure we use stateless session; session won't be used to store user's state.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // handle an authorized attempts
                .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                // Add a filter to validate the tokens with every request
                .addFilterAfter(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
                // authorization requests config
                .authorizeRequests()
                // allow all who are accessing "auth" service
                .antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll()
                // must be an admin if trying to access admin area (authentication is also required here)
                .antMatchers("/v1/cooks").access("hasRole('ADMIN')")
                //for other uris
             //   .antMatchers(HttpMethod.GET, "/v1/**").hasRole("USER")
                // Any other request must be authenticated
                .anyRequest().authenticated();
    }
    
    }
    

    Controller

    @RestController
    
    
    public class CookController {
    
    @Autowired
    private CookService cookService;
    // Get All Cooks
    @GetMapping("/v1/cooks")
    public List<Cook> getAllCooks(){
        return cookService.getAllCooks();
    }
    

    application.properties

    zuul.routes.auth-service.path=/auth/**
    zuul.routes.auth-service.service-id=AUTH-SERVICE
    zuul.routes.auth-service.strip-prefix=false
    zuul.routes.auth-service.sensitive-headers=Cookie,Set-Cookie  
    
    spring.application.name=auth-service
    server.port=9100
    eureka.client.service-url.default-zone=http://localhost:8761/eureka
    

    the call to v1/cooks always returns 401. What part of the configuration I am missing? I followed the documentation at https://medium.com/omarelgabrys-blog/microservices-with-spring-boot-authentication-with-jwt-part-3-fafc9d7187e8

    but I am completely lost now. Request url is GET http://localhost:9100/v1/cooks

    Response is

    { "timestamp": "2018-10-13T20:08:13.804+0000", "status": 401, "error": "Unauthorized", "message": "No message available", "path": "/v1/cooks" }

    pom.xml

     <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.restdocs</groupId>
            <artifactId>spring-restdocs-mockmvc</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.16</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0-b170201.1204</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
    

    EDIT: Added application.properties and dependencies from pom

  • kk1957
    kk1957 over 5 years
    I did that, however now I am not able to use my first resource which is http://localhost:9100/auth/ which generates a token. It says, 404.
  • kk1957
    kk1957 over 5 years
    Still same problem.
  • kk1957
    kk1957 over 5 years
    added pom dependencies in the original question, also added application.properties.
  • kj007
    kj007 over 5 years
    @kk1957 define SecurityCredentialsConfig order 2..check my updated answer.
  • Jonathan JOhx
    Jonathan JOhx over 5 years
    Okay, I've taken a look on your logs, you are using last version of spring boot 2.0.5 this version asks that you work on spring security 5, could you remove all dependencies of security and jwt and add dependency below <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId‌​> <version>2.0.5.RELEASE</version> </dependency>
  • Jonathan JOhx
    Jonathan JOhx over 5 years
    If you added it and it shows another error on your logs, you'll need to follow migration to spring security 5..
  • kk1957
    kk1957 over 5 years
    still same problem
  • kk1957
    kk1957 over 5 years
    I did that, still no changes. Any other suggestions?
  • kk1957
    kk1957 over 5 years
    What do you mean by spring security 5? The latest version is 3 something.
  • kj007
    kj007 over 5 years
    would you mind sharing your sample code at github..I can look at exactly..
  • kj007
    kj007 over 5 years
    @kk1957 found missing Configuration notation on WebSecurity. could you please add and try again if still issue can you advise how you are generating token by calling localhost:9100/auth but i didnt find any controller for it..let me know so i can complete test both end points.
  • Jonathan JOhx
    Jonathan JOhx over 5 years
    Well, when you use spring security 5 , you don't use some configurations that I saw on your code. I cloned your repository, I have a question. Did you generated an access token from localhost/9100/auth ? because I wasn't be able to generate it.
  • kk1957
    kk1957 over 5 years
    Yes I was able to. Your URL seems incorrect. localhost:9100/auth
  • kk1957
    kk1957 over 5 years
  • kk1957
    kk1957 over 5 years
    Thanks, it works now! I always suspected the two configurations that extended WebSecurityConfigurerAdapter.
  • kj007
    kj007 over 5 years
    You can configure multiple http security if required multiple authentications types, your case does not required.