随着个人网站项目的逐一开放,不同WebApp都需要使用不同用户登录,不但记忆起来麻烦,管理起来也麻烦。于是决定搭建ACS服务器(Access Control System,访问控制系统)。其实类似这样的系统也叫SSO(Single Sign-in System,单点登录系统),或CAS(Central Authenticate System,中央认证系统)。之所以我将此命名为ACS,是因为我还打算实现访问控制功能,即不同用户能访问的页面不同。而要实现这种类似于单点登录的功能,自然就需要一套认证系统,最终我选择了OAuth2.0,来搭建认证系统。
OAuth2.0简述
在没有OAuth的时代,一些WebApp需要访问存储在别的WebApp的用户数据时,需要用户授权,而那时授权方式就是用户直接提供用户名和密码。比如某个打印服务WebServer需要获取某个用户的网盘WebService上的照片以打印,此时用户就必须提供给打印服务WebService以网盘WebService的用户名和密码。这样做非常不安全,因为打印服务WebService会存储用户在网盘WebService上的密码,相当于用户的网盘账号就不被用户掌握了。
而OAuth,就解决了这个问题。OAuth在经过了用户授权后,会将一个授权码发给打印WebService,打印WebService凭借这授权码就可以访问用户在网盘的数据。
OAuth 角色定义
Client-客户端(WebApp)
运行在第三方服务器上的网页应用,注意这里指的是网页应用的后台程序而不是运行在浏览器的前端页面,前端页面是用户代理,属于后面的“资源拥有者”。
Resource Server-资源服务器(认证中心存储用户信息api)
一般来说,资源服务器提供用户基本信息,比如微信的资源服务器就提供用户的头像、昵称、wxid等信息。当然这些信息需要认证才能被获取到。
Authorization Server-认证服务器(认证中心认证api)
这个服务器提供认证服务,当第三方应用需要获取用户信息时,就会首先联系该服务器。该服务器随后引导用户授权。具体流程请见下一节。
Resource Owner-资源拥有者(User-Agent用户浏览器)
用户代理一般来说就是浏览器,它运行前端页面,也可以一定方式与后端服务交互。认证的相关信息需要通过用户代理转发。而这个环节也最不安全,用户浏览器环境千差万别,甚至可能根本工作在不可信环境中,如何预防各种漏洞,让用户安全地登录,也是一大学问。关于OAuth漏洞将会再本文章稍后部分提到。
OAuth认证流程
假设现在用户打开了一个第三方WebApp页面,比如一个网页投票应用。投票应用自然要防止刷票,那么就要求用户登录。于是此时浏览器就弹出了一个页面,要求用户登录。然而这位用户不想注册那么多账号,正好看见下面有一个“使用QQ登录”,于是他就点进去了,此时,OAuth正式开始。
阶段1 浏览器代理接受投票WebApp的一些参数,并且将这些参数通过GET方式附加在网址后面,向认证服务器请求页面。这个网址可能是这样的:
https://auth-server.com/api/auth?
client_id=webapp_voting&
redirect_url=https://toupiao.com/api/callback&
state=123xyz
这里投票WebApp需要通过浏览器转发三个参数给认证服务器:
client_id 标识了WebApp的唯一标识码,让认证服务器知道这是哪个WebApp
redirect_url 标识了待会儿认证成功跳转回的页面,这个参数需要让认证服务器校验,确定有效才能授权
state 是一个安全参数,在这篇文章稍后面会讲到
阶段2 浏览器请求认证服务器成功,认证服务器确定用户身份。此时认证服务器将向用户展示一个网页,比如让用户填写用户名密码,放个“授权”按钮让用户确认,高级一些的还可以让用户选择授权的内容。
阶段3 用户确认登录后,即点击“确认”按钮后,用户将会跳转到刚刚的redirect_url网址,并且在后面附带一些参数,包括code——用户授权码:
https://toupiao.com/api/callback?code=1a2b3c4d5e&state=123xyz
阶段4 此时浏览器虽然没有什么动作,但是此时投票WebApp后台程序却波涛汹涌,它根据传回来的code——授权码,向认证服务器请求access_token——访问令牌,WebApp后端可能是这样请求的:
POST https://auth-server.com/api/token
code=1a2b3c4d5e&
client_id=webapp_voting&
client_key=2g3t4f6d7y6j7f8g&
redirect_url=https://toupiao.com/api/callback
然后认证服务器回应:
{
"access_token": "2g37shdg38h8dhe3ujdbe",
"expires_in": 3600
}
WebApp后台得到access_token,然后向资源服务器申请获取用户数据,如此请求:
POST https://resource-server.com/api/userdata
token=2g37shdg38h8dhe3ujdbe&
client_id=webapp_voting&
client_key=2g3t4f6d7y6j7f8g&
redirect_url=https://toupiao.com/api/callback
资源服务器可能是这样回应的:
{
"user_uuid": "26shgd2634g3d6hd",
"user_name": "orange juice",
"user_avatar": "https://resource-server.com/temp/1s2gd46s.png",
"other": {blablabla...}
}
对于简化的OAuth认证过程,资源服务器和认证服务器看成是一体的,事实上绝大部分OAuth服务器也是如此。OAuth过程就此结束。
阶段5 WebApp后台获取到了用户信息后,将用户添加到本地数据库中,为用户发放session,将用户跳转回登录前的页面等等。
OAuth安全性探讨
最初的OAuth也不是完美的,不过随着漏洞的修复,其正在逐步趋于完善。
OAuth比较需要注意的漏洞主要是:
信息泄露
攻击者可能会截获用户的code,从而向认证服务器申请access_token,进而获取到用户资源。
解决方法:
1)全程采用加密协议传输数据,特别保护redirect_url和access_token;
2)获取access_token时要求客户端提供client_id、client_key,信息不完整或者不正确的不允许获取access_token,并立即销毁相关的code;
3)code只允许使用一次,获取到access_token后立即销毁;
4)为code设置有效时间,因为用户获取到code后立即跳转到客户端的callback页面,客户端可以立即使用code,所以有效时间可以设置较短的时间,如10分钟。
篡改redirect_url
可能存在攻击者故意创建钓鱼网页,伪造redirect_url为自己的服务器callback地址,从而获取到code,进而获取到acces_token和个人数据。
解决方法:可将其看作code泄露,解决方法同上。
CSRF攻击
这种攻击往往出现在账号绑定的情况,即受害者已经在WebApp上拥有一个账号,他想将其绑定到另外一个账号(如微信绑定QQ账号),就出问题了:攻击者在自己的服务器上部署一个网页,该网页和某个第三方WebApp网页登录界面完全一致。不过这个按钮直接将受害者带到了WebApp的callback处理页面。这个按钮网址可能是这样的:
https://webapp.com/api/callback?code=1a2b3c4d5e
不过注意,这里网址附加的code是攻击者的code(攻击者通过自己的浏览器,可以轻易获得code)
受害者进入这个网址后,WebApp后台还是像往常一样通过code获取用户信息,此时受害者获取的是攻击者的用户信息。相当于受害者的微信账号和攻击者的QQ账号绑在一起了。
随后攻击者通过OAuth登录WebApp,既然已经绑定账号,那么攻击者就相当于登录了受害者的账号,就可以为所欲为了。
解决方法:正如上文所说的,在请求授权阶段和callback阶段添加一个参数state,就可以避免这种漏洞。请求OAuth认证时WebApp生成一个state,callback时校验该参数是否与之前的一致,就可以确定中间过程是否分离。存在这个state参数,即相当于将用户授权和callback绑定在一起了,无法分离操作。
在这里需要进一步说明state的工作机制。首先,state参数有以下几个性质:
1)最重要的,和session关联。确保请求OAuth认证时是在一个session下完成的;
2)state应该足够随机,攻击者不能猜到;
3)state应该设有时间限制,超过一定时间范围就销毁