从安全的角度来讲,《中篇》介绍的Implicit类型的Authorization Grant存在这样的两个问题:其一,授权服务器没有对客户端应用进行认证,因为获取Access Token的请求只提供了客户端应用的ClientID而没有提供其ClientSecret;其二,Access Token是授权服务器单独颁发给客户端应用的,照理说对于其他人(包括拥有被访问资源的授权者)应该是不可见的。Authorization Code类型的Authorization Grant很好地解决了这两个问题。

Authorization Code Authorization Grant授权流程

https://login.live.com/oauth20_authorize.srf”,相应的参数同样以查询字符串的形式提供。与Implicit类型Authorization Grant获取Access Token的请求一样,此时需要提供如下4个完全一样的参数。
  • response_type:表示请求希望获取的对象类型,在此我们希望获取的是Authorization Code,所以这里指定的值为“code”。
  • redirect_uri:表示授权服务器在获得用户授权并完成对用户的认证之后重定向的地址,Authorization Code就以查询字符串(?code={authorizationcode})的方式附加在该URL后面。客户端应用利用这个地址接收Authorization Code。
  • client_id: 唯一标识被授权客户端应用的ClientID。
  • scope:表示授权的范围,根据具体需要的权限集而定。
?code={authorizationcode})的方式附加在重定向URL的后面。重定向的请求被客户端应用接收后,Authorization Code被提取并保存起来。

接下来客户端应用会利用得到的Authorization Code向授权服务器获取Access Token,这一般为HTTP-POST请求。作为请求消息主体传递的内容除了作为参数“code”的Authorization Code之外,还包含如下一些必需的参数。

  • client_id: 唯一标识被授权客户端应用的ClientID。
  • client_secret:唯一标识被授权客户端应用的ClientSecret。
  • redirect_uri:之前获取Authorization Code时指定的重定向地址。
  • grant_type:采用的Authorization Grant类型,参数值为“ authorization_code”。

授权服务器接受到请求之后,除了利用提供的ClientID和ClientSecrete对客户端应用实施验证之外,还会检验之前获取Authorization Code提供的ClientID和重定向地址是否与本次提供的一致。成功完成检验之后,授权服务器会生成一个Access Token作为响应内容发送给客户端应用。整个响应内容除了Access Token之外,还包含其他一些与之相关的属性。

1: {
,
:3600,
,
,
   7:   }

授权服务器返回Access Token的完整响应内容如上所示,我们可以看到这是一个以JSON格式表示的对象。除了Access Token自身的内容之外,还可以获取其他一些相关的信息,比如Access Token的类型(token_type)、过期时间(“expires_in”,单位为秒)、授权范围(“scope”,与获取Authorization Code时指定的一致)以及表示认证身份的安全令牌(“authentication_token”)。

客户端应用接受到响应之后从中提取出Access Token。当它试图获取受保护资源的时候,将此Access Token附加到请求上,便会以授权用户的名义得到它所需要的资源。对于我们的应用场景来说,客户端应用直接将Access Token作为请求的查询字符串(?access_token={accesstoken})访问地址“https://apis.live.net/v5.0/me”便会成功获取当前登录用户的基本信息。

通过上面对Authorization Code类型的Authorization Grant整个授权流程的介绍,可以看出Implicit Authorization Grant的两个安全问题得到了很好的解决:虽然客户端获取Authorization Code时不需要指定ClientSecret,但是在获取Access Token时ClientRecret则是必需的,授权服务器只有在成功验证客户端应用身份的情况下才会颁发Access Token;针对Access Token的消息交换仅限于授权服务器和客户端应用之间进行,所以第三方(包括 当前用户)都无法获取到正确的Access Token。

Refresh Token

处于安全性考虑,Access Token并非终身有效,而是具有一个过期时间。上面我们给出了授权服务器返回Access Token的响应内容,其“expires_in”属性表示的就是Access Token的有效期限。那么,Access Token过期之后该如何处理呢?是否需要重新获得Authorization Code并利用它得到新的Access Token呢?

实际上这是不需要的,当我们得到Authorization Code之后,可以在利用它获取Access Token的时候,让授权服务器一并返回一个叫做Refresh Token的令牌。与Access Token不同,Refresh Token是一个长期有效的安全令牌,当Access Token过期之后,我们可以利用它获取一个新的Access Token。

对于Windows Live Connection来说,如果希望在获取Access Token的时候让授权服务器返回一个Refresh Token,其指定的授权范围必须具有一个名为“wl.offline_access”的Scope,它表示允许客户端程序在任何时候(包括用户尚未登录Windows Live Connect的情况下)读取和更新用户信息。对于具有如此授权范围的Access Token请求,授权服务器返回的响应中会按照如下的形式包含Refresh Code的内容。

   1: {
,
:3600,
, 
,      
, 
   8: }

在客户端应用从响应内容成功提取出Refresh Token之后,可以在任何时候向授权服务器(地址依然是“https://login.live.com/oauth20_authorize.srf”)发送获取新的Access Token的请求。和直通过Authorization Code获取Access Token一样,这通常也是一个HTTP-POST请求,其主体内容携带如下的参数。

  • client_id: 唯一标识被授权客户端应用的ClientID。
  • client_secret:唯一标识被授权客户端应用的ClientSecret。
  • redirect_uri:之前获取Authorization Code时指定的重定向地址。
  • grant_type:采用的Authorization Grant类型,这里自然就是“ refresh_code”。
  • refresh_token:之前利用Authorization Code获取的Access Token。

授权服务器对请求作必要验证后,会将新的Access Token置于响应的主体内容返回给客户端应用。完整地响应内容如下所示,我们不难看出:其中不仅仅包含新的Access Token,还返回了一个新的Refresh Token。

   1: {
,
:3600,
,
,
,
   8: }

实例演示:创建采用Authororization Code Authorization Grant的Web API应用

在《中篇》提供的实例中,我们演示了如何利用一个自定义AuthenticationFilter创建一个集成了Windows Live Connect认证的ASP.NET Web API应用。我们在这个实例中采用的Authorization Grant类型为Implicit,现在我们对这个AuthenticationFilter进行改造使之采用Authorization Code类型的Authorization Grant。

如果采用Authorization Code类型的Authorization Grant,客户端应用直接在Web服务器与授权服务器进行消息交换,所以无需在应用的AuthenticateAttribute特性上再指定一个在浏览器中收集和转发Access Token的Web页面对应的地址了。如下面的代码片断所示,应用在DemoController上的AuthenticateAttribute特性不均有任何参数。按照上面的方式利用浏览器来调用定义在DemoController中的Action方法GetProfile,我们依然可以得到希望的效果。

[Authenticate] 
class DemoController : ApiController
   3: {
public HttpResponseMessage GetProfile()
   5:     {
//省略实现
   7:     }
   8: }

如下所示的新AuthenticateAttribute的定义,其中将Access Token添加到响应Cookie中的ExecuteActionFilterAsync方法没有任何变化,我们修改的只是实现自IAuthenticationFilter接口的两个方法。

class AuthenticateAttribute : FilterAttribute, IAuthenticationFilter, IActionFilter
   2: {
;
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
   5:     {
//从请求中获取Access Token
string accessToken;
out accessToken))
   9:         {
null);
  11:         }
  12:  
//从请求中获取Authorization Code,并利用它来获取Access Token
string authorizationCode;
out authorizationCode))
  16:         { 
, authorizationCode);
  18:             
//但前请求URI去除“?code={authorizationcode}”部分作为rediect_uri参数
'?');
new HttpClient())
  22:             {
string>();
);
, callbackUri);
);
, authorizationCode);
);
new FormUrlEncodedContent(postData);
, httpContent).Result;
  31:  
//得到Access Token并Attach到请求的Properties字典中
if (tokenResponse.IsSuccessStatusCode)
  34:                 {
string content = tokenResponse.Content.ReadAsStringAsync().Result;
  36:                     JObject jObject = JObject.Parse(content);
];
  38:                     context.Request.AttachAccessToken(accessToken);
  39:  
null);
  41:                 }
else
  43:                 {
return Task.FromResult<HttpResponseMessage>(tokenResponse);
  45:                 }
  46:             }
  47:         }
null);
  49:     }
  50:  
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
  52:     {
string accessToken;
out accessToken))
  55:         {
;
string redirectUri = context.Request.RequestUri.ToString();
;
;
;
;
  62:             uri = String.Format(url, redirectUri, clientId, scope);
new Uri(uri), context.Request);
  64:         }
null);
  66:     }
  67:  
public Task<HttpResponseMessage> ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken,Func<Task<HttpResponseMessage>> continuation)
  69:     {
  70:         HttpResponseMessage response = continuation().Result;
string accessToken;
out accessToken))
  73:         {
  74:             response.SetAccessToken(actionContext.Request, accessToken);
  75:         }
return Task.FromResult<HttpResponseMessage>(response);
  77:     }
  78: }

在实现的AuthenticateAsync方法中,我们首选调用自定义的扩展方法TryGetAccessToken试着从当前请求中提取Access Token。如果Access Token不存在,我们在调用另一个扩展方法TryGetAuthorizationCode试着从当前请求中提取Authorization Code。在成功得到Authorization Code之后,我们将它作为参数调用Windows Live Connect API获取相应的Access Token,并调用扩展方法AttachAccessToken将此Access Token附加到当前请求上。

对于另一个实现的ChallengeAsync方法来说,如果通过调用扩展方法TryGetAccessToken不能从当前请求中得到相应的Access Token,我们通过为当前HttpAuthenticationChallengeContext的Result属性设置一个RedirectResult对象实现了重定向。重定向的地址正是一个用于获取Authorization Code的URL(“?response_type=code”),当前请求的URI作为其redirect_uri参数。

如下所示的上面提及的针对HttpRequestMessage类型的3个扩展方法的定义。方法TryGetAuthorizationCode从请求URL的查询字符串(“code”)中提取Authorization Code;方法AttachAccessToken将Access Token添加到请求的属性字典中;TryGetAccessToken方法则先后从请求的Cookie和属性字典中提取Access Token。

class Extensions
   2: {
//其他成员
string authorizationCode)
   5:     {
];
string.IsNullOrEmpty(authorizationCode);
   8:     }
   9:  
string accessToken)
  11:     {
string token;
out token))
  14:         {
  15:             request.Properties[AuthenticateAttribute.CookieName] = accessToken;
  16:         }
  17:     }
  18:  
string accessToken)
  20:     {
//从请求的Cookie中获取Access Token
null;
  23:         CookieHeaderValue cookieValue = request.Headers.GetCookies(AuthenticateAttribute.CookieName).FirstOrDefault();
null != cookieValue)
  25:         {
  26:             accessToken = cookieValue.Cookies.FirstOrDefault().Value;
true;
  28:         }
  29:  
//获取Attach的Access Token
object token;
out token))
  33:         {
string)token;
true;
  36:         }            
false;
  38:     }   
  39: }

当我们利用浏览器第一次调用定义在DemoController的Action方法GetProfile时(假设采用的URI为“https://www.artech.com/webapi/api/demo”),DemoController上的AuthenticateAttribute特性的AuthenticateAsync方法会率先被执行,但是Access Token和Authorization Code均不存在于当前请求之中,所以并不会执行任何操作。接下来ChallengeAsync方法被执行,浏览器被重定向到Windows Live Connect的授权页面(如果当前用户尚未登录到Windows Live Connect,在此之前会先被重定向到登录页面。如果之前已经完成了授权,授权页面不会再出现)。

在取得了用户授权的情况下,授权服务器会生成一个Authorization Code,并将其作为查询字符串附加到请求提供的重定向地址,然对针对这个URL实施重定向。由于我们设置的重定向地址为“https://www.artech.com/webapi/api/demo”,所以最终进行重定向的目标地址为“https://www.artech.com/webapi/api/demo?code={authorizationcode}”。

毫无疑问,该地址指向的依然是定义在DemoController中的Action方法GetProfile。在此情况下,AuthenticateAttribute的AuthenticateAsync方法再次被执行。此时它依然不能从请求中得到Access Token,但是却能得到Authorization Code。于是AuthenticateAttribute利用该Authorization Code调用Windows Live Connect API得到Access Token,并将其添加到请求的属性字典中。

接下来,Action方法GetProfile方法得以执行,它直接从当前请求(实际上是当前请求的属性字典中)中获得Access Token,并利用它调用Windows Live Connect API得到当前登录用户的个人信息。目标Action方法执行结束之后,AuthenticateAttribute又会将Acess Token添加到当前响应的Cookie集合中,所以浏览器在进行Web API调用时会自动将Access Token以Cookie的形式进行发送。

我们提供的这个实例并没有演示如何获取Refresh Token以及在Access Token过期的时候利用它来获取新的Access Token,有兴趣的读者朋友不妨将此功能一并实现在我们自定义的AuthenticateAttribute之中。


[1] 这里介绍的“客户端应用”是针对OAuth 2.0授权角色而言,表示被授权的客户端应用。从运行环境来讲,这个应用可以运行于单纯的客户端上下文(既包括运行于浏览器环境中的Web应用以及在客户端安装的各种App),也可以运行于服务器(比如Web应用中运行于Web Server的那部分程序)。

谈谈基于OAuth 2.0的第三方认证 [上篇]
谈谈基于OAuth 2.0的第三方认证 [中篇]
谈谈基于OAuth 2.0的第三方认证 [下篇]

相关文章: