IdentityServer4系列 | 混合模式


一、前言

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

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

在幾篇關於授權模式篇章中,其中我們也使用了關於OpenID Connect 的簡化流程,在簡化流程中,所有令牌(身份令牌、訪問令牌)都通過瀏覽器傳輸,這對於身份令牌(IdentityToken)來說是沒有問題的,但是如果是訪問令牌(AccessToken)直接通過瀏覽器傳輸,就增加了一定的安全問題。因為訪問令牌比身份令牌更敏感,在非必須的情況下,我們不希望將它們暴露給外界。

所以我們就會考慮增加安全性,在OpenID Connect 包含一個名為“Hybrid(混合)”的流程,它為我們提供了兩全其美的優勢,身份令牌通過瀏覽器傳輸,因此客戶端可以在進行任何更多工作之前對其進行驗證。如果驗證成功,客戶端會通過令牌服務的以獲取訪問令牌。

二、初識

在認識混合模式(Hybrid Flow)時候,可以發現這里跟上一篇的授權碼模式有很多相似的地方,具體可以查看授權碼模式

查看使用OpenIDConnect時的安全性和隱私注意事項相關資料可以發現,

授權碼模式混合模式的流程步驟分別如下:

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:

  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

三、實踐

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

(這里重復之前的代碼,防止被爬抓后內容的缺失不完整)

在示例實踐中,我們將創建一個授權訪問服務,定義一個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.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,

五、總結

  1. 由於令牌都通過瀏覽器傳輸,為了提高更好的安全性,我們不想暴露訪問令牌, OpenID Connect包含一個名為“Hybrid(混合)”的流程,它可以讓身份令牌(id_token)通過前端瀏覽器通道傳輸,因此客戶端可以在做更多的工作之前驗證它。 如果驗證成功,客戶端會打開令牌服務的后端服務器通道來檢索訪問令牌(access_token)。

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

  3. 如果有不對的或不理解的地方,希望大家可以多多指正,提出問題,一起討論,不斷學習,共同進步。

  4. 項目地址

六、附加

OpenID Connect資料

Grant Types 類型


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM