RestSharp with JWT-authentication doesn't work - c#

This is the page where I "learned" how to do it: https://stormpath.com/blog/token-authentication-asp-net-core
But for me this is not working (doesn't work with Fiddler, too)
There is this controller for my ApplicationUser-model:
[Authorize] //works when it's not set, doesn't work when it's set
[Route("api/[controller]")]
public class ApplicationUserController : Controller
{
private IRepository<ApplicationUser> _applicationUserRepository;
public ApplicationUserController(IRepository<ApplicationUser> applicationUserRepository)
{
_applicationUserRepository = applicationUserRepository;
}
[HttpGet("{id}")]
public ApplicationUser Get(int id)
{
return _applicationUserRepository.Get(id);
}
}
and there's my wrapper for RestSharp to get all applicationusers:
public Task<T> GetResponseContentAsync<T>(string resource, int id) where T : new()
{
RestRequest request = new RestRequest($"{resource}/{{id}}", Method.GET);
request.AddUrlSegment("id", id);
if (!AuthenticationToken.IsNullOrEmpty(true))
{
request.AddHeader("Authorization", string.Format("Bearer {0}", AuthenticationToken));
_client.Authenticator = new JwtAuthenticator(AuthenticationToken);
_client.Authenticator.Authenticate(_client, request);
}
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
_client.ExecuteAsync<T>(request, response =>
{
tcs.SetResult(response.Data);
});
return tcs.Task;
}
From my web-client application I want to login with JWT (Token-Authentication) what works. After login I get e.g. this access_token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJURVNUIiwianRpIjoiZTBjYjE0NjgtYzBmOS00ZTM4LTg4ZjgtMGM4ZjNmYjMyNjZmIiwiaWF0IjoxNDcwOTUwMTA0LCJuYmYiOjE0NzA5NTAxMDQsImV4cCI6MTQ3MDk1MDQwNCwiaXNzIjoiRXhhbXBsZUlzc3VlciIsImF1ZCI6IkV4YW1wbGVBdWRpZW5jZSJ9.a9_JK2SG3vzc6NSOB0mZXqHlM9UAEXUHHrrijAQUsX0
without the Authorize-attribute I get the ApplicationUser, but when setting the Attribute, the result is null (since the web-api is not getting called)
the wrapper-call looks like this:
//this works, token-value is set
string token = new RepositoryCall("http://localhost:54008/").Login("token", "TEST", "TEST123");
string accessToken = JsonConvert.DeserializeObject<Dictionary<string, string>>(token)["access_token"];
ViewData["Result"] = accessToken;
ApplicationUser userAfterLogin = await new RepositoryCall("http://localhost:54008/api")
{ AuthenticationToken = accessToken }
.GetResponseContentAsync<ApplicationUser>("ApplicationUser", 2);
and here userAfterLogin is null.
I'm trying to get the login since two weeks but I still don't get it right..
Any idea what I'm doing wrong? Maybe a wrong request-header-value for authorization?
Update
this is my Startup.Configure where I configured to use the Bearer / JWT:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentity();
var secretKey = "mysupersecret_secretkey!123";
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
// Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715
var options = new TokenProviderOptions
{
Audience = "ExampleAudience",
Issuer = "ExampleIssuer",
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
};
var tokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
// Validate the JWT Issuer (iss) claim
ValidateIssuer = true,
ValidIssuer = "ExampleIssuer",
// Validate the JWT Audience (aud) claim
ValidateAudience = true,
ValidAudience = "ExampleAudience",
// Validate the token expiry
ValidateLifetime = true,
// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = TimeSpan.Zero
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters
});
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = "Cookie",
CookieName = "access_token",
TicketDataFormat = new CustomJwtDataFormat(
SecurityAlgorithms.HmacSha256,
tokenValidationParameters)
});
app.UseMiddleware<TokenProviderMiddleware>(Options.Create(options));
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

If you recive authorizacion error or using postman you realize that you are being asked to redirect to login just decorate your class with:
[Authorize(AuthenticationSchemes = "Bearer")]
By default .Net uses cookie based auth, with that annotation you swicht to token based one

So you are using 2 middlewares for identity. One provided by asp.net identity (cookie based) and another token based. Now both of the middleware use the same attribute for
handling the request [Authorize]. More precisely look at the code here
https://github.com/aspnet/Security/blob/dev/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs
for JWTBearer
and
https://github.com/aspnet/Security/blob/dev/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs
for Cookie
Because both are activated in middleware pipeline the principal will have the data when you send auth token or cookie.
But because both of them are active either of them will return Unauthorized for the request that doesnt have cookie or JwtBearer.
For the solution you are looking for you need to create a middleware on top of existing cookie and token based to route the request to either based on if authorization header is present.

In Fiddler you would see, if you are redirected to login page (it would report 2 Results, one with 302 (redirect) and then the 404 - is that the case?
You have DebugLogger activated, so try AddDebug(LogLevel.Trace) and view the Debug output window, it is very helpful in analysing which of authentication steps fail. It also shows if authentication fails or authorization, and if has a valid token etc. So it points to the direction to look for problems.

Related

Is there a simpler way for creating JWTs in ASP.NET core? aka “Best Practices for JWT Authentication?”

.NET5 has a ton of middleware and infrastructure for authentication. You can include a handful of NuGet packages, wire some configuration in Startup.cs, and your application understands cookies and sessions. All the examples I'm finding online for JWT-based authentication has me manually instantiating a JwtSecurityDescriptor and setting properties inside of it. This really feels like something that should be part of the SignInManager.PasswordSignInAsync flow. Examples would be:
https://www.faciletechnolab.com/Blog/2021/4/5/how-to-implement-jwt-token-authentication-in-aspnet-core-50-web-api-using-jwt
https://weblog.west-wind.com/posts/2021/Mar/09/Role-based-JWT-Tokens-in-ASPNET-Core
http://www.binaryintellect.net/articles/1fdc8b3f-06a1-4f36-8c0b-7852bf850f52.aspx
Am I missing something? Is this a case where the online examples are skipping several steps in favor of "simplifying" the demonstration? Every one of the preceding examples creates the JWT manually but delegates a lot of the interpretation of it to the middleware.
To clarify, I am not looking for instruction on how to validate passwords (I do that with IPasswordHasher) or locate users (I do that with IUserPasswordStore). I am specifically wondering if there is something I am missing to cause ASP.NET to handle the JWT generation workflow for me. I can't tell if the online examples are eliding a lot of functionality "for demonstration" or not (similar to how a lot of guides online put DB calls directly inside controllers for "simplicity").
Generally, when we using JWT authentication, the workflow as below:
Client sends a request (which contains the user information, such as: name and password) to server for token
Server receives the user information and checking for authorization. If validated success, server generates a JWT token.
Client receives the token and stores it somewhere locally.
Client sends the token in the future requests.
Server gets the token from request header, computes Hash again by using a) Header from token b) payload from token c) secret key which server already has.
If ("newly computed hash" = "hash came in token"), token is valid otherwise it is tempered or not valid
You could refer to the following steps to use JWT Authentication In ASP.NET Core.
Configure the authentication schema with JWT bearer options.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddControllersWithViews();
}
In this example, I have stored these values in appsettings.json file.
{
"Jwt": {
"Key": "ThisismySecretKey",
"Issuer": "Test.com"
}
}
Call the app.UseAuthentication() method in the Configure method of startup class.
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
Generate JSON Web Token
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using Test.Models;
namespace Test.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
private IConfiguration _config;
public LoginController(IConfiguration config)
{
_config = config;
}
[AllowAnonymous]
[HttpPost]
public IActionResult Login([FromBody] UserModel login)
{
IActionResult response = Unauthorized();
var user = AuthenticateUser(login);
if (user != null)
{
var tokenString = GenerateJSONWebToken(user);
response = Ok(new { token = tokenString });
}
return response;
}
private string GenerateJSONWebToken(UserModel userInfo)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, userInfo.Username),
new Claim(JwtRegisteredClaimNames.Email, userInfo.EmailAddress),
new Claim("DateOfJoing", userInfo.DateOfJoing.ToString("yyyy-MM-dd")),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(120),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private UserModel AuthenticateUser(UserModel login)
{
UserModel user = null;
//Validate the User Credentials
//Demo Purpose, I have Passed HardCoded User Information
if (login.Username == "Jignesh")
{
user = new UserModel { Username = "Jignesh Trivedi", EmailAddress = "test.btest#gmail.com" };
}
return user;
}
}
}
Then, if you request the "API/login" method to generate the token, you have to passed the following JSON in the request body.
{"username": "Jignesh", "password": "password"}
Then, after getting the JWT token, you could add the "Authorization" property in the request header when you access other API controller(with the [Authorize] attribute).
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKaWduZXNoIFRyaXZlZGkiLCJlbWFpbCI6InRlc3QuYnRlc3RAZ21haWwuY29tIiwiRGF0ZU9mSm9pbmciOiIwMDAxLTAxLTAxIiwianRpIjoiYzJkNTZjNzQtZTc3Yy00ZmUxLTgyYzAtMzlhYjhmNzFmYzUzIiwiZXhwIjoxNTMyMzU2NjY5LCJpc3MiOiJUZXN0LmNvbSIsImF1ZCI6IlRlc3QuY29tIn0.8hwQ3H9V8mdNYrFZSjbCpWSyR1CNyDYHcGf6GqqCGnY
More detail information, refer the followng links:
JWT Authentication In ASP.NET Core
JWT Validation on separate web Api

Acess token refresh with OIDC in .NET Core 3.1, OnValidatePrincipal is never called/thrown

I am making a POC of a small website that uses Keycloak as an OIDC provider, for now I am just using the "standard" scaffolded website that .NET Core generates. The Privacy page has an authorize attribute so that it can only be accessed if the user is authenticated.
My StartUp.cs looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(i => new CookieAuthenticationOptions
{
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
return OnValidatePrincipal(context);
}
}
})
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "http://localhost:8080/auth/realms/WatchList/";
options.RequireHttpsMetadata = false;
options.ClientId = "ClientID";
options.ClientSecret = "20ea1950-af47-4251-85e9-7c4f33189c77";
options.ResponseType = OpenIdConnectResponseType.Code;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "groups",
ValidateIssuer = true
};
});
services.AddAuthorization();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
private async Task OnValidatePrincipal(CookieValidatePrincipalContext context)
{
const string accessTokenName = "access_token";
const string refreshTokenName = "refresh_token";
const string expirationTokenName = "expires_at";
if (context.Principal.Identity.IsAuthenticated)
{
var exp = context.Properties.GetTokenValue(expirationTokenName);
if (exp != null)
{
var expires = DateTime.Parse(exp, CultureInfo.InvariantCulture).ToUniversalTime();
if (expires < DateTime.UtcNow)
{
// If we don't have the refresh token, then check if this client has set the
// "AllowOfflineAccess" property set in Identity Server and if we have requested
// the "OpenIdConnectScope.OfflineAccess" scope when requesting an access token.
var refreshToken = context.Properties.GetTokenValue(refreshTokenName);
if (refreshToken == null)
{
context.RejectPrincipal();
return;
}
var cancellationToken = context.HttpContext.RequestAborted;
// Obtain the OpenIdConnect options that have been registered with the
// "AddOpenIdConnect" call. Make sure we get the same scheme that has
// been passed to the "AddOpenIdConnect" call.
//
// TODO: Cache the token client options
// The OpenId Connect configuration will not change, unless there has
// been a change to the client's settings. In that case, it is a good
// idea not to refresh and make sure the user does re-authenticate.
var serviceProvider = context.HttpContext.RequestServices;
var openIdConnectOptions = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenIdConnectOptions>>().Get("Cookies");
var configuration = openIdConnectOptions.Configuration ?? await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(cancellationToken).ConfigureAwait(false);
// Set the proper token client options
var tokenClientOptions = new TokenClientOptions
{
Address = configuration.TokenEndpoint,
ClientId = openIdConnectOptions.ClientId,
ClientSecret = openIdConnectOptions.ClientSecret
};
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
using var httpClient = httpClientFactory.CreateClient();
var tokenClient = new TokenClient(httpClient, tokenClientOptions);
var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken, cancellationToken: cancellationToken).ConfigureAwait(false);
if (tokenResponse.IsError)
{
context.RejectPrincipal();
return;
}
// Update the tokens
var expirationValue = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn).ToString("o", CultureInfo.InvariantCulture);
context.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = refreshTokenName, Value = tokenResponse.RefreshToken },
new AuthenticationToken { Name = accessTokenName, Value = tokenResponse.AccessToken },
new AuthenticationToken { Name = expirationTokenName, Value = expirationValue }
});
// Update the cookie with the new tokens
context.ShouldRenew = true;
}
}
}
}
}
This all works as I expect it would: i can access the Home page without having to authenticate but if I want access to the Privacy page I am redirected to the Keycloak login page and after successfully logging in I have access to the Privacy Page as well.
However I want to access the Acces Token and Refresh Token (because I want to use the access token to access an api) as well, this in itself isn't a problem either:
[Authorize]
public IActionResult Privacy()
{
var accessToken = HttpContext.GetTokenAsync("access_token").Result;
Debug.WriteLine(accessToken);
var refreshToken = HttpContext.GetTokenAsync("refresh_token").Result;
Debug.WriteLine(refreshToken);
return View();
}
The problem is that these might be expired and I want to retrieve a new access token, but this should happen automatically.
I've tried the solution that was proposed here, and like you can see in my StartUp.cs, I "catch" the OnValidatePrincipalEvent and execute the code below.
But here is the problem: for some reason this event is never called. I would expect it to be called everytime I access the Privacy page. However this isn't the case, the event seems not be thrown ever.
Things I've tried:
I've followed this comment: https://stackoverflow.com/a/61396951/9784279, that seems to do exactly what I want, except for the fact that the event isn't thrown.
Checked if other events work like expected: I have confirmed that OnTokenValidated and OnTokenResponseReceived are called like expected, however these are OpenIdConnectEvents and not CookieAuthenticationEvents.
Tried playing with the StartUp.cs file, mainly the order/place of app.UseAuthentication() and app.UseAuthorization(). But this didn't change anything.
I thought maybe I could create an extension method GetOrUpdateTokenAsync on HttpContext that retrieves the access token and if it is expired it will retrieve a new one. The problem is that I don't think I have access to StoreTokens in
CookieValidatePrincipalContext
[1]: https://stackoverflow.com/a/61396951/9784279

Configure ASP.NET Core authentication middleware to always return 401 when Authorization header is invalid

I created a default ASP.NET Core (2.1) empty web application, and added JWT bearer authentication. The Startup.cs class looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var keyByteArray = Convert.FromBase64String(Constants.JwtSecretKey);
var signinKey = new SymmetricSecurityKey(keyByteArray);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = Constants.Audience,
ValidateIssuer = true,
ValidIssuer = Constants.Issuer,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = signinKey
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseMvc();
}
The controller looks like this:
[Route("values")]
[ApiController]
public class ValuesController : Controller
{
[HttpGet("")]
public IActionResult Get()
{
return new StatusCodeResult(StatusCodes.Status200OK);
}
}
I would like my endpoint to return a 401 HTTP status code when the Authorization header is present but invalid (with an error message containing the failure reason) - but NOT when the header is missing. Is it possible to configure the middleware in such a way?
I tried fiddling with the OnAuthenticationFailed event from JwtBearerEvents, but couldn't get anything done with it.
options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
// Not fired when the Authorization header is "Bearer foo",
// but fired when the header is "Bearer foo.bar.baz"
return Task.CompletedTask;
}
};
According to this document, JwtBearerEvents only support the below 4 kind of event
OnAuthenticationFailed(this one only triggered after Token failed to be Authenticated, in your case I think you need a validation instead of authentication)
OnChallenge
OnMessageReceived
OnTokenValidated(this one however only triggered after Token is successfully validated, that's why I think it does not work for your case)
What you want should be something like OnTokenValidateFailed but it is not there, one workaround would be register the OnMessageReceived events and try to validate token there
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
options.Events=new JwtBearerEvents(){
OnMessageReceived =context=>{
var header =context.Request.Headers["Authorization"];
//Your validation logic here
//if validate failed
//{
//context.Response.StatusCode=401;
//}
return Task.CompletedTask;
}
};
});
This way you can validate token by yourself however this approach might not short circuit the whole request pipeline and might conflict with other middleware(Not tested tested with other middleware but I assume this might happen)
Another workround would be like below
Add below code in your Config of start.cs in your API project
app.Use(async(context,next)=>{
var authHeader=context.Request.Headers["Authorization"];
//Your validation here
if(validation failed)
{
context.Response.StatusCode=401;
await context.Response.CompleteAsync();
}
else{
await next.Invoke();}
});
Add this before UseAuthenticate to overwrite default Authernticate middleware
First workaournd would trigger when it comes to JwtBearerToken authenticate while the second one will always be triggered since it is registered in the asp.net core middleware pipeline, so it is your choice to decidede to use which one

AuthenticationFailedContext.Success() has no effect when handling JwtBearerEvents.OnAuthenticationFailed

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.

Use multiple JWT Bearer Authentication

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

Categories