[ Office 365 開發系列 ] 身份認證

任澤華Ryan發表於2016-04-29

前言

本文完全原創,轉載請說明出處,希望對大家有用。

通常我們在開發一個應用時,需要考慮使用者身份認證及授權,Office 365使用AAD(Azure Active Directory)作為其認證機構,為應用程式提供身份認證及授權服務。因此,在開發Office 365應用前,我們需要了解AAD的認證和授權機制。

閱讀目錄

  1. AAD認證授權機制
  2. 授權程式碼授予流和客戶端憑證授予流
  3. 應用示例

正文

AAD認證授權機制

當前的AAD支援多種身份認證標準:

  1. OpenId Connect
  2. OAuth2
  3. SAML-P
  4. WS-Federation and WS-Trust
  5. 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是Owin中的基礎介面型別,用於內部擴充不同的驗證機制,使用IDictionary<string, object> Properties { get; }這樣一個字典型別儲存我們應用程式驗證所需的資訊。SetDefaultSignInAsAuthenticationType指明Owin預設使用的驗證方式,為了保持使用者的登入狀態,我們使用cookie作為預設驗證方式,當cookie未登入時,Owin繼續使用下面註冊的其他方式嘗試驗證。
app.UseCookieAuthentication(new CookieAuthenticationOptions());
UseCookieAuthentication是Owin實現的Cookie驗證方式。在Owin的原始碼中,每一種方式都包含基本的處理類:
  • AuthenticationDefaults.cs
  • AuthenticationExtensions.cs
  • AuthenticationHandler.cs
  • AuthenticationMiddleware.cs
  • AuthenticationOptions.cs
此時我們使用new CookieAuthenticationOptions()初始化Cookie驗證預設配置。當Cookie中無驗證資訊時,會進入到
app.UseOpenIdConnectAuthentication
在OpenID驗證中,配置瞭如下引數:
  1. ClientId:應用程式ID,標識我們在AAD中的應用
  2. Authority:發起驗證請求的目標地址,如當前的https://login.windows.net,這裡要說明一下,根據我的實測,https://login.microsoftonline.com也是可以的。
  3. TokenValidationParameters:這個方法是為了驗證通過OpenID驗證的使用者是否為本應用程式的合法使用者,可根據業務實際情況編寫自己的驗證機制。
  4. 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
        }
    }
View Code

這裡我修改了一些程式碼,示例中的程式碼是使用者每次獲取新資源的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的機制,希望大家繼續關注。

 

相關文章