前言
前文介绍了Authorization Code flow的基本内容,可以看出其拥有不错的安全机制。但是仍然存在局限,如果客户端是运行在服务器上的Web应用程序(这类客户端称为机密客户端)当然是个不错授权模式,因为很多涉及安全隐患的步骤(如AccessToken)都是通过后端通道由web服务器和授权服务器直接通信的,而不需要经过用户的浏览器或者其他的地方。虽然如此,但是Authorization Code仍然是通过前端通道传递的,如果code被泄露,就仍然存在安全隐患,典型的有Cut and pasted code attack,就是一种盗用code的攻击来获取用户的权限。因此,官方更加建议使用“Hybrid Flow”或“PKCE”从而增加安全性,但是官方更加推荐使用PKCE。
Hybrid Flow
先介绍一下ID_Token具体是什么,ID_Token就像我们的身份证或者户口本,客户端程序可以通过ID_Token获取用户的Claims。ID_Token由三个部分(头Header,体Body和签名Signature)组成并且是通过JWT形式呈现。ID_Token中包含的主要内容有iss(Issuing authority)、sub(A unique identifier for the end-user issued by the issuer)、aud、exp、iat、auth_time等。
Hybrid Flow在流程上大致是和Authorization Code flow一样的,唯一的区别是在完成用户的身份认证之后,通过authorization endpoint返回的数据不同。而这一步返回的数据是根据我们发送请求时的response_type参数决定的,这使得流程更加灵活。
Hybrid Flow根据response_type的不同,authorization endpoint返回可以分为三种情况。
- response_type = code + id_token ,即包含Authorization Code和identity Code
- response_type = code + token ,即包含Authorization Code和Access Code
- response_type = code + id_token + token,即包含Authorization Code、identity Code和Access Token
可以看到,code都是一定会返回的。如果ID_Token在authorization endpoint和token endpoint都被返回了,那么
- iss和sub的值必须是相同的
- 所有关于身份认证的Claims在两者中应该都包含
- 但是authorization endpoint返回的Claim数量应该会少一点,这也是出于对隐私的考虑
如果Access Token在authorization endpoint和token endpoint都被返回了,那么
- 那么两次值可能相同也可能不同
- 这都是取决于这两个终结点的安全特性
那么到底为什么要使用hybrid flow呢?让我们的应用程序在前端通道和后端通道都可以接收到分开的token。接下来进行一些简单的实践,有了前面AuthorizationCode模式代码的经验,编写Hybrid Flow也是很简单的。
授权服务器上新增Hybrid的Client
在之前代码的基础上,新增一个授权方式是Hybrid的client,这样就ok了。
创建一个Hybrid流程的MVC客户端
创建一个新的ASP.NET Core MVC程序,取名为“MVC_Hybrid”,使用Nuget添加IdentityModel和OpenIdConnect的引用。
在Startup.cs中进行配置,代码和Authorization Code Flow的基本一致,只是我们在配置ResponseType时需要使用Hybrid定义的三种情况之一,具体代码如下。
随后在Controller中,和前面介绍Authorization Code时一样,我们通过扩展方法在授权过程中获取的几个token,并显示到界面上
1 public async Task<IActionResult> Index() 2 { 3 //我们利用拓展方法获取存下来的Access Token 4 var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); 5 ViewBag.idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken); 6 var client = new HttpClient(); 7 //携带上AccessToken 8 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); 9 //去请求受保护的api1上的资源 10 var response = await client.GetAsync("http://localhost:5001/identity"); 11 if (!response.IsSuccessStatusCode) 12 { 13 ViewBag.Json = response.ReasonPhrase; 14 } 15 var content = await response.Content.ReadAsStringAsync(); 16 ViewBag.Json = JArray.Parse(content).ToString(); 17 return View(); 18 }
1 @{ 2 ViewBag.Title = "Json"; 3 Layout = "_Layout"; 4 } 5 <p>@ViewBag.idToken</p> 6 <pre>@ViewBag.Json</pre>
以上,准备工作就已经完成了。接下来运行授权服务器和mvc客户端,运行成功后使用内置的用户登录,完成授权后进入获取到数据。
在整个过程中,我们使用Fiddler抓取请求,可以看到在Authorization Endpoint同时返回了code和id_token。
使用https://jwt.io/解析Authorization EndPoint返回的Id_Token和Token EndPoint返回的id_Token,可以看到其中包含的用户信息都是一样的。
关于Hybrid Flow的其他两种模式也类似,我们只需要在客户端的修改请求时的ResponseType即可,就不再赘述了。
在使用Hybrid时我们看到授权终结点返回的Id Token中包含at_hash(Access Token的哈希值)和c_hash(Code的哈希值),规范中定义了以下的一些检验规则。
- 两个id_token中的 iss 和 sub 必须相同。
- 如果任何一个 id token 中包含关于终端用户的声明,两个令牌中提供的值必须相同。
- 关于验证事件的声明必须都提供。
- at_hash 和 c_hash 声明可能会从 token 端点返回的令牌中忽略,即使从 authorize 端点返回的令牌中已经声明。
Proof Key for Code Exchange
在授权过程中,Authorization Code通过前端通道传递有被泄露的风险,在Hybrid Flow中Id_Token也在前端通道传递的同时也将用户数据暴露了出来。因此这里介绍的Proof Key for Code Exchange(PKCE) 就是用来降低威胁的一种方法。概括下PKCE参与在授权验证中的主要流程。
- 客户端在请求code前随机生成一段字符串,称为code_verifier
- 将code_verifier通过加密算法生成加密后的字符串,称为code_challenge
- 当客户端在向授权服务器的授权终结点请求code的同时发送code_challenge给授权服务器,同时为了告诉服务器我使用的是什么加密算法,将算法标识放在
- code_challenge_method中;
- 当客户端使用code向Token终结点请求AccessToken的同时会发送code_verifier
- 授权服务器使用相同的加密算法将code_verifier变换后与之前收到的code_challenge比对,比对成功才会发放Access Token。
再画一个简图直观的看一下上面的流程。
其实在第二篇文章中其实我们已经看到了这个方法的影子,在Fiddler查看授权请求时我们看到了code_challeng、code_challenge_method和code_verifier,这是因为我们在配置OpenIdConnect时对PKCE的支持是默认开启的,只不过当时我们没有在授权服务器端打开验证。
那么在授权服务器端我们如何开启使用PKCE验证呢,也很简单,在对client进行配置时,只需添加一句配置。
1 AllowedGrantTypes = GrantTypes.Code, 2 RequirePkce = true, //开启PCKE
根据官方的说法,还是更加推荐使用PKCE。因为涉及到的加密过程都需要在客户端中实现,相比Hybrid模式实现PKCE的客户端就十分简洁,并且也在前端通道中增加其他的响应元素,最主要的还是因为在实现起来,PKCE更加简单,在ASP.NET Core3开始后已经增加了对PKCE的默认支持,只需简单的一句设置(RequirePkce = true)就能搞定了。
参考资料:
https://identityserver4.readthedocs.io/en/latest/topics/grant_types.html
https://medium.com/identity-beyond-borders/openid-connect-hybrid-flow-1123bc9461fe
https://tools.ietf.org/html/rfc7636
https://tonyxu.io/zh/posts/2018/oauth2-pkce-flow/