I have an ASP.NET Core project that receives data from Sendgrid Webhook and provides authenticated APIs to business users (Azure AD).
Sendgrid is capable to be configured with OAuth 2.0 with client-credentials-flow as authentication to the webhook receiver. It doesn't support basic authentication. OAuth or none.
I had successfully configured Sendgrid's OAuth authentication to my app, leveraging OpenIddict, leaving other APIs unprotected for a while. Now I need to protect these other APIs using OAuth implicit flow before going live to production. And Sendgrid must authenticate itself to the webhook. I prefer not to deploy an additional microservice.
Question in short
Is it possible, and how, in ASP.NET Core to validate a JWT from different issuers? Examples are those application where you can login either using Facebook or Twitter or Google etc. (see note 1)
Now, to be perfectly clear to external audience, I'll be adding a detailed boring explanation.
My work so far
This is what I did to configure OpenIddict.
public static IServiceCollection ConfigureOpenIddictAuthentication(this IServiceCollection services)
{
services.AddDbContext<OpenIddictDbContext>(ef => ef
// Configure the context to use an in-memory store.
// This prevents multiple cluster instances from deployment
.UseInMemoryDatabase(nameof(OpenIddictDbContext))
// Register the entity sets needed by OpenIddict.
.UseOpenIddict()
)
.AddOpenIddict(options =>
options.AddServer(server => server
.DisableAccessTokenEncryption() //Just for development
//Development: no time to waste on certificate management today
.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey()
.RegisterClaims(OpenIddictConstants.Claims.Role)
.RegisterScopes(OpenIddictConstants.Scopes.Roles)
.SetTokenEndpointUris("/api/v1/Auth/token")
.SetAuthorizationEndpointUris("/api/v1/Auth/authorize")
.AllowClientCredentialsFlow() //Only one supported by Sendgrid
.UseAspNetCore()
.EnableTokenEndpointPassthrough())
.AddCore(core => core.UseEntityFrameworkCore(ef => ef.UseDbContext<OpenIddictDbContext>()))
.AddValidation(validation => validation
.UseLocalServer(_ => { })
.UseAspNetCore(_ => { })
)
)
.AddHostedService<OpenIddictHostedService>()
.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)
;
return services;
}
The above code (and the OpenIddictHostedService
not shown) provide the client-credentials-flow /api/v1/Auth/token
URL, all infrastructure required to verify the Bearer
token provided by Sendgrid and the secret credentials buried in the environment.
I can run tests with Postman to submit Sendgrid test data, using a client credential that is hosted in the dev environment. And it works
Adding MSAL back end
I have then switched off OpenIddict for a while in my code to perform new coding. I have configured an MS AAD application registration with OpenID Connect and OAuth's implicit flow (required by Angular and Swagger). By adding the following code, and the appropriate [Authorize]
attribute, I can protect the rest of my APIs with code:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration)
;
The configuration appSettings.json
contains tenant, app ID and OIDC metadata URL. There is another section, related to Swagger, that configures Swagger for OIDC using MS Azure metadata
services.AddSwaggerGen(swagger =>
{
swagger.SwaggerDoc("v1", new OpenApiInfo { Title = "...", Version = "v1" });
swagger.OperationFilter<AssignOAuth2SecurityRequirements>();
swagger.AddSecurityDefinition("AzureAD", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OpenIdConnect,
OpenIdConnectUrl = new Uri("https://login.microsoftonline.com/......./v2.0/.well-known/openid-configuration"),
});
swagger.AddSecurityRequirement(AssignOAuth2SecurityRequirements.APISECURITY);
});
public static readonly OpenApiSecurityRequirement APISECURITY = new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2"
}
},
new[] { "AzureAD" }
}
};
The result of the above fragments is that Swagger now allows me to invoke other protected APIs of my project using the JWT issued by MS AAD
And it damn works. But now Sendgrid's OAuth authentication is shut off.
Merging the two
Now I need that every API protected by [Authorize]
check for any of the JWT tokens provided in the header (note 3: I'll use scopes on the next iteration of coding to distinguish) to authenticate the request, whether it comes from Sendgrid/Postman or Swagger/Angular.
I tried to uncomment all my code
services.ConfigureOpenIddictAuthentication();
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration);
But it miserably fails at authentication using OpenIddict authorization server. Ie. it only checks for the MSAL token and rejects the whole request. Clearly, the authorization service registered later overrides the earlier.
Note 1
Actually, from a design point of view, in order to implement authentication using multiple providers one should implement an endpoint to exchange a token from the external provider for a token issued by the local authority. But Microsoft AAD uses JWTs issued from Microsoft Azure itself as bearers. Why not use them?