spring security resource server response 500 instead of 401

56 Views Asked by At

I used org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.1.4, everything works fine but it returns 500 instead of 401. I have a custom JwtDecoder to check token integrity and expiration. Here are some tests.

Test 1: Works fine, token is expired then it returns 401.

$ curl -i -H "Authorization: Bearer $JWT" \
http://localhost:8380/api/notifications
HTTP/1.1 401 
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: JSESSIONID=929D2DE566B4070749F8D0D82B310661; Path=/; HttpOnly
WWW-Authenticate: Bearer
Content-Length: 0
Date: Mon, 25 Dec 2023 03:26:32 GMT

Test 2: $JWT is same as Test 1, and i add a header with cookie JSESSIONID, then it returns 500.

curl -i -H "Authorization: Bearer $JWT" -H "Cookie: JSESSIONID=B0B02F92145546F4BD16DBE77EB2ED7D" \
http://localhost:8380/api/notifications
HTTP/1.1 500 
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 25 Dec 2023 03:25:47 GMT
Connection: close

{"timestamp":"2023-12-25T03:25:47.620+00:00","status":500,"error":"Internal Server Error","path":"/api/notifications"}%

Test 3: Wait for session timeout(i guess, not sure) then send Test 2 input again, it returns 401 which i want.

curl -i -H "Authorization: Bearer $JWT" -H "Cookie: JSESSIONID=B0B02F92145546F4BD16DBE77EB2ED7D" \
http://localhost:8380/api/notifications
HTTP/1.1 401 
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: JSESSIONID=30789CBCCE2B971720F0F4CF6A697129; Path=/; HttpOnly
WWW-Authenticate: Bearer
Content-Length: 0
Date: Mon, 25 Dec 2023 04:08:06 GMT

Here is Test 2 stacktrace from console.

ERROR [http-nio-8380-exec-3] Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
org.springframework.security.authentication.AuthenticationServiceException: JWT is expired.
    at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.getJwt(JwtAuthenticationProvider.java:106)
    at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.authenticate(JwtAuthenticationProvider.java:88)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182)
    at org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter.doFilterInternal(BearerTokenAuthenticationFilter.java:137)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter.doFilterInternal(DefaultLogoutPageGeneratingFilter.java:58)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.doFilter(DefaultLoginPageGeneratingFilter.java:188)
    at org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.doFilter(DefaultLoginPageGeneratingFilter.java:174)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(OAuth2AuthorizationRequestRedirectFilter.java:181)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: org.springframework.security.oauth2.jwt.JwtException: JWT is expired.
    at com.mycom.MyJwtDecoder.decode(MyJwtDecoder.java:62)
    at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.getJwt(JwtAuthenticationProvider.java:99)
    ... 70 more

Here are some configs.

@Configuration
public class SecurityConfig {
    @Bean
    public JwtDecoderFactory<ClientRegistration> jwtDecoderFactory(JwtDecoder jwtDecoder) {
        return (ClientRegistration context) -> jwtDecoder;
    }

    @Bean
    JwtDecoder jwtDecoder() {
        return new MyJwtDecoder("mysecret");
    }   

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,
                                           JwtDecoderFactory<ClientRegistration> jwtDecoderFactory) throws Exception {
        http
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .decoder(jwtDecoderFactory.createDecoder(null))
                )
            )
        ;
        return http.build();
    }
}
package com.mycom;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import java.text.ParseException;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;

@Slf4j
public class MyJwtDecoder implements JwtDecoder {

    private final String secret;

    private final boolean throwIfExpired;

    public MyJwtDecoder(String secret) {
        this.secret = secret;
        this.throwIfExpired = true;
    }

    public MyJwtDecoder(String secret, boolean throwIfExpired) {
        this.secret = secret;
        this.throwIfExpired = throwIfExpired;
    }

    @Override
    public Jwt decode(String token) throws JwtException {
        LOGGER.debug("Decode token: {}", token);
        try {
            JWT jwt = JWTParser.parse(token);
            SignedJWT jws = (SignedJWT)jwt;
            MACVerifier verifier = new MACVerifier(secret);
            if (!jws.verify(verifier)) {
                throw new JwtException("JWT is invalid.");
            }
            if (throwIfExpired) {
                Instant exp = jwt.getJWTClaimsSet().getExpirationTime().toInstant();
                if (exp.isBefore(ZonedDateTime.now().toInstant())) {
                    throw new JwtException("JWT is expired.");
                }
            }
            return createJwt(token, jwt);
        } catch (ParseException e) {
            LOGGER.warn("Parse jwt failed. Error: {}", e.getMessage());
            throw new JwtException(e.getMessage());
        } catch (JOSEException e) {
            LOGGER.warn("Verify failed. Error: {}", e.getMessage());
            throw new JwtException(e.getMessage());
        }
    }

    private Jwt createJwt(String token, JWT parsedJwt) {
        try {
            Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
            Map<String, Object> claims = new HashMap<>(parsedJwt.getJWTClaimsSet().getClaims());
            if (claims.get(JwtClaimNames.IAT) instanceof Date) {
                Date iat = (Date)claims.get(JwtClaimNames.IAT);
                claims.put(JwtClaimNames.IAT, Instant.ofEpochMilli(iat.getTime()));
            }
            if (claims.get(JwtClaimNames.EXP) instanceof Date) {
                Date exp = (Date)claims.get(JwtClaimNames.EXP);
                claims.put(JwtClaimNames.EXP, Instant.ofEpochMilli(exp.getTime()));
            }

            return Jwt.withTokenValue(token)
                .headers(h -> h.putAll(headers))
                .claims(c -> c.putAll(claims))
                .build();
        } catch (Exception ex) {
            LOGGER.error("Decode jwt error.", ex);
            throw new JwtException(ex.getMessage());
        }
    }
}

I have no idea how to deal with this problem, please help.

0

There are 0 best solutions below