Spring Authorization Server撤销access token报invalid_client错误
当前使用 Spring Authorization Server 0.3.1 版本,通过OAuth客户端(angular-oauth2-oidc)调用 /oauth2/revoke 端点,请求载荷如下:
client_id: my-client-id token: eyJraWQiOiJlYzA3N2Y2OC1jMjQ1LTQ[the rest is stripped for readability] token_type_hint: access_token
接口响应返回错误invalid_client。
经过调试定位,异常在org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider#authenticate方法中抛出,相关代码如下:
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthentication = (OAuth2TokenRevocationAuthenticationToken) authentication; OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(tokenRevocationAuthentication);
其中getAuthenticatedClientElseThrowInvalidClient方法的实现逻辑如下:
static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) { OAuth2ClientAuthenticationToken clientPrincipal = null; if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) { clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal(); } if (clientPrincipal != null && clientPrincipal.isAuthenticated()) { return clientPrincipal; } throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); }
经分析,OAuth2TokenRevocationAuthenticationProvider要求传入的authentication对象的principal属性为OAuth2ClientAuthenticationToken实例,但实际运行时principal是JwtAuthenticationToken实例。排查发现,由于请求携带了bearer token,服务端完成认证后会向SecurityContextHolder中设置JwtAuthenticationToken,相关调试日志如下:
2022-06-28 23:27:01.392 DEBUG 16904 --- [ XNIO-1 task-1] o.s.security.web.FilterChainProxy : Securing POST /oauth2/revoke 2022-06-28 23:27:01.393 DEBUG 16904 --- [ XNIO-1 task-1] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext 2022-06-28 23:27:01.394 DEBUG 16904 --- [ XNIO-1 task-1] o.s.s.o.s.r.a.JwtAuthenticationProvider : Authenticated token 2022-06-28 23:27:01.397 DEBUG 16904 --- [ XNIO-1 task-1] .o.s.r.w.BearerTokenAuthenticationFilter : Set SecurityContextHolder to JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@1615fb2d, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[SCOPE_openid, SCOPE_profile, SCOPE_offline_access, SCOPE_email]] 2022-06-28 23:27:01.398 DEBUG 16904 --- [ XNIO-1 task-1] o.s.s.w.a.i.FilterSecurityInterceptor : Authorized filter invocation [POST /oauth2/revoke] with attributes [authenticated]
该场景下是否遗漏了相关配置?如何调整才能让/oauth2/revoke端点获取到OAuth2ClientAuthenticationToken而非JwtAuthenticationToken?
问题根因
invalid_client错误核心是安全过滤链的配置顺序与拦截规则错误:
/oauth2/revoke是OAuth2协议定义的令牌撤销端点,Spring Authorization Server对该端点的鉴权逻辑要求必须先完成客户端身份认证,从SecurityContext中获取OAuth2ClientAuthenticationToken类型的已认证客户端主体。- 当前配置将
/oauth2/revoke纳入了普通资源服务的JWT认证规则范围,请求携带用户access_token时,BearerTokenAuthenticationFilter优先执行,直接将代表用户身份的JwtAuthenticationToken写入SecurityContext,后续客户端认证逻辑完全不触发,自然无法获取到要求的客户端认证对象。
从贴出的调试日志可以直接验证:请求到达端点前仅完成了JWT用户认证,没有任何客户端认证流程的执行记录。
修复步骤
1. 调整多安全过滤链优先级
Spring Authorization Server会自带处理OAuth2协议端点的过滤链,必须保证该过滤链优先级高于自定义的资源服务过滤链,避免资源服务的JWT认证逻辑提前拦截授权服务器端点,示例配置如下:
@Bean @Order(Ordered.HIGHEST_PRECEDENCE) // 授权服务器过滤链设置为最高优先级 public SecurityFilterChain authorizationServerSecurityChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); http.apply(authorizationServerConfigurer); // 此处可追加授权服务器的自定义配置,比如客户端存储、令牌生成规则、端点权限配置等 return http.build(); } @Bean @Order(2) // 资源服务过滤链优先级低于授权服务器链 public SecurityFilterChain resourceServerSecurityChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize // 不要在此处将/oauth2/**下的端点配置为需要JWT认证 .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build(); }
2. 修正撤销端点的请求方式
当前请求仅传递了client_id表单参数,缺少客户端认证凭证,且不要在请求中携带用户的Bearer Token作为Authorization头,避免被JWT认证过滤器拦截:
- 如果使用
client_secret_post客户端认证方式:需要在表单参数中额外传递client_secret,请求不携带Authorization头 - 如果使用
client_secret_basic客户端认证方式:将client_id和client_secret编码后放入Basic类型的Authorization请求头,不需要在表单中重复传递客户端凭证
3. 校验客户端注册配置
确认授权服务器中注册的my-client-id客户端,配置的认证方式和实际调用时使用的方式一致:比如使用client_secret_post调用时,客户端的clientAuthenticationMethods必须包含CLIENT_SECRET_POST。
验证
调整配置后重启服务,不携带用户Bearer Token、按选定的客户端认证方式传递凭证调用撤销端点,调试日志会出现ClientSecretAuthenticationProvider的认证记录,SecurityContext中会存入OAuth2ClientAuthenticationToken,接口即可正常响应,不再返回invalid_client错误。
内容的提问来源于stack exchange,提问作者Andrey Grigoriev

