在ASP.NET Core上加入 IdentityServer4
實現客戶端授權模式、密碼模式、刷新token
0. 環境
- ASP.NET Core 3.1
- IdentityServer 4
- SampleCode 包含三個專案,分別為
- IdentityServer
- WebApi
- Client
1. 客戶端授權模式
Identity Server
安裝 Nuget Package
dotnet add package IdentityServer4
Config
新增一個 IdentityServer 的設定檔,這部分也可以從 appSetting.json 設定,詳細可參考官方的這篇
public class IdentityConfig
{
// 定義有哪些API資源,相當於 SampleCode 的 WebApi
public static IEnumerable<ApiResource> GetResources()
{
return new[]
{
new ApiResource("api1", "MY API")
};
}
// 定義使用者,相當於 SampleCode 的 Client
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256()),
},
// 這個 client 允許使用的 scope
AllowedScopes = { "api1" }
}
};
}
public static IEnumerable<ApiScope> GetScopes()
{
return new List<ApiScope>()
{
new ApiScope()
{
Name = "api1"
}
};
}
}
Startup
IdentityServer 有提供 InMemory的方式,將資料儲存在記憶體中,好處是可以免去準備 Database 存放資料,缺點就是服務重開時資料就會一起清空了
這邊先使用InMemory的方式,直接將上面設定檔的各個資料 add 進去即可
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddIdentityServer()
.AddInMemoryApiResources(IdentityConfig.GetResources())
.AddInMemoryApiScopes(IdentityConfig.GetScopes())
.AddInMemoryClients(IdentityConfig.GetClients())
// 自動建立開發人員用的密鑰(tempkey.jwk),不存在時會自動建立
.AddDeveloperSigningCredential();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseIdentityServer();
}
執行
執行後訪問 http://localhost:5000/.well-known/openid-configuration ,這邊會顯示 IdentityServer 的各種資訊
取得 Token
Postman
如圖 Post http://localhost:5000/connect/token ,可以拿到 token 以及相關資訊,這個 token 就可以直接拿來呼叫 WebApi
- client_id : 比照設定檔的 ClientId
- client_secret : 比照設定檔的 ClientSecrets
- grant_type : 在這邊使用 client_credentials
Token 使用的是 JWT,故可以直接貼到 https://jwt.io/ 查看 token 內容,切記不要放敏感的資訊在上面
WebApi
Nuget Package
安裝 Nuget Package,用來解析 JWT
dotnet add pacakage Microsoft.AspNetCore.Authentication.JwtBearer
Startup
簡單加上驗證
- Authority : 檢查發行人
- RequireHttpsMetadata : 在這邊先忽略 https 的檢查
- Audience : JWT 的發行對象
- ValidateAudience : 上面產生的 token 還沒有 aud 的資訊,若沒有這行驗證會拿到401失敗 (註1)
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
記得也要加上驗證/授權的 middleware
app.UseAuthentication();
app.UseAuthorization();
Api
新增一個簡單的Api,並加上 AuthorizeAttribute,這邊會回應 User 的所有 Claims
[Authorize]
[Route("Home/{action}")]
public class HomeController : ControllerBase
{
[HttpGet]
public JsonResult Test()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
執行
取得 token 並放在 header 後就可以正常訪問 api
token 錯誤時則返回 status code 401
註1
若需要做進一步的JWT檢查,可以
- 在 JWT 加入 aud 的資訊
- 檢查 scope 的內容是否包含特定的 scope
(1) 在 JWT 加入 aud 的資訊
只需要加入有 “.” 隔開的 scope,產生的 token 就會包含 aud 的資訊
WebApi的檢查就可以把 ValidateAudience 更改為 true
(2) 檢查 scope 的內容是否包含特定的 scope
調整 WebAPi 的 Startup,加入規則後就會在請求時檢查了
// ConfigureServices
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", builder =>
{
builder.RequireAuthenticatedUser();
builder.RequireClaim("scope", "MyApi.inner");
});
});
// Configure
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers()
.RequireAuthorization("ApiScope");
});
Client
這邊的步驟非必要,只是讓呼叫端可以使用 IdentityServer 提供的 nuget package 更方便的取得/操作 token
Nuget Package
dotnet add package IdentityModel
取得 http://localhost:5000/.well-known/openid-configuration 定義的內容,主要是取得 TokenEndpoint
private async Task<string> EndpointUrl()
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
if (disco.IsError)
{
throw new Exception(disco.Error);
}
return disco.TokenEndpoint;
}
取得 AccessToken
private async Task<string> AccessToken(string endpointUrl)
{
var client = new HttpClient();
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = endpointUrl,
ClientId = "client",
ClientSecret = "BA5D32BB0CF9498CA591D38ABA95DC88",
});
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
return tokenResponse.AccessToken;
}
Set header,然後呼叫 WebApi,這邊應該會拿到跟 Postman 測試時一樣的回應內容
private static async Task<string> CallApi(string accessToken)
{
var apiClient = new HttpClient();
apiClient.SetBearerToken(accessToken);
var response = await apiClient.GetAsync("http://localhost:5002/Home/Test");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
throw new Exception(response.StatusCode.ToString());
}
return await response.Content.ReadAsStringAsync();
}
下一篇會再寫密碼授權以及刷新token的方式,參考的文檔也會一併放在下一篇