springspring-securityoauth-2.0spring-security-oauth2spring-oauth2

JwtAuthenticationToken is not in the allowlist, Jackson issue


I have created my authorization server using org.springframework.security:spring-security-oauth2-authorization-server:0.2.2 and my client using org.springframework.boot:spring-boot-starter-oauth2-client. The users are able to sign in and out successfully, however, while testing I noticed that if I log in successfully then restart the client (but not the server) without signing out and try to login in again the server throws the following error in an endless loop of redirects

java.lang.IllegalArgumentException: The class with org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken and name of org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details

I tried to follow this link https://github.com/spring-projects/spring-security/issues/4370 but the solution on it did not work for me. I also tried a different solution described in this link https://github.com/spring-projects/spring-authorization-server/issues/397#issuecomment-900148920 and modified my authorization server code as follows:- Here is my Jackson Configs

@Configuration
public class JacksonConfiguration {

    /**
     * Support for Java date and time API.
     *
     * @return the corresponding Jackson module.
     */
    @Bean
    public JavaTimeModule javaTimeModule() {
        return new JavaTimeModule();
    }

    @Bean
    public Jdk8Module jdk8TimeModule() {
        return new Jdk8Module();
    }

    /*
     * Support for Hibernate types in Jackson.
     */
    @Bean
    public Hibernate5Module hibernate5Module() {
        return new Hibernate5Module();
    }

    /*
     * Module for serialization/deserialization of RFC7807 Problem.
     */
    @Bean
    public ProblemModule problemModule() {
        return new ProblemModule();
    }

    /*
     * Module for serialization/deserialization of ConstraintViolationProblem.
     */
    @Bean
    public ConstraintViolationProblemModule constraintViolationProblemModule() {
        return new ConstraintViolationProblemModule();
    }

    /**
     * To (de)serialize a BadCredentialsException, use CoreJackson2Module:
     */
    @Bean
    public CoreJackson2Module coreJackson2Module() {
        return new CoreJackson2Module();
    }

    @Bean
    @Primary
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(coreJackson2Module());
        mapper.registerModule(javaTimeModule());
        mapper.registerModule(jdk8TimeModule());
        mapper.registerModule(hibernate5Module());
        mapper.registerModule(problemModule());
        mapper.registerModule(constraintViolationProblemModule());
        return mapper;
    }
}

and here is my Authorization server config

@Configuration(proxyBeanMethods = false)
public class AuthServerConfig {

    private final DataSource dataSource;
    private final AuthProperties authProps;
    private final PasswordEncoder encoder;

    public AuthServerConfig(DataSource dataSource, AuthProperties authProps, PasswordEncoder encoder) {
        this.dataSource = dataSource;
        this.authProps = authProps;
        this.encoder = encoder;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();
        authorizationServerConfigurer.tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint
                .revocationResponseHandler((request, response, authentication) -> {
                    Assert.notNull(request, "HttpServletRequest required");
                    HttpSession session = request.getSession(false);
                    if (!Objects.isNull(session)) {
                        session.removeAttribute("SPRING_SECURITY_CONTEXT");
                        session.invalidate();
                    }
                    SecurityContextHolder.getContext().setAuthentication(null);
                    SecurityContextHolder.clearContext();
                    response.setStatus(HttpStatus.OK.value());
                })
        );
        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

        http
                .requestMatcher(endpointsMatcher)
                .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                .apply(authorizationServerConfigurer);

        return http.formLogin(Customizer.withDefaults()).build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, TokenSettings tokenSettings) {
        JdbcRegisteredClientRepository clientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        RegisteredClient webClient = RegisteredClient.withId("98a9104c-a9c7-4d7c-ad03-ec61bcfeab36")
                .clientId(authProps.getClientId())
                .clientName(authProps.getClientName())
                .clientSecret(encoder.encode(authProps.getClientSecret()))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:8000/login/oauth2/code/web-client")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .tokenSettings(tokenSettings)
                .build();

        clientRepository.save(webClient);
        return clientRepository;
    }

    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
                                                           RegisteredClientRepository registeredClientRepository,
                                                           ObjectMapper objectMapper) {
        JdbcOAuth2AuthorizationService authorizationService =
                new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
        JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);

        ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
        objectMapper.registerModules(SecurityJackson2Modules.getModules(classLoader));
        objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
        // You will need to write the Mixin for your class so Jackson can marshall it.
        // objectMapper.addMixIn(UserPrincipal .class, UserPrincipalMixin.class);
        rowMapper.setObjectMapper(objectMapper);
        authorizationService.setAuthorizationRowMapper(rowMapper);

        return authorizationService;
    }

    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
                                                                         RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    private static RSAKey generateRsa() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        return new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    }

    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder()
                .issuer(authProps.getIssuerUri())
                .build();
    }

    @Bean
    public TokenSettings tokenSettings() {
        return TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofDays(1))
                .refreshTokenTimeToLive(Duration.ofDays(1))
                .build();
    }

}

But am still facing the same issue.

How do I solve this? Any assistance is highly appreciated.


Solution

  • After trying out different solutions this was how I was able to solve it.

    I changed my OAuth2AuthorizationService bean to look like this.

    @Bean
        public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
                                                               RegisteredClientRepository registeredClientRepository) {
            JdbcOAuth2AuthorizationService authorizationService =
                    new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
            JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper =
                    new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
            JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper oAuth2AuthorizationParametersMapper =
                    new JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper();
    
            ObjectMapper objectMapper = new ObjectMapper();
            ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
            List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
            objectMapper.registerModules(securityModules);
            objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
            objectMapper.addMixIn(JwtAuthenticationToken.class, JwtAuthenticationTokenMixin.class);
    
            rowMapper.setObjectMapper(objectMapper);
            oAuth2AuthorizationParametersMapper.setObjectMapper(objectMapper);
    
            authorizationService.setAuthorizationRowMapper(rowMapper);
            authorizationService.setAuthorizationParametersMapper(oAuth2AuthorizationParametersMapper);
    
            return authorizationService;
        }
    

    and here is my JwtAuthenticationTokenMixin configurations

    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
    @JsonDeserialize(using = JwtAuthenticationTokenDeserializer.class)
    @JsonAutoDetect(
            fieldVisibility = JsonAutoDetect.Visibility.ANY,
            getterVisibility = JsonAutoDetect.Visibility.NONE,
            isGetterVisibility = JsonAutoDetect.Visibility.NONE)
    @JsonIgnoreProperties(ignoreUnknown = true)
    public abstract class JwtAuthenticationTokenMixin {}
    
    
    class JwtAuthenticationTokenDeserializer extends JsonDeserializer<JwtAuthenticationToken> {
    
        @Override
        public JwtAuthenticationToken deserialize(JsonParser parser, DeserializationContext context) throws IOException {
            ObjectMapper mapper = (ObjectMapper) parser.getCodec();
            JsonNode root = mapper.readTree(parser);
            return deserialize(parser, mapper, root);
        }
    
        private JwtAuthenticationToken deserialize(JsonParser parser, ObjectMapper mapper, JsonNode root)
                throws JsonParseException {
            JsonNode principal = JsonNodeUtils.findObjectNode(root, "principal");
            if (!Objects.isNull(principal)) {
                String tokenValue = principal.get("tokenValue").textValue();
                long issuedAt = principal.get("issuedAt").longValue();
                long expiresAt = principal.get("expiresAt").longValue();
                Map<String, Object> headers = JsonNodeUtils.findValue(
                        principal, "headers", JsonNodeUtils.STRING_OBJECT_MAP, mapper);
                Map<String, Object> claims = new HashMap<>();
                claims.put("claims", principal.get("claims"));
                Jwt jwt = new Jwt(tokenValue, Instant.ofEpochMilli(issuedAt), Instant.ofEpochMilli(expiresAt), headers, claims);
                return new JwtAuthenticationToken(jwt);
            }
            return null;
        }
    }
    
    abstract class JsonNodeUtils {
    
        static final TypeReference<Set<String>> STRING_SET = new TypeReference<Set<String>>() {
        };
    
        static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<Map<String, Object>>() {
        };
    
        static String findStringValue(JsonNode jsonNode, String fieldName) {
            if (jsonNode == null) {
                return null;
            }
            JsonNode value = jsonNode.findValue(fieldName);
            return (value != null && value.isTextual()) ? value.asText() : null;
        }
    
        static <T> T findValue(JsonNode jsonNode, String fieldName, TypeReference<T> valueTypeReference,
                               ObjectMapper mapper) {
            if (jsonNode == null) {
                return null;
            }
            JsonNode value = jsonNode.findValue(fieldName);
            return (value != null && value.isContainerNode()) ? mapper.convertValue(value, valueTypeReference) : null;
        }
    
        static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) {
            if (jsonNode == null) {
                return null;
            }
            JsonNode value = jsonNode.findValue(fieldName);
            return (value != null && value.isObject()) ? value : null;
        }
    
    }