Finbuckle / Finbuckle.MultiTenant

Finbuckle.MultiTenant is an open-source multitenancy middleware library for .NET. It enables tenant resolution, per-tenant app behavior, and per-tenant data isolation.
https://www.finbuckle.com/multitenant
Apache License 2.0
1.34k stars 266 forks source link

Per-Tenant Authentication with JwtBearerOptions Not Working #855

Open DXFJaimik opened 4 months ago

DXFJaimik commented 4 months ago

I am using Okta IDP provider, and it works with both static implementation and multitenancy using Finbuckle. However, the Per-tenant authentication with JwtBearerOptions flow does not function correctly on Finbuckle multitenancy with per-tenant authentication.

I have followed all the provided steps, and without per-tenant authentication, the tenant is resolved properly. Tenant information is obtained based on the Host Strategy. In JwtBearerEvents, during MessageReceived, I receive all options resolved by the ConfigurePerTenant method. However, during the Challenge method, I encounter an unusual result with no error message or description. I have included a screenshot of this issue.

image

image

DXFJaimik commented 4 months ago

Added code for reference:

Program.cs:

using Finbuckle.MultiTenant;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.OpenApi.Models;
using PerTenantAuthTest;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.OAuth2,
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow
            {
                AuthorizationUrl = new Uri("https://okta-tenant-id.okta.com/oauth2/default/v1/authorize"),
                TokenUrl = new Uri("https://okta-tenant-id.okta.com/oauth2/default/v1/token"),
                Scopes = new Dictionary<string, string>
                        {
                            { "Application.FullAccess", "Access API" }
                        }
            }
        }
    });
    options.OperationFilter<AuthorizeCheckOperationFilter>();
});

builder.Services.AddMultiTenant<MyTenantInfo>()
    .WithHostStrategy("__tenant__")
    .WithConfigurationStore(builder.Configuration, "Tenant:Stores:ConfigurationStore")
.WithPerTenantAuthentication();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.ConfigurePerTenant<JwtBearerOptions, MyTenantInfo>(JwtBearerDefaults.AuthenticationScheme,
    (options, tenantInfo) =>
    {
        options.Authority = tenantInfo.Authority;
        options.Audience = "api://default";
        options.RequireHttpsMetadata = false;
        options.SaveToken = true;
        options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
        {
            RequireAudience = true,
            ValidateAudience = true,
        };
        options.Events = new MultitenantJwtBearerEvents();
    });

var app = builder.Build();

app.UseMiddleware<ExceptionHandlingMiddleware>();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.DefaultModelsExpandDepth(-1);
        options.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
        options.OAuthAppName("ApplicationName");
        options.OAuthClientId("ClientId");
        options.OAuthUsePkce();
    });
}

app.UseHttpsRedirection();

app.UseMultiTenant();

app.UseAuthentication();

app.UseAuthorization();

app.MapControllers();

app.Run();

MultitenantJwtBearerEvents.cs:

using Microsoft.AspNetCore.Authentication.JwtBearer;

namespace PerTenantAuthTest;

public class MultitenantJwtBearerEvents : JwtBearerEvents
{
    public override Task AuthenticationFailed(AuthenticationFailedContext context)
    {
        return base.AuthenticationFailed(context);
    }
    public override Task MessageReceived(MessageReceivedContext context)
    {
        return base.MessageReceived(context);
    }
    public override async Task TokenValidated(TokenValidatedContext context)
    {        
        await base.TokenValidated(context);
    }

    public override Task Challenge(JwtBearerChallengeContext context)
    {
        context.HandleResponse();
        if (!context.Response.HasStarted)
        {
            throw new UnauthorizedAccessException($"{context.Error!} {context.ErrorDescription!}");
        }

        return Task.CompletedTask;
    }

    public override Task Forbidden(ForbiddenContext context)
    {
        throw new Exception();
    }
}
AndrewTriesToCode commented 4 months ago

Hi, this looks like a tough one. Just to check, do you mean to say "pre" tenant authentication or "per" tenant authentication? And the issue is only on validating the JWT token AFTER the whole oauth2 workflow has completed?

One quick thing to try, place your AddAuthentication line before the AddMultiTenant in your setup-- MultiTenant overrides some of the authentication related DI which might be why you are seeing this.

DXFJaimik commented 4 months ago

Thank you for your quick reply. This is per tenant authentication, and there is an issue with JWT token validation.

I tried your suggestion to place the AddAuthentication line before the AddMultiTenant, but it is returning the same result.

AndrewTriesToCode commented 4 months ago

So looking at this again, in the jwt scheme handler the challenge method doesn’t really do much before calling the event handler. If you tell it you handled the event (via your call to context HandledResponse) it will not generate the 401 challenge response because it assumes you have done so. Is that what you intended?

In your top screenshot I can’t tell if the options passed to the challenge event were resolved correctly for the tenant. can you include that part in the screenshot?