OAuth2.0安全设计之Authorization Code

原文地址 www.cnblogs.com

OAuth 2.0 有 4 种认证流程:

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

下面以微信为例介绍最常见的也是最安全的 Authorization Code 认证流程。

一、授权流程说明

微信 OAuth2.0 授权登录让微信用户使用微信身份安全登录第三方应用或网站,在微信用户授权登录已接入微信 OAuth2.0 的第三方应用后,第三方可以获取到用户的接口调用凭证(access_token),
通过 access_token 可以进行微信开放平台授权关系接口调用,从而可实现获取微信用户基本开放信息和帮助用户实现基础开放功能等。
  微信 OAuth2.0 授权登录目前支持 authorization_code 模式,适用于拥有 server 端的应用授权。该模式整体流程为:

获取 access_token 时序图:

二、具体实现过程

下面具体介绍一下微信对这个协议的具体实现过程。

第 1 步:开发者在微信开放平台申请接入并成功获取到 appid 和 AppSecret,并配置回调域名。

第 2 步:构造微信登录二维码的超链接如下:

https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

参数说明

参数
是否必须
说明

appid

应用唯一标识(前面认证网页应用中获得)

redirect_uri

重定向地址,需要进行UrlEncode(前面认证网页应用中获得)

response_type

填code

scope

应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即可

state

用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止 csrf 攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加 session 进行校验

返回说明

用户允许授权后,将会重定向到 redirect_uri 的网址上,并且带上 code 和 state 参数

redirect_uri?code=CODE&state=STATE

若用户禁止授权,则重定向后不会带上 code 参数,仅会带上 state 参数

redirect_uri?state=STATE

实际抓包示例:

https://open.weixin.qq.com/connect/qrconnect?response_type=code&appid=wx2198c66352420194&redirect_uri=https%3A%2F%2Fpassport.vivo.com.cn%2Fv3%2Fweb%2Flogin%2FwechatCallBack%3FisPc%3D1%26randomNum%3D%26redirect_uri%3Dhttps%253A%252F%252Fwww.vivo.com%252F%253Fopenid%253D14996839a7398ce8%26client_id%3D30&scope=snsapi_login&state=1614336736067

其中 appid 参数为开发者在第一步中申请到的 appid, scope 参数为授权应用的权限列表,redirect_uri 为授权成功后的回调地址。

第 3 步:假如用户同意授权,在微信登录成功后会跳转到 redirect_uri 参数指定的 URL,并在 URL 尾部追加 code 参数(即 Authorization Code),如上述示例:http://www.qq.com/my.php%EF%BC%8C%E5%88%99%E4%BC%9A%E8%B7%B3%E8%BD%AC%E5%88%B0%EF%BC%9A

https://passport.vivo.com.cn/v3/web/login/wechatCallBack?isPc=1&randomNum=&redirect_uri=https%3A%2F%2Fwww.vivo.com%2F%3Fopenid%3D14996839a7398ce8&client_id=30&code=053isZFa12PMAA0NVeGa1yR9300isZFe&state=1614336736067

然后,我们可以通过Authorization Code去获取用户openid和access_token,进而获得用户的信息。

第 4 步:通过 Authorization Code 获取 Access Token 和 openid,服务器端构造如下请求即可获取 Access Token 和 openid:

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

参数解释如下:

grant_type

授权类型,此值固定为 “authorization_code”。

client_id

申请微信登录成功后,分配给网站的 appid。

client_secret

申请微信登录成功后,分配给网站的 appkey。

code

上一步返回的 Authorization Code。

redirect_uri

与上面一步中传入的 redirect_uri 保持一致。

返回说明

正确的返回:

{
    "access_token":"ACCESS_TOKEN",
    "expires_in":7200,
    "refresh_token":"REFRESH_TOKEN",
    "openid":"OPENID",
    "scope":"SCOPE",
    "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

参数说明:

参数

说明

access_token

接口调用凭证

expires_in

access_token 接口调用凭证超时时间,单位(秒)

refresh_token

用户刷新 access_token

openid

授权用户唯一标识

scope

用户授权的作用域,使用逗号(,)分隔

unionid

当且仅当该网站应用已获得该用户的 userinfo 授权时,才会出现该字段。

错误返回样例:

{"errcode":40029,"errmsg":"invalid code"}

第 5 步:使用 Access Token 以及 OpenID 来访问用户数据

构造如下请求即可访问用户数据:

https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID

参数说明

参数

是否必须

说明

access_token

调用凭证(上一个请求中获得)

openid

普通用户的标识,对当前开发者帐号唯一(上一个请求中获得)

lang

国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语,默认为 zh-CN

返回说明 正确的Json返回结果:

{
    "openid":"OPENID",
    "nickname":"NICKNAME",
    "sex":1,
    "province":"PROVINCE",
    "city":"CITY",
    "country":"COUNTRY",
    "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
    "privilege":[
        "PRIVILEGE1",
        "PRIVILEGE2"
    ],
    "unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

参数

说明

openid

普通用户的标识,对当前开发者帐号唯一

nickname

普通用户昵称

sex

普通用户性别,1 为男性,2 为女性

province

普通用户个人资料填写的省份

city

普通用户个人资料填写的城市

country

国家,如中国为 CN

headimgurl

用户头像,最后一个数值代表正方形头像大小(有 0、46、64、96、132 数值可选,0 代表 640*640 正方形头像),用户没有头像时该项为空

privilege

用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)

unionid

用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的 unionid 是唯一的。

错误的Json返回示例:

{ 
     "errcode":40003,"errmsg":"invalid openid"
}

三、常见不安全设计造成的风险

风险1:redirect_uri 回调域名欺骗

(1)未验证 redirect_uri 是否与注册的回调地址匹配

在上述实现的第二步中将 redirect_uri 修改为攻击者控制站点,用户在授权登录后将携带 Authorization Code 跳转到攻击者控制站点,攻击者从 URL 参数中即可获得 Authorization Code 并实现用户劫持。服务端必须验证 client_id(APPID)和 redirect_uri 规定的域一致,如果不一致则无法登陆
其实腾讯在实现第三方登录接入的时候早就考虑过这种老套的攻击方式,于是,开发者在集成微信登录时必须在微信开放平台上填写网站的回调地址,在进行登录验证的时候如果 redirect_uri 中的值与设置好的回调地址不同则会拒绝访问:

这样就防止了攻击者篡改 redirect_uri 为恶意站点的钓鱼攻击。

但是现在又提出了一种看似合理的绕过方法:

利用合法网站的 URL 重定向漏洞绕过 redirect_uri 中的域名白名单限制。

假设我有一个合法的网站 whitehat.com,攻击者控制一个恶意站点 hacker.com

攻击者可以构造这样一个链接来绕过 redirect_uri 中的域名白名单限制:

http://whitehat.com/index.php?Redirect=http%3a%2f%2fhacker.com%2findex.php

其中 Redirect 参数指定的为重定向地址

这样的话,把这个 URL 地址传给 redirect_uri 即可构造一个恶意链接,实现用户授权微信登录后跳转到 http://hacker.com/

但是用户的授权令牌 Authorization Code 真的会被传送到 http://hacker.com/ 吗?

我们把上述 URL 传给 redirect_uri,跳转到的 URL 地址如下:

http://whitehat.com/index.php?Redirect=hacker.com)index.php?code=****

细心的人已经发现了,这个链接还是跳转到 http://hacker.com/index.php 而不是 http://hacker.com/index.php?code=****](http://hacker.com/index.php%E8%80%8C%E4%B8%8D%E6%98%AFhttp://hacker.com/index.php?code=****

这是因为 code 参数前面的 & 符号没有 URL 编码,因此 code 参数被 http://whitehat.com/ 处理而不是属于 Redirect 参数的一部分。

因此,第一种攻击模型只能用来构造登录后的钓鱼攻击,通常情况下 Authorization Code 不会被传送到攻击者控制的站点中

(2)未设置 Authorization Code 使用一次就失效

将第二步实现的 redirect_uri 改为可以引入外链的合法 URL 地址,这样当合法用户登录后加载此页面的外链时,攻击者就可以从其控制的服务器中在 referer 消息头中获得泄露的 Authorization Code。据说这种攻击方法横扫国内各大站点,这个攻击方法在此 RFC 文档的 Security Considerations 中已经提到过:

同时也给出了相应的安全建议:

即 Authorization Code 在获取后必须在短时间内失效而且只能被使用一次。这种方法在理论上确实可以有效的阻止上述的攻击方式,情景分析如下:

(1)攻击者构造恶意链接发送给用户,其中redirect_uri=http://bbs.test.com/index.php
(2)用户点击链接登录后,回调地址为:http://bbs.test.com/index.php?code=****
(3)用户携带code向服务器发送请求加载此页面,加载的页面中含有攻击者放置的外链(例如头像中的图片链接等),用户加载外链中的图片,攻击者从referer消息头中获得用户的code

由于 Authorization Code 是通过 redirect_uri 浏览器回调传输,容易被截取,服务器生成的临时 Authorization Code 必须是一次有效,客户端使用一次后立即失效并且有效期很短,一般推荐 30s 有效期,可以保证临时 Authorization Code 被客户端正常消费后不会被再次使用

风险 2:redirect_url XSS 跨域攻击

比如构造一个认证请求,redirect_uri = [http://app.com/test?callback=](http://app.com/test?callback=)<script src="http://app2.com?getToken.php"></script>

服务器端需要对 redirect_uri 进行检查禁止特殊字符输入,并且对 redirect_uri 进行全匹配,不做模糊匹配可以杜绝 XSS 攻击。

风险 3:未添加 State 防止 CSRF

第 2 步认证请求 url 中 state 参数是最容易被忽略的,大部分 IDP 不会强制要求客户端使用 state 参数。与 CSRF 攻击类似,如果 state 参数为空,作为攻击者,

1. 先申请一个新的,专门用于攻击他人的账号;
2. 然后走正常流程,跳到微信上去登录此账号;
3. 登录成功之后,微信带着 code 回跳到第三方站点,如www.test.com,这个时候,攻击者拦截自己的请求让他不再往下进行,而直接将带 code 的链接发给受害者,并欺骗受害者点击;
4. 受害人点击链接之后,继续攻击者账号的登录流程,不知不觉登录了攻击者的账号

受害者如果这个时候没察觉此账号不是他本人的,传了一些隐私文件,如照片啥的,攻击者立马就能通过自己的账号看到。

而 state 参数如果利用起来,当作 CSRF Token,就能避免此事的发生:

1. 攻击者依旧获取 code 并打算骗受害者点击
2. 受害者点击链接,但因服务器(比如 www.test.com)分配给受害者的设备的 state 值和链接里面的(分配给攻击者的)state 值不一样,服务器(test.com)直接返回验证 state 失败。

所以安全的实现是:

客户端每次请求生成唯一字符串在请求中放到 state 参数中,服务端认证成功返回 Authorization Code 会带上 state 参数,客户端验证 state 是否是自己生成的唯一串,可以确定这次请求是有客户端真实发出的,不是黑客伪造的请求

风险 4:Access_Token 泄露

  • 由于 Access_Token 是通过 http 协议从服务器端传输给客户端,为了防止旁路监听泄露 Access_Token,服务器必须提供 https 来保证传输通道的安全性(TSL 的要求)
  • 客户端获取 Access_Token,应该在后台与服务端交互获取 Access_Token,不允许 Access_Token 传给前端直接使用
  • 需要保证 Access_Token 信息的不可猜测性,以防止被猜测得到

风险 5:令牌有效性漏洞

  • 维持 refresh_token 和第三方应用的绑定,刷新失效机制的设计不允许长期有效的 token 存在;

四、增强 OAuth2.0 协议设计及使用规范

OAuth2.0 协议安全性进行进一步增强。

  • 对颁发出去的 token 权限进行限制,不同用户申请的 token 根据人员所属组织、角色、岗位进行数据隔离
  • 对登录过程安全性增强,对登录验证方式进行丰富,支持静态密码、手机验证码、OTP、生物识别、FIDO
  • 对 Token 颁发后的生命周期管理,可以按策略主动注销颁发的 Token
  • 对使用 OAuth 过程进行行为分析,对登录过程进行风险识别
  • 按照不同应用的安全等级进行分级,不同安全级别应用实现二次认证, 保障关键系统的安全访问

参考资料:

https://cloud.tencent.com/developer/article/1447723
https://www.anquanke.com/post/id/98392
https://www.freebuf.com/articles/web/252254.html
https://xz.aliyun.com/t/2260
https://wooyun.js.org/drops/OAuth%202.0%E5%AE%89%E5%85%A8%E6%A1%88%E4%BE%8B%E5%9B%9E%E9%A1%BE.html