首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >IdentityServer4直接对API使用ApiKey或基本身份验证

IdentityServer4直接对API使用ApiKey或基本身份验证
EN

Stack Overflow用户
提问于 2020-09-02 05:49:28
回答 3查看 3.5K关注 0票数 5

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

代码语言:javascript
复制
        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服务器和标识服务器都使用了api Core3.1。

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

代码语言:javascript
复制
using (var client = new HttpClient())
{
    client.BaseAddress = new Uri(URL_HOST_API);
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("ApiKey", "123456456123456789");
…
}

有什么想法?

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2020-09-20 01:28:43

事实证明,IdentityServer4并没有为ApiKeys提供支持.但是.Net Core3.1有IAuthorizationHandler,它允许您提交自己对ApiKeys的授权,并通过依赖注入将其插入到流中。

我的做法是..。拥有ApiKey和ApiKeySecret。这样UserId就不会暴露在.我的IdentityServer4 (服务器C)上有一个名为ApiKey的数据库表,其中包含字段(ApiKeyId、UserId、ApiKey和ApiKeySecret).ApiKeySecret是一个单向散列,就像密码一样。

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

所以..。跟随流动:

服务器A: ThirdParty .Net Core 3.1Web服务器

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

服务器C: MyIdentityerServer4 .Net Core 3.1 IndentityServer4

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

然后,服务器A使用头中的ApiKey和ApiKeySecret调用my (Server ):

代码语言:javascript
复制
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;
}

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

基本上,这个系统已经为(我相信)services.AddAuthentication(“Bearer”)定义了一个services.AddAuthentication.所以当添加第二个(或更多).他们每个人都会被召唤,如果一个人返回成功,就不会再被召唤.如果他们都失败了,那么授权就会失败。

代码语言:javascript
复制
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中:

代码语言:javascript
复制
services.AddSingleton<IAuthorizationHandler, ApiKeyAuthorizationHandler>();

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

ApiKeyController.cs

代码语言:javascript
复制
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

代码语言:javascript
复制
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);
    }        
}

我的模型:

代码语言:javascript
复制
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可以通过两种方式之一获得登录用户.通过比勒access_token或我的ApiKeys:

代码语言:javascript
复制
        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
票数 8
EN

Stack Overflow用户

发布于 2020-09-02 22:24:51

您可以实现OAuth密码授予,因为这是服务器到服务器身份验证。要实施这些步骤,请执行以下步骤:

  1. 为您的API项目在IdentityServer上注册客户端,下面是一个示例
代码语言:javascript
复制
public static IEnumerable<Client> GetClients()
{
    return new List<Client>
    {
        new Client
        {
            ClientId = "resourceownerclient",
            ClientSecrets=  new List<Secret> { new Secret("secret".Sha256()) },

            AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
            AccessTokenType = AccessTokenType.Jwt,
            AccessTokenLifetime = 120, //86400,
            IdentityTokenLifetime = 120, //86400,
            UpdateAccessTokenClaimsOnRefresh = true,
            SlidingRefreshTokenLifetime = 30,
            AllowOfflineAccess = true,
            RefreshTokenExpiration = TokenExpiration.Absolute,
            RefreshTokenUsage = TokenUsage.OneTimeOnly,
            AlwaysSendClientClaims = true,
            Enabled = true,
      
            AllowedScopes = {
                IdentityServerConstants.StandardScopes.OpenId, 
                IdentityServerConstants.StandardScopes.Profile,
                IdentityServerConstants.StandardScopes.Email,
                IdentityServerConstants.StandardScopes.OfflineAccess
            }
        }
  1. 在API项目IdentityModel https://www.nuget.org/packages/IdentityModel/上安装https://www.nuget.org/packages/IdentityModel/
  2. 请求在头上接收到的使用用户名/密码的API令牌,如下所示:
代码语言:javascript
复制
private static async Task<TokenResponse> RequestTokenAsync(string user, string password)
{
    var _httpClient = new HttpClient();
    var _disco = await HttpClientDiscoveryExtensions.GetDiscoveryDocumentAsync(
                    _httpClient,
                    _stsUrl);
    var response = await _httpClient.RequestPasswordTokenAsync(new PasswordTokenRequest
    {
        Address = _disco.TokenEndpoint,
 
        ClientId = "resourceownerclient",
        ClientSecret = "secret",
        Scope = "email openid offline_access",
 
        UserName = user,
        Password = password
    });
 
    return response;
}
  1. 检查response - IsError属性以确保请求成功
  2. 如果您需要自定义IdentityServer上的任何内容以获得密码授予,则需要实现IResourceOwnerPasswordValidator,读取更多的这里
票数 0
EN

Stack Overflow用户

发布于 2020-09-02 23:58:19

@nahidf有一个很好的方法,我只想补充一下,您也可以使用自定义资源所有者密码验证器。它允许您实现您自己的密码验证器,如下所示

编辑:这是为了让你不用使用身份模型

代码语言:javascript
复制
public class ApiResourceOwnerPasswordValidation : IResourceOwnerPasswordValidator
{
        public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
        {
            var user = await _dbContext.Users.FirstOrDefaultAsync(a =>
            (a.UserName == context.UserName || a.Email.ToLower().Trim() == context.UserName.ToLower().Trim())
            && context.Password.VerifyHashedPassword(a.Password));

            if (user != null)
            {
                context.Result = new GrantValidationResult(
                    subject: user.Id.ToString(),
                    authenticationMethod: "custom",
                    claims: AssambleClaims(user)
                );

                user.LoggedIn = DateTime.UtcNow;
                await _dbContext.CommitAsync();
            }
            else
            {
                context.Result = new GrantValidationResult(
                    TokenRequestErrors.InvalidGrant,
                    "Invalid credentials");
            }

        }
}
票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/63699424

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档