Authorization Code 授权原理和实现方法
简单介绍 Oauth2.0 原理这篇文章里简单介绍了 Oauth2.0 的授权,那么具体到授权的细节是怎样的呢?如何自己实现授权流程呢?
Oauth2.0 定义了几种授权流程,其中最重要、也最基础的是 Authorization Code 授权。先看流程图:
1,用户访问 A 网站
2,A 打开 B 的授权页面
3,用户看到授权界面,输入账号密码进行授权
4,B 验证用户是合法的之后,生成一个 authorization code,然后通过 302 跳转的方式,把这个 code 发送给 A
5,A 用这个 code 调用 B 的 api,获得 access token
6,A 用 access token 访问资源
问题来了,authorization code 是什么?
- code 一个一次性的临时凭证,可以用它来兑换 access token
- code 是由鉴权服务器生成的一个随机且唯一的字符串
- 鉴权服务器会记录下 code 的生成时间、将要兑换给谁(例子里是 A)、授权的用户是谁
- 在第 5 步,鉴权服务器会验证 A 的身份,并且检查 code 的有效期。通过的话就会生成 access token
- code 是一次性的,在第 5 步成功换取 access token 之后就会作废
- code 的有效期非常短,可能不会超过 1 分钟。A 在第 4 步获取到 code 之后,要立刻走第 5 步
为什么不在第 4 步直接把 access token 发送给 A,而要通过看似多余的 code 再转换一下,搞这么复杂呢?这个问题的答案非常重要,是整个授权流程的关键所在。
如果在第 4 步直接把 access token 发送给 A,会有什么问题呢?
问题就是,第 4 步是一个 302 跳转,如果在第 4 步传递 access token,会导致 access token 暴露在 url 中,这是非常不安全的方式,因为 url 很容易捕捉到。
所以我们不能在第 4 步直接传递 access token。那换成一个 code,再用 code 去换,就更安全了吗?你可以能会问,code 同样会泄漏,那攻击者拿到 code,也可以用 code 去换 access token,有什么区别呢?
区别在于,攻击者拿到 code 是不能拿去换 access token 的,两个原因:
1,上面说了,换取 access token 的时候,鉴权服务器会验证 A 的身份。具体怎么验证,在后面说明。
2,code 的有效期极短,留给攻击者的时间极短
另外,即便攻击者成功突破了上面两个限制,拿到了 access token,鉴权服务器也能很快发现。这是因为,攻击者必须抢在 A 之前去兑换 access token。那么,等到 A 走第 5 步的时候,鉴权服务器就会发现,这个 code 已经被用过了。此时这就是一个攻击已经发生的信号:有两个不同的服务器,尝试用同一个 code 来兑换 access token,正常情况下是不应该发生的。此时鉴权服务器可以立刻采取措施。例如,可以把刚才兑换出去的 access token 作废。所以,搞出来一个 code,确实会安全很多。
那么,怎么验证 A 的身份呢?
所有的 A,应该说都是 B 的合作方。那么在开始合作之前,一定需要做一些前期的准备。具体来说,B 通常会提供一个申请网站,A 通过这个网站填写一些信息,提交合作申请。B 的工作人员会审核,通过之后,系统会给 A 自动分配一个 client_id 和一个 client_secret。client_id 就是一个唯一 id,client_secret 是一个密钥。A 需要把 client_secret 妥善保管在服务器上,不能泄露。在用 code 兑换 access token 的时候,需要用这个 client_secret 对请求生成 MAC(消息验证码)。鉴权服务器就是依靠这个 MAC 来验证 A 的身份的。
这里需要用到密码学的知识,本文就不展开解释了,有兴趣请看 什么是消息验证码MAC 。总之,攻击者因为没有这个密钥,无法对请求做出 MAC,因此就算窃取到 code,也不能用 code 来兑换 access token。
根据上面的梳理,可以知道,要实现一套 Authorization Code 授权流程,需要做两个 endpoint:
1,Auth Endpoint: GET /oauth2/auth 这就是第 2 步的授权页面地址
参数:
client_id 发起授权的合作方的 client_id。必填
redirect_uri 回调地址,也就是第 4 步 302 跳转的地址,用于接收 code。这个参数是可选的。因为在前期发起合作申请的时候,A 是需要提前将这个地址注册给 B 的。如果没有传,则使用注册的默认值
scope 授权范围。这个也是可选的。B 可以把资源进行划分,分成多个 scope。A 发起授权的时候,可以指明要哪些 scope 的访问权限
state 一个由 A 生成的,随机且唯一的字符串。可选,但强烈建议要有。这个是用来防 CSRF 攻击的,具体请看 Oauth2.0 里面的 state 参数是干什么的
这个 endpoint 需要做以下几件事情:
- 检查 client_id 是否合法
- 检查 redirect_url 是否和事先注册的相符
- 检查 scope。如果申请的时候有注册要使用的 scope,这里要检查传入的 scope 是否相符
- 展示登录框对用户进行验证,展示提示信息,比如“即将授权给 xxx”等
- 验证用户,通过后生成 code,并 302 跳转到 redirect_url,附上 code 和 state。其中 state 就是参数 state,原封不动传回去即可
2,Token Endpoint: POST /oauth2/token 这就是第 5 步用 code 兑换 access token 的接口
参数:
code 必填
client_id 必填
redirect_uri 可选
hmac 必填
这个 endpoint 需要做以下几件事情:
- 检查 code 是否已过期
- 检查 code 是否已经使用过,如果是,很可能已经泄漏,需要采取相关措施
- 检查 client_id 是否合法,client_id 和 code 对应的 client_id 是否一致
- 检查 redirect_uri 参数和上面调用 Auth Endpoint 时传的 redirect_uri 是否完全一致。为什么这么做,请看 如果攻击者操控了 redirect_uri,会怎样?
- 检查 hmac
- 检查都通过,废弃掉 code,生成 access token 并返回
以上就是实现最基本、最核心的 authorization code 授权流程需要做的事情。但实际中,这样很有可能是不够的。主要是因为 access token 是有有效期的。过期之后,会有两个很常见的需求:
a,刷新 access token。具体请看 这篇文章
b,静默授权。如果 A 方没有 refresh token,或者 refresh token 过期了,但又不想让用户重新进行授权操作,希望可以在无需用户参与的情况下获得新的 access token
为了满足需求 a,需要给 Token Endpoint 增加两个参数:grant_type 和 refresh_token,如下:
Token Endpoint: POST /oauth2/token
参数:
grant_type 必填。如果是用 code 兑换 access token,传 authorization_code;如果是用 refresh token 刷新,传 refresh_token
code 当 grant_type = authorization_code 时必填
client_id 必填
redirect_uri 可选
refresh_token 当 grant_type = refresh_token 时必填
hmac 必填
为了满足需求 b,需要给 Auth Endpoint 增加一个参数: prompt,如下:
Auth Endpoint: GET /oauth2/auth
参数:
client_id 必填
redirect_uri 可选
scope 可选
prompt 可选。可传 none 或不传。如果不传,则按正常展示授权页面,如果传 prompt = none,则不展示页面,改为检查该用户最近是否执行过相同的授权,如果授权过,这次则直接自动执行授权而无需用户参与,也就是静默授权。但是如果不符合条件,报错
state 可选
实现了这两个 api,基本上就实现了基本的 Authorization Code 授权流程。
有问题可以直接评论。