Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Unauthorized status] Accessing Remote APIs via Simple HTTP Forwarder in BFF Configuration #1511

Open
NongHaHoang opened this issue Dec 8, 2024 · 7 comments
Labels

Comments

@NongHaHoang
Copy link

NongHaHoang commented Dec 8, 2024

Which version of Duende BFF are you using?
2.2.0

Which version of .NET are you using?
net9.0

Describe the bug

Unauthorized error when making API calls from a Blazor WebAssembly Hosted application (with both server and client projects) to an API Project. Here's a summary of my setup:

  • Identity Server: Configured as the authority for authentication and authorization.
  • Blazor WebAssembly Hosted: Includes both server and client projects, configured to use Identity Server for login/logout.
  • API Project: Configured to use Identity Server for authentication, just like the Blazor WASM project.

What works:

  • I have successfully implemented login/logout functionality in the Blazor WebAssembly Hosted application.
  • The Identity Server is correctly handling authentication, and the login process works as expected.

The issue:

  • Despite successful login/logout, all API calls to the API Project are being denied with an Unauthorized (401) status. The API Project is also set up to use Identity Server as its authority, and I've ensured the correct authentication tokens are passed along with the requests.

To Reproduce

  1. Identity Server Configuration

Config.cs

public static class Config
{
    public static IEnumerable<IdentityResource> IdentityResources =>
        [
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResources.Email(),
            new IdentityResource("user_types", ["user_types"]),
            new IdentityResource("dob", ["dob"]),
            new IdentityResource("phone_number", ["phone_number"]),
            new IdentityResource("avatar_url", ["avatar_url"]),
        ];

    public static IEnumerable<ApiScope> ApiScopes =>
        [
            // local API
            new ApiScope(IdentityServerConstants.LocalApi.ScopeName),
        ];

    public static IEnumerable<Client> Clients =>
        [
            // interactive hosted local client
            new Client
            {
                ClientId = "wasm.hosted",
                ClientName = "Wasm Hosted",
                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                },
                ClientUri = "https://localhost:7160",

                AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,

                RedirectUris = { "https://localhost:7160/signin-oidc" },
                FrontChannelLogoutUri = "https://localhost:7160/signout-oidc",
                PostLogoutRedirectUris = { "https://localhost:7160/signout-callback-oidc" },

                AllowedCorsOrigins = { "https://localhost:7160" },

                AllowOfflineAccess = true,
                RefreshTokenUsage = TokenUsage.ReUse,
                RefreshTokenExpiration = TokenExpiration.Sliding,

                AllowedScopes = { "openid", "profile", "email", "user_types", "dob", "phone_number", "avatar_url", IdentityServerConstants.LocalApi.ScopeName }
            }
        ];
}
  1. Blazor WASM Hosted Configuration

Client-side Program.cs

var builder = WebAssemblyHostBuilder.CreateDefault(args);

// HTTP client configuration
builder.Services.AddTransient<AntiforgeryHandler>();

builder.Services.AddHttpClient("backend", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<AntiforgeryHandler>();
builder.Services.AddHttpClient("IdentityServer", client => client.BaseAddress = new Uri("https://localhost:5001"))
    .AddHttpMessageHandler<AntiforgeryHandler>();
builder.Services.AddHttpClient("API", client => client.BaseAddress = new Uri("https://localhost:7075"))
    .AddHttpMessageHandler<AntiforgeryHandler>();

builder.Services
        .AddKeyedScoped("ISAPI", (sp, _) => sp.GetRequiredService<IHttpClientFactory>().CreateClient("IdentityServer"))
        .AddKeyedScoped("WebAPI", (sp, _) => sp.GetRequiredService<IHttpClientFactory>().CreateClient("API"));

builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("backend"));

builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, BffAuthenticationStateProvider>();

await builder.Build().RunAsync();

Server-side Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents();

builder.Services.AddControllers();

builder.Services.AddRadzenComponents();

builder.Services.AddBff(options =>
    {
        options.RevokeRefreshTokenOnLogout = false;
    })
    .AddRemoteApis();

builder.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 = "https://localhost:5001";
        options.ClientId = "wasm.hosted";
        options.ClientSecret = "secret";
        options.ResponseType = "code";
        options.ResponseMode = "query";

        options.CallbackPath = "/signin-oidc";
        options.SignedOutCallbackPath = "/signout-oidc";

        options.Scope.Clear();
        options.Scope.Add("offline_access");
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("email");

        options.Scope.Add("user_types");
        options.Scope.Add("dob");
        options.Scope.Add("phone_number");
        options.Scope.Add("avatar_url");

        options.Scope.Add("IdentityServerApi");

        options.ClaimActions.MapJsonKey("user_types", "user_types");
        options.ClaimActions.MapJsonKey("dob", "dob");
        options.ClaimActions.MapJsonKey("phone_number", "phone_number");
        options.ClaimActions.MapJsonKey("avatar_url", "avatar_url");
        options.ClaimActions.MapJsonKey("IdentityServerApi", "IdentityServerApi");

        options.MapInboundClaims = false;
        options.GetClaimsFromUserInfoEndpoint = true;
        options.SaveTokens = true;
    });

builder.Services.AddAuthorization();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseAntiforgery();

app.UseAuthentication();
app.UseBff();
app.UseAuthorization();

app.MapBffManagementEndpoints();

app.MapControllers()
    .RequireAuthorization()
    .AsBffApiEndpoint();

app.MapRemoteBffApiEndpoint("/api/localapi", "https://localhost:5001")
    .RequireAccessToken(Duende.Bff.TokenType.User);

app.MapRemoteBffApiEndpoint("/api/bookings", "https://localhost:7075")
    .RequireAccessToken(Duende.Bff.TokenType.User);

app.MapRemoteBffApiEndpoint("/api/email", "https://localhost:7075")
    .RequireAccessToken(Duende.Bff.TokenType.User);

app.MapRemoteBffApiEndpoint("/api/Facilities", "https://localhost:7075")
    .RequireAccessToken(Duende.Bff.TokenType.User);

app.MapRemoteBffApiEndpoint("/api/images", "https://localhost:7075")
    .RequireAccessToken(Duende.Bff.TokenType.User);

app.MapRemoteBffApiEndpoint("/api/notifications", "https://localhost:7075")
    .RequireAccessToken(Duende.Bff.TokenType.User);

app.MapStaticAssets();
app.MapRazorComponents<App>()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(SBAPrototype.UI.WebHosted.Client._Imports).Assembly);

app.Run();
  1. API Project Configuration

Program.cs

// Add services to the container.
builder.Services.AddControllers();

// Add database context
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));

builder.Services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        // Configure the Authority to the expected value for
        // the authentication provider. This ensures the token
        // is appropriately validated.
        options.Authority ="https://localhost:5001";

        options.TokenValidationParameters.ValidateAudience = false;
    });
builder.Services.AddAuthorization();

builder.Services.AddScoped(_ => new EmailSender(emailSenderAPIKey!));

// Add policy for CORS
const string myAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddCors(options =>
        options.AddPolicy(name: myAllowSpecificOrigins,
        policy => policy
            .WithOrigins("https://localhost:7160")
            .AllowAnyHeader()
            .AllowAnyMethod()
        )
    );

 var app = builder.Build();

app.UseHttpsRedirection();

app.UseCors(myAllowSpecificOrigins);

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

Expected behavior

API calls from the Blazor WebAssembly Hosted app should be authenticated successfully, and the API Project should respond without returning an Unauthorized error.

Could you please provide guidance on what might be causing this issue or any potential misconfigurations?

@NongHaHoang NongHaHoang added the BFF label Dec 8, 2024
@amiriltd
Copy link

amiriltd commented Dec 9, 2024

Based on your configuration, you are using BFF in your server-side program.cs therefore you must add the anti-forgery header configured whenever you are calling the endpoints that were setup. For what I can tell this is the issue.

Can we see your AntiForgeryHandler class?

GET /endpoint

x-csrf: 1

Image

@NongHaHoang
Copy link
Author

The below is AntiforgeryHandler.cs in the client-side

public class AntiforgeryHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-CSRF", "1");
        return base.SendAsync(request, cancellationToken);
    }
}

Are you suggesting that I should also use the "anti-forgery header" in the server-side as well? Is yes, what is the best way to use "anti-forgery header" with REMOTE api?

Thank you.

@amiriltd
Copy link

amiriltd commented Dec 9, 2024

Thanks you for the code snippet. The "anti-forgery headers" are for the client side code. Do you still get the error if you append "SkipAntiforgery()"

app.MapRemoteBffApiEndpoint("/api/bookings", "https://localhost:5001")
    .RequireAccessToken(Duende.Bff.TokenType.User)
    .SkipAntiforgery();

to the server-side endpoints? If you are still getting the error you can rule out the Antiforgery handler and focus on the API setup. In my API code, I usually set my schemes in the AddAuthentication and AddJwtBearer calls. Can you try this?

builder.Services.AddAuthentication("token")
    .AddJwtBearer("token", options =>
    {
        // Configure the Authority to the expected value for
        // the authentication provider. This ensures the token
        // is appropriately validated.
        options.Authority ="https://localhost:5001";

        options.TokenValidationParameters.ValidateAudience = false;
    });
builder.Services.AddAuthorization();

@NongHaHoang
Copy link
Author

Unfortunately, I tried both recommendations, but the errors still persist. Interestingly, I also have a standalone WASM client with a similar setup, and it works fine.

@amiriltd
Copy link

So what are the differences as you see them? I'm curious

@RolandGuijt
Copy link

In addition:

  • Please check the logs of the API to check why the token was rejected
  • Where do you attach the token in the wasm project? Can you show us how you're doing that?

@NongHaHoang
Copy link
Author

@amiriltd @RolandGuijt apologies for the delayed response. I will create a minimal repository to reproduce the issue ASAP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants