IdentityServer4

初探IdentityServer4 - 為API增加角色驗證機制

沈蓓菁 2020/12/10 13:01:19
425

什麼是IdentityServer4

    IdentityServer4是個開源且用於ASP.NET Core的OpenID Connect和OAuth 2.0框架,目的是提供一種通用方式

    來驗證所有應用程式的身份驗證請求。

Source:https://docs.identityserver.io/en/latest/intro/terminology.html

     

    包含功能如下:

    1. 管理及驗證Client端。

    2. 給予Clinet端身份與訪問權限Token。

    3. 驗證Token。

    4. 管理Session與單點登入/註銷。

    5. 支援第三方登入。

    6. 其他...

   

IdentityServer4如何提供幫助

    IdentityServer是個中介層, 可使用它來為應用程式增加必要的協定,透過這些協定讓程式間互相溝通。

   Source:https://docs.identityserver.io/en/latest/intro/big_picture.html

 

範例

    有兩個API(DevAPI 與 UatAPI),其中DevAPI只有admin身分者才可使用、UatAPI則是有user身份者皆可用:

 

環境設定

    建立名為IdentityServer的空白Web專案,並再建立一個ASP.NET Web API專案,版本皆為.Net Core 3.1:

 

安裝IdentityServer4

    於IdentityServer專案下安裝,安裝當下最新版本為4.1.1:

dotnet add package IdentityServer4 --version 4.1.1

 

設定API資源

    ApiResource的userClaims加入角色:

using System.Collections.Generic;
using IdentityModel;
using IdentityServer4.Models;

namespace IdentityServer
{
    internal class Resources
    {
        // 設定有哪些API可使用
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("DevApi", "DEV Api", new List<string>{ JwtClaimTypes.Role }),
                new ApiResource("UatApi", "UAT Api", new List<string>{ JwtClaimTypes.Role })
            };
        }

        // 設定API範圍(for Client)
        public static IEnumerable<ApiScope> GetApiScopes()
        {
            return new List<ApiScope>
            {
                new ApiScope("DevApi", "DEV Api"),
                new ApiScope("UatApi", "UAT Api")
            };
        }
    }
}

 

設定Client帳號

    設定兩個帳號(Admin與User),授權方式使用ClientCredentials,並加入角色Claim:

using System.Collections.Generic;
using IdentityModel;
using IdentityServer4.Models;

namespace IdentityServer
{
    internal class Clients
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientId = "Admin",
                    // IdentityServer提供多種授權方式,這裡使用客戶授權模式
                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    // from ApiScope, 這裡若ApiScope不同,取Token時會有invalid_scope錯誤
                    AllowedScopes = { "DevApi", "UatApi" },
                    ClientSecrets = { new Secret("adminSecret".Sha256())},
                    // 因admin也要能使用user身份的api,故兩種角色都要加入
                    Claims = new List<ClientClaim>
                    {
                        new ClientClaim(JwtClaimTypes.Role, "admin"),
                        new ClientClaim(JwtClaimTypes.Role, "user")
                    },
                    /* 若無以下這行,Token回傳的欄位名稱會是client_role而不是role, 
                     * 這樣會因對應不到role而導致驗證失敗 */
                    ClientClaimsPrefix = string.Empty
                },
                new Client
                {
                    ClientId = "User",
                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    AllowedScopes = { "UatApi" },
                    ClientSecrets = { new Secret("userSecret".Sha256())},
                    Claims = new List<ClientClaim>
                    {
                        new ClientClaim(JwtClaimTypes.Role, "user")
                    },
                    ClientClaimsPrefix = string.Empty
                }
            };
        }
    }
}

 

配置與啟用IdentityServer4

    1. 將IdentityServer加入DI Container:

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentityServer()
            // 方便測試選擇in-memory
            .AddInMemoryClients(Clients.GetClients())
            .AddInMemoryApiResources(Resources.GetApiResources())
            .AddInMemoryApiScopes(Resources.GetApiScopes())
            // 方便開發階段於啟動時產生暫時密鑰(tempkey.jwk)
            .AddDeveloperSigningCredential();

    services.AddControllers();
}

    2. 啟用IdentityServer:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });

    // 啟用IdentityServer
    app.UseIdentityServer();
}

    3. 配置完成後run,並對以下端點發出Get請求

https://localhost:5001/.well-known/openid-configuration

    即可看到以下JSON,Client端和API會使用這些內容來下載必要的配置數據:

{
    "issuer": "https://localhost:5001",
    "jwks_uri": "https://localhost:5001/.well-known/openid-configuration/jwks",
    "authorization_endpoint": "https://localhost:5001/connect/authorize",
    "token_endpoint": "https://localhost:5001/connect/token",
    "userinfo_endpoint": "https://localhost:5001/connect/userinfo",
    "end_session_endpoint": "https://localhost:5001/connect/endsession",
    "check_session_iframe": "https://localhost:5001/connect/checksession",
    "revocation_endpoint": "https://localhost:5001/connect/revocation",
    "introspection_endpoint": "https://localhost:5001/connect/introspect",
    "device_authorization_endpoint": "https://localhost:5001/connect/deviceauthorization",
    "frontchannel_logout_supported": true,
    "frontchannel_logout_session_supported": true,
    "backchannel_logout_supported": true,
    "backchannel_logout_session_supported": true,
    "scopes_supported": [
        "DevApi",
        "UatApi",
        "offline_access"
    ],
    "claims_supported": [],
    "grant_types_supported": [
        "authorization_code",
        "client_credentials",
        "refresh_token",
        "implicit",
        "urn:ietf:params:oauth:grant-type:device_code"
    ],
    "response_types_supported": [
        "code",
        "token",
        "id_token",
        "id_token token",
        "code id_token",
        "code token",
        "code id_token token"
    ],
    "response_modes_supported": [
        "form_post",
        "query",
        "fragment"
    ],
    "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post"
    ],
    "id_token_signing_alg_values_supported": [
        "RS256"
    ],
    "subject_types_supported": [
        "public"
    ],
    "code_challenge_methods_supported": [
        "plain",
        "S256"
    ],
    "request_parameter_supported": true
}

 

安裝IdentityServer4.AccessTokenValidation

    於APIs專案下安裝,安裝當下最新版本為3.0.1:

dotnet add package IdentityServer4.AccessTokenValidation --version 3.0.1

 

加入JWT驗證

    IdentityServer預設使用JWT,加入後要設定IdentityServer位址:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddJwtBearer(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
            {
                options.Authority = "https://localhost:5001";
                options.RequireHttpsMetadata = false;
                // 此時產生的Token還未有aud資料,若沒設定ValidateAudience = false,則call API會回傳401
                options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
                {
                    ValidateAudience = false
                };
            });
}

 

啟用驗證機制

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    //app.UseHttpsRedirection();

    app.UseRouting();

    // 啟用驗證
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

 

新增API

    在APIs專案下建立兩個API,其中DevGet角色權限設為admin,UatGet則設為user:

[HttpGet("DevGet")]
[Authorize(Roles = "admin")]
public ActionResult<string> DevGet()
{
    return "Yes, only Admin can accrss this API";
}

[HttpGet("UatGet")]
[Authorize(Roles = "user")]
public ActionResult<string> UatGet()
{
    return "Yes, user can accrss this API";
}

 

測試

    1. 角色User:

    (1) 發出請求JWT

POST /connect/token HTTP/1.1
Host: localhost:5001
Content-Type: application/x-www-form-irlencoded
client_id=User&
client_secret=userSecret&
grant_type=client_crednetials&
scope=UatApi

    回傳以下JSON:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkRDQTU5NkQ2RTU4MzQyOTE0OEFDQjlCODhBQzc4QUVGIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2MDc0MjYzMzksImV4cCI6MTYwNzQyOTkzOSwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMSIsImNsaWVudF9pZCI6IlVzZXIiLCJyb2xlIjoidXNlciIsImp0aSI6IkJFM0YzMUMyODBFMkM1NEVBRkI1MzZDQkFCOEQxMDVEIiwiaWF0IjoxNjA3NDI2MzM5LCJzY29wZSI6WyJVYXRBcGkiXX0.EKeETNcEH9DnOsOcdAe3GCiC3dl6QkZdkcUNwWInwQUygbl8wZX1VmpGTB6b9Kz9cdarMHSwH0jSn_Ol2592Vw15_evmL3LFyCtwJy91_r8zLh5-5zGaYZT4b8n4-xA3zSB0QDqUnNUShdBjjTuchK5sjr51gVgbA_YUZR8qxBjZm8821Z6sICqL14voasoKvEMKR3RdalNDE8mwmodb3Ctr6lO0jFWvozx9ISDYd5WIZjLO10SH8DfGZHeN2qTXha5ykksyJoWokAUEZKP74WRYHqi2ay-O4KNrwLJxD_-YToYtJoS_qHI3P2LQzfdhL_VlhB3vufNN2ZtwZ0AL7Q",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "UatApi"
}

    從jwt.io可解析Token內容,可看出Token有帶role:


    (2) 呼叫DevGet API,回傳403無權限:

    (3) 呼叫UatGet API,回傳200成功:

 

 

    2. 角色Admin: 

    (1) 取得Token,並確認Token內包含role

POST /connect/token HTTP/1.1
Host: localhost:5001
Content-Type: application/x-www-form-irlencoded
client_id=Admin&
client_secret=adminSecret&
grant_type=client_crednetials&
scope=DepApi UatApi

    (2) 呼叫DevGet API,回傳200成功:

    (3) 呼叫UatGet API,回傳200成功:

      

 

參考資料

    IdentityServer4 Docs

 

沈蓓菁