大家好,我是本期实验室研究员——坐等天黑。今天我们的研究对象是OAuth扩展协议PKCE。在OAuth2.1草案中,推荐使用AuthorizationCode+PKCE的授权方式。为什么PKCE如此重要?接下来,就让我们一起去实验室一探究竟吧!前言PKCE的全称是ProofKeyforCodeExchange,发布于2015年,是OAuth2.0核心的扩展协议,可以与现有的授权方式结合使用,如AuthorizationCode+PKCE。这也是一个最佳实践。PKCE最初是为移动和本机应用程序创建的,主要是为了减轻对公共客户端的授权代码拦截攻击。在最新的OAuth2.1规范(草案)中,建议所有客户端都使用PKCE,而不仅仅是公共客户端,并且移除了Implicit和Password模式。之前使用过这两种模式的客户呢??是的,现在可以尝试使用AuthorizationCode+PKCE的授权方式。那么为什么PKCE会有这种魔力呢?其实它的原理是客户端提供一个自己创建的证书给授权服务器,授权服务器用它来验证客户端,并向真正的客户端颁发访问令牌(access_token)。不是伪造的。ClientType前面提到PKCE主要是为了减少公共客户端的授权码拦截攻击,所以有必要引入以下两种客户端类型。OAuth2.0核心规范定义了两种类型的客户端,机密的和公开的。区分这两种类型的方法是判断客户端是否有能力维护自己的保密凭证client_secret。对于一个普通的网站,虽然用户可以访问到前端页面,但是数据来自服务器的后端API服务。前端只获取授权码code,将code换成access_token的步骤是在后端API完成的。由于是内部服务器,客户端有能力维护密码或密钥信息,属于机密客户端。公共客户端本身不具备保存关键信息的能力,比如桌面软件、手机App、单页程序(SPA),因为这些应用被发布,实际上根本没有安全可言,恶意攻击者可以反编译等.方法是查看客户端的key,是publicclient。OAuth2.0授权码方式(AuthorizationCode)下,客户端通过授权码code从授权服务器获取访问令牌(access_token)时,还需要在请求中携带客户端密钥(client_secret),授权服务器执行验证access_token是否颁发给合法客户端。对于公共客户端,存在密钥泄露的风险,所以不能使用常规的OAuth2.0授权码模式,所以对于这种不能使用client_secret的场景,衍生出Implicit隐式模式,从一开始就是不安全的。一段时间后,推出了PKCE扩展协议,解决了公共客户端的授权安全问题。授权码拦截攻击以上就是OAuth2.0授权码方式的完整流程。授权码拦截攻击发生在图中的步骤C,即授权服务器向客户端返回授权码时。为什么步骤C不在这么多步骤中?安全性如何?在OAuth2.0核心规范中,授权服务器的anthorizeendpoint和tokenendpoint必须通过TLS(TransportLayerSecurity)进行保护,但是当授权服务器携带授权码code返回给客户端的回调地址时,可能不受影响。有了TLS的保护,恶意程序就可以拦截这个过程中的授权码代码。拿到code之后,下一步就是用code换取授权服务器的访问令牌access_token。对于机密客户端,客户端在请求client_secret时需要携带access_token,密钥保存在后台服务器,所以对于恶意程序通过拦截获取授权码代码是没有用的,对于公共客户端(手机APP,桌面端)applications),它们不具备保护client_secret的能力,因为client_secret可以通过反编译等方式得到,授权码code可以换成access_token。此时,恶意应用程序可以使用令牌来请求资源服务器。state参数,在OAuth2.0核心协议中,在将token换成code的步骤中,建议使用state参数将request和response关联起来,可以防止跨站请求伪造-CSRF攻击,但是国家无法阻止上面的授权码拦截攻击,因为请求和响应都不是伪造的,只是响应的授权码被恶意程序拦截了。PKCE协议流程PKCE协议本身是对OAuth2.0的扩展。和之前的授权码流程大致相同。不同的是,在请求授权服务器的authorize端点时,需要额外的code_challenge和code_challenge_method参数,而token端点请求时,需要额外的code_verifier参数,最后授权服务器会比较验证这三个参数,并且通过后颁发令牌。code_verifier对于每个OAuth授权请求,客户端首先会创建一个代码验证器code_verifier,它是一个高熵加密的随机字符串,使用URI非保留字符(Unreservedcharacters),范围[A-Z]/[a-z]/[0-9]/“-”/“。”/“_”/“~”,因为非保留字符在传递时不需要进行URL编码,而code_verifier的最小长度是43,最大是128,code_verifier必须有足够的熵才能做到猜测。code_verifier的扩展巴克斯形式(ABNF)如下:code-verifier=43*128unreservedunreserved=ALPHA/DIGIT/"-"/"."/"_"/"~"ALPHA=%x41-5A/%x61-7ADIGIT=%x30-39简单来说,在[A-Z]/[a-z]/[0-9]/"-"/"”/“_”/“~”,生成一个43-128位的随机字符串。javascript示例//必需:Node.js加密模块//https://nodejs.org/api/crypto.html#crypto_cryptofunctionbase64URLEncode(str){returnstr.toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');}varverifier=base64URLEncode(crypto.randomBytes(32));java示例//要求:ApacheCommonsCodec//https://commons.apache.org/proper/commons-codec///导入Base64类。//importorg.apache.commons.codec.binary.Base64;SecureRandomsr=newSecureRandom();byte[]code=newbyte[32];sr.nextBytes(code);Stringverifier=Base64.getUrlEncoder().withoutPadding().encodeToString(code);c#示例publicstaticstringrandomDataBase64url(intlength){RNGCryptoServiceProviderrng=新的RNGCryptoServiceProvider();byte[]bytes=newbyte[length];rng.GetBytes(字节);返回base64urlencodeNoPadding(bytes);}publicstaticstringbase64urlencodeNoPadding(byte[]buffer){stringbase64=Convert.ToBase64String(缓冲区);base64=base64.Replace("+","-");base64=base64.Replace("/","_");base64=base64.Replace("=","");returnbase64;}stringcode_verifier=randomDataBase64url(32);code_challenge_method转换code_verifier,这个参数会传给授权服务器,授权服务器会记住这个参数,在发token的时候比较,code_challenge==code_challenge_method(code_verifier),如果一致,就发tokencode_challenge_method即可设置为普通(原始值)或S256(sha256哈希)。code_challenge使用code_challenge_method转换code_verifier得到code_challenge,可以使用如下方法转换plaincode_challenge=code_verifierS256code_challenge=BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))客户端首先要考虑使用S256进行转换,如果不支持,再使用plain,此时code_challenge和code_verifier的值是相等的。javascript示例//必需:Node.js加密模块//https://nodejs.org/api/crypto.html#crypto_cryptofunctionsha256(buffer){returncrypto.createHash('sha256').update(buffer).digest();}varchallenge=base64URLEncode(sha256(verifier));java示例//依赖:ApacheCommonsCodec//https://commons.apache.org/proper/commons-codec///导入Base64类。//importorg.apache.commons.codec.binary.Base64;byte[]bytes=verifier.getBytes("US-ASCII");MessageDigestmd=MessageDigest.getInstance("SHA-256");md.update(bytes,0,bytes.length);byte[]digest=md.digest();Stringchallenge=Base64.encodeBase64URLSafeString(digest);C#示例publicstaticstringbase64urlencodeNoPadding(byte[]buffer){stringbase64=Convert.ToBase64String(buffer);base64=base64.Replace("+","-");base64=base64.Replace("/","_");base64=base64.Replace("=","");returnbase64;}[InternetShortcut]URL=https://segmentfault.com/a/1190000041093435/edit###stringcode_challenge=base64urlencodeNoPadding(sha256(code_verifier));原理分析上面我们提到了授权码拦截攻击,也就是说在整个授权过程中,只需要拦截授权服务器回调给客户端的授权码代码去授权服务器申请一个token,因为client是public的,即使有keyclient_secret也没用。恶意程序拿到accesstoken后,就可以公然请求资源服务器了。PKCE如何运作?由于固定的client_secret并不安全,所以为每个请求生成一个随机密钥(code_verifier)。第一次请求到授权服务器的authorize端点时,携带了code_challenge和code_challenge_method,即code_verifier的转换值和转换方法,然后授权服务器需要Cache这两个参数,在请求token端点时第二次携带生成的随机密钥的原值(code_verifier),然后授权服务器使用如下方式进行验证:plaincode_challenge=code_verifierS256code_challenge=BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))发出传递token之后,那么如何将授权服务器的授权端点和token端点这两个请求联系起来呢?只是传递授权码code,所以即使恶意程序拦截了授权码code,但是如果没有code_verifier,是无法获取到访问令牌的。当然PKCE也可以用在机密(confidential)客户端,即client_secret+code_verifier双密钥。最后看一个请求参数的例子:GET/oauth2/authorizehttps://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_krhUE2M0lOIH3jcxaZzfs5J4jtai5hOX4POST/oauth2/tokenAuthorization:BasicczZCaGRSa3F0MzpnWDFmQmF0M2JWContent-Type:application/x-www-form-urlencodedhttps://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方式的授权流程。参考https://www.rfc-editor.org/rf...https://www.rfc-editor.org/rf...https://oauth.net/2/pkcehttps://datatracker。ietf.org/...MicrosoftMostValuableProfessional(MVP)MicrosoftMostValuableProfessional是MicrosoftCorporation授予第三方技术专业人员的全球性奖项。28年来,世界各地的技术社区领导者都因在在线和离线技术社区中分享专业知识和经验而获得此奖项。MVP是一个精心挑选的专家团队,他们代表了最有技能和最聪明的人,对社区充满热情并愿意帮助他人的专家。MVP致力于通过演讲、论坛问答、创建网站、撰写博客、分享视频、开源项目、组织会议等方式帮助他人,帮助微软技术社区用户最大程度地使用微软技术。更多详情请登录官网:https://mvp.microsoft.com/zh-cn欢迎关注微软中国MSDN订阅号获取更多最新发布!
