question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

ClaimsStrategy with multiple authentication schemes: Am I doing this right?

See original GitHub issue

I 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:

  1. The source I referenced for ClaimStrategy does not check the AuthenticationResult returned by AuthenticateAsync()–is this check skipped intentionally or should I be ensuring the result succeeded before trying to pull my tenant ID claim?

  2. 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?

  3. I noticed IAuthenticationHandler.GetRequestHandlerSchemesAsync() as a potential alternative to IAuthenticationHandler.GetAllSchemesAsync(), but in my project GetRequestHandlerSchemesAsync() 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 if GetAllSchemesAsync() is fine for my purposes.

  4. 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:closed
  • Created 2 years ago
  • Comments:10 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
AndrewTriesToCodecommented, Oct 1, 2021

that strategy only uses the default authentication scheme

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.

  1. The source I referenced for ClaimStrategy does not check the AuthenticationResult

This is by design. The strategy is meant only to extract a tenant identifier from a claim and not to actually perform authentication.

  1. is there any sort of additional validation I should perform on the authenticated principal

Depends on your use case, but the real authentication validation is handled in UseAuthentication(). In the ClaimStrategy 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 the OnValidate (or something like that) event on the options for your authentication scheme.

  1. IAuthenticationHandler.GetRequestHandlerSchemesAsync() as a potential alternative to IAuthenticationHandler.GetAllSchemesAsync()

Do you mean IAuthenticationSchemeProvider?

A request handler is one that can provide an immediate response to a request (implements IAuthenticationRequestHandler instead of just IAuthenticationHandler– 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 handler

  1. Do I need to consider a possibility of a specially-crafted request with two valid tokens for different, non-default schemes?

If 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 skipping WithPerTenantAuthentication and instead using your own OnValidate 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!

1reaction
AndrewTriesToCodecommented, Sep 29, 2021

hi, thanks for the detailed question. I will take a closer look and get you some answers today

Read more comments on GitHub >

github_iconTop Results From Across the Web

Multiple Authentication Schemes ASPNET Core 3
I'm using Azure AD B2C and I'd like my users to be able to log in thru my web app as well as...
Read more >
How to Use Multiple Authentication Schemes in .NET
In this article, we learned how to use multiple authentication schemes when adding authorization to our endpoints. We saw that we could have...
Read more >
Multiple authentication schemes: Azure AD and ...
I need to use two authentication schemes in my app: 1.) Azure AD B2C user login. 2.) Azure AD machine-to-machine daemon login. I...
Read more >
Multiple authentication schemes do not work on Azure (app ...
I try to work with two app registrations that uses one app ... Multiple authentication schemes do not work on Azure (app fails...
Read more >
Mixed-mode authentication
Describes a common use case for mixed-mode authentication, having one authentication provider for your back-end users and another provider for website users.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found