IdentityServer4系列 | 混合模式

1、前言

在上一篇關於受權碼模式中, 已經介紹了關於受權碼的基本內容,認識到這是一個擁有更爲安全的機制,但這個仍然存在侷限,雖然在文中咱們說到經過後端的方式去獲取token,這種由web服務器和受權服務器直接通訊,不須要通過用戶的瀏覽器或者其餘的地方,可是在這種模式中,受權碼仍然是經過前端通道進行傳遞的,並且在訪問資源的中,也會將訪問令牌暴露給外界,就仍存在安全隱患。html

快速回顧一下以前初識基礎知識點中提到的,IdentityServer4OpenID Connect + OAuth2.0 相結合的認證框架,用戶身份認證和API的受權訪問,兩個結合一塊,實現了認證和受權的結合。前端

在幾篇關於受權模式篇章中,其中咱們也使用了關於OpenID Connect 的簡化流程,在簡化流程中,全部令牌(身份令牌、訪問令牌)都經過瀏覽器傳輸,這對於身份令牌(IdentityToken)來講是沒有問題的,可是若是是訪問令牌(AccessToken)直接經過瀏覽器傳輸,就增長了必定的安全問題。由於訪問令牌比身份令牌更敏感,在非必須的狀況下,咱們不但願將它們暴露給外界。git

因此咱們就會考慮增長安全性,在OpenID Connect 包含一個名爲「Hybrid(混合)」的流程,它爲咱們提供了一箭雙鵰的優點,身份令牌經過瀏覽器傳輸,所以客戶端能夠在進行任何更多工做以前對其進行驗證。若是驗證成功,客戶端會經過令牌服務的以獲取訪問令牌。github

2、初識

在認識混合模式(Hybrid Flow)時候,能夠發現這裏跟上一篇的受權碼模式有不少類似的地方,具體能夠查看受權碼模式web

查看使用OpenIDConnect時的安全性和隱私注意事項相關資料能夠發現,數據庫

受權碼模式混合模式的流程步驟分別以下:c#

Authorization Code Flow Steps

The Authorization Code Flow goes through the following steps.後端

  1. Client prepares an Authentication Request containing the desired request parameters.
  2. Client sends the request to the Authorization Server.
  3. Authorization Server Authenticates the End-User.
  4. Authorization Server obtains End-User Consent/Authorization.
  5. Authorization Server sends the End-User back to the Client with an Authorization Code.
  6. Client requests a response using the Authorization Code at the Token Endpoint.
  7. Client receives a response that contains an ID Token and Access Token in the response body.
  8. Client validates the ID token and retrieves the End-User's Subject Identifier.

Hybrid Flow Steps

The Hybrid Flow follows the following steps:api

  1. Client prepares an Authentication Request containing the desired request parameters.
  2. Client sends the request to the Authorization Server.
  3. Authorization Server Authenticates the End-User.
  4. Authorization Server obtains End-User Consent/Authorization.
  5. Authorization Server sends the End-User back to the Client with an Authorization Code and, depending on the Response Type, one or more additional parameters.
  6. Client requests a response using the Authorization Code at the Token Endpoint.
  7. Client receives a response that contains an ID Token and Access Token in the response body.
  8. Client validates the ID Token and retrieves the End-User's Subject Identifier.

由以上對比發現,codehybrid同樣都有8個步驟,大部分步驟也是相同的。最主要的區別在於第5步。瀏覽器

在受權碼模式中,成功響應身份驗證

HTTP/1.1 302 Found
  Location: https://client.example.org/cb?
    code=SplxlOBeZQQYbYS6WxSbIA
    &state=af0ifjsldkj

在混合模式中,成功響應身份驗證:

HTTP/1.1 302 Found
  Location: https://client.example.org/cb#
    code=SplxlOBeZQQYbYS6WxSbIA
    &id_token=eyJ0 ... NiJ9.eyJ1c ... I6IjIifX0.DeWt4Qu ... ZXso
    &state=af0ifjsldkj

其中多了一個id_token

在使用這些模式的時候,成功的身份驗證響應,存在指定的差別。這些受權端點的結果以不一樣的的依據返回。其中code是必定會返回的,access_token和id_token的返回依據 response_type 參數決定。

混合模式根據response_type的不一樣,authorization endpoint返回能夠分爲三種狀況。

  1. response_type = code + id_token ,即包含Access Token和ID Token
  2. response_type = code + token ,即包含Authorization Code和Access Token
  3. response_type = code + id_token + token,即包含Authorization Code、identity Token和Access Token

3、實踐

接着咱們進行一些簡單的實踐,由於有了前面受權碼模式代碼的經驗,編寫混合模式也是很簡單的。

(這裏重複以前的代碼,防止被爬抓後內容的缺失不完整)

在示例實踐中,咱們將建立一個受權訪問服務,定義一個MVC客戶端,MVC客戶端經過IdentityServer上請求訪問令牌,並使用它來訪問API。

3.1 搭建 Authorization Server 服務

搭建認證受權服務

3.1.1 安裝Nuget包

IdentityServer4 程序包

3.1.2 配置內容

創建配置內容文件Config.cs

public static class Config
{
    public static IEnumerable<IdentityResource> IdentityResources =>
        new IdentityResource[]
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
    };

    public static IEnumerable<ApiScope> ApiScopes =>
        new ApiScope[]
    {
        new ApiScope("hybrid_scope1")
    };

    public static IEnumerable<ApiResource> ApiResources =>
        new ApiResource[]
    {
        new ApiResource("api1","api1")
        {
            Scopes={ "hybrid_scope1" },
            UserClaims={JwtClaimTypes.Role},  //添加Cliam 角色類型
            ApiSecrets={new Secret("apipwd".Sha256())}
        }
    };

    public static IEnumerable<Client> Clients =>
        new Client[]
    {
        new Client
        {
            ClientId = "hybrid_client",
            ClientName = "hybrid Auth",
			ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },
            AllowedGrantTypes = GrantTypes.Hybrid,

            RedirectUris ={
                "http://localhost:5002/signin-oidc", //跳轉登陸到的客戶端的地址
            },
            // RedirectUris = {"http://localhost:5002/auth.html" }, //跳轉登出到的客戶端的地址
            PostLogoutRedirectUris ={
                "http://localhost:5002/signout-callback-oidc",
            },
            ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },

            AllowedScopes = {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
                "hybrid_scope1"
            },
            //容許將token經過瀏覽器傳遞
            AllowAccessTokensViaBrowser=true,
            // 是否須要贊成受權 (默認是false)
            RequireConsent=true
        }
    };
}

RedirectUris : 登陸成功回調處理的客戶端地址,處理回調返回的數據,能夠有多個。

PostLogoutRedirectUris :跳轉登出到的客戶端的地址。

這兩個都是配置的客戶端的地址,且是identityserver4組件裏面封裝好的地址,做用分別是登陸,註銷的回調

由於是混合受權的方式,因此咱們經過代碼的方式來建立幾個測試用戶。

新建測試用戶文件TestUsers.cs

public class TestUsers
    {
        public static List<TestUser> Users
        {
            get
            {
                var address = new
                {
                    street_address = "One Hacker Way",
                    locality = "Heidelberg",
                    postal_code = 69118,
                    country = "Germany"
                };

                return new List<TestUser>
                {
                    new TestUser
                    {
                        SubjectId = "1",
                        Username = "i3yuan",
                        Password = "123456",
                        Claims =
                        {
                            new Claim(JwtClaimTypes.Name, "i3yuan Smith"),
                            new Claim(JwtClaimTypes.GivenName, "i3yuan"),
                            new Claim(JwtClaimTypes.FamilyName, "Smith"),
                            new Claim(JwtClaimTypes.Email, "i3yuan@email.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                            new Claim(JwtClaimTypes.WebSite, "http://i3yuan.top"),
                            new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
                        }
                    }
                };
            }
        }
    }

返回一個TestUser的集合。

經過以上添加好配置和測試用戶後,咱們須要將用戶註冊到IdentityServer4服務中,接下來繼續介紹。

3.1.3 註冊服務

在startup.cs中ConfigureServices方法添加以下代碼:

public void ConfigureServices(IServiceCollection services)
        {
            var builder = services.AddIdentityServer()
               .AddTestUsers(TestUsers.Users); //添加測試用戶

            // in-memory, code config
            builder.AddInMemoryIdentityResources(Config.IdentityResources);
            builder.AddInMemoryApiScopes(Config.ApiScopes);
            builder.AddInMemoryApiResources(Config.ApiResources);
            builder.AddInMemoryClients(Config.Clients);

            // not recommended for production - you need to store your key material somewhere secure
            builder.AddDeveloperSigningCredential();
            services.ConfigureNonBreakingSameSiteCookies();
        }

3.1.4 配置管道

在startup.cs中Configure方法添加以下代碼:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseIdentityServer();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            }); 
        }

以上內容是快速搭建簡易IdentityServer項目服務的方式。

這搭建 Authorization Server 服務跟上一篇受權碼模式有何不一樣之處呢?

  1. 在Config中配置客戶端(client)中定義了一個AllowedGrantTypes的屬性,這個屬性決定了Client能夠被哪一種模式被訪問,GrantTypes.Hybrid混合模式。因此在本文中咱們須要添加一個Client用於支持受權碼模式(Hybrid)。

3.2 搭建API資源

實現對API資源進行保護

3.2.1 快速搭建一個API項目

3.2.2 安裝Nuget包

IdentityServer4.AccessTokenValidation 包

3.2.3 註冊服務

在startup.cs中ConfigureServices方法添加以下代碼:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        services.AddAuthentication("Bearer")
          .AddIdentityServerAuthentication(options =>
          {
              options.Authority = "http://localhost:5001";
              options.RequireHttpsMetadata = false;
              options.ApiName = "api1";
              options.ApiSecret = "apipwd"; //對應ApiResources中的密鑰
          });
    }

AddAuthentication把Bearer配置成默認模式,將身份認證服務添加到DI中。

AddIdentityServerAuthentication把IdentityServer的access token添加到DI中,供身份認證服務使用。

3.2.4 配置管道

在startup.cs中Configure方法添加以下代碼:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }    
            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }

UseAuthentication將身份驗證中間件添加到管道中;

UseAuthorization 將啓動受權中間件添加到管道中,以便在每次調用主機時執行身份驗證受權功能。

3.2.5 添加API資源接口

[Route("api/[Controller]")]
[ApiController]
public class IdentityController:ControllerBase
{
    [HttpGet("getUserClaims")]
    [Authorize]
    public IActionResult GetUserClaims()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
}

在IdentityController 控制器中添加 [Authorize] , 在進行請求資源的時候,需進行認證受權經過後,才能進行訪問。

3.3 搭建MVC 客戶端

實現對客戶端認證受權訪問資源

3.3.1 快速搭建一個MVC項目

3.3.2 安裝Nuget包

IdentityServer4.AccessTokenValidation 包

3.3.3 註冊服務

要將對 OpenID Connect 身份認證的支持添加到MVC應用程序中。

在startup.cs中ConfigureServices方法添加以下代碼:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddAuthorization();

        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
               .AddCookie("Cookies")  //使用Cookie做爲驗證用戶的首選方式
              .AddOpenIdConnect("oidc", options =>
              {
                  options.Authority = "http://localhost:5001";  //受權服務器地址
                  options.RequireHttpsMetadata = false;  //暫時不用https
                  options.ClientId = "hybrid_client";
                  options.ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A";
                  options.ResponseType = "code id_token"; //表明
                  options.Scope.Add("hybrid_scope1"); //添加受權資源
                  options.SaveTokens = true; //表示把獲取的Token存到Cookie中
                  options.GetClaimsFromUserInfoEndpoint = true;
              });
         services.ConfigureNonBreakingSameSiteCookies();
    }
  1. AddAuthentication注入添加認證受權,當須要用戶登陸時,使用 cookie 來本地登陸用戶(經過「Cookies」做爲DefaultScheme),並將 DefaultChallengeScheme 設置爲「oidc」,
  2. 使用 AddCookie 添加能夠處理 cookie 的處理程序。
  3. AddOpenIdConnect用於配置執行 OpenID Connect 協議的處理程序和相關參數。Authority代表以前搭建的 IdentityServer 受權服務地址。而後咱們經過ClientIdClientSecret,識別這個客戶端。 SaveTokens用於保存從IdentityServer獲取的token至cookie,ture標識ASP.NETCore將會自動存儲身份認證session的access和refresh token。
  4. 咱們在配置ResponseType時須要使用Hybrid定義的三種狀況之一,具體代碼如上所述。

3.3.4 配置管道

而後要確保認證服務執行對每一個請求的驗證,加入UseAuthenticationUseAuthorizationConfigure中,在startup.cs中Configure方法添加以下代碼:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }

UseAuthentication將身份驗證中間件添加到管道中;

UseAuthorization 將啓動受權中間件添加到管道中,以便在每次調用主機時執行身份驗證受權功能。

3.3.5 添加受權

在HomeController控制器並添加[Authorize]特性到其中一個方法。在進行請求的時候,需進行認證受權經過後,才能進行訪問。

[Authorize]
        public IActionResult Privacy()
        {
            ViewData["Message"] = "Secure page.";
            return View();
        }

還要修改主視圖以顯示用戶的Claim以及cookie屬性。

@using Microsoft.AspNetCore.Authentication

<h2>Claims</h2>

<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl>

<h2>Properties</h2>

<dl>
    @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
    {
        <dt>@prop.Key</dt>
        <dd>@prop.Value</dd>
    }
</dl>

訪問 Privacy 頁面,跳轉到認證服務地址,進行帳號密碼登陸,Logout 用於用戶的註銷操做。

3.3.6 添加資源訪問

HomeController控制器添加對API資源訪問的接口方法。在進行請求的時候,訪問API受保護資源。

/// <summary>
        /// 測試請求API資源(api1)
        /// </summary>
        /// <returns></returns>
        public async Task<IActionResult> getApi()
        {
            var client = new HttpClient();
            var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            if (string.IsNullOrEmpty(accessToken))
            {
                return Json(new { msg = "accesstoken 獲取失敗" });
            }
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            var httpResponse = await client.GetAsync("http://localhost:5003/api/identity/GetUserClaims"); 
            var result = await httpResponse.Content.ReadAsStringAsync();
            if (!httpResponse.IsSuccessStatusCode)
            {
                return Json(new { msg = "請求 api1 失敗。", error = result });
            }
            return Json(new
            {
                msg = "成功",
                data = JsonConvert.DeserializeObject(result)
            });
        }

測試這裏經過獲取accessToken以後,設置client請求頭的認證,訪問API資源受保護的地址,獲取資源。

3.4 效果

咱們經過對比受權碼模式與混合模式 能夠發現,在大部分步驟是相同的,但也存在一些差別。

在整個過程當中,咱們使用抓取請求,能夠看到在Authorization Endpoint中二者的區別以下:

受權碼模式:

混合模式:

在Authorization EndPoint返回的Id_Token和Token EndPoint返回的id_Token中,能夠看到兩次值是可能不相同的,可是其中包含的用戶信息都是同樣的。

在使用Hybrid時咱們看到受權終結點返回的Id Token中包含at_hash(Access Token的哈希值)和s_hash(State的哈希值),規範中定義瞭如下的一些檢驗規則。

  1. 兩個id_token中的 iss 和 sub 必須相同。
  2. 若是任何一個 id token 中包含關於終端用戶的聲明,兩個令牌中提供的值必須相同。
  3. 關於驗證事件的聲明必須都提供。
  4. at_hash 和 s_hash 聲明可能會從 token 端點返回的令牌中忽略,即便從 authorize 端點返回的令牌中已經聲明。

4、問題

4.1 設置RequirePkce

在指定基於受權碼的令牌是否須要驗證密鑰,默認爲true。

解決方法:

修改Config中的RequirePkce爲false便可。這樣服務端便不在須要客戶端提供code challeng。

RequirePkce = false,//v4.x須要配置這個

4.2 設置ResponseType

在上文中提到的MVC客戶端中配置ResponseType時可使用Hybrid定義的三種狀況。

而當設置爲"code token", "code id_token token"中的一種,即只要包含token,都會報以下錯誤:

解決方法:

受權服務端中的Config中增長容許將token經過瀏覽器傳遞

AllowAccessTokensViaBrowser = true,

5、總結

  1. 因爲令牌都經過瀏覽器傳輸,爲了提升更好的安全性,咱們不想暴露訪問令牌, OpenID Connect包含一個名爲「Hybrid(混合)」的流程,它可讓身份令牌(id_token)經過前端瀏覽器通道傳輸,所以客戶端能夠在作更多的工做以前驗證它。 若是驗證成功,客戶端會打開令牌服務的後端服務器通道來檢索訪問令牌(access_token)。

  2. 在後續會對這方面進行介紹繼續說明,數據庫持久化問題,以及如何應用在API資源服務器中和配置在客戶端中,會進一步說明。

  3. 若是有不對的或不理解的地方,但願你們能夠多多指正,提出問題,一塊兒討論,不斷學習,共同進步。

  4. 項目地址

6、附加

OpenID Connect資料

Grant Types 類型

相關文章
相關標籤/搜索