Set up is

  • Identity Provider server with Duende ver.6, with registered client with Grant Type Code

     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 with the following config

                 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();
             });
    
  • And a BFF Webclient with config

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

Whenever we manually call the API we do so with a named http client, created by an httpfactory with access token attached to it. The access token is gathered from the HttpContext. Users need to login before they can use the endpoints, so the access token is valid within the 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
            };
        }

    }

Manual calls from the BFF to the API successfully pass, but when we check the logging, something is not right.

Duende BFF logs:

[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

We get an error for an Unauthorized Client

The Identity Server logs are:

[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.

We see that a request is made to the Identity server with a Grant Type Client credentials but Duende BFF is registered with the Grant Type code.

The http request made by the typed client passes, as the attached access token is valid, but the logging behavior of the BFF and IDP is bizare.

Any ideas or leads on what might be causing the BFF to make such calls to the IDP?

1

There are 1 answers

0
newLoop On BEST ANSWER

Found the issue. I was registering the named http client as AddClientAccessTokenHttpClient

     services.AddClientAccessTokenHttpClient(AuthorizedClient, configureClient: client =>
 {
     //This is the address of the Mono API
     client.BaseAddress = new Uri(configuration["ApiConfig:BaseAddress"]);
 });

Which caused request made by it to have the grant type set to Client Credentials.

Fix is to register with the correct http client - AddUserAccessTokenHttpClient

            services.AddUserAccessTokenHttpClient(AuthorizedClient, configureClient: client =>
        {
            //This is the address of the Mono API
            client.BaseAddress = new Uri($"{configuration["ApiConfig:BaseAddress"]}");
        });

Now requests have the grant type set to code.