ASP.NET Core Api Auth with multiple Identity Providers

This article shows how an ASP.NET Core API can be secured using multiple access tokens from different identity providers. ASP.NET Core schemes and policies can be used to set this up.

Code: https://github.com/damienbod/AspNetCoreApiAuthMultiIdentityProvider

History

2023-04-29 Updated packages and revert to default JWT authorization packages due to errors on update.

The ASP.NET Core API has a single API and needs to accept access tokens from three different identity providers. Auth0, OpenIddict and Azure AD are used as identity providers. OAuth2 is used to acquire the access tokens. I used self contained access tokens and only signed, not encrypted. This can be changed and would result in changes to the ForwardDefaultSelector implementation. Each of the access tokens need to be validated fully and also the signatures. How to validate a self contained JWT access token is documented in the OAuth2 best practices. We use an ASP.NET Core authentication handler to validate the specific claims from the different identity providers.

The authentication is added like any API implementation, except the default scheme is setup to a new value which is not used by any of the specific identity providers. This scheme is used to implement the ForwardDefaultSelector switch. When the API receives a HTTP request, it must decide what token this is and implement the token validation for this identity provider. The Auth0 token validation is implemented used standard AddJwtBearer which validates the issuer, audience and the signature.

services.AddAuthentication(options =>
{
	options.DefaultScheme = "UNKNOWN";
	options.DefaultChallengeScheme = "UNKNOWN";

})
.AddJwtBearer(Consts.MY_AUTH0_SCHEME, options =>
{
	options.Authority = Consts.MY_AUTH0_ISS;
	options.Audience = "https://auth0-api1";
	options.TokenValidationParameters = new TokenValidationParameters
	{
		ValidateIssuer = true,
		ValidateAudience = true,
		ValidateIssuerSigningKey = true,
		ValidAudiences = Configuration.GetSection("ValidAudiences").Get<string[]>(),
		ValidIssuers = Configuration.GetSection("ValidIssuers").Get<string[]>()
	};
})

AddJwtBearer is also used to implement the Azure AD access token validation. I normally use Microsoft.Identity.Web for Microsoft Azure AD access tokens but this adds some extra magic overwriting the default middleware and preventing the other identity providers from working. This is where client security gets really complicated as each identity provider vendor push their own client solution with different methods and different implementations hiding the underlying OAuth2 implementation. If the identity provider vendor specific client does not override the default schemes, policies of the ASP.NET Core middleware, then it ok to use. I like to implement as little as possible as this makes it easier to maintain over time. Creating these wrapper solutions hiding some of the details probably makes the whole security story more complicated. If these wrappers where compatible with 80% of non-specific vendor solutions, then the clients would be good.

.AddJwtBearer(Consts.MY_AAD_SCHEME, jwtOptions =>
{
	jwtOptions.MetadataAddress = Configuration["AzureAd:MetadataAddress"]; 
	jwtOptions.Authority = Configuration["AzureAd:Authority"];
	jwtOptions.Audience = Configuration["AzureAd:Audience"]; 
	jwtOptions.TokenValidationParameters = new TokenValidationParameters
	{
		ValidateIssuer = true,
		ValidateAudience = true,
		ValidateIssuerSigningKey = true,
		ValidAudiences = Configuration.GetSection("ValidAudiences").Get<string[]>(),
		ValidIssuers = Configuration.GetSection("ValidIssuers").Get<string[]>()
	};
})

Or for OpenIddict

Note: you cannot use the OpenIddict client lib because this breaks the authentication from the other schemes as like the Microsoft.Identity.Web nuget package. When using multiple schemes, use the default ASP.NET Core impleementation.

.AddJwtBearer(Consts.MY_OPENIDDICT_SCHEME, options =>
{
	options.Authority = Consts.MY_OPENIDDICT_ISS;
	options.Audience = "rs_dataEventRecordsApi";
	options.TokenValidationParameters = new TokenValidationParameters
	{
		ValidateIssuer = true,
		ValidateAudience = true,
		ValidateIssuerSigningKey = true,
		ValidAudiences = Configuration.GetSection("ValidAudiences").Get<string[]>(),
		ValidIssuers = Configuration.GetSection("ValidIssuers").Get<string[]>()
	};
})

The AddPolicyScheme method is used to implement the ForwardDefaultSelector switch. The default scheme is set to UNKNOWN and so per default access tokens will use this first. Depending on the issuer, the correct scheme is set and the access token is fully validated using the correct signatures etc. You could also implement logic here for reference tokens using introspection or cookies authentication etc. This implementation will always be different depending on how you secure the API. Sometimes you use cookies, sometimes reference tokens, sometimes encrypted tokens and so you need to identity the identity provider somehow and forward this on to the correct validation.

.AddPolicyScheme("UNKNOWN", "UNKNOWN", options =>
{
	options.ForwardDefaultSelector = context =>
	{
		string authorization = context.Request.Headers[HeaderNames.Authorization];
		if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
		{
			var token = authorization.Substring("Bearer ".Length).Trim();
			var jwtHandler = new JwtSecurityTokenHandler();

			// it's a self contained access token and not encrypted
			if (jwtHandler.CanReadToken(token)) 
			{
				var issuer = jwtHandler.ReadJwtToken(token).Issuer;
				if(issuer == Consts.MY_OPENIDDICT_ISS) // OpenIddict
				{
					return Consts.MY_OPENIDDICT_SCHEME;
				}

				if (issuer == Consts.MY_AUTH0_ISS) // Auth0
				{
					return Consts.MY_AUTH0_SCHEME;
				}

				if (issuer == Consts.MY_AAD_ISS) // AAD
				{
					return Consts.MY_AAD_SCHEME;
				}
			}
		}

		// We don't know what it is
		return Consts.MY_AAD_SCHEME;
	};
});

Now that the signature, issuer and the audience is validated, specific claims can also be checked using an ASP.NET Core policy and a handler. The AddAuthorization is used to add this.

services.AddSingleton<IAuthorizationHandler, AllSchemesHandler>();

services.AddAuthorization(options =>
{
	options.AddPolicy(Consts.MY_POLICY_ALL_IDP, policyAllRequirement =>
	{
		policyAllRequirement.Requirements.Add(new AllSchemesRequirement());
	});
});

The handler checks the specific identity provider access claims using the iss cliam as the switch information. You can add scopes, roles or whatever and this is identity provider specific. All do this differently.

using Microsoft.AspNetCore.Authorization;

namespace WebApi;

public class AllSchemesHandler : AuthorizationHandler<AllSchemesRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        AllSchemesRequirement requirement)
    {
        var issuer = string.Empty;
  
        var issClaim = context.User.Claims.FirstOrDefault(c => c.Type == "iss");
        if (issClaim != null)
            issuer = issClaim.Value;

        if (issuer == Consts.MY_OPENIDDICT_ISS) // OpenIddict
        {
            var scopeClaim = context.User.Claims.FirstOrDefault(c => c.Type == "scope" 
                && c.Value == "dataEventRecords");
            if (scopeClaim != null)
            {
                // scope": "dataEventRecords",
                context.Succeed(requirement);
            }
        }

        if (issuer == Consts.MY_AUTH0_ISS) // Auth0
        {
            // add require claim "gty", "client-credentials"
            var azpClaim = context.User.Claims.FirstOrDefault(c => c.Type == "azp"
                && c.Value == "naWWz6gdxtbQ68Hd2oAehABmmGM9m1zJ");
            if (azpClaim != null)
            {
                context.Succeed(requirement);
            }
        }

        if (issuer == Consts.MY_AAD_ISS) // AAD
        {
            // "azp": "--your-azp-claim-value--",
            var azpClaim = context.User.Claims.FirstOrDefault(c => c.Type == "azp"
                && c.Value == "46d2f651-813a-4b5c-8a43-63abcb4f692c");
            if (azpClaim != null)
            {
                context.Succeed(requirement);
            }
        }

        return Task.CompletedTask;
    }
}

An authorize attribute can be added to the controller exposing the API and the policy is added. The AuthenticationSchemes is used to add a comma separated string of all the supported schemes.

[Authorize(AuthenticationSchemes = Consts.ALL_MY_SCHEMES, Policy = Consts.MY_POLICY_ALL_IDP)]
[Route("api/[controller]")]
public class ValuesController : Controller
{
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "data 1 from the api", "data 2 from the api" };
    }
}

This works good and you can force the authentication at the application level. Using this, you can implement a single API to use multiple access tokens but this does not mean that you should do this. I would always separate the APIs and identity providers to different endpoints if possible. Sometimes you need this and ASP.NET Core makes this easy as long as you use the standard implementations. If you use specific vendor client libraries to implement the security, then you need to understand what the wrapper do and how the schemes, policies in the ASP.NET Core middleware are implemented. Setting the default scheme affects all the clients and not just the specific vendor implementation.

Links

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/policyschemes

7 comments

  1. […] ASP.NET Core Api Auth with multiple Identity Providers (Damien Bowden) […]

  2. […] ASP.NET Core Api Auth with multiple Identity Providers – Damien Bowden […]

  3. Alexandre Jobin · · Reply

    Thank you for this article! One question, why do you say “I would always separate the APIs and identity providers to different endpoints if possible”? You mean that the identity providers should be validated at another level and the API should not know about them?

    1. Thanks! An API should validate tokens or cookies. How you get these is a different security flow. Getting these depends on the client type, UI, public, confidential etc. This has nothing to do with the API and does not belong in the same APP unless implementing a monolith which would then use cookies probably and not tokens.

      Hard to explain in a quick answer but it depends 🙂

      Greetings Damien

  4. […] ASP.NET Core Api Auth with multiple Identity&nbsp;Providers […]

  5. Markus Mayer · · Reply

    in “services.AddOpenIddict().AddValidation(options =>….”:
    // disable access token encryption for this
    options.UseAspNetCore(); <- regarding to the comment, this makes no sense. Could you please clarify or (if needed) correct this.

  6. Thank you for this article! It would be great if the provider Ad he premis was also added.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.