前言
本文完全原創,轉載請說明出處,希望對大家有用。
通常我們在開發一個應用時,需要考慮用戶身份認證及授權,Office 365使用AAD(Azure Active Directory)作為其認證機構,為應用程序提供身份認證及授權服務。因此,在開發Office 365應用前,我們需要了解AAD的認證和授權機制。
正文
AAD認證授權機制
當前的AAD支持多種身份認證標准:
- OpenId Connect
- OAuth2
- SAML-P
- WS-Federation and WS-Trust
- Graph web api
這幾種身份認證標准會應用在不同的場景中,如OAuth2.0應用於Office 365應用程序接口,SAML-P多應用於Office 365的混合部署,如果想要詳細了解,可以參閱此文章,詳細介紹了Office 365身份認證支持的各項協議。我們在開發應用的過程中,最主要是使用OpenID Connect和OAuth2.0.因此,本篇內容中只涉及到OpenID和OAuth2.0兩種類型的身份認證分析,后續文章中會涉及到Office 365的混合部署及令牌交換協議內容。
OAuth2.0是OAuth的最新版本,升級並簡化了驗證的過程,相關描述可以查看RFC 6749,在資源授權方面,OAuth2.0支持多種授予流,Office 365使用授權代碼授予流和客戶端憑證授予流,兩者適用於不同的應用場景,同時在AAD中配置權限也進行了區分,稍后會具體講解。下圖為標准的OAuth2.0處理過程:
+--------+ +---------------+ | |--(A)- Authorization Request ->| Resource | | | | Owner | | |<-(B)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(C)-- Authorization Grant -->| Authorization | | Client | | Server | | |<-(D)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(E)----- Access Token ------>| Resource | | | | Server | | |<-(F)--- Protected Resource ---| | +--------+ +---------------+
OpenID是目前各大網站普遍支持的開放協議,OpenID Connect 1.0是基於OAuth2.0設計的用戶認證標准,Azure Active Directory (Azure AD) 中的 OpenID Connect 1.0 允許你使用 OAuth 2.0 協議進行單一登錄。 OAuth 2.0 是一種授權協議,但 OpenID Connect 擴展了 OAuth 2.0 的身份驗證協議用途。OpenID Connect 協議(OpenId Connect 1.0)的主要功能是返回 id_token,后者用於對用戶進行身份驗證。 下圖為OpenID的標准處理過程:
+--------+ +--------+ | | | | | |---------(1) AuthN Request-------->| | | | | | | | +--------+ | | | | | | | | | | | End- |<--(2) AuthN & AuthZ-->| | | | | User | | | | RP | | | | OP | | | +--------+ | | | | | | | |<--------(3) AuthN Response--------| | | | | | | |---------(4) UserInfo Request----->| | | | | | | |<--------(5) UserInfo Response-----| | | | | | +--------+ +--------+
OpenID的標准過程需要以下幾步:
1. 客戶端(RP)發送一個請求到OpenID的提供商(OP);
2. OP驗證用戶,如果用戶尚未授權,則跳轉到授權頁面;
3. 用戶授權后,OP會引導用戶返回到客戶端,並會攜帶一個Token和id token;
4. RP使用收到的Token請求用戶其他信息資源;
5. OP返回請求的資源信息
通過上述的步驟,第三方應用(也就是客戶端)不僅可以驗證用戶的合法性,同時可以在用戶授權的情況下獲取用戶基本信息。在AAD中使用的OpenID Connect 1.0為Auth2.0進行了擴展,在返回Token的同時,會返回一個JWT形式的id_token。AAD中的OpenID終結點配置信息可通過訪問此鏈接查看:https://login.windows.net/common/.well-known/openid-configuration 。id_token包含用戶的基本信息,作為應用的CurrentUser屬性。獲取到Token后,應用可以通過此憑證請求資源,Office 365使用Bearer方式獲取資源,請參閱Bearer Token Usage
授權代碼流和客戶端憑證授予流
AAD中的授權代碼授予流使用如下流程:
(此圖引用自msdn)
對比OAuth2.0的標准流程,授權代碼流會以授權代碼(Code)的方式返回授權標識,用戶通過使用Code請求資源Token,應用程序使用獲取到的Token調用資源Web API。
當我們的Office 365應用使用授權代碼授予流時,需要我們在AAD中設置資源代理權限,設置過程如下:
(一)通過Office 365設置頁面進入Azure AD:

(二)進入AD中的應用程序,並找到我們的注冊應用(如何注冊應用請參考),進入應用的Configure頁面,如下圖:

(三)設置資源的Delegated Permissions,如果我們使用過授權代碼流來請求資源,只需設置Delegation Permissions

AAD中的客戶端憑證授予流使用如下流程:
(此圖引用自msdn)
與標准OAuth2.0流程相比,客戶端憑證授予流不需要用戶授權,而是由應用程序直接訪問AAD請求token。請注意,如果使用此方式,則應用程序對資源有最大權限。
當我們的Office 365應用使用授權代碼授予流時,需要我們在AAD中設置資源的應用權限,與授權代碼授予流只是配置權限不同,設置的是Application Permission,如下圖:

應用示例
在實際應用中,我們通常會使用Owin中間件來完成用戶身份認證,我們使用Office Dev Center中的實例來分析。
先來看如何實現用戶登錄后驗證,我們貼出重要代碼來分析:
public partial class Startup { public void ConfigureAuth(IAppBuilder app) { app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); app.UseCookieAuthentication(new CookieAuthenticationOptions()); app.UseOpenIdConnectAuthentication( new OpenIdConnectAuthenticationOptions { ClientId = SettingsHelper.ClientId, Authority = SettingsHelper.Authority, TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters { ValidateIssuer = false }, Notifications = new OpenIdConnectAuthenticationNotifications() { AuthorizationCodeReceived = (context) => { var code = context.Code; ClientCredential credential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey); string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value; String signInUserId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value; AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}/{1}", SettingsHelper.AuthorizationUri, tenantID), new ADALTokenCache(signInUserId)); AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, SettingsHelper.AADGraphResourceId); return Task.FromResult(0); }, RedirectToIdentityProvider = (context) => { string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase; context.ProtocolMessage.RedirectUri = appBaseUrl + "/"; context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl; return Task.FromResult(0); }, AuthenticationFailed = (context) => { context.HandleResponse(); return Task.FromResult(0); } } }); }
上述代碼在項目中的App_Start文件夾下Startup.Auth.cs,是Owin的Server端配置內容。Owin中間件是在應用啟動時注冊,注冊方式是掃描跟文件夾下的Startup.cs,存在則使用該配置類注冊。針對OWIN的處理機制,我們在后續的章節中單獨分析OWIN中間件的架構,當前我們主要聚焦在如何使用OpenID及OAuth。在上面的代碼中,有這么一句:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
- AuthenticationDefaults.cs
- AuthenticationExtensions.cs
- AuthenticationHandler.cs
- AuthenticationMiddleware.cs
- AuthenticationOptions.cs
app.UseOpenIdConnectAuthentication
- ClientId:應用程序ID,標識我們在AAD中的應用
- Authority:發起驗證請求的目標地址,如當前的https://login.windows.net,這里要說明一下,根據我的實測,https://login.microsoftonline.com也是可以的。
- TokenValidationParameters:這個方法是為了驗證通過OpenID驗證的用戶是否為本應用程序的合法用戶,可根據業務實際情況編寫自己的驗證機制。
- Notifications:在OpenID驗證並返回后,Owin調用Notifications方法,也正是我們使用OAuth進行用戶授權的觸發方法。
下面是一個TokenValidationParameters參數的示例:
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters { IssuerValidator = (issuer, token) => { return DoesIssuerBelongToMyCustomersList(issuer);//DoesIssuerBelongToMyCustomersList方法根據當前登陸人信息判斷是否在用戶列表中,如果不存在,則返回false } }
接下來分析Notifications參數:
AuthorizationCodeReceived = (context) => { var code = context.Code; ClientCredential credential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey); String signInUserId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value; AuthenticationContext authContext = new AuthenticationContext("https://login.windows.net/common", new ADALTokenCache(signInUserId)); AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, SettingsHelper.AADGraphResourceId); return Task.FromResult(0); }, RedirectToIdentityProvider = (context) => { string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase; context.ProtocolMessage.RedirectUri = appBaseUrl + "/"; context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl; return Task.FromResult(0); }, AuthenticationFailed = (context) => { context.HandleResponse(); return Task.FromResult(0); }
在Notifications參數方法中定義了3個委托方法,當調用OpenId驗證通過並返回Code參數時,Owin調用AuthorizationCodeReceived,RedirectToIdentityProvider方法用於定義驗證通過后的返回頁面地址,AuthenticationFailed定義驗證失敗后的處理方法。在AuthorizationCodeReceived這個方法中,我們使用文檔開始提到的AAD授權代碼流方式為用戶授權。SettingsHelper.ClientId是應用程序ID,是應用程序在AAD中的唯一標識。SettingsHelper.AppKey是應用程序中新建的keys(可以使用多個),新建的方法如下:
進入AAD中的應用程序管理,添加app key,這個key是有過期時間的,最多2年。這里提醒一下,新建key以后需要保存才能看到key字符串,而且只有第一次能查看,如果忘記了只能重新建一個。

接着往下面看,context對象是Owin根據返回的id_token生成的上下文,這里的signInUserId是用戶在AAD中對象標識符:
String signInUserId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
我們使用signInUserId來唯一標識用戶的Token緩存對象,新建一個AuthenticationContext對象,這個對象是基於ADAL 創建,如有想要了解什么是ADAL,請參閱The New Token Cache in ADAL v2。創建對象的同時,我們將對象ADALTokenCache作為TokenCache傳入對象,ADALTokenCache是我們自定義用來緩存用戶Token的類,如下:
public class ADALTokenCache : TokenCache { string User; UserTokenCache Cache; // constructor public ADALTokenCache(string user) { // associate the cache to the current user of the web app User = user; this.AfterAccess = AfterAccessNotification; this.BeforeAccess = BeforeAccessNotification; this.BeforeWrite = BeforeWriteNotification; using (ApplicationDbContext db = new ApplicationDbContext()) { Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == User); } this.Deserialize((Cache == null) ? null : Cache.cacheBits); } public override void Clear() { base.Clear(); using (ApplicationDbContext db = new ApplicationDbContext()) { Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == User); if (Cache != null) db.UserTokenCacheList.Remove(Cache); db.SaveChanges(); } } void BeforeAccessNotification(TokenCacheNotificationArgs args) { using (ApplicationDbContext db = new ApplicationDbContext()) { if (Cache == null) { // first time access Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == User); } else { // retrieve last write from the DB var status = from e in db.UserTokenCacheList where (e.webUserUniqueId == User) select new { LastWrite = e.LastWrite }; // if the in-memory copy is older than the persistent copy if (status != null && status.Count() > 0 && status.First().LastWrite > Cache.LastWrite) { Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == User); } } } this.Deserialize((Cache == null) ? null : Cache.cacheBits); } // Notification raised after ADAL accessed the cache. // If the HasStateChanged flag is set, ADAL changed the content of the cache void AfterAccessNotification(TokenCacheNotificationArgs args) { if (this.HasStateChanged) { using (ApplicationDbContext db = new ApplicationDbContext()) { if (Cache == null || Cache.UserTokenCacheId == 0) { Cache = new UserTokenCache { webUserUniqueId = User, cacheBits = this.Serialize(), LastWrite = DateTime.Now }; } else { Cache.cacheBits = this.Serialize(); Cache.LastWrite = DateTime.Now; } db.Entry(Cache).State = Cache.UserTokenCacheId == 0 ? EntityState.Added : EntityState.Modified; db.SaveChanges(); } this.HasStateChanged = false; } } void BeforeWriteNotification(TokenCacheNotificationArgs args) { // if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry } }
這里我修改了一些代碼,示例中的代碼是用戶每次獲取新資源的Token時新增一條Cache數據,為了多用戶訪問,我將緩存機制改為每個用戶對應一條Cache數據。
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, SettingsHelper.AADGraphResourceId);
AcquireTokenByAuthorizationCode是ADAL幫我們定義好的授權代碼流方法,用於通過code獲取token,同時我們指定請求SettingsHelper.AADGraphResourceId(AAD Graph web resource)資源,這樣可以驗證我們的應用是否有對應資源的訪問權限。當然,這個參數是可選的。
驗證邏輯圖如下:

結束語
Office 365開發系列的身份認證就到這里了,如有不明白的地方,請在評論中提出。后續章節我們會繼續深入了解OWIN及ADAL的機制,希望大家繼續關注。
