You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Security验证Okta JWT失败:application/okta-internal-at+jwt头不被允许

问题:Spring Boot WebFlux集成Okta OAuth2时JWT令牌typ头验证失败

使用Spring Boot 3.4.1结合WebFlux集成Okta OAuth2客户端,配置完成后遇到JWT令牌验证错误,报错提示JOSE头类型application/okta-internal-at+jwt不被允许。

已尝试自定义ReactiveJwtDecoder和令牌验证器来兼容Okta内部令牌,但自定义验证器未生效,仍因不支持的typ头导致验证失败。


相关配置信息

pom.xml

Spring Boot Version <version>3.4.1</version>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

应用配置

# Octa Security
spring.security.oauth2.client.registration.okta.client-id=0oan0wkeiuzfUuHsw5d7
spring.security.oauth2.client.registration.okta.client-secret=uHLcvr9G8jT69Lmbxb_PoHJ7ltPeE5zd4PphpxrwNvD41gP7vHALXdF7CMMjdhr2
spring.security.oauth2.client.registration.okta.scope=openid, profile, email, offline_access
spring.security.oauth2.client.registration.okta.provider=okta
spring.security.oauth2.client.provider.okta.issuer-uri=https://dev-52900394.okta.com
spring.security.oauth2.client.registration.okta.redirect-uri=http://localhost:8093/login/oauth2/code/okta

SecurityConfig

package com.gateway.ApiGateway.security;

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

  @Bean
  public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {

      httpSecurity
              .authorizeExchange(exchanges -> exchanges
                      .anyExchange()
                      .authenticated()
              )
              .oauth2Login()
              .and()
              .oauth2ResourceServer()
              .jwt();

      return httpSecurity.build();
  }

    @Bean
    public ReactiveJwtDecoder jwtDecoder() {
        NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder
                .withJwkSetUri("https://dev-52900394.okta.com/oauth2/v1/keys")
                .build();

        // Custom JWT validator to allow Okta internal tokens
        OAuth2TokenValidator<Jwt> customValidator = jwt -> {
            String tokenType = (String) jwt.getHeaders().get("typ");

            if (!"application/okta-internal-at+jwt".equals(tokenType) && !"JWT".equals(tokenType)) {
                throw new JwtValidationException("Invalid token type", null);
            }

            // Apply default validators (expiry, signature, etc.)
            return JwtValidators.createDefault().validate(jwt);
        };

        jwtDecoder.setJwtValidator(customValidator);
        return jwtDecoder;
    }

    @Bean
    public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
        ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            List<GrantedAuthority> authorities = ((List<String>) jwt.getClaims().getOrDefault("roles", List.of()))
                    .stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());

            return Flux.fromIterable(authorities);
        });
        return converter;
    }
}

测试请求

curl --location 'http://172.17.80.1:8093/questions' \
--header 'Authorization: Bearer eyJraWQiOiJaWTdvNEk5YWpMSmtQeENJNTFEVVJaQ1NvaEEtZjNKdFNDOWtLd1pva2ZnIiwidHlwIjoiYXBwbGljYXRpb25cL29rdGEtaW50ZXJuYWwtYXQrand0IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULm5UTDRlRUxBc1RPRUVqZ21HMFlRc2NFdEQwbUlveXdhbDRzeXd4ODJWWTgub2FyMndybTE3ZEFYdllkdHA1ZDciLCJpc3MiOiJodHRwczovL2Rldi01MjkwMDM5NC5va3RhLmNvbSIsImF1ZCI6Imh0dHBzOi8vZGV2LTUyOTAwMzk0Lm9rdGEuY29tIiwic3ViIjoicmF2aWRvYmFyaXlhOTUzN0BnbWFpLmNvbSIsImlhdCI6MTczODIxODc4NiwiZXhwIjoxNzM4MjIyMzg2LCJjaWQiOiIwb2FuMHdrZWl1emZVdUhzdzVkNyIsInVpZCI6IjAwdW4wd202NTRrb3NZRm5mNWQ3Iiwic2NwIjpbIm9wZW5pZCIsInByb2ZpbGUiLCJlbWFpbCIsIm9mZmxpbmVfYWNjZXNzIl0sImF1dGhfdGltZSI6MTczODIxODc4Mn0.m4VJTDqpCl64PbE9dGYtzKokrU7Gf82UHfbPcroyVx4bO_psuLAjgzIinbqhbuiVRfHlN-nLGLS7jzSwY6Gib5uE1Fo-ioxkq1TUs7_bOYOI82O2XMoIe8H970FeMqprSZVdXYPMfEx5JlFdGSbl6rkQ6OG9QRRfkCk-GeZpG5O5BzE1eb9vMwO09oHntKaruv0OQJrIFu2lK1wxtfVcSBkM7xeTAWqrCQmHmuEjrj58PJFXU0Ci5RZgX6Yipp5LS0IhR7YnDmowy64PPkc9D1hOOJDxuDivT_X7-OcLhOZbH4bOhegr52va79nmMxquJlOsnE56xNCcCLA72xo6Aw' \
--header 'Cookie: SESSION=928008dd-57a2-470a-9565-13e606aaeb5d'

报错信息

org.springframework.security.oauth2.server.resource.InvalidBearerTokenException: Failed to validate the token
    at org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager.onError(JwtReactiveAuthenticationManager.java:79) ~[spring-security-oauth2-resource-server-6.4.2.jar:6.4.2]
    at reactor.core.publisher.Mono.lambda$onErrorMap$28(Mono.java:3848) ~[reactor-core-3.7.1.jar:3.7.1]
    at reactor.core.publisher.Mono.lambda$onErrorResume$30(Mono.java:3938) ~[reactor-core-3.7.1.jar:3.7.1]
    at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:94) ~[reactor-core-3.7.1.jar:3.7.1]
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) ~[netty-transport-4.1.116.Final.jar:4.1.116.Final]
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.116.Final.jar:4.1.116.Final]
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.116.Final.jar:4.1.116.Final]
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.116.Final.jar:4.1.116.Final]
    at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
Caused by: org.springframework.security.oauth2.jwt.BadJwtException: Failed to validate the token
    at org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.createClaimsSet(NimbusReactiveJwtDecoder.java:295) ~[spring-security-oauth2-jose-6.4.2.jar:6.4.2]
    at org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder$JwkSetUriReactiveJwtDecoderBuilder.lambda$processor$13(NimbusReactiveJwtDecoder.java:449) ~[spring-security-oauth2-jose-6.4.2.jar:6.4.2]
    at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) ~[reactor-core-3.7.1.jar:3.7.1]
    ... 232 common frames omitted
Caused by: com.nimbusds.jose.proc.BadJOSEException: JOSE header typ (type) application/okta-internal-at+jwt not allowed
    at com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier.verify(DefaultJOSEObjectTypeVerifier.java:149) ~[nimbus-jose-jwt-9.37.3.jar:9.37.3]
    at com.nimbusds.jwt.proc.DefaultJWTProcessor.process(DefaultJWTProcessor.java:341) ~[nimbus-jose-jwt-9.37.3.jar:9.37.3]
    at com.nimbusds.jwt.proc.DefaultJWTProcessor.process(DefaultJWTProcessor.java:303) ~[nimbus-jose-jwt-9.37.3.jar:9.37.3]
    at org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.createClaimsSet(NimbusReactiveJwtDecoder.java:292) ~[spring-security-oauth2-jose-6.4.2.jar:6.4.2]
    ... 234 common frames omitted

解决方案

问题根源在于Nimbus库的DefaultJOSEObjectTypeVerifier默认只允许JWT类型的令牌,且这个验证逻辑在Spring Security自定义验证器执行前就会触发,导致自定义验证器无法生效。

需要直接修改Nimbus的JWTProcessor配置,添加允许的JOSE令牌类型:

@Bean
public ReactiveJwtDecoder jwtDecoder() {
    NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder
            .withJwkSetUri("https://dev-52900394.okta.com/oauth2/v1/keys")
            // 自定义Nimbus JWT处理器,添加允许的令牌类型
            .jwtProcessorCustomizer(jwtProcessor -> {
                DefaultJOSEObjectTypeVerifier verifier = new DefaultJOSEObjectTypeVerifier();
                verifier.setAllowedTypes(
                        new JOSEObjectType("JWT"),
                        new JOSEObjectType("application/okta-internal-at+jwt")
                );
                jwtProcessor.setJOSEObjectTypeVerifier(verifier);
            })
            .build();

    // 保留原有自定义验证逻辑(用于额外业务校验)
    OAuth2TokenValidator<Jwt> customValidator = jwt -> {
        String tokenType = (String) jwt.getHeaders().get("typ");
        if (!"application/okta-internal-at+jwt".equals(tokenType) && !"JWT".equals(tokenType)) {
            throw new JwtValidationException("Invalid token type", null);
        }
        return JwtValidators.createDefault().validate(jwt);
    };

    jwtDecoder.setJwtValidator(customValidator);
    return jwtDecoder;
}

说明

  1. 通过jwtProcessorCustomizer直接修改Nimbus的核心处理器,让它认可application/okta-internal-at+jwt类型的令牌,解决底层验证拦截问题。
  2. 原有的自定义验证器可以保留,用于补充业务层面的额外校验规则。

修改后重启服务,使用原测试请求调用即可通过验证。


内容的提问来源于stack exchange,提问作者Ravi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 17:23:13