前言

阅读本文前需要了解 OAuth 2.0 授权协议的相关内容, 可以参考我的上一篇文章 OAuth 2.0 的探险之旅

PKCE 全称是 Proof Key for Code Exchange, 在2015年发布, 它是 OAuth 2.0 核心的一个扩展协议, 所以可以和现有的授权模式结合使用,比如 Authorization Code + PKCE, 这也是最佳实践,PKCE 最初是为移动设备应用和本地应用创建的, 主要是为了减少公共客户端的授权码拦截攻击。

在最新的 OAuth 2.1 规范中(草案), 推荐所有客户端都使用 PKCE, 而不仅仅是公共客户端, 并且移除了 Implicit 隐式和 Password 模式, 那之前使用这两种模式的客户端怎么办? 是的, 您现在都可以尝试使用 Authorization Code + PKCE 的授权模式。那 PKCE 为什么有这种魔力呢? 实际上它的原理是客户端提供一个自创建的证明给授权服务器, 授权服务器通过它来验证客户端,把访问令牌(access_token) 颁发给真实的客户端而不是伪造的。

客户端类型

上面说到了 PKCE 主要是为了减少公共客户端的授权码拦截攻击, 那就有必要介绍下两种客户端类型了。

OAuth 2.0 核心规范定义了两种客户端类型, confidential 机密的, 和 public 公开的, 区分这两种类型的方法是, 判断这个客户端是否有能力维护自己的机密性凭据 client_secret。

  • confidential

    对于一个普通的web站点来说,虽然用户可以访问到前端页面, 但是数据都来自服务器的后端api服务, 前端只是获取授权码code, 通过 code 换取access_token 这一步是在后端的api完成的, 由于是内部的服务器, 客户端有能力维护密码或者密钥信息, 这种是机密的的客户端。

  • public

    客户端本身没有能力保存密钥信息, 比如桌面软件, 手机App, 单页面程序(SPA), 因为这些应用是发布出去的, 实际上也就没有安全可言, 恶意攻击者可以通过反编译等手段查看到客户端的密钥, 这种是公开的客户端。

在 OAuth 2.0 授权码模式(Authorization Code)中, 客户端通过授权码code向授权服务器获取访问令牌(access_token) 时,同时还需要在请求中携带客户端密钥(client_secret), 授权服务器对其进行验证, 保证 access_token 颁发给了合法的客户端, 对于公开的客户端来说, 本身就有密钥泄露的风险, 所以就不能使用常规 OAuth 2.0 的授权码模式, 于是就针对这种不能使用 client_secret 的场景, 衍生出了 Implicit 隐式模式, 这种模式从一开始就是不安全的。在经过一段时间之后, PKCE 扩展协议推出, 就是为了解决公开客户端的授权安全问题。

授权码拦截攻击

上面是OAuth 2.0 授权码模式的完整流程, 授权码拦截攻击就是图中的C步骤发生的, 也就是授权服务器返回给客户端授权码的时候, 这么多步骤中为什么 C 步骤是不安全的呢? 在 OAuth 2.0 核心规范中, 要求授权服务器的 anthorize endpoint 和 token endpoint 必须使用 TLS(安全传输层协议)保护, 但是授权服务器携带授权码code返回到客户端的回调地址时, 有可能不受TLS 的保护, 恶意程序就可以在这个过程中拦截授权码code, 拿到 code 之后, 接下来就是通过 code 向授权服务器换取访问令牌 access_token , 对于机密的客户端来说, 请求 access_token 时需要携带客户端的密钥 client_secret , 而密钥保存在后端服务器上, 所以恶意程序通过拦截拿到授权码code 也没有用, 而对于公开的客户端(手机App, 桌面应用)来说, 本身没有能力保护 client_secret, 因为可以通过反编译等手段, 拿到客户端 client_secret, 也就可以通过授权码 code 换取 access_token, 到这一步,恶意应用就可以拿着 token 请求资源服务器了。

state 参数, 在 OAuth 2.0 核心协议中, 通过 code 换取 token 步骤中, 推荐使用 state 参数, 把请求和响应关联起来, 可以防止跨站点请求伪造-CSRF攻击, 但是 state 并不能防止上面的授权码拦截攻击,因为请求和响应并没有被伪造, 而是响应的授权码被恶意程序拦截。

PKCE 协议流程

PKCE 协议本身是对 OAuth 2.0 的扩展, 它和之前的授权码流程大体上是一致的, 区别在于, 在向授权服务器的 authorize endpoint 请求时,需要额外的 code_challengecode_challenge_method 参数, 向 token endpoint 请求时, 需要额外的 code_verifier 参数, 最后授权服务器会对这三个参数进行对比验证, 通过后颁发令牌。

code_verifier

对于每一个OAuth 授权请求, 客户端会先创建一个代码验证器 code_verifier, 这是一个高熵加密的随机字符串, 使用URI 非保留字符 (Unreserved characters), 范围 [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", 因为非保留字符在传递时不需要进行 URL 编码, 并且 code_verifier 的长度最小是 43, 最大是 128, code_verifier 要具有足够的熵它是难以猜测的。

code_verifier 的扩充巴科斯范式 (ABNF) 如下:

code-verifier = 43*128unreserved
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
ALPHA = %x41-5A / %x61-7A
DIGIT = %x30-39

简单点说就是在 [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" 范围内,生成43-128位的随机字符串。

javascript 示例

// Required: Node.js crypto module
// https://nodejs.org/api/crypto.html#crypto_crypto
function base64URLEncode(str) {
return str.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
var verifier = base64URLEncode(crypto.randomBytes(32));

java 示例

// Required: Apache Commons Codec
// https://commons.apache.org/proper/commons-codec/
// Import the Base64 class.
// import org.apache.commons.codec.binary.Base64;
SecureRandom sr = new SecureRandom();
byte[] code = new byte[32];
sr.nextBytes(code);
String verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(code);

c# 示例


public static string randomDataBase64url(int length)
{
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
byte[] bytes = new byte[length];
rng.GetBytes(bytes);
return base64urlencodeNoPadding(bytes);
} public static string base64urlencodeNoPadding(byte[] buffer)
{
string base64 = Convert.ToBase64String(buffer);
base64 = base64.Replace("+", "-");
base64 = base64.Replace("/", "_");
base64 = base64.Replace("=", "");
return base64;
} string code_verifier = randomDataBase64url(32);

code_challenge_method

对 code_verifier 进行转换的方法, 这个参数会传给授权服务器, 并且授权服务器会记住这个参数, 颁发令牌的时候进行对比, code_challenge == code_challenge_method(code_verifier) , 若一致则颁发令牌。

code_challenge_method 可以设置为 plain (原始值) 或者 S256 (sha256哈希)。

code_challenge

使用 code_challenge_method 对 code_verifier 进行转换得到 code_challenge, 可以使用下面的方式进行转换

  • plain

    code_challenge = code_verifier

  • S256

    code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

客户端应该首先考虑使用 S256 进行转换, 如果不支持,才使用 plain , 此时 code_challenge 和 code_verifier 的值相等。

javascript 示例

// Required: Node.js crypto module
// https://nodejs.org/api/crypto.html#crypto_crypto
function sha256(buffer) {
return crypto.createHash('sha256').update(buffer).digest();
}
var challenge = base64URLEncode(sha256(verifier));

java 示例

// Dependency: Apache Commons Codec
// https://commons.apache.org/proper/commons-codec/
// Import the Base64 class.
// import org.apache.commons.codec.binary.Base64;
byte[] bytes = verifier.getBytes("US-ASCII");
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(bytes, 0, bytes.length);
byte[] digest = md.digest();
String challenge = Base64.encodeBase64URLSafeString(digest);

C# 示例

public static string base64urlencodeNoPadding(byte[] buffer)
{
string base64 = Convert.ToBase64String(buffer);
base64 = base64.Replace("+", "-");
base64 = base64.Replace("/", "_");
base64 = base64.Replace("=", "");
return base64;
} string code_challenge = base64urlencodeNoPadding(sha256(code_verifier));

原理分析

上面我们说了授权码拦截攻击, 它是指在整个授权流程中, 只需要拦截到从授权服务器回调给客户端的授权码 code, 就可以去授权服务器申请令牌了, 因为客户端是公开的, 就算有密钥 client_secret 也是形同虚设, 恶意程序拿到访问令牌后, 就可以光明正大的请求资源服务器了。

PKCE 是怎么做的呢? 既然固定的 client_secret 是不安全的, 那就每次请求生成一个随机的密钥(code_verifier), 第一次请求到授权服务器的 authorize endpoint时, 携带 code_challenge 和 code_challenge_method, 也就是 code_verifier 转换后的值和转换方法, 然后授权服务器需要把这两个参数缓存起来, 第二次请求到 token endpoint 时, 携带生成的随机密钥的原始值 (code_verifier) , 然后授权服务器使用下面的方法进行验证:

  • plain

    code_challenge = code_verifier

  • S256

    code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

通过后才颁发令牌, 那向授权服务器 authorize endpoint 和 token endpoint 发起的这两次请求,该如何关联起来呢? 通过 授权码 code 即可, 所以就算恶意程序拦截到了授权码 code, 但是没有 code_verifier, 也是不能获取访问令牌的, 当然 PKCE 也可以用在机密(confidential)的客户端, 那就是 client_secret + code_verifier 双重密钥了。

最后看一下请求参数的示例:

GET /oauth2/authorize
https://www.authorization-server.com/oauth2/authorize?
response_type=code
&client_id=s6BhdRkqt3
&scope=user
&state=8b815ab1d177f5c8e
&redirect_uri=https://www.client.com/callback
&code_challenge_method=S256
&code_challenge=FWOeBX6Qw_krhUE2M0lOIH3jcxaZzfs5J4jtai5hOX4
POST /oauth2/token
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded https://www.authorization-server.com/oauth2/token?
grant_type=authorization_code
&code=d8c2afe6ecca004eb4bd7024
&redirect_uri=https://www.client.com/callback
&code_verifier=2D9RWc5iTdtejle7GTMzQ9Mg15InNmqk3GZL-Hg5Iz0

下边使用 Postman 演示了使用 PKCE 模式的授权过程

References

https://www.rfc-editor.org/rfc/rfc6749

https://www.rfc-editor.org/rfc/rfc7636.html

https://oauth.net/2/pkce

https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04

欢迎关注微信公众号【全球技术精选】


OAuth 2.0 扩展协议之 PKCE的更多相关文章

  1. 转 OAuth 2.0授权协议详解

    http://www.jb51.net/article/54948.htm 作者:阮一峰 字体:[增加 减小] 类型:转载 时间:2014-09-10我要评论 这篇文章主要介绍了OAuth 2.0授权 ...

  2. OAuth 2.0 / RCF6749 协议解读

    OAuth是第三方应用授权的开放标准,目前版本是2.0版,以下将要介绍的内容和概念主要来源于该版本.恐篇幅太长,OAuth 的诞生背景就不在这里赘述了,可参考 RFC 6749 . 四种角色定义: R ...

  3. OpenID Connect:OAuth 2.0协议之上的简单身份层

    OpenID Connect是什么?OpenID Connect(目前版本是1.0)是OAuth 2.0协议(可参考本人此篇:OAuth 2.0 / RCF6749 协议解读)之上的简单身份层,用 A ...

  4. OAuth 2.0 的探险之旅

    前言 OAuth 2.0 全称是 Open Authorization 2.0, 是用于授权(authorization)的行业标准协议. OAuth 2.0 专注于客户端开发人员的简单性,同时为 W ...

  5. 一个功能完备的.NET开源OpenID Connect/OAuth 2.0框架——IdentityServer3

    今天推荐的是我一直以来都在关注的一个开源的OpenID Connect/OAuth 2.0服务框架--IdentityServer3.其支持完整的OpenID Connect/OAuth 2.0标准, ...

  6. OAuth 2.0协议在SAP产品中的应用

    阮一峰老师曾经在他的博文理解OAuth 2.0里对这个概念有了深入浅出的阐述. http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html 本文会结合我 ...

  7. OAuth 2.0 了解了,OAuth 2.1 呢?

    OAuth 2.0 OAuth 2.0 是工业级标准授权协议. OAuth 2.0 聚焦于客户端开发者便利性,为网页应用程序.桌面客户端.手机.客厅设备提供特定的授权流程. RFC6749 OAuth ...

  8. OAuth 2.0、OIDC 原理

    OAuth 目录 OAuth 什么是 OAuth? 为什么是 OAuth? SAML OAuth 和 API OAuth 主要组件 OAuth 作用域 OAuth 参与者 OAuth 令牌 OAuth ...

  9. 谈谈基于OAuth 2.0的第三方认证 [上篇]

    对于目前大部分Web应用来说,用户认证基本上都由应用自身来完成.具体来说,Web应用利用自身存储的用户凭证(基本上是用户名/密码)与用户提供的凭证进行比较进而确认其真实身份.但是这种由Web应用全权负 ...

随机推荐

  1. P5319-[BJOI2019]奥术神杖【0/1分数规划,AC自动机,dp】

    正题 题目链接:https://www.luogu.com.cn/problem/P5319 题目大意 一个长度为\(n\)的串\(T\),用\(0\sim 9\)填充所有的\(.\). 然后给出\( ...

  2. 前端快闪三:多环境灵活配置react

    你已经使用Create React App脚手架搭建了React应用,现在该部署了. 一般会使用npm run build或者yarn build构建出静态资源, 由web服务器承载. 您会体验到 多 ...

  3. Ubuntu系统的开机全流程介绍及grub美化

    目录 前言 Ubuntu开机经历的步骤 BIOS Boot Loader Kernel 配置 Grub 的个性化主题 /usr/share/grub/default/grub /etc/default ...

  4. struct结构体大小的计算(内存对齐)

    本次实验环境 环境1:Win10, QT 5.12 一. 背景 当普通的类型无法满足我们的需求的时候,就需要用到结构体了.结构体可衍生出结构体数组,结构体还可以嵌套结构体,这下子数据类型就丰富多彩了, ...

  5. Docker-初见

    目录 Docker概述 Docker历史 Docker Docker的基本组成 Docker安装 使用流程 底层原理 Docker的常用命令 Portainer 可视化面板安装 镜像原理之联合文件系统 ...

  6. Django实现用户登录注册

    本文将会介绍小白如何完成一个用户登录注册系统 新建一个Django项目,名字为login_register,并且使用命令manage.py startapp.User(名字自己随便起) 最终djang ...

  7. Java初步学习——2021.10.09每日总结,第五周周六

    (1)今天做了什么: (2)明天准备做什么? (3)遇到的问题,如何解决? 今天学习了菜鸟教程实例部分 一.字符串 1.字符串比较--compareTo方法 public class Main { p ...

  8. 阿里P8面试官:如何设计一个扛住千万级并发的架构?

    大家先思考一个问题,这也是在面试过程中经常遇到的问题. 如果你们公司现在的产品能够支持10W用户访问,你们老板突然和你说,融到钱了,会大量投放广告,预计在1个月后用户量会达到1000W,如果这个任务交 ...

  9. Java(15)面向对象之继承

    作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15201615.html 博客主页:https://www.cnblogs.com/testero ...

  10. 第十一章 Dockerfile安装Jenkins-2.249.3-1.1

    一.安装Docker Docker部署Jenkins前提已经安装Docker,这边脚本安装Docker. #1.编写Docker安装脚本 [root@ip-10-0-12-212 ~]# vim In ...