Spring Boot bearer token authentication giving 401
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();
}
}
kk1957
Updated on June 04, 2022Comments
-
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 codepublic 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-fafc9d7187e8but 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 over 5 yearsI 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 over 5 yearsStill same problem.
-
kk1957 over 5 yearsadded pom dependencies in the original question, also added application.properties.
-
kj007 over 5 years@kk1957 define SecurityCredentialsConfig order 2..check my updated answer.
-
Jonathan JOhx over 5 yearsOkay, 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 over 5 yearsIf you added it and it shows another error on your logs, you'll need to follow migration to spring security 5..
-
kk1957 over 5 yearsstill same problem
-
kk1957 over 5 yearsI did that, still no changes. Any other suggestions?
-
kk1957 over 5 yearsWhat do you mean by spring security 5? The latest version is 3 something.
-
kj007 over 5 yearswould you mind sharing your sample code at github..I can look at exactly..
-
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 over 5 yearsWell, 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 over 5 yearsYes I was able to. Your URL seems incorrect. localhost:9100/auth
-
kk1957 over 5 yearsLet us continue this discussion in chat.
-
kk1957 over 5 yearsThanks, it works now! I always suspected the two configurations that extended WebSecurityConfigurerAdapter.
-
kj007 over 5 yearsYou can configure multiple http security if required multiple authentications types, your case does not required.