ClaimsStrategy with multiple authentication schemes: Am I doing this right?
See original GitHub issueI was struggling to get Finbuckle.MultiTenant to recognize my OpenIddict access tokens, even though the claims strategy was working fine for my B2C tokens. After barking up several wrong trees and examining the source for ClaimsStrategy
, I realized this was because that strategy only uses the default authentication scheme (in my case, B2C), so it would happily pull my B2C tenant ID claim but never even see the OpenIddict tokens.
I created a new IMultiTenantStrategy
which is basically just a modified ClaimsStrategy
that checks each authentication scheme until it finds the claim, and that finally fixed my issue.
However, I’m not intimately familiar with the nitty-gritty of authentication in ASP.NET Core and I am very wary of doing something wrong here, since this seems like an excellent place to accidentally introduce a vulnerability! I would very much appreciate if anyone could let me know if I’ve done this correctly. I’m assuming there was a good reason the original strategy only checked the default scheme, but I don’t know if it was for a reason I can safely ignore in my use case.
My questions/concerns:
-
The source I referenced for
ClaimStrategy
does not check theAuthenticationResult
returned byAuthenticateAsync()
–is this check skipped intentionally or should I be ensuring the result succeeded before trying to pull my tenant ID claim? -
Related to the first question, is there any sort of additional validation I should perform on the authenticated principal before pulling my claim? Or is this far enough along in the pipeline that I don’t need to worry?
-
I noticed
IAuthenticationHandler.GetRequestHandlerSchemesAsync()
as a potential alternative toIAuthenticationHandler.GetAllSchemesAsync()
, but in my projectGetRequestHandlerSchemesAsync()
doesn’t return any auth schemes, so I’m not sure if this is potentially a better method to use but needs additional configuration, or ifGetAllSchemesAsync()
is fine for my purposes. -
Do I need to consider a possibility of a specially-crafted request with two valid tokens for different, non-default schemes? e.g. an attacker is an admin in tenant 1, but a regular user in tenant 2, and while logged into tenant 1 supplies a tenant 2 token containing a different tenant ID, tricking my strategy into pulling the tenant ID for tenant 2 and giving admin access to the more restricted tenant 2 resources? Or is that out of scope here? Something about just willy-nilly looping through authentication schemes until I get the claim I want strikes me as having some implicit assumptions that could bite me in the butt. I’m a little paranoid about auth stuff.
public class RegisteredSchemesClaimsStrategy : IMultiTenantStrategy
{
public async Task<string> GetIdentifierAsync(object context)
{
var httpContext = context as HttpContext;
if (httpContext is null)
{
return null;
}
var schemeProvider = httpContext.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
// Try each registered AuthenticationScheme, starting with the default
await foreach (var scheme in EnumerateAuthenticationSchemes(schemeProvider))
{
var handler = (IAuthenticationHandler)ActivatorUtilities.CreateInstance(
httpContext.RequestServices, scheme.HandlerType);
await handler.InitializeAsync(scheme, httpContext).ConfigureAwait(false);
var handlerResult = await handler.AuthenticateAsync().ConfigureAwait(false);
// QUESTION: Should I check handlerResult.Succeeded before proceeding?
var identifierClaim = handlerResult.Principal?.FindFirst("tenantId");
if (identifierClaim != null)
{
return identifierClaim.Value;
}
}
return null;
}
/// <summary>
/// Enumerate registered authentication schemes and associated handlers, beginning with the default scheme.
/// </summary>
private static async IAsyncEnumerable<AuthenticationScheme> EnumerateAuthenticationSchemes(
IAuthenticationSchemeProvider authenticationSchemeProvider,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (authenticationSchemeProvider is null)
{
throw new ArgumentNullException(nameof(authenticationSchemeProvider));
}
if (cancellationToken.IsCancellationRequested)
{
yield break;
}
// Yield the default scheme first
var defaultScheme = await authenticationSchemeProvider.GetDefaultAuthenticateSchemeAsync().ConfigureAwait(false);
yield return defaultScheme;
// Yield remaining schemes
var allSchemes = await authenticationSchemeProvider.GetAllSchemesAsync().ConfigureAwait(false);
foreach (var scheme in allSchemes.Where(
x => !string.Equals(x.Name, defaultScheme.Name, StringComparison.Ordinal)))
{
if (cancellationToken.IsCancellationRequested)
{
yield break;
}
yield return scheme;
}
}
}
Issue Analytics
- State:
- Created 2 years ago
- Comments:10 (6 by maintainers)
This is just because I was short sighted. There is a current PR that among other things lets you tell it what scheme you want it to use. I think you should probably try something like that rather than looping through all the schemes. I will eventually get this added into the library.
This is by design. The strategy is meant only to extract a tenant identifier from a claim and not to actually perform authentication.
Depends on your use case, but the real authentication validation is handled in
UseAuthentication()
. In theClaimStrategy
you’ll see I set a bypass for validation which the wrapper validation it puts in place around yours will skip yours – this is because sometimes validation needs to know the tenant (e.g. for a database call) and at this point we are trying to resolve the tenant so such validation fails. So the idea with this strategy is just to grab a claim from the cookie but not actual do the user authentication – that comes later with the authentication middleware. It is smart enough not to have to reparse the entire cookie during the actual authentication. If you want to do additional validate look into theOnValidate
(or something like that) event on the options for your authentication scheme.Do you mean
IAuthenticationSchemeProvider
?A request handler is one that can provide an immediate response to a request (implements
IAuthenticationRequestHandler
instead of justIAuthenticationHandler
– like the OpenId Connect handler which handles the a callback from the identity service then redirects the user. This will only return schemes with such a handlerIf you use
WithPerTenantAuthentication
the library adds extra authentication that encodes the tenant into the authentication properties which ASP.NET encodes into its tokens. On subsequent authentication the tenant that was encoded into the authentication properties is compared to the current detected tenant and if they don’t match it rejects the authentication. Note that the current tenant could be from the claims strategy or from some other strategy. It just makes sure it matches the encoded tenant from the time of token creation. If your tokens are not created in ASP.NET Core this might cause you some issues. Your specific use case might warrant skippingWithPerTenantAuthentication
and instead using your ownOnValidate
event handling for the scheme.WithPerTenantAuthentication
’s validation was more meant for cookie scenarios. Sounds like you might be dealing with APIs / tokens? I’d suggest that if the token is signed (and you trust the provider) you can probably trust it for that single request in terms of the tenant in the claim. You would just want to make sure all authorization is derived from only that token for the request.Thanks for the tough questions!
hi, thanks for the detailed question. I will take a closer look and get you some answers today