How-to
Во-первых, нам нужен AuthenticationHandler & Опции для аутентификации запроса с помощью токена API (Client Secret). Предположим, вы создали такой обработчик и параметры аутентификации:
public class ClientSecretAuthenOpts : AuthenticationSchemeOptions
{
public const string DefaultAuthenticationSchemeName = "ClientSecret";
}
public class ClientSecretAuthenticationHandler : AuthenticationHandler<ClientSecretAuthenOpts>
{
public ClientSecretAuthenticationHandler(IOptionsMonitor<ClientSecretAuthenOpts> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// ... authenticate request
}
}
А затем зарегистрируйте несколько схем аутентификации по одной :
// add mulitple authentication schemes
services.AddAuthentication(AzureADDefaults.AuthenticationScheme) // set AzureAD as the default for users (using the UI)
.AddAzureAD(options => Configuration.Bind("AzureAD", options)) // setup AzureAD Authentication
.AddScheme<ClientSecretAuthenOpts,ClientSecretAuthenticationHandler>( // setup ClientSecret Authentication
ClientSecretAuthenOpts.DefaultAuthenticationSchemeName,
opts=>{ }
);
// post configuration for OIDC
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>{
options.Authority = options.Authority + "/v2.0/"; // Microsoft identity platform
options.TokenValidationParameters.ValidateIssuer = false; // accept several tenants
});
Наконец, для одновременного включения нескольких схем аутентификации нам необходимо переопределить политику по умолчанию :
services.AddAuthorization(opts => {
// allow AzureAD & our own ClientSecret Authentication at the same time
var pb = new AuthorizationPolicyBuilder(
ClientSecretAuthenOpts.DefaultAuthenticationSchemeName,
"AzureAD"
);
opts.DefaultPolicy = pb.RequireAuthenticatedUser().Build();
});
Демонстрация и тестирование
Предположим, ваш токен API (секрет клиента) отправляется в заголовке запроса, как показано ниже:
GET https://localhost:5001/Home/Privacy HTTP/1.1
<b>Api-Subscription-Id</b>: Smith
<b>Api-Subscription-Key</b>: top secret
Чтобы избежать жесткого кодирования имени заголовка, я создаю addдва свойства в опциях:
public class ClientSecretAuthenOpts : AuthenticationSchemeOptions
{
public const string DefaultAuthenticationSchemeName = "ClientSecret";
public string ApiClientIdHeadername {get;set;}= "Api-Subscription-Id";
public string ApiClientTokenHeaderName {get;set;}= "Api-Subscription-Key";
}
Для аутентификации вышеупомянутого запроса я создаю собственный обработчик аутентификации, как показано ниже:
public class ClientSecretAuthenticationHandler : AuthenticationHandler<ClientSecretAuthenOpts>
{
public ClientSecretAuthenticationHandler(IOptionsMonitor<ClientSecretAuthenOpts> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// if there's no header for Client ID & Client Sercet, skip
if(
Context.Request.Headers.TryGetValue(Options.ApiClientIdHeadername, out var clientIdHeader) &&
Context.Request.Headers.TryGetValue(Options.ApiClientTokenHeaderName, out var clientSecretHeader)
){
// validate client's id & secret
var clientId = clientIdHeader.FirstOrDefault();
var clientKey = clientSecretHeader.FirstOrDefault();
var (valid, id) = await ValidateApiKeyAsync(clientId, clientKey);
if(!valid){
return AuthenticateResult.Fail($"invalid token:{clientKey}");
}else{
var principal = new ClaimsPrincipal(id);
var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), this.Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
return AuthenticateResult.NoResult();
}
private Task<(bool, ClaimsIdentity)> ValidateApiKeyAsync(string clientId,string clientSecret)
{
ClaimsIdentity id = null;
// fake: need check key against the Database or other service
if(clientId=="Smith" && clientSecret == "top secret"){
id = new ClaimsIdentity(
new Claim[]{
new Claim(ClaimTypes.NameIdentifier, "client id from db or from the request"),
new Claim("Add Any Claim", "add the value as you like"),
// ...
}
,this.Scheme.Name
);
return Task.FromResult((true, id));
}
return Task.FromResult((false,id));
}
}
Test
Допустим, у нас есть действие контроллера, аннотированное атрибутом [Authorize]
[Authorize]
public IActionResult Privacy()
{
return Ok("hello,world");
}
При доступе к URL-адресу в браузере (пользовательский интерфейс без заголовка) пользователь будет перенаправлен на проверку подлинности Azure AD. если он не вошел в систему.
При тестировании AboМы запросим секрет клиента,
И мы получим ответ "Здравствуй, мир":