Is it possible to support multiple JWT Token issuers in ASP.NET Core 2?
I want to provide an API for external service and I need to use two sources of JWT tokens - Firebase and custom JWT token issuers. In ASP.NET core I can set the JWT authentication for Bearer auth scheme, but only for one Authority:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://securetoken.google.com/my-firebase-project"
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "my-firebase-project"
ValidateAudience = true,
ValidAudience = "my-firebase-project"
ValidateLifetime = true
};
}
I can have multiple issuers and audiences, but I can't set several Authorities.
You can totally achieve what you want:
services
.AddAuthentication()
.AddJwtBearer("Firebase", options =>
{
options.Authority = "https://securetoken.google.com/my-firebase-project"
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "my-firebase-project"
ValidateAudience = true,
ValidAudience = "my-firebase-project"
ValidateLifetime = true
};
})
.AddJwtBearer("Custom", options =>
{
// Configuration for your custom
// JWT tokens here
});
services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Firebase", "Custom")
.Build();
});
Let's go through the differences between your code and that one.
AddAuthentication has no parameter
If you set a default authentication scheme, then on every single request the authentication middleware will try to run the authentication handler associated with the default authentication scheme. Since we now have two possible authentication schemes, there's no point in running one of them.
Use another overload of AddJwtBearer
Every single AddXXX method to add an authentication has several overloads:
One where the default authentication scheme associated with the authentication method is used, as you can see here for cookies authentication
One where you pass, in addition to the configuration of the options, the name of the authentication scheme, as on this overload
Now, because you use the same authentication method twice but authentication schemes must be unique, you need to use the second overload.
Update the default policy
Since the requests won't be authenticated automatically anymore, putting [Authorize] attributes on some actions will result in the requests being rejected and an HTTP 401 will be issued.
Since that's not what we want because we want to give the authentication handlers a chance to authenticate the request, we change the default policy of the authorization system by indicating both the Firebase and Custom authentication schemes should be tried to authenticate the request.
That doesn't prevent you from being more restrictive on some actions; the [Authorize] attribute has an AuthenticationSchemes property that allows you to override which authentication schemes are valid.
If you have more complex scenarios, you can make use of policy-based authorization. I find the official documentation is great.
Let's imagine some actions are only available to JWT tokens issued by Firebase and must have a claim with a specific value; you could do it this way:
// Authentication code omitted for brevity
services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Firebase", "Custom")
.Build();
options.AddPolicy("FirebaseAdministrators", new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Firebase")
.RequireClaim("role", "admin")
.Build());
});
You could then use [Authorize(Policy = "FirebaseAdministrators")] on some actions.
A final point to note: If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed. Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
This is an extension of Mickaël Derriey's answer.
Our app has a custom authorization requirement that we resolve from an internal source. We were using Auth0 but are switching to Microsoft Account authentication using OpenID. Here is the slightly edited code from our ASP.Net Core 2.1 Startup. For future readers, this works as of this writing for the versions specified. The caller uses the id_token from OpenID on incoming requests passed as a Bearer token. Hope it helps someone else trying to do an identity authority conversion as much as this question and answer helped me.
const string Auth0 = nameof(Auth0);
const string MsaOpenId = nameof(MsaOpenId);
string domain = "https://myAuth0App.auth0.com/";
services.AddAuthentication()
.AddJwtBearer(Auth0, options =>
{
options.Authority = domain;
options.Audience = "https://myAuth0Audience.com";
})
.AddJwtBearer(MsaOpenId, options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = "00000000-0000-0000-0000-000000000000",
ValidateIssuer = true,
ValidIssuer = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0",
ValidateIssuerSigningKey = true,
RequireExpirationTime = true,
ValidateLifetime = true,
RequireSignedTokens = true,
ClockSkew = TimeSpan.FromMinutes(10),
};
options.MetadataAddress = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0/.well-known/openid-configuration";
}
);
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes( Auth0, MsaOpenId )
.Build();
var approvedPolicyBuilder = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(Auth0, MsaOpenId)
;
approvedPolicyBuilder.Requirements.Add(new HasApprovedRequirement(domain));
options.AddPolicy("approved", approvedPolicyBuilder.Build());
});
The solution to your question, is available in following blog post https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
Basically the solutions exists of overriding the regular JWTBearer handler with you own generic handler that can check through the JWTBearerConfig if the issuer in the cfg is the same to the isseur in your token.
The blog post suggests to use seperate handlers for each scheme, that doesn't seem to be needed, a generic class JWTAuthenticationHandler that overrides the HandleAuthenticateAsync method seems to suffice!
Code wise you could implement your startup like this:
//Using multiple schemes can cause issues when validating the issuesSigningKey therefore we need to implement seperate handlers for each scheme! => cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
services.AddAuthentication()
//Set the authenticationScheme by using the identityServer helper methods (we are using a Bearer token)
.AddScheme<JwtBearerOptions, JWTAuthenticationHandler>(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
{
//TO DO Get the origin url's from configuration file, instead of setting all url's here
options.Authority = _identityServerSettings.Authority;
options.Audience = _identityServerSettings.Audience;
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
return Task.CompletedTask;
},
//When using multiple JwtBearer schemes we can run into "OnAuthenticationFailed" for instance when logging in via IdentityServer the AuthenticationHandler will still check in these events, this can be ignored...
//Cfr => https://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication
//If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed.Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
//We managed to fix this issue by adding seperate AuthenticationHandlers for each type of bearer token... cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
OnAuthenticationFailed = context =>
{
return Task.CompletedTask;
},
OnMessageReceived = context =>
{
return Task.CompletedTask;
},
OnForbidden = context =>
{
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
}
};
})
//Set the authentication scheme for the AzureAd integration (we are using a bearer token)
.AddScheme<JwtBearerOptions, JWTAuthenticationHandler>("AzureAD", "AzureAD", options =>
{
options.Audience = _azureAdSettings.Audience; //ClientId
options.Authority = _azureAdSettings.Authority; //"https://login.microsoftonline.com/{tenantId}/v2.0/"
options.TokenValidationParameters = new TokenValidationParameters
{
//Set built in claimTypes => Role
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
};
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
return Task.CompletedTask;
},
//When using multiple JwtBearer schemes we can run into "OnAuthenticationFailed" for instance when logging in via IdentityServer the AuthenticationHandler will still check in these events, this can be ignored...
//Cfr => https://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication
//A final point to note: If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed.Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
//We managed to fix this issue by adding seperate AuthenticationHandlers for each type of bearer token... cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
OnAuthenticationFailed = context =>
{
return Task.CompletedTask;
},
OnMessageReceived = context =>
{
return Task.CompletedTask;
},
OnForbidden = context =>
{
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
}
};
});
}
The JWTAuthenticationHandlerClass can look like this
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace WebAPI.Auth
{
public class JWTAuthenticationHandler: JwtBearerHandler
{
public JWTAuthenticationHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{ }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//Fetch OIDC configuration for the IDP we are handling
var authorityConfig = await this.Options.ConfigurationManager.GetConfigurationAsync(this.Context.RequestAborted);
//Determine the issuer from the configuration
var authorityIssuer = authorityConfig.Issuer;
var jwtToken = this.ReadTokenFromHeader();
var jwtHandler = new JwtSecurityTokenHandler();
//Check if we can read the token as a valid JWT, if not let the JwtBearerHandler do it's thing...
if (jwtHandler.CanReadToken(jwtToken))
{
//Read the token and determine if the issuer in config is the same as the one in the token, if this is true we know we want to let the JwtBearerHandler continue, if not we skip and return noResult
//This way the next IDP configuration will pass here until we find a matching issuer and then we know that is the IDP we are dealing with
var token = jwtHandler.ReadJwtToken(jwtToken);
if (string.Equals(token.Issuer, authorityIssuer, StringComparison.OrdinalIgnoreCase))
{
return await base.HandleAuthenticateAsync();
}
else
{
// return NoResult since the issuer in cfg did not match the one in the token, so no need to proceed to tokenValidation
this.Logger.LogDebug($"Skipping jwt token validation because token issuer was {token.Issuer} but the authority issuer is: {authorityIssuer}");
return AuthenticateResult.NoResult();
}
}
return await base.HandleAuthenticateAsync();
}
//Fetch the bearer token from the authorization header on the request!
private string ReadTokenFromHeader()
{
string token = null;
string authorization = Request.Headers["Authorization"];
//If we don't find the authorization header return null
if (string.IsNullOrEmpty(authorization))
{
return null;
}
//get the token from the auth header
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}
return token;
}
}
}
One thing that was missing in Mickael's answer is that scheme needs to be specified in Authorize attribute (If you want to use authorization)
[Authorize(AuthenticationSchemes = "Firebase,Custom", Policy ="FirebaseAdministrators")]
Without AuthenticationSchemes provided, and AddAuthentication() has no parameter, NetCore fails to Authenticate and Request.HttpContext.User.Identity.IsAuthenticated is set to false
Related
I have a gateway API in .NET 6.0.0 (using Ocelot) that needs to authenticate users based on either the SSO (JWT generated and authenticated using Azure Authority in the token) or the special id generated by our app ecosystem for users that use the standard login method. Ocelot only allows specifying one AuthenticationProviderKey per endpoint. Because of that, I wanted to create a custom authentication handler that would authenticate a route based on whether there is a JWT token or the special id present in the request. The problem happens when it comes to moving SSO to that custom handler. Since it's generated by an external source and the app uses the configuration from Microsoft for that, I have no idea how to move it to the custom handler. The current state of the Program.cs - the part that handles token authentication:
var authenticationProviderKey = "AzureAdJwtBearer";
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(authenticationProviderKey, opt =>
{
opt.Audience = config.GetValue<string>("AzureAd:Audience");
opt.Authority = $"address1" + config.GetValue<string>("AzureAd:TenantId"); ;
opt.MetadataAddress = $"address2" + config.GetValue<string>("AzureAd:TenantId") + "/v2.0/.well-known/openid-configuration";
});
What I tried to do but I can't figure out - Program.cs:
builder.Services.AddAuthentication(options => options.DefaultScheme = TwoAuthSchemeOptions.Name)
.AddScheme<TwoAuthsSchemeOptions, TwoAuthsHandler>(TwoAuthsSchemeOptions.Name, options => { });
TwoAuthsSchemeOptions.cs:
public class TwoAuthsSchemeOptions:AuthenticationSchemeOptions
{
public const string Name = "TwoAuthsScheme";
}
TwoAuthsHandler.cs:
public class TwoAuthsHandler : AuthenticationHandler<TwoAuthsSchemeOptions>
{
public TwoAuthsHandler(IOptionsMonitor<TwoAuthsSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var loginTypeHeader = Request.Headers["logintype"].ToString();
switch (loginTypeHeader)
{
case "sso":
try
{
//I would like to use the authentication here that is used by default when setting up the original Program.cs
//in this case
return Task.FromResult(AuthenticateResult.Success(ticket));
}
catch(Exception ex)
{
return Task.FromResult(AuthenticateResult.Fail("User authentication failed or session expired"));
}
case "special":
try
{
//Special id authentication that I have implemented
return Task.FromResult(AuthenticateResult.Success(ticket));
}
catch
{
return Task.FromResult(AuthenticateResult.Fail("User authentication failed or session expired"));
}
default:
return Task.FromResult(AuthenticateResult.Fail("User authentication failed or session expired"));
}
}
}
Based on the case I either want that special id authentication or the SSO authentication however I can't use this:
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secret")),
ValidateIssuer = true,
ValidIssuer = "someissuer",
ValidAudience = "someaudience",
ValidateAudience = true,
ClockSkew = TimeSpan.Zero
}, out SecurityToken securityToken);
Because the token comes from the outside, not from the app. All the authentication happens based on opt.Authority and opt.MetadataAddress option for SSO in the original setup so I don't see how I can implement it myself.
Create the custom authentication handler to handle the special authentication.
Define a policy schema and set the ForwardDefaultSelector delegate
Use the policy schema name in Ocelot.json file (AuthSelector in the example below)
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = ...;
options.DefaultSignInScheme = ...;
options.DefaultChallengeScheme = ...;
})
.AddPolicyScheme(
authenticationScheme: "AuthSelector",
displayName: "AuthSelector",
options => options.ForwardDefaultSelector = httpContext =>
{
if (httpContext.Request.Headers.ContainsKey("xxx"))
{
return "special";
}
return "AzureAdJwtBearer";
})
.AddJwtBearer("AzureAdJwtBearer", ...)
.AddScheme<SpecialAuthOptions, SpecialAuthHandler>("special", options => { });
I have an ASP.NET Core Web API targeting 2.2, and I'm having trouble handling an authentication request in the event that it has failed. The authentication request needs to pass a valid refresh token. My code is handling the failed event when the JWT has expired (I also provide a lifetime validator), and then issue a new JWT and refresh token when the provided refresh token is correctly validated, and thus consumed. However, the pipeline does not continue executing and instead returns a 403 regardless of my code calling Success() on the context.
Here is the relevant Startup.cs section:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Authentication:JwtSecret"])),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
LifetimeValidator = l10n.Api.Code.Auth.Bearer.JwtBearerExtensions.LifetimeValidator
};
options.Authority = Configuration["Authentication:JwtAuthority"];
options.ClaimsIssuer = Configuration["Authentication:JwtIssuer"];
options.Events = new JwtBearerEvents();
options.Events.OnAuthenticationFailed = async context => await context.AuthenticationFailed();
})
And the fail handler:
public static async Task AuthenticationFailed(this AuthenticationFailedContext context)
{
ILoggerFactory loggerFactory = context.HttpContext.RequestServices.GetService<ILoggerFactory>();
ILogger logger = loggerFactory.CreateLogger(nameof(JwtBearerExtensions));
string refreshToken = context.HttpContext.Request.Cookies[Defaults.RefreshTokenCookie];
if (string.IsNullOrEmpty(refreshToken))
{
logger.LogWarning("No refresh token supplied with invalid JWT, Cookies are {0}", string.Join(", ", context.HttpContext.Request.Cookies.Keys));
return;
}
logger.LogInformation("Processing refresh token '{0}'.", refreshToken);
IConfiguration configuration = context.HttpContext.RequestServices.GetService<IConfiguration>();
IUserService userService = context.HttpContext.RequestServices.GetService<IUserService>();
ITokenHandler handler = context.HttpContext.RequestServices.GetService<ITokenHandler>();
long? userId = await handler.ValidateRefreshToken(refreshToken);
if (userId.HasValue)
{
User user = await userService.GetUserAsync(userId.Value);
refreshToken = await handler.GenerateRefreshToken(userId.Value);
string jwtToken = BuildJwtToken(user, configuration);
context.HttpContext.Response.AddBearerAuthorization(jwtToken);
context.HttpContext.Response.AddRefreshTokenCookie(refreshToken);
context.Principal = new ClaimsPrincipal(BuildClaimsIdentity(user));
context.Success();
}
}
When inspecting the 403 result in Postman, I can see the new JWT in the Authorization header, and the new refresh token in the cookie. Those are correctly attributed to the Response object. It's just the pipeline that abandons the rest of the processing, and my controller action is never invoked.
How do I allow the request to continue and complete, returning the expected JSON and also issuing (or refreshing) a new auth session?
The solution was to use the correct constructor overload for the ClaimIdentity by passing in the authentication type. More information at this github issue.
For those interested, in this solution, the 403 result happened because of app.UseAuthorization(), in which a policy was failing to pass as a result of the authentication issue. In the github issue post, the purpose-built repro did not have this and resulted in the 401 instead.
I need the Authorize attribute in our Controller can accept two different tokens.
One token, is provided from one private ADFS, and other token is provided from AzureAd.
Several Ionic clients go to over ADFS, other Ionic clients go to over Azure AD
My dev scenario: ASP.NET Core 2.2 Web API
My actual startup.cs (abbreviated)
ConfigureService()
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer((options =>
{
options.Audience = Configuration["Adfs:Audience"];
options.Authority = Configuration["Adfs:Issuer"];
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
}));
}
I need here the other Authentication with AzureAD. How?
The Configure method of Startup.cs
Configure(…)
{
app.UseAuthentication()
}
With this code, only can access the ADFS Token and this users, can obtains result from the controllers. However, the AzureAD user's can't obtain access
I don't know how make this code for double token authorization, and our controllers can response if one token is from ADFS or other token is from AzureAD
You can set multiple JWT Bearer Authentication with different schema name :
services.AddAuthentication()
.AddJwtBearer("ADFS",options =>
{
options.Audience = Configuration["Adfs:Audience"];
options.Authority = Configuration["Adfs:Issuer"];
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
})
.AddJwtBearer("AAD", options =>
{
//AAD jwt validation configuration
});
If you want to make your controller/action to accept two jwt tokens , tokens from AAD or ADFS are ok to access your controller/action , you can make a policy to let both the AAD and ADFS authentication schemes tried to authenticate the request :
services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("AAD", "ADFS")
.Build();
});
In addition , if you want to know which schema the token is from , you can check the particular claim in user's identity , or directly add authentication schema value to user claims in events :
options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents
{
OnTokenValidated = (context) =>
{
var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
//add your custom claims here
claimsIdentity.AddClaim(new Claim("schema", "AAD"));
return Task.FromResult(0);
}
};
And get in action after authentication :
var result = User.Claims.Where(c=>c.Type=="schema").FirstOrDefault().Value;
I have an Asp Net Core API without any controller implementation. Client (Auth0 implementation) is passing a JWT token (RS256 alg) that I need to verify if signature is valid or not. I have gone through the Auth0 official documentation that suggest to implement JwtBearer and set the app to UseAuthentication in startup configuration
Microsoft.AspNetCore.Authentication.JwtBearer
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// 1. Add Authentication Services
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = "https://myapi.auth0.com/";
options.Audience = "API/Endpoint";
});
}
As mentioned above, there is no controller in this API, I can't decorate the method with Authorize attrubute so I am left with the option of verifying this signature manually. For this reason, I have been through stack overflow posts where people have mentioned different approaches such as the use of
System.IdentityModel.Tokens.Jwt
while other have opposed to it and suggested to use low level implementation etc. I have tried couple but no success so far.
Let's say following method is the entry point of the API that receives the JWT token. Would please someone tell me what I need to do in order to verify the signature manually
public Task InvokeAsync(HttpContext context)
{
var accessToken = context.Request.Headers["Authorization"];
// Here I wan't to verify the signature?
// This token has RS256 alg
}
Following is the JWT decoded result
You could do something like this:
public Task InvokeAsync(HttpContext context)
{
var accessToken = context.Request.Headers["Authorization"];
var secretKey = "Insert your secret key here";
var validationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true;
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
// Add any other validations: issuer, audience, lifetime, etc
}
var handler = new JwtSecurityTokenHandler();
var principal = handler.ValidateToken(accessToken, validationParameters, out var validToken);
JwtSecurityToken validJwt = validToken as JwtSecurityToken;
if (validJwt == null)
{
throw new ArgumentException("Invalid JWT");
}
if (!validJwt.Header.Alg.Equals(SecurityAlgorithms.RsaSha256Signature, StringComparison.Ordinal))
{
throw new ArgumentException("Algorithm must be RS256");
}
// Add any validations which cannot be included into TokenValidationParameters
// Validation passed, continue with your logic
}
It is based on this article, which explains how to validate jwt tokens received through cookies. Although the objective is different from yours, the way of validating tokens can be applied to your problem.
I am implementing security on an ASP.NET Core 1.0.1 application, which is used as a Web API. I am trying to understand if and how to implement 2 different authentication schemes.
Ideally, I would like to allow authentication via Azure Active Directory or via username/password for specific back-end services that contact the application.
Is it possible to configure ASP.NET Core for such a setup where an endpoint either authenticates through Azure AD or JWT token?
I tried with something like this, but upon calling the generate token endpoint, I get a 500 with absolutely no information. Removing the Azure AD configuration makes the endpoint work perfectly:
services.AddAuthorization(configuration =>
{
configuration.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
configuration.AddPolicy("OpenIdConnect", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(OpenIdConnectDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAD:ClientId"],
Authority
= Configuration["Authentication:AzureAd:AADInstance"]
+ Configuration["Authentication:AzureAd:TenantId"],
ResponseType = OpenIdConnectResponseType.IdToken,
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme
});
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ClockSkew = TimeSpan.FromMinutes(1),
IssuerSigningKey = TokenAuthenticationOptions.Credentials.Key,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = TokenAuthenticationOptions.Audience,
ValidIssuer = TokenAuthenticationOptions.Issuer
}
});
Use the OpenIdConnectDefaults.AuthenticationScheme constant when you add the authorization policy and when you add the authentication middleware.
Here you are using OpenIdConnectDefaults. Good. Keep that line.
services.AddAuthorization(configuration =>
{
...
configuration.AddPolicy("OpenIdConnect", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(OpenIdConnectDefaults.AuthenticationScheme) // keep
.RequireAuthenticatedUser().Build());
});
Here you are using CookieAuthenticationDefaults. Delete that line.
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
...
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme // delete
});
Why?
When your OpenIdConnect authorization policy runs, it will look for an authentication scheme named OpenIdConnectDefaults.AuthenticationScheme. It will not find one, because the registered OpenIdConnect middleware is named CookieAuthenticationDefaults.AuthenticationScheme. If you delete that errant line, then the code will automatically use the appropriate default.
Edit: Commentary on the sample
A second reasonable solution
The linked sample application from the comments calls services.AddAuthentication and sets SignInScheme to "Cookies". That changes the default sign in scheme for all of the authentication middleware. Result: the call to app.UseOpenIdConnectAuthentication is now equivalent to this:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme
}
That is exactly what Camilo had in the first place. So why did my answer work?
My answer worked because it does not matter what SignInScheme name we choose; what matters is that those names are consistent. If we set our OpenIdConnect authentication sign in scheme to "Cookies", then when adding an authorization policy, we need to ask for that scheme by name like this:
services.AddAuthorization(configuration =>
{
...
configuration.AddPolicy("OpenIdConnect", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme) <----
.RequireAuthenticatedUser().Build());
});
A third reasonable solution
To emphasize the importance of consistency, here is a third reasonable solution that uses an arbitrary sign in scheme name.
services.AddAuthorization(configuration =>
{
configuration.AddPolicy("OpenIdConnect", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("Foobar")
.RequireAuthenticatedUser().Build());
});
Here you are using CookieAuthenticationDefaults. Delete that line.
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
SignInScheme = "Foobar"
});