初探IdentityServer4 - 為API增加角色驗證機制
什麼是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成功:
參考資料