前言
本文完全原創,轉載請說明出處,希望對大家有用。
通常我們在開發一個應用時,需要考慮使用者身份認證及授權,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的機制,希望大家繼續關注。