PKCE与OAuth

啥,这个还需要单独记录?

又是工作相关日志。最近收到升级客户端VK SDK的ticket,要求升级SDK以适配OAuth 2.1,阅读接入文档以后遇到了PKCE这个概念。

概念

全称 (Proof Key for Code Exchange),根据gemini解答是OAuth 2.0模式的一个扩展。过去在Web端auth code通过重定向返回,而在移动互联网时代变成了通过custom scheme返回。问题是手机系统支持存在多个相同custom scheme。因此恶意应用可以监听相同scheme进而对auth code截胡。

而PKCE做的就是客户端获取auth code时请求参数携带一串哈希过的数据(challenge)作为令牌,平台服务器会缓存,随后客户端用auth code兑换token时携带哈希前的数据(verifier),然后由平台服务器进行相同加密计算并与之前的缓存数据进行比较。

所以也就解释了为什么在实现服务器侧token-exchange时要先在服务器中生成PKCE。以及为什么这个叫”proof” key。

都到这了索性往前再看看。

OAuth历史

OAuth 1.0 ( 2007 )

OAtuh的最初设计目的,是在不给第三方平台用户账号密码的情况下,让它可以访问用户的数据。这和现在人们直接将他当作某种“登录”方式的理解有很大出入。

过去如果想允许某个应用读取用户数据,我们必须提供账号密码,并且没有专门的途径”撤回“这个行为: 只能是修改密码。 OAuth 1.0引入了Token的概念,类似一张临时房卡,第三方平台可以拿着用户交给他的临时房卡来访问用户房间(数据)。

授权流程

授权流程分为三步

  1. 获取临时token
    1. 第三方应用(Consumer) 向服务提供者(Service Provider)表明身份,发送一个经过签名的请求到服务器的Request Token URL。携带参数consumer_key、oauth_nonce、oauth_timestamp 和 oauth_callback
    2. 服务器验证签名后,返回一个Request Token 和 Token Secret
  2. 用户授权
    1. 第三方应用拿着Request Token将用户引导到授权页面。 按我们现在的理解,就是展示“授权登录”这个页面。
    2. 用户在页面上点击确认授权,服务器返回一个 Verification Code
    3. 服务器生成code的同时会在本地将他和传过来的Request Token进行关联记录。另外这个code并不是返回给用户(Consumer),而是转发Callback URL。这个Callback URL是Consumer在请求Request Token时会携带过去的。
  3. 换取正式access token
    1. 第三方应用拿着 Verification Code 和 Request Token 请求服务器接口
    2. 服务器检查这个Code和 Request Token是否匹配

签名生成

生成签名用到signing key,它由两部分组成: “${consumerSecret}&${tokenSecret}”。

有个细节是,在用户完成授权拿到Access Token这一步之前,我们只有提前在服务器配置好的consumer-secret。所以在完成授权之前的步骤中我们仍然会携带签名,只是这时我们用的key只有consumer-secret,还没有使用token secret。因此,可以说整个流程中,前一段是用了半截key进行签名直到完成授权。

签名信息传输

OAuth 1.0 对签名信息的传输没有严格的要求,他可以在 Header里(比如Authorization: xxx),或者在GET请求中拼接在url后面作为请求参数,又或者是POST请求时直接在body里传递。所以他主要关注的是两端接收后如何验证,而不在于传输方式。

callback问题

1.0时期,callback还不需要提前在provider服务器中进行配置。毕竟经过了签名校验,所以provider service也认为传递过来的callback URL是可靠的。实践中,平台可能会有自己的额外实现逻辑,比如Google/Twitter会要求Callback URL至少要与注册的应用域名一致。

与SSL的渊源

根据考征,OpenSSL诞生比OAuth1.0还要早,但因为时代的局限吧,浏览器这个场景还没有大量投入使用这个技术。如今我们将采用HTTP2或者HTTPS视为很自然,但在OAuth1.0那个时期浏览器仍在大量使用HTTP。因为这个客观条件,OAuth1.0非常关注安全性问题。

OAuth 2.0 (2012)

OAuth 2.0经过了重新设计,因此与1.0完全不兼容。

这个时期HTTPS已经足够普及,因此OAuth 2.0的协议设计也建立在OpenSSL的基础上。他明确说明要求配合TLS/SSL环境使用,所以不再需要在应用层面进行签名与校验,而是在请求头中携带Bearer Token明文传输(安全性交给SSL)。

新特性

  1. Bearer Token

    1. 这个时期HTTPS已经足够普及,因此OAuth 2.0的协议设计也建立在OpenSSL的基础上,因此他明确说明要求配合TLS/SSL环境使用
    2. 不需要计算签名,但需要在请求头中携带Bearer Token进行鉴权。
  2. 多种Grant Type

    1. Authorization code

      最经典的模式,和1.0相似,服务器拿到code以后再交换access token

    2. Implicit

      曾用于没有后端的纯前端应用(PKCE就是解决这个场景的问题)

      2.1中被废弃

    3. Resource owner password credentials

      允许直接输入用户名密码

      2.1中被废弃

    4. client credentials

      用于机器对机器(M2M)的通信

  3. Refresh Token

    1. 1.0时期,下发的Token有效期可以很长,有的长达1年
    2. 2.0中引入了refresh token概念。现在access token只会有很短的有效期,但可以通过refresh token重新获取新的access token,已避免要求用户反复授权的问题。
  4. Scope

    1. 引入了Scope得概念,用于展示consumer具体将有哪些数据权限

授权流程

  1. 获取授权码

    第三方应用(Client)引导用户跳转到服务商的授权页面。

    此时携带 client_id、redirect_uri、response_type=code 以及 scope(权限范围)。

  2. 兑换访问令牌

    客户端后端拿到 code 后,在后台(Server to Server)向服务商发起请求

callback URL变化

1.0时期,规范中callback URL不需要提前注册至服务器,而2.0开始,redirect_uri就变成必须提前注册了。没错,这个uri仍然是由consumer发送的。

OAuth 2.1

OAuth 2.1 并不是对2.0的重写,而是整合了多年来的相关扩展和实践,以提供一个统一&安全的规范。

目前为止OAuth2.1仍然在制订中,它整个规范并没有完全定稿。除了上述PKCE以外还有以下已经确定的改进:

  • Redirect URIs 必须完全匹配
  • 移除隐式模式 (Implicit)
  • 移除密码模式
  • 禁止在 URL 查询字符串中传输 Bearer Token
  • 加强刷新令牌 (Refresh Tokens) 的安全性

grant type变化

移除一些grant type以后,2.1中还支持的授权方式分别是:

  • auth code
  • client credentials

实践与变化

除了协议细节的区别以外,用户使用情景也有了变化:

  1. 不管是callback URL还是叫 redirect uri,原本设计目的都是在授权以后让浏览器302转发到这个地址,从而让consumer server处理auth code并在服务器侧完成相关code exchange,然后再返回结果给浏览器。
  2. 经历移动互联网爆发以后,现在的授权流程变成通过移动端App完成,所以有了两点变化:
    1. 不管是consumer还是service provider,在这个场景下都变成了mobile App,service provider的App通过API与服务器进行通信,完成一些鉴权交互。
    2. redirect uri并不是转发到服务器地址,而是配置了custom scheme,从而在授权完以后跳转本地自定义scheme地址,一般是consumer对应的App来接收并自己处理code的交换。
    3. 正是因为现在这种通过custom scheme变成了常态,所以PKCE的引入才显得更有意义。否则任何App都可以声明相同scheme来接收授权结果。

Server端实践

根据客户端是否需要直接向平台发起请求,在登录时现在有两种实现,一种是授权后直接返回对应platform的access token,另一种是服务器实现自己的access token,也就是说,客户端不持有平台access token。

  • 如果是服务器自己实现token可能就会有更多考量:应对用户量过大可能会引入雪花ID的概念,或者通过uuid来避免暴露基本的用户id索引给外部,防止别人通过递增id来获取额外信息。
  • 自己实现token时,另外就是控制权限和有效期。另外在集群环境还有数据一致性问题。常规做法可能是将token存入一个redis中,从而在集群间保证一致性。
  • 也有更极端的做法,就是服务器只负责生成,但不进行保存。token可能是通过base64进行编码,自己携带了过期时间,这种的就没有了服务器侧的维护压力,但同时也失去了对token进行分别控制/注销的能力。并对办法就是缩短access token的有效期。