[ASP.NET Core] 初見 IdentityServer4:客戶授權模式

在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檢查,可以

  1. 在 JWT 加入 aud 的資訊
  2. 檢查 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的方式,參考的文檔也會一併放在下一篇