OAuth2设计上的思考以及CSRF攻击概述
OAuth2授权码模式的基本授权流程
此处以github第三方认证为例, 说明如何通过github的OAuth2服务来以github的身份登陆网站A
请注意, 事先网站A已经在github平台申请了一个oauth2应用了, 填写了回调地址, 且拥有了
client_id
和client_secret
1.用户浏览器向网站A发起一个http请求(GET方法访问https://www.justfortest.com/login/oauth2/github
)
2.网站A接收到请求, 立马重定向(redirect)到github授权页面(此处是https://github.com/login/oauth/authorize?client_id=github上申请的client_id
)
标准的授权码模式(不代表github一定支持), 此处可选的query有:
- response_type: 我们所介绍的授权方式固定为”code”, 代表授权码模式, 其它模式暂不讨论
- client_id: github上申请的一个id, 对应创建的某个oauth认证应用
- redirect_uri: 重定向url, 可选, 代表接收code的重定向地址, 但是github给写死了, 这个query对github没用
- scope: 表示申请的权限范围, 可选项, 此处我们用github不填写这个
- state: 可以指定任意值, 认证服务器在在回调返回code的时候原样返回, 这里是安全选项, 我们此处不填写这个, 后面再来讨论它有什么作用
3.用户输入密码, 然后点击授权
4.点击授权后, github重定向到网站A的一个回调接口, 并带上一个时效很短的code
。比如这个回调可能是https://www.justfortest.com/login/oauth2/github/callback?code=一个code
(值得注意的是, 这个code
只能用于提取token
, 且只能提取一次, 提取后即失效, code
失效很短, 可能几分钟就是失效了)
5.然后网站A拿到了这个code
, 向github服务器发送请求, 拿到一个token
。比如网站A向github发送这个请求https://github.com/login/oauth/access_token?client_id=github配置的应用id&client_secret=github上配置的应用密钥&code=之前拿到的code
。然后github返回的结果就是一个token
6.最后网站A以这个token
为鉴权密钥, 访问github的api接口(https://api.github.com/user
, 注意带上之前提到的Bearer token
), 拿取用户信息
CSRF概述
如果用户进入黑客的网站, 它可以用两种方式伪造跨域请求。下面展示一种POST跨域请求, 它利用了在mybankwebsite
已经登陆的用户的cookie
信息完成了转账工作:
1 | <form action="https://www.mybankwebsite.com/transfer_money" method="POST"> |
具体原理就是, 相当于用户可以用post请求访问了https://www.mybankwebsite.com/transfer_money
。mybankwebsite
检查到了cookie里面携带的登陆信息(比如可以用session维护用户银行账户的登陆状态)。此时用户的钱就被打走了
此外, get请求也是可以的:
1 | <img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000> |
同样能通过用户在网站mybankwebsite
之前携带的登陆cookie, 悄无生息地用用户的身份转走钱
所以CSRF如何防御?
问题就在, 明明我是在黑客网站访问信息的。然而黑客网站却能对被攻击网站发送网络请求, 并携带用户浏览器在被攻击网站的cookie, 这显然是很abnormal的
下面描述被攻击网站的的一些情况, 这些将是被黑客所利用的信息: 如果用户用post方法访问https://www.mybankwebsite.com/transfer_money?&money=1000000&for=Markity
, 那么登陆者将进行一个转账操作
那么要进行防御, 得从服务端入手, 要防范这个攻击, 可以从以下方法入手
1.对关键的url, 限制其访问的来源(别理就完了)
比如需要访问http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory
, 用户必须先登陆 bank.example, 然后通过点击页面上的按钮来触发转账事件。这时,该转帐请求的Referer值就会是转账按钮所在的页面的URL。在关键的url中, 检查这个Referer是否来自本网站, 是完全能够防范攻击的
然而缺点是, 这种操作与浏览器有关, 对于某的老浏览器, 比如IE6, 在黑客网站有能力篡改Referer, 那就无大语了
并且即使是最新的浏览器, 也允许用户禁用Referer来保证用户的隐私, 有些代理服务器和防火墙也会将引用地址信息过滤掉, 那么就别用Referer来做csrf了, 别理它就完了
2.对需要表单提交的请求, 检查csrf-token(目前最好的办法)
在转账页面, 构造如下表单:
1 | <form action="https://www.mybankwebsite.com/transfer_money" method="POST"> |
对于转账操作, 会将csrf-token传给服务端, 那么服务端可以检测这个token, 来保证表单POST的安全性
下面是参考django的思路:
django会检查每个http请求的headers里面的X-CSRFToken项的值是否和cookie里面保存的值相同,如果不相同或者缺失,就拒绝这个请求,如果相同,说明这次请求是从真实用户发起的
3.cookie的samesite选项(别理就完了)
前面提到, csrf的根本就是在黑客网站发送给银行网站的http请求 居然 携带了用户之前访问银行账户的cookie。这个cookie就携带了用户的登陆状态, 此时黑客网站就能通过发送请求的方式利用用户的登陆状态干坏事。
所以如果能让在银行网站设置的cookie, 在进行跨域请求时不传递, 那就万事大吉了。这是浏览器的一个很新的功能, 可以设置某个cookie的严格程度, 下面是它的三种设置选项:
1 | - Strict |
这也涉及到了浏览器支持的问题, 因此也不是好办法, 别理它就完了
所以CSRF对OAuth2产生了什么影响?
假如现在网站A集成了账户绑定github账户的功能, 访问https://www.a.com/user/github_bind
, 用户就跳转到github的第三方登陆界面。登陆后重定向到https://www.a.com/user/github_bind/callback
。那么此时, 用户就能将当前session的账号绑定到github账户上了。之后用github第三方登陆, 就能登陆用户账号。也就是说这个网站提供了github绑定和github登陆两个功能
看下面的步骤:
0.用户事先已经登陆到了网站A, 并且在网站A上有cookie保持登陆状态
1.攻击者自己访问被攻击应用网站的github第三方登陆功能, 然后输入自己的github账户密码, 点击登陆
2.通过工具截获github的code, 但是自己不访问对应的接收code的接口, 而是保存这个code的”活性”(code使用一次就无效了, 我们需要保持它处于有效的状态)
3.用户访问黑客网站, 黑客通过重定向的方式将用户导到被攻击网站的github绑定的接口(https://www.a.com/user/github_bind/callback
), 并带上之前的code, code是上面截获的有活性的, 没过期的code
4.被攻击网站用这个code, 搭配自己的client_id
, client_secret
换取token, 拿取用户信息(这是攻击者的用户信息), 并将攻击者的github信息写入数据库, 用户就将攻击者的github和自己账户绑定了!
5.现在攻击者能用自己的github账户, 第三方登陆到用户的账户了, 用户的数据就完了
那么怎么解决?之前提到的state
就提供了帮助, 下面是具体的方案:
用户访问https://www.a.com/user/github_bind
后, 重定向带有state
属性, 需要放加密的签名信息
只需要在https://www.a.com/user/github_bind/callback
请求那里, 检验state
的合法性就行了, 先进行解密, 然后校验签名是否合法
疑问与讨论
我们尝试列举一些问题, 并思考为何如此, 下面以QA的形式进行
- 为什么不能这么设计?即github回调直接返回
token
, 而不需要code
我来列举两个原因, 其中A原理最为主要, B原因比较牵强:
A. 因为OAuth2不强制要求使用https
, 因此很有可能这个token
会被监听, 被盗取。 因此, code
不能携带任何敏感信息, 只有网站A(也就是github应用密钥的唯一知情人)才能通过code
拿到token
, 进一步拿到用户信息。如此在http协议下, 也能保证安全
B. 我们不能让用户能够拿着token
为所欲为(试想用户如果知道token
怎么用, 那么他可以拿着这个token
自己玩, 这看上去就很不靠谱), 而是让网站A才有权力做这一切
- 为什么不这么设计?即不需要申请
token
, 用code
+密钥配合访问github接口直接返回用户信息?
我来表述一下我的思考, 我认为这么设计是完全合理且安全的。但是OAuth的设计初衷是使用第三方登陆服务做授权, 获取网站A的RESTFUL API的token
(现在的API设计多用token做鉴权)。
github可以在第5步, 返回的结果不是token
, 而直接就是用户信息, 这样做一样安全, 且简化了步骤(即不用token
再启动一个http连接来得到用户信息, 更为快速且简洁)。
但是OAuth就不是拿来做这个的, OAuth的目的只在获得接口token, 至于这个token是用来干嘛的, 此协议管不着。
因此OIDC(OpenID Connect)应运而生, 它就是来解决这个问题的
OIDC是什么?
OIDC没有什么新鲜的设计, 只是在用code
换取token
的那一步, 可以选择是否需要认证服务器返回一个id token
(通过在换取token
的那个请求上加一个Query: scope="openid"
)。id token
是一个JWT格式的信息。这个id token
包含了一些基本的用户信息, 足以来区分不同的用户。下面是一个典型的JWT信息(只包含了payload部分):
1 | { |
负责签名的是认证服务器, 但是如果拥有应用密钥, 可以在本地对id token
进行验签
OIDC解决了什么?
如果是拿来做认证, 像QQ, github压根没有提供这个OIDC。只是规定了用token
访问特定的接口, 然后就能拿到用户信息。所以仅仅对一般网站的第三方认证来说, OIDC没有解决任何事情, 压根没有多少平台实现了这个协议, 也无从用它来做认证一说了。