Skip to content
On this page

Client Credentials

在了解了Identity Server的基础知识后,本节我们简单演示如何使用最简单的Client Credentials方式来保护API资源。相关案例代码已分享到Github

Client Credentials授权过程

1. Identity Server

首先我们建立Identity Server,官方提供了IdentityServer templates项目模板。

bash
# 安装 IdentityServer 官方项目模板
dotnet new -i IdentityServer4.Templates

我们使用IdentityServer4 Empty模板建立一个空的Identity Server4项目。

接下来我们配置被保护的资源和客户端。项目模板中默认会创建一个Config.cs文件用于演示在内存中配置被保护资源和客户端,生产项目中可以根据实际情况从配置文件或DB中加载配置。

csharp
public static class Config
{
    //Identity data
    public static IEnumerable<IdentityResource> IdentityResources =>
        new IdentityResource[] {new IdentityResources.OpenId(),};

    //APIs
    public static IEnumerable<ApiScope> ApiScopes =>
        new[]
        {
            new ApiScope("WeatherApi", "天气预报")
        };

    //Clients
    public static IEnumerable<Client> Clients =>
        new[]
        {
            new Client
            {
                ClientId = "ClientCredentialConsoleClient",
                ClientSecrets = {new Secret("ClientCredentialConsoleClient".Sha256())},
                AllowedGrantTypes = GrantTypes.ClientCredentials,
                AllowedScopes = {"WeatherApi"}
            }
        };
}

接下来我们注册Identity Server服务和中间件。

csharp
public void ConfigureServices(IServiceCollection services)
{
    var builder = services.AddIdentityServer(options => options.EmitStaticAudienceClaim = true)
        .AddInMemoryIdentityResources(Config.IdentityResources)
        .AddInMemoryApiScopes(Config.ApiScopes)
        .AddInMemoryClients(Config.Clients);

    // not recommended for production - you need to store your key material somewhere secure
    builder.AddDeveloperSigningCredential();
}

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

    app.UseIdentityServer();
}

至此IdentityServer已配置完成,运行后访问https://localhost:5001/.well-known/openid-configuration可以看到discovery document配置,注册到IdentityServer的客户端和API都通过此discovery document获取必要的配置数据。

2. API

我们创建一个标准的Asp.Net Web API程序,在Startup中注册认证授权服务和中间件。这里我们使用JWT认证方案,并通过其Authority属性将认证服务指向IdentityServer即可。

IdentityServer认证客户端访问被保护的API资源时会携带名为scopeClaim对象。在API中可以以此进行鉴权,本案例中我们使用scope建立对应授权策略进行鉴权。

json
{
  "IdentityServerOptions": {
    "Address": "https://localhost:5000",
    "Scopes": [
      "WeatherApi"
    ]
}
csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    var identityServerOptions =
        Configuration.GetSection(nameof(IdentityServerOptions)).Get<IdentityServerOptions>();
    services
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
        {
            options.Authority = identityServerOptions.Address;
            options.TokenValidationParameters.ValidateAudience = false;
        });
    services.AddAuthorization(options =>
    {
        foreach (var scope in identityServerOptions.Scopes)
            options.AddPolicy(scope, policy =>
            {
                policy.RequireAuthenticatedUser();
                policy.RequireClaim("scope", scope);
            });
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints => endpoints.MapControllers());
}

public class IdentityServerOptions
{
    public string Address { get; set; }
    public IEnumerable<string> Scopes { get; set; }
}

API我们使用默认的WeatherForecastController即可,在控制器中使用AuthorizationAttribute鉴权。

csharp
[ApiController]
[Route("[controller]")]
[Authorize("WeatherApi")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries =
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
    }
}

至此API项目配置完成,启动应用并访问https://localhost:10000/WeatherForecast得到401响应码,说明API需要授权且已被IdentityServer保护。

3. Client

本节我们创建一个控制台程序作为客户端。IdentityModelNuget包对HttpClient进行了扩展用于与IdentityServer交互。

IdentityServer和API相关配置如下。

json
"IdentityServerOptions": {
    "Address": "https://localhost:5000",
    "ClientId": "ClientCredentialConsoleClient",
    "ClientSecret": "ClientCredentialConsoleClient",
    "Scopes": [
      {
        "Name": "WeatherApi",
        "Url": "https://localhost:10000/WeatherForecast"
      }
    ]
  }

使用HttpClientGetDiscoveryDocumentAsync方法连接IdentitySeverdiscovery endpoint可以获得相关认证服务器的相关配置。IdentityServer默认仅支持Https协议,本地开发环境第一次可以运行dotnet dev-certs https --trust信任开发证书。或者通过如下16行代码关闭Https验证。

csharp
static async Task Main(string[] args)
{
    var options = new ConfigurationBuilder()
        .AddJsonFile("appsettings.json")
        .Build()
        .GetSection(nameof(IdentityServerOptions))
        .Get<IdentityServerOptions>();

    using var client = new HttpClient();
    //发现IdentityServer配置
    var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
    {
        Address = options.Address,
        // Policy = new DiscoveryPolicy
        // {
        //     RequireHttps = false,
        //     ValidateEndpoints = false,
        //     ValidateIssuerName = false
        // }
    });
    if (disco.IsError)
    {
        Console.WriteLine(disco.Error);
        return;
    }

    //获取Token
    var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
    {
        Address = disco.TokenEndpoint,
        ClientId = options.ClientId,
        ClientSecret = options.ClientSecret,
        Scope = options.Scope
    });
    if (tokenResponse.IsError)
    {
        Console.WriteLine(tokenResponse.Error);
        return;
    }

    //API调用
    using var apiClient = new HttpClient();
    apiClient.SetBearerToken(tokenResponse.AccessToken);
    var response = await apiClient.GetAsync(options["WeatherApi"]);
    if (!response.IsSuccessStatusCode)
    {
        Console.WriteLine(response.StatusCode);
        return;
    }
    var content = await response.Content.ReadAsStringAsync();
    Console.WriteLine(JArray.Parse(content));
}

通过HttpClientRequestClientCredentialsTokenAsync方法,使用ClientId/ClientSecret/Scope等认证数据在IdentityServer获取Access Token。获取AccessToken后就可以使用令牌调用被保护的API了。

4. Discovery Endpoint

IdentityServer4提供了一个Discovery Endpoint用于服务发现,它定义了一个API(/.well-known/openid-configuration),这个API返回一个json数据结构,其中包含了一些OIDC中提供的服务以及其支持情况的描述信息,这样只需要知道IdentityServer基地址就可以通过 Discovery方便的使用各种端点和配置,而不再需要硬编码服务接口信息。

json
{
  "issuer": "https://localhost:5000",
  "jwks_uri": "https://localhost:5000/.well-known/openid-configuration/jwks",
  "authorization_endpoint": "https://localhost:5000/connect/authorize",
  "token_endpoint": "https://localhost:5000/connect/token",
  "userinfo_endpoint": "https://localhost:5000/connect/userinfo",
  "end_session_endpoint": "https://localhost:5000/connect/endsession",
  "check_session_iframe": "https://localhost:5000/connect/checksession",
  "revocation_endpoint": "https://localhost:5000/connect/revocation",
  "introspection_endpoint": "https://localhost:5000/connect/introspect",
  "device_authorization_endpoint": "https://localhost:5000/connect/deviceauthorization",
  "frontchannel_logout_supported": true,
  "frontchannel_logout_session_supported": true,
  "backchannel_logout_supported": true,
  "backchannel_logout_session_supported": true,
  "scopes_supported": [
    "openid",
    "profile",
    "WeatherApi",
    "offline_access"
  ],
  "claims_supported": [
    "sub",
    "name",
    "family_name",
    "given_name",
    "middle_name",
    "nickname",
    "preferred_username",
    "profile",
    "picture",
    "website",
    "gender",
    "birthdate",
    "zoneinfo",
    "locale",
    "updated_at"
  ],
  "grant_types_supported": [
    "authorization_code",
    "client_credentials",
    "refresh_token",
    "implicit",
    "password",
    "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
}

以上访问Discovery Endpoint得到的示例数据。可以看到其包含了IdentitySever中各服务端点和支持的内容等配置信息。

使用HttpClient.GetDiscoveryDocumentAsync()方法会先请求/.well-known/openid-configuration发现服务,然后请求/.well-known/openid-configuration/jwks(公钥开放端点)获取解析JWT令牌的公钥。

发现IdentityServer配置并非必需,也可以直接请求具体服务端点,如请求https://localhost:5000/connect/token(token_endpoint)获取AccessToken

Client Credentials 登录网络请求

5. 证书管理

IdentitySever4Token一般采用JWT方案,它使用私钥来签名JWT token,公钥验证签名,一般情况下我们通过一个证书提供私钥和公钥。在开发环境中一般通过IIdentityServerBuilder.AddDeveloperSigningCredential()注册开发密钥签名。在程序第一次启动时IdentityServer会自动创建一个tempkey.jwk文件保存密钥,此文件不存在则会在程序启动时自动重建。打开tempkey.jwk文件即可得到密钥内容,此方式安全性较低,攻击者获得文件会导致密钥直接泄露。在生产环境中我们一般会生成一个加密的安全证书来提供私钥和公钥。

csharp
public void ConfigureServices(IServiceCollection services)
{
    var builder = services.AddIdentityServer(options => options.EmitStaticAudienceClaim = true)
        .AddInMemoryIdentityResources(Config.IdentityResources)
        .AddInMemoryApiScopes(Config.ApiScopes)
        .AddInMemoryClients(Config.Clients);

    if (_env.IsDevelopment() || _env.IsStaging())
        builder.AddDeveloperSigningCredential();
    else
        builder.AddSigningCredential(
            new X509Certificate2(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "is4.pfx"),
                "5C6CE27CBA3DD15B4EFBE5A7EC679CBBE79D14F5"));
}

在上面代码中我们使用了Pfx证书,Pfx证书除了包含cer/crt证书的公钥,还可以选择性的包含key密钥文件,同时还可以设置证书密码保护。简而言之通过Pfx证书可以安全的提供一对公钥私钥。

生成Pfx证书的方式有很多,这里我们推荐一款免费跨平台的证书管理工具——KeyManager

screenshot-20210419-030135.jpg

生成Pfx证书流程如上图所示,导出证书时的加密私钥从思源管理中获取即可。如果不确定哪个私钥可以通过相关证书确认。

参考文献

Released under the MIT License.