I have an Mulit-tenant application using KeyCloak as Identity Broker. There is TenantId in token through which , we will be able to identity which Tenant's user it is. Now, when authenticating the token, we use AddJwtBearer with Authority. I want that Authority to be fetched from configuration based on TenantId.
Is there any way to achieve this?
public static class KeycloakAuthenticationExtensions
{
public static void AddKeycloakAuthentication(this IServiceCollection services, IConfiguration configuration)
{
var jwtConfiguration = configuration.GetSection(Core.Authentication.Constants.JwtConfigurationSection).Get<JwtConfiguration>();
services.AddOptions<JwtConfiguration>().Bind(configuration.GetSection(Authentication.Constants.JwtConfigurationSection));
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = jwtConfiguration!.Authority;
options.SaveToken = false;
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudiences = jwtConfiguration.Audiences,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
//https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims?view=aspnetcore-6.0#extend-or-add-custom-claims-using-iclaimstransformation
//Name claim and role claim mapping
NameClaimType = ApiConstants.PreferredUserNameClaim
};
options.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return context.Response.WriteAsync(context.Request.Headers[ApiConstants.AuthorizationHeader].ToString());
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
}
};
});
services.AddSingleton<ITokenProvider, KeyCloakJwtTokenProvider>();
services.AddSingleton<ITokenStore, InMemoryCachedTokenStore>();
services.AddTransient<AuthenticationHttpMessageHandler>();
services.AddHttpClient<ITokenProvider, KeyCloakJwtTokenProvider>(client =>
{
client.BaseAddress = new Uri(jwtConfiguration!.Authority + ApiConstants.TokenEndpoint);
});
}
}
I have changed that jwtConfiguration to be a list of Tenants information.
For each request , AddJwtBearer is called to validate the token. Based on tenant Id coming as part of token I want to Set Authority and validate the token. I am expecting something as below where I will be able to get that TenantId and get the tenant info from list of tenants stored in configuration
services.AddOptions<TenantConfiguration>().Bind(configuration.GetSection(Authentication.Constants.TenantConfiguration));
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
var tenant1 = jwtConfiguration!.TenantInfo.FirstOrDefault(x => x.TenantId.Equals("**tenantId**", StringComparison.OrdinalIgnoreCase));
options.Authority = royHillTenant?.Authority;
options.SaveToken = false;
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudiences = royHillTenant?.Audiences,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
//https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims?view=aspnetcore-6.0#extend-or-add-custom-claims-using-iclaimstransformation
//Name claim and role claim mapping
NameClaimType = ApiConstants.PreferredUserNameClaim
};
options.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return context.Response.WriteAsync(context.Request.Headers[ApiConstants.AuthorizationHeader].ToString());
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
}
};
});
services.AddSingleton<ITokenProvider, KeyCloakJwtTokenProvider>();
services.AddSingleton<ITokenStore, InMemoryCachedTokenStore>();
services.AddTransient<AuthenticationHttpMessageHandler>();
services.AddHttpClient<ITokenProvider, KeyCloakJwtTokenProvider>();// client =>
//{
// client.BaseAddress = new Uri(jwtConfiguration!.Authority + ApiConstants.TokenEndpoint);
//});
}
UPDATE 1 I tried the way mentioned by @Qiang Fu. I created a middleware and added before authentication pipeline, it modifies the authority dynamically but getting error saying " Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace AND validationParameters.ValidIssuers is null or empty.". I know we can set validIssuer params but I have used same params as the one present in service extensions in the new middleware.
namespace Optym.RMX.Core.Hosting.WebAPI
{
public class JwtBearerOptionMiddleware
{
private readonly RequestDelegate _next;
private readonly IRailMaxLogger _logger;
private readonly IServiceProvider serviceProvider;
private readonly IHttpContextAccessor _contextAccessor;
private readonly TenantConfiguration tenantConfiguration;
public JwtBearerOptionMiddleware(RequestDelegate next, IRailMaxLogger logger, IServiceProvider serviceProvider
, IHttpContextAccessor contextAccessor, IOptions<TenantConfiguration> tenantConfiguration)
{
this._logger = logger;
this._next = next;
this.serviceProvider = serviceProvider;
this._contextAccessor = contextAccessor;
this.tenantConfiguration = tenantConfiguration.Value;
}
public async Task InvokeAsync(HttpContext httpContext)
{
// string token = this._contextAccessor.HttpContext?.Request.Headers["Authorization"];
var accessToken = this._contextAccessor.HttpContext?.Request.Headers.Authorization.FirstOrDefault()?.Split(" ").Last();
var handler = new JwtSecurityTokenHandler();
var jwtDecoded = handler.ReadToken(accessToken) as JwtSecurityToken;
string tenantId = jwtDecoded?.Claims.First(claim => claim.Type == "TenantId").Value;
var tenantInfo = this.tenantConfiguration.TenantInfo.FirstOrDefault(x => x.TenantId.Equals(tenantId, StringComparison.OrdinalIgnoreCase));
if (tenantInfo == null)
{
throw new RmxException($"No tenant information found for Tenant Id {tenantId}");
}
//var claims = this._contextAccessor.HttpContext?.User.Identities.First().Claims;
// string tenantId = claims?.First(claim => claim.Type == "TenantId").Value;
var jwtoptionsMonitor = serviceProvider.GetService<IOptionsMonitor<JwtBearerOptions>>();
var jwtoptions = jwtoptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme);
//then you can modify jwtoptions as you like
jwtoptions.Authority = tenantInfo.Authority;
jwtoptions.TokenValidationParameters.ValidAudiences = tenantInfo.Audiences;
jwtoptions.SaveToken = false;
jwtoptions.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudiences = tenantInfo.Audiences,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
//https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims?view=aspnetcore-6.0#extend-or-add-custom-claims-using-iclaimstransformation
//Name claim and role claim mapping
NameClaimType = ApiConstants.PreferredUserNameClaim
};
jwtoptions.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return context.Response.WriteAsync(context.Request.Headers[ApiConstants.AuthorizationHeader].ToString());
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
}
};
await this._next(httpContext);
}
}
}
UPDATE 2 Adding ValidIssuer and IssuerSigningKeys in TokenValidationParameters has solved the issue
You could change any service options using IOptionsMonitor
later in the controller or in a custom middleware. Try following to modify the options in a controller
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IServiceProvider _provider;
public ValuesController(IServiceProvider provider)
{
this._provider = provider;
}
[HttpGet("test")]
public void Test()
{
var jwtoptionsMonitor = _provider.GetService<IOptionsMonitor<JwtBearerOptions>>();
var jwtoptions = jwtoptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme);
//then you can modify jwtoptions as you like
jwtoptions.Authority = "new value";
jwtoptions.TokenValidationParameters.ValidAudience = "new value";
}
}
(Note: Only using inject Iserviceprovider to get IoptionsMonitor works. If you inject IOptionsMonitor directly, it won't work.)