04 建立 IdentityServer4 项目,Client Credentials.mp4

大纲:

  • 介绍 ldentityServer4
  • 搭建 IdentityServer4 项目
  • 保护客户端:使用 OAuth 2.0 Client Credentials

介绍 IdentityServer4

参考官方文档 Terminology

image.png
(官方 Terminology 配图)

IdentityServer4 是一个 OpenID Connect provider,它的主要功能包括:

  • protect your resources
  • authenticate users using a local account store or via an external identity provider
  • provide session management and single sign-on(单点登录)
  • manage and authenticate clients
  • issue identity and access tokens to clients
  • validate tokens

Packaging and Builds 里面列出了 IdentityServer 4 相关的包的位置。

搭建 IdentityServer4 项目

通过 dotnet new is4inmem --name Idp 命令创建一个 IdentityServer4 with In-Memory Stores and Test Users 项目。

查看项目 Startup ConfigureServices 的代码,依次匹配官方示意图中的每个部分。

  1. var builder = services.AddIdentityServer(options =>
  2. {
  3. options.Events.RaiseErrorEvents = true;
  4. options.Events.RaiseInformationEvents = true;
  5. options.Events.RaiseFailureEvents = true;
  6. options.Events.RaiseSuccessEvents = true;
  7. })
  8. .AddTestUsers(TestUsers.Users);
  9. // in-memory, code config
  10. builder.AddInMemoryIdentityResources(Config.GetIdentityResources());
  11. builder.AddInMemoryApiResources(Config.GetApis());
  12. builder.AddInMemoryClients(Config.GetClients());

image.png

使用 OAuth 2.0 Client Credentials

参考官方文档“创建 Console 客户端,通过 Client Credentials 向 IdentityServer 请求 Access Token,并访问受保护资源”。

Client Credentials: The Client Credentials grant is used when applications request an access token to access their own resources, not on behalf of a user.

C# 7.1 异步的 Main 方法

创建 console client 时通过在 csproj 里面指定 Language Version 使项目支持异步的 Main 方法:

  1. <Project Sdk="Microsoft.NET.Sdk">
  2. <PropertyGroup>
  3. <OutputType>Exe</OutputType>
  4. <TargetFramework>netcoreapp2.2</TargetFramework>
  5. <LangVersion>latest</LangVersion>
  6. </PropertyGroup>
  7. ...
  8. </Project>

老写法:

  1. static void Main(string[] args)
  2. {
  3. var client = new HttpClient();
  4. Task.Run(async () =>
  5. {
  6. var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
  7. }).GetAwaiter().GetResult();
  8. }

7.1:

  1. static async Task Main(string[] args)
  2. {
  3. var client = new HttpClient();
  4. var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
  5. }

尝试访问身份认证资源

在 Idp 中给 console client 添加 OpenID Scope:

  1. public static IEnumerable<Client> GetClients()
  2. {
  3. return new[]
  4. {
  5. // client credentials flow client
  6. new Client
  7. {
  8. ClientId = "console client",
  9. ClientName = "Client Credentials Client",
  10. AllowedGrantTypes = GrantTypes.ClientCredentials,
  11. ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },
  12. AllowedScopes = { "api1" ,IdentityServerConstants.StandardScopes.OpenId}
  13. }
  14. };
  15. }

在 console client 中访问 OpenID 资源:

  1. static async Task Main(string[] args)
  2. {
  3. // Discoverty endpoint
  4. var client = new HttpClient();
  5. var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
  6. if (disco.IsError)
  7. {
  8. Console.WriteLine(disco.Error);
  9. return;
  10. }
  11. // Request access token
  12. var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
  13. {
  14. Address = disco.TokenEndpoint,
  15. ClientId = "console client",
  16. ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A",
  17. Scope = "api1 openid"
  18. });
  19. if (tokenResponse.IsError)
  20. {
  21. Console.WriteLine(tokenResponse.Error);
  22. // invalid_scope
  23. return;
  24. }
  25. ...
  26. }

tokenResponse 报错 invalid_scope。这是因为 Client Credentials 授权方式不代表任何用户,所以无法通过它访问 IdentityServer 上身份认证的资源。

创建并访问 Api1Resource

由于无法通过 Client Credentials 访问 OpenID,也就不方便演示。所以又参考官方教程创建了 Api1Resource。

Api1Resource 中返回了 User.Claims 的一些信息:

  1. [Route("identity")]
  2. [Authorize]
  3. public class IdentityController : ControllerBase
  4. {
  5. [HttpGet]
  6. public IActionResult Get()
  7. {
  8. return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
  9. }
  10. }

修改 Startup,添加 Authentication 服务,并将 Authentication 中间件添加到管道中:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddMvcCore()
  4. .AddAuthorization()
  5. .AddJsonFormatters();
  6. services.AddAuthentication("Bearer")
  7. .AddJwtBearer("Bearer", options =>
  8. {
  9. options.Authority = "http://localhost:5000";
  10. options.RequireHttpsMetadata = false;
  11. options.Audience = "api1";
  12. });
  13. }
  14. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  15. {
  16. app.UseAuthentication();
  17. app.UseMvc();
  18. }

AddAuthentication adds the authentication services to DI and configures "Bearer" as the default scheme. UseAuthentication adds the authentication middleware to the pipeline so authentication will be performed automatically on every call into the host.

Navigating to the controller http://localhost:5001/identity on a browser should return a 401 status code. This means your API requires a credential and is now protected by IdentityServer.

修改 console client 中的代码,以访问 Api1Resource,并打印 Claim 信息:

  1. static async Task Main(string[] args)
  2. {
  3. // Discoverty endpoint
  4. ...
  5. // Request access token
  6. ...
  7. // Call Api1Resource
  8. var apiClient = new HttpClient();
  9. apiClient.SetBearerToken(tokenResponse.AccessToken);
  10. var response = await apiClient.GetAsync("http://localhost:5001/identity");
  11. if (!response.IsSuccessStatusCode)
  12. {
  13. Console.WriteLine(response.StatusCode);
  14. }
  15. else
  16. {
  17. var content = await response.Content.ReadAsStringAsync();
  18. Console.WriteLine(JArray.Parse(content));
  19. }
  20. Console.ReadKey();
  21. }

效果:

image.png