你的微服务在用 OAuth2 却不知道 CSRF 和 PKCE ?

前言

微服务实战:基于Spring Cloud Gateway + AWS Cognito 的BFF案例一文中, 介绍了基于Amazon Cognito的OAuth授权码模式的认证流程。本文中,我们将研究可能针对此流程的恶意攻击以及如何防止它们。你将了解如何使用状态随机数(state nonce)来防止可能的 CSRF 攻击,以及 OAuth 推荐的用于防止重定向拦截攻击的 PKCE(Proof Key for Code Exchange)机制。最后将附上基于Spring Security的代码实现。

CSRF

Cognito 的 登录端点(LOGIN Endpoint) 只需要几个参数:

  • response_type=code
  • client_id
  • redirect_uri

这些都是公开信息,因为所有用户都使用相同的值。 因此,如果用户位于受攻击者控制的页面上,则攻击者可以将其重定向到有效的登录 URL。 如果用户最近登录过,Cognito 中的 AUTHORIZATION 端点将直接用有效code重定向,登录端点甚至可能不会提示用户登录。 因此,攻击者无需动一根手指就可让用户登录到 Web 应用程序。

攻击者无法控制 redirect_uri 参数,因为Cognito用户池配置了合法的重定向URI列表。 所以这块没有漏洞,攻击者没办法改变它。

但是攻击者控制的登录流程中有一个可选参数:state。 如果在 LOGIN 重定向中提供了该值,则 Cognito 将原封不动的返回给 Web 应用程序。

state本身不容易受到任何攻击。 但是 Web 应用程序可能会实现自定义逻辑,这种逻辑使用攻击者有机可乘。

想象一下,一个 CRM 应用程序处理客户帐户,它的实现方式是在访问令牌过期时自动重试失败的操作。 例如,管理员可能想要删除客户帐户,但后端返回一个错误,指出浏览器需要重新登录。 webapp 重定向到 Cognito登录页面,然后 webapp 获取新令牌,然后使用新令牌重试操作。 实现这一点的最简单方法是将失败的操作添加到状态参数(state)中,以便 webapp 可以知道要重试什么样的操作。

攻击

这样的实现为攻击者提供了一种欺骗用户向后端发送任意请求的方法。 这就是所谓的CSRF(跨站点请求伪造)攻击。 这是它的工作原理是:

即使没有这么严重的后果,webapp 也应该防止在合法登录尝试之外获取令牌的行为。

防御

为了实现对 CSRF 攻击的防御,WebApp 需要生成并存储一个 nonce(随机生成的值仅使用一次)并将其用作状态。 然后,当 Cognito 重定向回 WebApp 时,WebApp 需要对比存储的状态值和接收到的状态值,如果不匹配则抛出错误。

使用这种安全机制,攻击者将无计可施。

PKCE

PKCE,发音为“pixy”,是 Proof Key for Code Exchange 的首字母缩写。 PKCE 流程和标准授权代码流程之间的主要区别是用户不需要提供 client_secret。 PKCE 降低了本地应用程序(Native Application, OAuth客户端的一种)的安全风险,因为源代码中不需要嵌入secret,这限制了通过逆向工程的获取secret的可能性。

rfc7636 - Proof Key for Code Exchange by OAuth Public Clients 标准中定义的授权码重定向拦截攻击如下所示:

 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+| End Device (e.g., Smartphone)|||| +-------------+ +----------+ | (6) Access Token+----------+| |Legitimate | | Malicious|<--------------------||| |OAuth 2.0 App| | App|-------------------->||| +-------------+ +----------+ | (5) Authorization ||||^^ |Grant|||| \ | | ||||\ (4)| | |||(1) | \Authz| | ||| Authz|\ Code | | |Authz || Request| \ | | |Server|||\| | |||| \ | | |||v\| | ||| +----------------------------+ | ||| || | (3) Authz Code||| | Operating System/|<--------------------||| | Browser|-------------------->||| || | (2) Authz Request ||| +----------------------------+ | +----------++~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ Figure 1: Authorization Code Interception Attack 

它是如何工作的?

为了代替 client_secret,客户端应用程序创建了一个唯一的字符串值 code_verifier,并其进行哈希处理生成 code_challenge。 当客户端应用程序启动授权码流程的第一部分时,它会发送一个 code_challenge

一旦用户通过身份验证并将授权码返回给客户端应用程序,它就会用授权码请求换取一个 access_token

在此步骤中,客户端应用程序必须在 code_verifier 参数中包含原始唯一字符串值。 如果成功匹配,则身份验证完成并返回 access_token

攻击

当 Cognito 使用授权码参数重定向到 web 应用程序时,会出现上述安全问题。接下来会调用 TOKEN 端点,用得到的授权码换取访问令牌。 此调用需要以下参数:

  • grant_type=authorization_code
  • client_id
  • redirect_uri
  • code

除了授权码之外的所有信息都是公共信息,因为每个用户都是相同的。 因此,任何拥有有效授权码的人都可以获得有效的访问令牌。

同样,攻击者无法控制 Cognito 在登录后将用户重定向到何处,因为用户池配置了合法的重定向地址列表。 但在很多情况下,请求可能被捕获。 例如,移动应用程序可以为该特定 URL 注册一个处理程序,或者后端应用程序将这些请求日志记录到一个不安全的地方。 在这两种情况下,攻击者都可以读取授权码参数,然后获得有效的令牌。

防御

PKCE是一个 OAuth 2.0 的扩展,用于保护授权码重定向。 这类似于 state 参数,但它由 TOKEN 端点强制执行。 当 webapp 重定向到 LOGIN 端点时,它会设置一个 code_challenge。 Cognito 生成的授权码与这个code_challenge相关联,在获取令牌是,除了授权码之外,还需要一个 code_verifier 参数。

即使攻击者能够拦截重定向并获取到code参数,但没有webapp生成的code_verifier也是白搭。

实现

我们使用Spring Security对OAuth2认证流程提供支持。幸运的是Spring Security已经原生支持 CSRF 和 PKCE 了。使用Spring Security跳转到 LOGIN 端点时,会自动设置statenonce参数,对开发者来说确实很省心。

但是,需要注意的是,对于 PKCE ,Spring Security默认只支持 Public Client类型,比如Native Application或者基于浏览器的SPA等等。参考文档:Initiating the Authorization Request,Public Client需要满足如下条件:

1.在application.yaml中没有配置client-secret,或者值为空
2.client-authentication-method 设置为 “none” (ClientAuthenticationMethod.NONE)

那么问题来了,我们的OAuth客户端是采用的Spring Cloud Gateway,是属于Confidential Client,Spring Security默认是不支持对这种客户端采取 PKCE 机制的。幸好Spring Security的设计非常易于扩展,我们可以自定义一个ServerOAuth2AuthorizationRequestResolver,用于添加code_verifiercode_challenge参数。代码如下:

@Component
public classMyOAuth2AuthorizationRequestResolver implements ServerOAuth2AuthorizationRequestResolver {private DefaultServerOAuth2AuthorizationRequestResolver defaultResolver;private final StringKeyGenerator secureKeyGenerator =new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);public MyOAuth2AuthorizationRequestResolver(ReactiveClientRegistrationRepository clientRegistrationRepository) {defaultResolver = new DefaultServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);}@Overridepublic Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange) {return defaultResolver.resolve(exchange).map(req -> customizeAuthorizationRequest(req));}@Overridepublic Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange, String clientRegistrationId) {return defaultResolver.resolve(exchange, clientRegistrationId).map(req -> customizeAuthorizationRequest(req));}private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest req) {if (req == null) { return null; }Map<String, Object> attributes = new HashMap<>(req.getAttributes());Map<String, Object> additionalParameters = new HashMap<>(req.getAdditionalParameters());addPkceParameters(attributes, additionalParameters);return OAuth2AuthorizationRequest.from(req).attributes(attributes).additionalParameters(additionalParameters).build();}private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {String codeVerifier = this.secureKeyGenerator.generateKey();attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);try {String codeChallenge = createHash(codeVerifier);additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");} catch (NoSuchAlgorithmException e) {additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);}}private static String createHash(String value) throws NoSuchAlgorithmException {MessageDigest md = MessageDigest.getInstance("SHA-256");byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);}
} 

同时,在SecurityWebFilterChain的设置中,需要加入自定义的ServerOAuth2AuthorizationRequestResolver。如下所示:

 @Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository, MyAuthorizationManager authorizationManager, MyOAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver) {// Authenticate through configured OpenID Provider// user custom request resolver to add code_challenge & code_challenge_method(S256) for PKCEhttp.oauth2Login(oAuth2LoginSpec -> oAuth2LoginSpec.authorizationRequestResolver(oAuth2AuthorizationRequestResolver));// ...return http.build();
} 

这样,在API网关向Cognito发出的 LOGIN 请求时,code_verifiercode_challenge参数就被加上了。

总结

本文介绍了基于OAuth认证的微服务可能遇到的安全问题,即CSRF攻击和重定向攻击,同时介绍了基于Spring Security的解决方案。

如果对你有所帮助,请点赞订阅分享,感谢!

相关文章

参考链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值