7

ASP.NET Core Api Auth with multiple Identity Providers | Software Engineering

 2 years ago
source link: https://damienbod.com/2022/09/19/asp-net-core-api-auth-with-multiple-identity-providers/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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.

idps_api_01.png?w=1024

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[]>()
};
})

I also used AddOpenIddict to implement the JWT access token validation from OpenIddict. In this example, I use self contained unencrypted access tokens so I disable the default more secure solution using introspection and encrypted access tokens (reference). This would also need to be changed on the IDP. I used the vendor specific client here because it does not override to ASP.NET Core default middleware and so does not break the validation from the other vendors. You could also validate this access token like above with plain JWT OAuth.

// Register the OpenIddict validation components.
// Scheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme
services.AddOpenIddict()
.AddValidation(options =>
{
// Note: the validation handler uses OpenID Connect discovery
// to retrieve the address of the introspection endpoint.
options.SetIssuer("https://localhost:44318/");
options.AddAudiences("rs_dataEventRecordsApi");
// Configure the validation handler to use introspection and register the client
// credentials used when communicating with the remote introspection endpoint.
//options.UseIntrospection()
//        .SetClientId("rs_dataEventRecordsApi")
//        .SetClientSecret("dataEventRecordsSecret");
// disable access token encryption for this
options.UseAspNetCore();
// Register the System.Net.Http integration.
options.UseSystemNetHttp();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});

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 OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
}
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 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK