IdentityServer4 使用 ApiKey 或 Basic 认证直接到 API
IdentityServer4 using ApiKey or Basic authentication directly to API
我正在使用 IdentityServer4 让我的客户登录并访问来自 JavaScript 的 api 的网页,并且运行良好。但是,有一个新的要求,而不是使用用户名和密码从身份服务器获取访问令牌,然后使用它来访问 api 和 Bearer 身份验证......我需要调用 api 直接使用“基本”身份验证 header 和 api 将通过身份服务器确认身份。类似于下面用于访问 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 中是否有任何内置的东西可以适应这种方法?我正在为 api 服务器和身份服务器使用 .Net Core 3.1。
另一种(看似常见的)方法是为每个用户生成一个 api 密钥,然后允许用户像这样调用 api...
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(URL_HOST_API);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("ApiKey", "123456456123456789");
…
}
想法?
您可以实施 OAuth password grant,因为这是服务器到服务器的身份验证。
要实施,请执行以下步骤:
- 在 IdentityServer 上为您的 API 项目注册一个客户端,这里是一个例子
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
}
}
在 API 项目上安装 IdentityModel
包 https://www.nuget.org/packages/IdentityModel/
请求 token on API using username/password 在 header 收到,像这样:
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;
}
检查 response
- IsError
属性 以确保请求成功
如果您需要在 IdentityServer 上自定义任何内容以授予密码,则需要实施 IResourceOwnerPasswordValidator
,阅读更多 here
@nahidf 有一个很好的方法,我只想补充一点,您也可以使用自定义资源所有者密码验证器。它使您可以像这样实现自己的密码验证器
[编辑]:这是为了让您不必使用身份模型
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");
}
}
}
事实证明,IdentityServer4 没有内置对 ApiKeys 的支持...但是 .Net Core 3.1 具有 IAuthorizationHandler,它允许您为 ApiKeys 滚动自己的授权并通过依赖注入将其插入到流程中。
我的做法是...拥有一个 ApiKey 和一个 ApiKeySecret。这样,UserId 根本就不会被公开...我的 IdentityServer4(服务器 C)上有一个名为 ApiKey 的数据库 table,其中包含字段(ApiKeyId、UserId、ApiKey 和 ApiKeySecret)...ApiKeySecret 是像密码一样的单向哈希。
我将 ApiKeyController 添加到我的 IdentityServer4 项目(服务器 C)...这将允许 ApiRequest 验证 ApiKey。
所以……顺其自然:
服务器 A:第三方 .Net Core 3.1 Web 服务器
服务器 B:MyApiServer .Net Core 3.1 Web 服务器
服务器 C:MyIdentityerServer4 .Net Core 3.1 IndentityServer4
基于对服务器 A 的请求(可能来自浏览器)。
服务器 A 然后使用 headers 中的 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;
}
在我的 API 服务器(服务器 B)上,我添加了以下内容 class,如果 url 设置了 [授权] 类别,将验证 ApiKeys Header 通过调用 IdentityServer4(服务器 C)上的 ApiKeyController 并将 return 值(UserId)放在 HttpContext.Items collection.
上
基本上系统已经为(我相信)定义了一个 IAuthorizationHandler services.AddAuthentication("Bearer")...所以当添加第二个(或更多)时...它们将分别被调用,如果一个 returns Succeeded no more will be call...如果他们都失败了,那么 [Authorized] 就会失败。
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:
services.AddSingleton<IAuthorizationHandler, ApiKeyAuthorizationHandler>();
为了完整起见,我在 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 });
}
}
}
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:
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 让我的客户登录并访问来自 JavaScript 的 api 的网页,并且运行良好。但是,有一个新的要求,而不是使用用户名和密码从身份服务器获取访问令牌,然后使用它来访问 api 和 Bearer 身份验证......我需要调用 api 直接使用“基本”身份验证 header 和 api 将通过身份服务器确认身份。类似于下面用于访问 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 中是否有任何内置的东西可以适应这种方法?我正在为 api 服务器和身份服务器使用 .Net Core 3.1。
另一种(看似常见的)方法是为每个用户生成一个 api 密钥,然后允许用户像这样调用 api...
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(URL_HOST_API);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("ApiKey", "123456456123456789");
…
}
想法?
您可以实施 OAuth password grant,因为这是服务器到服务器的身份验证。 要实施,请执行以下步骤:
- 在 IdentityServer 上为您的 API 项目注册一个客户端,这里是一个例子
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
}
}
在 API 项目上安装
IdentityModel
包 https://www.nuget.org/packages/IdentityModel/请求 token on API using username/password 在 header 收到,像这样:
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;
}
检查
response
-IsError
属性 以确保请求成功如果您需要在 IdentityServer 上自定义任何内容以授予密码,则需要实施
IResourceOwnerPasswordValidator
,阅读更多 here
@nahidf 有一个很好的方法,我只想补充一点,您也可以使用自定义资源所有者密码验证器。它使您可以像这样实现自己的密码验证器
[编辑]:这是为了让您不必使用身份模型
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");
}
}
}
事实证明,IdentityServer4 没有内置对 ApiKeys 的支持...但是 .Net Core 3.1 具有 IAuthorizationHandler,它允许您为 ApiKeys 滚动自己的授权并通过依赖注入将其插入到流程中。
我的做法是...拥有一个 ApiKey 和一个 ApiKeySecret。这样,UserId 根本就不会被公开...我的 IdentityServer4(服务器 C)上有一个名为 ApiKey 的数据库 table,其中包含字段(ApiKeyId、UserId、ApiKey 和 ApiKeySecret)...ApiKeySecret 是像密码一样的单向哈希。
我将 ApiKeyController 添加到我的 IdentityServer4 项目(服务器 C)...这将允许 ApiRequest 验证 ApiKey。
所以……顺其自然:
服务器 A:第三方 .Net Core 3.1 Web 服务器
服务器 B:MyApiServer .Net Core 3.1 Web 服务器
服务器 C:MyIdentityerServer4 .Net Core 3.1 IndentityServer4
基于对服务器 A 的请求(可能来自浏览器)。
服务器 A 然后使用 headers 中的 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;
}
在我的 API 服务器(服务器 B)上,我添加了以下内容 class,如果 url 设置了 [授权] 类别,将验证 ApiKeys Header 通过调用 IdentityServer4(服务器 C)上的 ApiKeyController 并将 return 值(UserId)放在 HttpContext.Items collection.
上基本上系统已经为(我相信)定义了一个 IAuthorizationHandler services.AddAuthentication("Bearer")...所以当添加第二个(或更多)时...它们将分别被调用,如果一个 returns Succeeded no more will be call...如果他们都失败了,那么 [Authorized] 就会失败。
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:
services.AddSingleton<IAuthorizationHandler, ApiKeyAuthorizationHandler>();
为了完整起见,我在 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 });
}
}
}
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:
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