Ошибка неавторизованного клиента Duende BFF при вызове Identity Server. Код типа гранта изменяется на учетные данные клиента типа гранта
Настройка
Сервер поставщика удостоверений с Duende ver.6, с зарегистрированным клиентом с кодом типа гранта
new Client { ClientId = "test_client", RequireClientSecret = false, AllowOfflineAccess = true, ClientName = "Scope", AllowedGrantTypes = GrantTypes.Code, AllowedScopes = new List<string> { "openid", }, AllowedCorsOrigins = new List<string> { "https://localhost:5001", "https://localhost:5011", }, RedirectUris = new List<string> { "https://localhost:5011/signin-oidc" } }
API со следующей конфигурацией
services.Configure<CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.Strict; }) .AddAuthentication(sharedOptions => { sharedOptions.DefaultScheme = "smart"; sharedOptions.DefaultChallengeScheme = "smart"; }) .AddPolicyScheme("smart", "Authorization Bearer or OIDC", options => { options.ForwardDefaultSelector = context => { var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); if (authHeader?.StartsWith("Bearer ") == true) { return JwtBearerDefaults.AuthenticationScheme; } return "oidc"; }; }) .AddJwtBearer(jwtOptions => { jwtOptions.Authority = configuration["Authentication:Authority"]; jwtOptions.Audience = configuration["Authentication:Audience"]; jwtOptions.SaveToken = true; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; options.Authority = configuration["Authentication:Authority"]; options.ClientId = configuration["Authentication:ClientId"]; options.ResponseType = "code"; options.Prompt = "login"; options.GetClaimsFromUserInfoEndpoint = true; options.Scope.Add("openid"); options.SaveTokens = true; }); services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .AddAuthenticationSchemes("smart").Build(); });
И веб-клиент BFF с конфигурацией
services .AddBff() .AddRemoteApis(); services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { options.Cookie.Name = "__Host-blazor"; options.Cookie.SameSite = SameSiteMode.Strict; }) .AddOpenIdConnect("oidc", options => { options.Authority = configuration["Authentication:Authority"]; // confidential client using code flow + PKCE options.ClientId = configuration["Authentication:ClientId"]; options.ResponseType = "code"; options.ResponseMode = "query"; options.MapInboundClaims = false; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; // request scopes + refresh tokens options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("offline_access"); }); //services.AddAccessTokenManagement(); services.AddClientAccessTokenHttpClient(AuthorizedClient, configureClient: client => { //This is the address of the Mono API client.BaseAddress = new Uri(configuration["ApiConfig:BaseAddress"]); });
Всякий раз, когда мы вручную вызываем API, мы делаем это с помощью именованного http-клиента, созданного httpfactory с прикрепленным к нему токеном доступа. Токен доступа собирается из HttpContext. Пользователям необходимо войти в систему, прежде чем они смогут использовать конечные точки, поэтому токен доступа действителен в HttpContext.
public BaseHttpClient(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor, ILogger logger)
{
this.httpClient = httpClientFactory.CreateClient(AuthorizedClient);
this.httpContextAccessor = httpContextAccessor;
this.logger = logger;
}
protected async Task<HttpResponseMessage> SendAuthenticatedAsync(HttpRequestMessage request)
{
try
{
var token = await this.httpContextAccessor.HttpContext.GetUserAccessTokenAsync();
this.httpClient.SetBearerToken(token);
var responseMessage = await this.httpClient.SendAsync(request);
return responseMessage;
}
catch (Exception e)
{
this.logger?.Error(e, "Exception at sending the authenticated client");
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.BadRequest
};
}
}
Ручные вызовы из BFF в API успешно проходят, но когда проверяем логирование, что-то не так.
Журналы Duende BFF:
[19:05:39 DBG] AuthenticationScheme: cookie was successfully authenticated.
[19:05:39 DBG] AuthenticationScheme: cookie was successfully authenticated.
[19:05:39 INF] Start processing HTTP request POST https://localhost:5001/api/v1/property
[19:05:39 INF] Start processing HTTP request POST https://localhost:5001/api/v1/property
[19:05:39 DBG] Cache miss for access token for client: default
[19:05:39 DBG] Cache miss for access token for client: default
[19:05:39 DBG] Requesting client access token for client: default
[19:05:39 DBG] Requesting client access token for client: default
[19:05:39 DBG] Constructing token client configuration from OpenID Connect handler.
[19:05:39 DBG] Constructing token client configuration from OpenID Connect handler.
[19:05:39 DBG] Returning token client configuration for client: default
[19:05:39 DBG] Returning token client configuration for client: default
[19:05:39 INF] Start processing HTTP request POST https://localhost:5443/connect/token
[19:05:39 INF] Start processing HTTP request POST https://localhost:5443/connect/token
[19:05:39 INF] Sending HTTP request POST https://localhost:5443/connect/token
[19:05:39 INF] Sending HTTP request POST https://localhost:5443/connect/token
[19:05:39 INF] Received HTTP response headers after 160.5943ms - 400
[19:05:39 INF] Received HTTP response headers after 160.5943ms - 400
[19:05:39 INF] End processing HTTP request after 168.1572ms - 400
[19:05:39 INF] End processing HTTP request after 168.1572ms - 400
**HERE**
[19:05:39 ERR] Error requesting access token for client default. Error = unauthorized_client. Error description = null
[19:05:39 ERR] Error requesting access token for client default. Error = unauthorized_client. Error description = null
[19:05:39 INF] Sending HTTP request POST https://localhost:5001/api/v1/property
[19:05:39 INF] Sending HTTP request POST https://localhost:5001/api/v1/property
[19:05:39 INF] Received HTTP response headers after 110.934ms - 400
[19:05:39 INF] Received HTTP response headers after 110.934ms - 400
[19:05:39 INF] End processing HTTP request after 295.8003ms - 400
[19:05:39 INF] End processing HTTP request after 295.8003ms - 400
[19:05:39 INF] Executing StatusCodeResult, setting HTTP status code 200
[19:05:39 INF] Executing StatusCodeResult, setting HTTP status code 200
Получаем ошибку для неавторизованного клиента
Журналы Identity Server:
[19:05:39 VRB] Calling into client configuration validator: Duende.IdentityServer.Validation.DefaultClientConfigurationValidator
[19:05:39 DBG] client configuration validation for client test_client succeeded.
[19:05:39 DBG] Public Client - skipping secret validation success
[19:05:39 DBG] Client validation success
[19:05:39 INF] {"ClientId": "test_client", "AuthenticationMethod": "NoSecret", "Category": "Authentication", "Name": "Client Authentication Success", "EventType": "Success", "Id": 1010, "Message": null, "ActivityId": "0HMI8JMB87769:00000004", "TimeStamp": "2022-06-07T16:05:39.0000000Z", "ProcessId": 21368, "LocalIpAddress": "::1:5443", "RemoteIpAddress": "::1", "$type": "ClientAuthenticationSuccessEvent"}
[19:05:39 VRB] Calling into token request validator: Duende.IdentityServer.Validation.TokenRequestValidator
[19:05:39 DBG] Start token request validation
[19:05:39 DBG] Start client credentials token request validation
**HERE**
[19:05:39 ERR] Client not authorized for client credentials flow, check the AllowedGrantTypes setting{"clientId": "test_client"}, details: {"ClientId": "test_client", "ClientName": "Scope", "GrantType": "client_credentials", "Scopes": null, "AuthorizationCode": "********", "RefreshToken": "********", "UserName": null, "AuthenticationContextReferenceClasses": null, "Tenant": null, "IdP": null, "Raw": {"grant_type": "client_credentials", "client_id": "test_client"}, "$type": "TokenRequestValidationLog"}
[19:05:39 INF] {"ClientId": "test_client", "ClientName": "Scope", "RedirectUri": null, "Endpoint": "Token", "SubjectId": null, "Scopes": null, "GrantType": "client_credentials", "Error": "unauthorized_client", "ErrorDescription": null, "Category": "Token", "Name": "Token Issued Failure", "EventType": "Failure", "Id": 2001, "Message": null, "ActivityId": "0HMI8JMB87769:00000004", "TimeStamp": "2022-06-07T16:05:39.0000000Z", "ProcessId": 21368, "LocalIpAddress": "::1:5443", "RemoteIpAddress": "::1", "$type": "TokenIssuedFailureEvent"}
[19:05:39 VRB] Invoking result: Duende.IdentityServer.Endpoints.Results.TokenErrorResult
[19:05:39 DBG] Connection id "0HMI8JMB87769" completed keep alive response.
[19:05:39 DBG] 'ConfigurationDbContext' disposed.
Мы видим, что запрос сделан на сервер идентификации с учетными данными клиента типа гранта, но Duende BFF зарегистрирован с кодом типа гранта.
HTTP-запрос, сделанный типизированным клиентом, проходит, поскольку прикрепленный токен доступа действителен, но поведение регистрации BFF и IDP является странным.
Любые идеи или наводки о том, что может быть причиной того, что лучший друг делает такие звонки внутренне перемещенным лицам?
1 ответ
Нашел проблему. Я регистрировал именованный http-клиент как AddClientAccessTokenHttpClient.
services.AddClientAccessTokenHttpClient(AuthorizedClient, configureClient: client =>
{
//This is the address of the Mono API
client.BaseAddress = new Uri(configuration["ApiConfig:BaseAddress"]);
});
Это вызвало запрос, сделанный им, чтобы установить тип гранта на учетные данные клиента.
Исправление заключается в регистрации с правильным http-клиентом — AddUserAccessTokenHttpClient.
services.AddUserAccessTokenHttpClient(AuthorizedClient, configureClient: client =>
{
//This is the address of the Mono API
client.BaseAddress = new Uri($"{configuration["ApiConfig:BaseAddress"]}");
});
Теперь в запросах установлен тип гранта code.