Currently my Spring Boot app authenticates users using JWT which it also generates:
Security config:
public SecurityFilterChain customFilterChain(HttpSecurity http) throws Exception {
return http
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeRequests(auth -> auth
.anyRequest().authenticated()
)
.userDetailsService(userService)
.addFilter(new AuthenticationFilter(authenticationManager(), userService))
.addFilterBefore(new AuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
}
AuthorizationFilter:
public class AuthorizationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!hasAuthorizationToken(request)) {
filterChain.doFilter(request, response);
return;
}
String authorizationHeader = request.getHeader(AUTHORIZATION);
try {
UsernamePasswordAuthenticationToken authToken = JwtTokenUtil.decodeJwtToken(authorizationHeader);
SecurityContextHolder.getContext().setAuthentication(authToken);
} catch (Exception ex) {
log.error("Authorization error: " + ex.getMessage());
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
filterChain.doFilter(request, response);
}
}
Now I would like to add OAuth2 auth in addition to existing one. I have added oauth2ResourceServer to the config:
public SecurityFilterChain customFilterChain(HttpSecurity http) throws Exception {
return http
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeRequests(auth -> auth
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth -> {
oauth
.jwt()
.jwtAuthenticationConverter(oAuthJwtAuthenticationConverter);
})
.userDetailsService(userService)
.addFilter(new AuthenticationFilter(authenticationManager(), userService))
.addFilterBefore(new AuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
}
Now when sending a request with OAuth2, it works well, but when sending my custom JWT I get an error
An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
How can I handle two types of JWT?
Related
The following config (filterChain) works fine in SpringBoot-2.7.5, but after I tried to test it in SpringBoot-3.0.0-RC1, it is not working and shows the following message, anything I need to change if want to migrate to Spring-Boot-3.0.0. Thanks.
{
"timestamp": 1667794247614,
"status": 401,
"error": "Unauthorized",
"message": "An Authentication object was not found in the SecurityContext",
"path": "/api/admin/1" }
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationProvider).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/**").permitAll()
// private endpoints
.anyRequest().authenticated();
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
The following is the jwtTokenFilter:
#Component
public class **JwtTokenFilter** extends OncePerRequestFilter {
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
private JPAUserDetailService jpaUserDetailService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// Get authorization header and validate
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (isEmpty(header) || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
// Get jwt token and validate
final String token = header.split(" ")[1].trim();
if (!jwtTokenUtil.validate(token)) {
chain.doFilter(request, response);
return;
}
// Get user identity and set it on the spring security context
UserDetails userDetails = jpaUserDetailService.loadUserByUsername(jwtTokenUtil.getUsername(token));
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, (userDetails == null ? null : userDetails.getAuthorities()));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}
In Spring Security 6, the default behavior is that the SecurityContextHolderFilter will only read the SecurityContext from SecurityContextRepository and populate it in the SecurityContextHolder. Users now must explicitly save the SecurityContext with the SecurityContextRepository if they want the SecurityContext to persist between requests. This removes ambiguity and improves performance by only requiring writing to the SecurityContextRepository (i.e. HttpSession) when it is necessary.
SecurityContextHolder.setContext(securityContext);
securityContextRepository.saveContext(securityContext, httpServletRequest, httpServletResponse);
See https://docs.spring.io/spring-security/reference/5.8/migration.html#_explicit_save_securitycontextrepository
If that doesn't work, try going back to the 5.x default:
http
.securityContext((securityContext) ->
.requireExplicitSave(false)
)
I changed some codes as following, but still not working.
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// Get authorization header and validate
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (isEmpty(header) || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
// Get jwt token and validate
final String token = header.split(" ")[1].trim();
if (!jwtTokenUtil.validate(token)) {
chain.doFilter(request, response);
return;
}
// Get user identity and set it on the spring security context
UserDetails userDetails = jpaUserDetailService.loadUserByUsername(jwtTokenUtil.getUsername(token));
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, (userDetails == null ? null : userDetails.getAuthorities()));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextImpl scix = new SecurityContextImpl(authentication);
SecurityContextHolder.setContext(scix);
RequestAttributeSecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
securityContextRepository.saveContext(scix, request, response);
chain.doFilter(request, response);
}
And the configs following:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationProvider)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests((requests) - > requests
.requestMatchers("/swagger-ui", "/rest-api-docs").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
.securityContext((securityContext) - >
securityContext.requireExplicitSave(false)
);
return http.build();
}
I'd like to make people who hold the JWT can access all APIs but people can only access on EXCLUDE PATH now. what should I set up for that?
This is my WebConfig.
private static final String[] EXCLUDE_PATHS = {
"/api/user/**"
};
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(EXCLUDE_PATHS);
This is my interceptor.
public class JwtInterceptor implements HandlerInterceptor {
private static final String HEADER_AUTH = "Authorization";
private final JwtTokenProvider jwtTokenProvider;
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
final String token = request.getHeader(HEADER_AUTH);
if(token !=null && jwtTokenProvider.validateToken(token)){
return true;
}else{
throw new UnauthorizedException();
}
this is my validateToken fn
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
this is my doFilter
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
This is my security Config.
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf()
.ignoringAntMatchers("/h2-console/**")
.disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/h2-console/**").permitAll()
.antMatchers("/user/**").hasRole("USER")
.anyRequest().permitAll()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class);
}
Am I missing something? I add the Security Config.
You should use WebSecurity instead of interceptors.
Something like this for configuring which paths can be accessed and which cannot
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().authorizeRequests()
.antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
This link should help you well.
I'm trying to implement authentication and authorization using JWT token in SpringBoot REST API.
In my JWTAuthentication class
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,
FilterChain chain, Authentication auth) throws IOException, ServletException {
String token = Jwts.builder().setSubject(((User) auth.getPrincipal()).getUsername())
.claim("roles", ((User) auth.getPrincipal()).getAuthorities())
.setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SecurityConstants.SECRET.getBytes()).compact();
res.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
chain.doFilter(req, res);
System.out.println("Token:"+token);
}
When I test my code by sending by posting the following message to 127.0.0.1:8080/login URL, I see that authentication is successful.
{"username":"admin", "password":"admin"}
And then Spring calls my JWT Authorization class
#Override
protected void doFilterInternal(
HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String header = req.getHeader(SecurityConstants.HEADER_STRING);
if (header == null || !header.startsWith(SecurityConstants.TOKEN_PREFIX)) {
if (header == null) {
System.out.println("header null");
} else if (!header.startsWith(SecurityConstants.TOKEN_PREFIX)) {
System.out.println("token prefix missing in header");
}
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
It prints the message: "token prefix missing in header"
Although I add the TOKEN_PREFIX in the successfulAuthentication method, it can not find it in the header in doFilterInternal method.
By the way, my security config is like this:
#EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired private UserDetailsService userDetailsService;
#Autowired private BCryptPasswordEncoder bCryptPasswordEncoder;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.anyRequest()
.authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(
"/v2/api-docs",
"/configuration/ui",
"/swagger-resources/**",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**");
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
I checked the SpringBoot books but could not find a book that describes the inner details of the security framework. Since I did not understand how the framework works, I could not solve the problems by just looking at the blogs. Is there a book that you can suggest describing the details of SpringBoot Security?
Thanks
You set your token after you successfully authenticated the user to the header of
the Http response:
res.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
The internal JWT filter (from what I understand in your question is called after yours), looks in the Http headers of the request
String header = req.getHeader(SecurityConstants.HEADER_STRING);
and there they are not present.
In general, the second filter should not be active after you authenticated a user and should just return the JWT token to the client. Any subsequent call of the client should then include the JWT token in the Authorization header using Bearer: YourJWTToken for calling e.g. protected APIs.
I'm trying to implement JWT auth with a REST API in SpringBoot. When I debug my code, I see that the JWT Authenticator works correctly but I can't see that the JWT Authorization code is called by the Spring Security framework and there's no response sent to my REST client. Below are some parts of my code that I think are related to my problem.
I think my request is getting lost somewhere in the Spring Security flow...
WebSecurityConfig:
#EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.anyRequest()
.authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
JWTAuthenticationFilter:
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
setAuthenticationManager(authenticationManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (!HttpMethod.POST.matches(request.getMethod())) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
try {
JsonAuthenticationParser auth =
new ObjectMapper().readValue(request.getInputStream(), JsonAuthenticationParser.class);
System.out.println(auth.username);
System.out.println(auth.password);
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(auth.username, auth.password);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (Exception e) {
log.warn("Auth failed!!!!!!!!!!!!");
throw new InternalAuthenticationServiceException("Could not parse authentication payload");
}
}
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,
FilterChain chain, Authentication auth) throws IOException, ServletException {
String token = Jwts.builder().setSubject(((User) auth.getPrincipal()).getUsername())
.claim("roles", ((User) auth.getPrincipal()).getAuthorities())
.setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SecurityConstants.SECRET.getBytes()).compact();
res.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
System.out.println("Token:"+token);
}
JWTAuthorizationFilter
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
}
#Override
protected void doFilterInternal(
HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
System.out.println("++++++++++++++++++++++++++++AUTHERIZATION doFilterInternal++++++++++++++++++++++");
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
System.out.println("++++++++++++++++++++++++++++AUTHERIZATION getAuthentication++++++++++++++++++++++");
}
Background
When you add a filter to the filter chain without specifying the order (http.addFilter(...)), the comparator HttpSecurity uses to determine its order in the chain looks at the filter's parent class. UsernamePasswordAuthenticationFilter comes before BasicAuthenticationFilter (see FilterComparator).
The request comes in, reaches JWTAuthenticationFilter, and "ends" in the successfulAuthentication() method.
Solution
Continue the filter chain in JWTAuthenticationFilter:
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,
FilterChain chain, Authentication auth)
throws IOException, ServletException {
// ...
chain.doFilter(req, res);
}
I run into a problem when adding CSRF to my existing and working CORS configuration.
Everytime a POST, PUT or DELETE is triggered I get the error that the current token I have is not the right one (nvalid CSRF Token 'edff86dc-093a-4df9-8218-e5343506bdf9' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.).
But when I compare them it can't be caused by the tokens. Also if i trigger a GET after that (e.g. PUT) the token before is sent again and accepted.
So I assume there might be a problem with my security config but I don't see what I'm missing.
security config:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new CorsFilter(), ChannelProcessingFilter.class);
http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/*/**").permitAll()
.antMatchers("/logout", "/admin/**").authenticated();
http.csrf().ignoringAntMatchers("/guestbook/**");
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.formLogin().successHandler(authenticationSuccessHandler);
http.logout().logoutSuccessHandler(logoutSuccessHandler);
http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
}
token filter:
public class CsrfTokenResponseHeaderBindingFilter extends OncePerRequestFilter {
protected static final String REQUEST_ATTRIBUTE_NAME = "_csrf";
protected static final String RESPONSE_HEADER_NAME = "X-CSRF-HEADER";
protected static final String RESPONSE_PARAM_NAME = "X-CSRF-PARAM";
protected static final String RESPONSE_TOKEN_NAME = "X-CSRF-TOKEN";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, javax.servlet.FilterChain filterChain) throws ServletException, IOException {
CsrfToken token = (CsrfToken) request.getAttribute(REQUEST_ATTRIBUTE_NAME);
System.out.println(token.getToken());
if (token != null) {
response.setHeader(RESPONSE_HEADER_NAME, token.getHeaderName());
response.setHeader(RESPONSE_PARAM_NAME, token.getParameterName());
response.setHeader(RESPONSE_TOKEN_NAME , token.getToken());
}
filterChain.doFilter(request, response);
}
}
and for instance the cors filter:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
String origin = request.getHeader("Origin");
response.addHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.addHeader("Access-Control-Max-Age", "10");
response.addHeader("Access-Control-Expose-Headers", "X-CSRF-TOKEN");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
String headers = request.getHeader("Access-Control-Request-Headers");
if (!StringUtils.isEmpty(headers )) {
response.addHeader("Access-Control-Allow-Headers", headers );
}
if (request.getMethod().equals("OPTIONS")) {
try {
response.getWriter().print("OK");
response.getWriter().flush();
} catch (IOException e) {
e.printStackTrace();
}
} else{
chain.doFilter(request, response);
}
}
The problem not only occurs when I'm logged in. If i would not disable csrf on the guestbook path there would also be no POST possible.
I hope anybody can give me a hint.
Greetings
So I finally solved my problem. After a lot of seach and error I discovered the CsrfProtectionMatcher which can be used to enable CSRF on different paths.
Anyways this was very confusing to me, because I thought CSRF would be a always enabled on every request by default. So as soon as I applied the CsrfProtectionMatcher on my "/admin" path (allowing all possible methods, which is specified as null) it worked. requireCsrfProtectionMatcher on docs.spring.io , detailed article
Also it's now possible for me, to work with a more simple configuration but despite that my old one works too.
old one with CsrfProtectionMatcher :
protected void configure(HttpSecurity http) throws Exception {
RequestMatcher csrfRequestMatcher = new RequestMatcher() {
private RegexRequestMatcher requestMatcher =
new RegexRequestMatcher("/admin", null);
public boolean matches(HttpServletRequest request) {
if(requestMatcher.matches(request))
return true;
return false;
}
};
http.addFilterBefore(new CorsFilter(), ChannelProcessingFilter.class);
http
.csrf()
.requireCsrfProtectionMatcher(csrfRequestMatcher);
http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/*/**").permitAll()
.antMatchers("/login", "/**/**/**").permitAll()
.antMatchers("/logout", "/admin/**").authenticated();
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.formLogin().successHandler(authenticationSuccessHandler);
http.logout().logoutSuccessHandler(logoutSuccessHandler);
http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
}
more simple configuration:
http.addFilterBefore(new CorsFilter(), ChannelProcessingFilter.class);
http
.csrf()
.requireCsrfProtectionMatcher(csrfRequestMatcher)
.and()
.authorizeRequests()
.antMatchers("/**/**")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(authenticationSuccessHandler)
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler)
.permitAll();
http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
I have to admit I still don't know why CSRF has to be explicit enabled. If anyone has an answer to that please tell me.