IdentityServer4使用ApiKey或Basic身份验证直接访问API [英] IdentityServer4 using ApiKey or Basic authentication directly to API

查看:103
本文介绍了IdentityServer4使用ApiKey或Basic身份验证直接访问API的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am using IdentityServer4 to have my customers login and access web pages and api's from JavaScript and it is working well. However, there is a new requirement that rather than using username and password to get an access token from the identity server and then using that to access the api with Bearer authentication... I would need to call the api directly with a "Basic" authentication header and the api would confirm the identity with the identity server. Similar to the code below that is used to access the ZenDesk api...

        using (var client = new HttpClient())
        {
            var username = _configuration["ZenDesk:username"];
            var password = _configuration["ZenDesk:password"];
            var token = Convert.ToBase64String(Encoding.ASCII.GetBytes(username + ":" + password));
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", token);

            var response = client.PostAsync("https://...

有关如何实现此目标的任何帮助?IdentityServer4内建了什么可以适应这种方法的东西?我正在将.Net Core 3.1用于api服务器和身份服务器.

Any help on how I would implement this? Is there anything built into IdentityServer4 that would accommodate this approach? I am using .Net Core 3.1 for both the api server and for the identity server.

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

Another (seemingly common) approach would be to generate an api key for each user and then allow the user to call the api like this...

using (var client = new HttpClient())
{
    client.BaseAddress = new Uri(URL_HOST_API);
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("ApiKey", "123456456123456789");
…
}

有想法吗?

推荐答案

事实证明IdentityServer4尚未内置对ApiKeys的支持...但是.Net Core 3.1具有IAuthorizationHandler,它允许您对ApiKeys进行自己的授权并通过依赖注入将其插入流中.

It turns out that IdentityServer4 does not have built in support for ApiKeys... but .Net Core 3.1 has IAuthorizationHandler which allows you to roll your own authorization for ApiKeys and insert it into the flow with dependancy injection.

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

The way I did it was... to have an ApiKey and an ApiKeySecret. This way the UserId is not being exposed at all... I have a database table on my IdentityServer4 (Server C) called ApiKey that contains the fields (ApiKeyId, UserId, ApiKey, and ApiKeySecret)... ApiKeySecret is a one way hash like a password.

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

I added an ApiKeyController to my IdentityServer4 project (Server C)... this will allow an ApiRequest to Validate the ApiKeys.

所以...遵循流程:

服务器A:Third.Party .Net Core 3.1 Web服务器

Server A: ThirdParty .Net Core 3.1 Web Server

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

Server B: MyApiServer .Net Core 3.1 Web Server

服务器C:MyIdentityerServer4 .Net Core 3.1 IndentityServer4

Server C: MyIdentityerServer4 .Net Core 3.1 IndentityServer4

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

Based on a request (likely from a browser) to Server A.

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

Server A then calls my API (Server B) with an ApiKey and an ApiKeySecret in the headers:

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;
}

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

On my API Server (Server B) I have added the following class which, if the [Authorize] category is set for a url, will validate the ApiKeys in the Header by calling the ApiKeyController on the IdentityServer4 (Server C) and putting the return value (UserId) on the HttpContext.Items collection.

基本上,系统已经为(我相信)服务定义了一个IAuthorizationHandler.AddAuthentication("Bearer")...因此,当添加第二个(或多个)...时,如果一个返回成功,则将分别调用它们.不会再有电话了...如果它们都失败了,那么[Authorized]将会失败.

Basically the system already defines an IAuthorizationHandler for (I believe) services.AddAuthentication("Bearer")... so when adding a second one (or more)... they will each be called, if one returns Succeeded no more will be call... if they all fail, then the [Authorized] will fail.

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);
        }
    }
}

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

I also need to add the following to Startup.cs on Server B:

services.AddSingleton<IAuthorizationHandler, ApiKeyAuthorizationHandler>();

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

And for completeness my code on IdentityServer4 (Server C):

ApiKeyController.cs

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 });
        }

    }
}

ApiKeyService.cs

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);
    }        
}

我的模特:

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; }
}

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

And, then my WeatherForecastController can get the logged in user one of two ways... via a Bearer access_token or my 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

这篇关于IdentityServer4使用ApiKey或Basic身份验证直接访问API的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆