OAuth授权请求中的“state”参数的目的是什么?

54
在OAuth中,初始授权请求有一个state参数。显然,它是为了安全原因而存在的,但我不太明白它保护什么...例如,在GitHub上,此参数的描述如下:

一个难以猜测的随机字符串。它用于防止跨站点请求伪造攻击。

据我所见,来自授权请求的状态只是作为参数传递到重定向URL,像这样:
http://<redirect_url>?code=17b1a8df59ddd92c5c3b&state=a4e0761e-8c21-4e20-819d-5a4daeab4ea9

有人能解释一下这个参数的确切目的吗?


我在寻找相同问题时发现了这篇很棒的文章:https://medium.com/@benjamin.botto/oauth-replay-attack-mitigation-18655a62fe53 - ahmedakef
我不会感到惊讶,如果这是某种遗物,因为它显然不能用于任何CSRF保护,考虑到它作为GET请求参数传递,而不是在cookies中传递,因此它不满足双重提交Cookie模式的要求。 - undefined
在进一步查看时,这个参数似乎更多地用于客户端请求认证,以确保响应确实是他们所发出的请求。这似乎完全没有用处。任何中间人攻击都可以轻易拦截这个。那么真正需要的应该只是HSTS。 - undefined
4个回答

61

状态参数用于防止跨站请求伪造(XSRF)攻击。你的应用程序生成一个随机字符串,并使用状态参数将其发送到授权服务器。授权服务器将返回状态参数。如果两个状态参数相同,则为“OK”。如果状态参数不同,则表示有其他人发起了请求。

Google提供的示例可能更加清晰:https://developers.google.com/accounts/docs/OAuth2Login?hl=zh-CN#createxsrftoken


6
“someone else has initiated the request”翻译为“有人已经发起了请求”。谢谢,这正是我所缺少的。我不在Web应用程序的环境中,因此它不适用于我的情况(我只是在桌面应用程序中检测WebBrowser控件中的重定向,没有人会向我发送请求...)。 - Thomas Levesque
19
ckanext-oauth2的开发者还使用state参数来存储有关先前访问页面的信息,以便在登录后将用户重定向回该页面,例如:{"came_from": "/dashboard"}。他们使用base64编码使其URL安全,并将其用于“state”参数。 - jeverling
2
@jeverling 那不是可以猜到的吗? - Sriram Kailasam
9
你错过了一个非常重要的点,state参数应该与你的会话相关联。 - Aditya
2
你认为这只有在需要高度安全的应用程序中才是必要的吗?还是每个使用Google oauth2的应用程序/网站都应该实施这一点?毕竟,这是可选的。 - Florian Walther
显示剩余3条评论

6

state在发送到redirect_uri的查询字符串中被回显。它有两个目的:

  1. 根据其名称,最初的用途是将状态信息从发起网页传输到redirect_uri。例如,我有一个流程,向用户发送一个链接,允许他们将其帐户链接到其他资源。该链接包含描述该资源的信息(令牌)。因此,用户点击链接后,他们会被带到一个网页,然后重定向到认证服务器。我需要该令牌通过认证过程返回到redirect_uri,以便redirect_uri背后的业务逻辑可以完成将资源链接到用户帐户。我通过使用"state"来携带这些信息。由于"state"没有大小限制,因此可以用来携带各种非常有用的信息。

  2. 作为防止XSRF攻击的手段...第三方很容易伪造请求发送到认证服务器,并欺骗您的redirect_uri接受响应。如果状态信息是随机生成的值,并且redirect_uri能够验证其为自己的值,那么您就可以保护自己免受此类攻击。

这是一个误解,即随机值必须绑定或存储在某个会话中。虽然这是一种验证状态的有效方式,但它存在缺陷。如果身份验证有延迟(例如用户在登录界面停留数小时),那么如果会话已过期,这种验证方法将失败。如果用户错误地或双击了多次登录,也会失败。
我认为更好的方法是使用私钥对随机值进行签名。例如,将随机数+私钥的SHA1或SHA256哈希作为签名。将状态值设置为随机数和签名的组合。注意要将二进制值转换为Base64。通常使用句点作为分隔符,与JWT相同的方式:
state = random + "." + signature 其中,signature = BASE64_SHA256(random + privateKey)
您可以通过在句点处拆分状态值来验证:
verifyValue = BASE64_SHA256(split(state,".")[0] + privateKey)
如果 verifyValue = split(state,".")[1],则状态有效。无需繁琐的会话存储,因此验证速度更快。

将两个目的完美地合并为一个是可行的。也就是说,您可以将您的redirect_url希望了解的已知值与随机数相加,然后将签名作为这两个值的SHA256哈希添加进去。例如:

sate = data + "." + random + "." + signature

其中,signature= BASE64_SHA256(data + "." + random + privateKey)

这样非常安全,因为所有状态信息都被完整地签名和保护,而如果您使用简单的安全令牌存储在会话中(如https://developers.google.com/identity/openid-connect/openid-connect?hl=en#createxsrftoken方法所示),则无法实现。


根据Maxicl在下面的帖子中的内容,我进行了编辑。 - undefined

1
我能想到的唯一看似有用的state参数存在的目的是为了编码关于用户的信息,以便你知道要将收到的响应与哪个用户关联起来。
然后,在成功请求回调端点后,你可以解码返回的state参数,验证传递的信息是否对应于回调的已登录用户信息,然后继续获取授权令牌。

1
补充@CaptainBeOS的回答,如果私钥硬编码到客户端应用程序(例如浏览器)中,使用私钥可能不起作用,因为如果任何人(攻击者)可以通过加载网页来看到这个私钥,他们也可以执行第一步,即对随机数和签名进行编码,这将每次都正确验证,因为这与自己签名没有区别。因此,要么您需要将签名存储在会话中(那么为什么要费心),要么您需要每次给新的浏览器一个新的私钥。这还依赖于私钥存储在每个会话或后端以获取的情况。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接