IdentityServer4使用ApiKey或Basic身份验证直接到API

Bri*_*ice 5 c# basic-authentication api-key identityserver4

我正在使用 IdentityServer4 让我的客户通过 JavaScript 登录并访问网页和 api,并且运行良好。然而,有一个新的要求,而不是使用用户名和密码从身份服务器获取访问令牌,然后使用它通过承载身份验证访问 api...我需要使用“Basic”直接调用 api身份验证标头和 api 将与身份服务器确认身份。类似于下面用于访问 ZenDesk api 的代码...

\n
        using (var client = new HttpClient())\n        {\n            var username = _configuration["ZenDesk:username"];\n            var password = _configuration["ZenDesk:password"];\n            var token = Convert.ToBase64String(Encoding.ASCII.GetBytes(username + ":" + password));\n            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", token);\n\n            var response = client.PostAsync("https://...\n
Run Code Online (Sandbox Code Playgroud)\n

对我如何实施这个有任何帮助吗?IdentityServer4 中是否内置了任何可以适应这种方法的东西?我将 .Net Core 3.1 用于 api 服务器和身份服务器。

\n

另一种(看似常见)方法是为每个用户生成一个 api 密钥,然后允许用户像这样调用 api...

\n
using (var client = new HttpClient())\n{\n    client.BaseAddress = new Uri(URL_HOST_API);\n    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("ApiKey", "123456456123456789");\n\xe2\x80\xa6\n}\n
Run Code Online (Sandbox Code Playgroud)\n

想法?

\n

Bri*_*ice 8

事实证明,IdentityServer4 没有内置对 ApiKeys 的支持...但是 .Net Core 3.1 有 IAuthorizationHandler,它允许您为 ApiKeys 滚动自己的授权,并将其插入到具有依赖注入的流程中。

我这样做的方式是......拥有一个 ApiKey 和一个 ApiKeySecret。这样,UserId 就不会被公开...我的 IdentityServer4(服务器 C)上有一个名为 ApiKey 的数据库表,其中包含字段(ApiKeyId、UserId、ApiKey 和 ApiKeySecret)...ApiKeySecret 是一种单向哈希就像密码一样。

我向我的 IdentityServer4 项目(服务器 C)添加了一个 ApiKeyController...这将允许 ApiRequest 验证 ApiKeys。

所以...遵循流程:

服务器A:第三方.Net Core 3.1 Web服务器

服务器 B:MyApiServer .Net Core 3.1 Web 服务器

服务器 C:MyIdentityerServer4 .Net Core 3.1 IndentityServer4

基于对服务器 A 的请求(可能来自浏览器)。

然后,服务器 A 使用标头中的 ApiKey 和 ApiKeySecret 调用我的 API(服务器 B):

using (var client = new HttpClient())
{
    var url = _configuration["MyApiUrl"] + "/WeatherForecast";
    var apiKey = _configuration["MyApiKey"];
    var apiKeySecret = _configuration["MyApiKeySecret"];
    client.DefaultRequestHeaders.Add("x-api-key", apiKey);
    client.DefaultRequestHeaders.Add("secret-api-key", apiKeySecret);

    var response = client.GetAsync(url).Result;
    if (response.IsSuccessStatusCode)
    {
        var contents = response.Content.ReadAsStringAsync().Result;
        return contents;
    }
    return "StatusCode = " + response.StatusCode;
}
Run Code Online (Sandbox Code Playgroud)

在我的 API 服务器(服务器 B)上,我添加了以下类,如果为 url 设置了 [Authorize] 类别,它将通过调用 IdentityServer4(服务器 C)上的 ApiKeyController 并返回来验证标头中的 ApiKeys HttpContext.Items 集合上的值 (UserId)。

基本上,系统已经为(我相信)services.AddAuthentication("Bearer") 定义了一个 IAuthorizationHandler...所以当添加第二个(或更多)时...它们将被调用,如果一个返回 Succeeded 则不再有调用...如果它们都失败,那么[授权]将失败。

public class ApiKeyAuthorizationHandler : IAuthorizationHandler
{
    private readonly ILogger<ApiKeyAuthorizationHandler> _logger;
    private readonly IConfiguration _configuration;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ApiKeyAuthorizationHandler(
        ILogger<ApiKeyAuthorizationHandler> logger,
        IConfiguration configuration,
        IHttpContextAccessor httpContextAccessor
        )
    {
        _logger = logger;
        _configuration = configuration;
        _httpContextAccessor = httpContextAccessor;
    }

    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        try
        {
            string apiKey = _httpContextAccessor.HttpContext.Request.Headers["x-api-key"].FirstOrDefault();
            string apiKeySecret = _httpContextAccessor.HttpContext.Request.Headers["secret-api-key"].FirstOrDefault();

            if (apiKey != null && apiKeySecret != null)
            {
                if (Authorize(apiKey, apiKeySecret))
                    SetSucceeded(context);
            }
            return Task.CompletedTask;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "HandleAsync");
            return Task.CompletedTask;
        }
    }

    public class ValidateResponse
    {
        public string UserId { get; set; }
    }
    private bool Authorize(string apiKey, string apiKeySecret)
    {
        try
        {
            using (var client = new HttpClient())
            {
                var url = _configuration["AuthorizationServerUrl"] + "/api/ApiKey/Validate";
                var json = JsonConvert.SerializeObject(new
                {
                    clientId = "serverb-api", // different ApiKeys for different clients
                    apiKey = apiKey,
                    apiKeySecret = apiKeySecret
                });
                var response = client.PostAsync(url, new StringContent(json, Encoding.UTF8, "application/json")).Result;
                if (response.IsSuccessStatusCode)
                {
                    var contents = response.Content.ReadAsStringAsync().Result;
                    var result = JsonConvert.DeserializeObject<ValidateResponse>(contents);
                    _httpContextAccessor.HttpContext.Items.Add("UserId", result.UserId);
                }
                return response.IsSuccessStatusCode;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Authorize");
            return false;
        }
    }

    private void SetSucceeded(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();
        foreach (var requirement in pendingRequirements)
        {
            context.Succeed(requirement);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我还需要将以下内容添加到服务器 B 上的 Startup.cs 中:

services.AddSingleton<IAuthorizationHandler, ApiKeyAuthorizationHandler>();
Run Code Online (Sandbox Code Playgroud)

为了完整起见,我在 IdentityServer4(服务器 C)上的代码:

ApiKeyController.cs

using System;
using MyIdentityServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace MyIdentityServer
{
    [Route("api/[controller]")]
    [ApiController]
    public class ApiKeyController : ControllerBase
    {
        private readonly ILogger<ApiKeyController> _logger;

        private readonly IApiKeyService _apiKeyService;
        public ApiKeyController(
            IApiKeyService apiKeyService,
            ILogger<ApiKeyController> logger
            )
        {
            _apiKeyService = apiKeyService;
            _logger = logger;
        }
        public class ValidateApiKeyRequest
        {
            public string ClientId { get; set; }
            public string ApiKey { get; set; }
            public string ApiKeySecret { get; set; }
        }
        [HttpPost("Validate")]
        [AllowAnonymous]
        [Consumes("application/json")]
        public IActionResult PostBody([FromBody] ValidateApiKeyRequest request)
        {
            try
            {
                (var clientId, var userId) = _apiKeyService.Verify(request.ApiKey, request.ApiKeySecret);

                if (request.ClientId == clientId && userId != null)
                    return Ok(new { UserId = userId });
                    // return new JsonResult(new { UserId = userId }); // maybe also return claims for client / user

                return Unauthorized();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "HandleValidateApiKey apiKey={request.ApiKey} apiKeySecret={request.ApiKeySecret}");
                return Unauthorized();
            }
        }

        public class GenerateApiKeyRequest
        {
            public string ClientId { get; set; }
            public string UserId { get; set; }
        }
        [HttpPost("Generate")]
        [AllowAnonymous]
        public IActionResult Generate(GenerateApiKeyRequest request)
        {
            // generate and store in database
            (var apiKey, var apiKeySecret) = _apiKeyService.Generate(request.ClientId, request.UserId);

            return new JsonResult(new { ApiKey = apiKey, ApiKeySecret = apiKeySecret });
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

ApiKeyService.cs

using Arch.EntityFrameworkCore.UnitOfWork;
using EQIdentityServer.Data.Models;
using System;
using System.Security.Cryptography;

public namespace MyIndentityServer4.Services

public interface IApiKeyService
{
    (string, string) Verify(string apiKey, string apiKeySecret);
    (string, string) Generate(string clientId, string userId);
}

public class ApiKeyService : IApiKeyService
{
    IUnitOfWork _unitOfWork;

    public ApiKeyService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public (string, string) Verify(string apiKey, string apiKeySecret)
    {
        var repoApiKey = _unitOfWork.GetRepository<ClientUserApiKey>();

        var item = repoApiKey.GetFirstOrDefault(predicate: p => p.ApiKey == apiKey);
        if (item == null)
            return (null, null);

        if (!OneWayHash.Verify(item.ApiKeySecretHash, apiKeySecret))
            return (null, null);

        return (item?.ClientId, item?.UserId);
    }

    public (string, string) Generate(string clientId, string userId)
    {
        var repoApiKey = _unitOfWork.GetRepository<ClientUserApiKey>();

        string apiKey = null;
        string apiKeySecret = null;
        string apiKeySecretHash = null;

        var key = new byte[30];
        using (var generator = RandomNumberGenerator.Create())
            generator.GetBytes(key);
        apiKeySecret = Convert.ToBase64String(key);
            
        apiKeySecretHash = OneWayHash.Hash(apiKeySecret);

        var item = repoApiKey.GetFirstOrDefault(
            predicate: p => p.ClientId == clientId && p.UserId == userId
            );
        if (item != null)
        {
            // regenerate only secret for existing clientId/userId
            apiKey = item.ApiKey; // item.ApiKey = apiKey; // keep this the same, or you could have multiple for a clientId if you want
            item.ApiKeySecretHash = apiKeySecretHash;
            repoApiKey.Update(item);
        }
        else
        {
            // new for user
            key = new byte[30];

            while (true)
            {
                using (var generator = RandomNumberGenerator.Create())
                    generator.GetBytes(key);
                apiKey = Convert.ToBase64String(key);

                var existing = repoApiKey.GetFirstOrDefault(
                    predicate: p => p.ApiKey == apiKey
                    );

                if (existing == null)
                    break;
            }

            item = new ClientUserApiKey() { ClientId = clientId, UserId = userId, ApiKey = apiKey, ApiKeySecretHash = apiKeySecretHash };
            repoApiKey.Insert(item);
        }
        _unitOfWork.SaveChanges();

        return (apiKey, apiKeySecret);
    }        
}
Run Code Online (Sandbox Code Playgroud)

我的型号:

public class ClientUserApiKey
{
    public long ClientUserApiKeyId { get; set; }

    [IndexColumn("IX_ApiKey_ClientIdUserId", 0)]
    public string ClientId { get; set; }

    [IndexColumn("IX_ApiKey_ClientIdUserId", 1)]
    public string UserId { get; set; }

    [IndexColumn]
    public string ApiKey { get; set; }

    [StringLength(128)]
    public string ApiKeySecretHash { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

然后,我的 WeatherForecastController 可以通过以下两种方式之一获取登录用户...通过 Bearer access_token 或我的 ApiKeys:

        string userId = null;
        if (User?.Identity.IsAuthenticated == true)
            userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier).Value;
        else
            userId = this.HttpContext.Items["UserId"]?.ToString(); // this comes from ApiKey validation
Run Code Online (Sandbox Code Playgroud)