ASP.NET Core JWT and Claims - jwt

I have a question regarding JWT authentication in ASP.NET Core and Claims, because I don't know if I get everything correctly.
When I create a JWT token in ASP.NET I add some Claims, some of which can be custom. What happens when the request with JWT token is sent from the client to API. How is User.Claims filled ? Does it use the claims that are read from JWT?
I would like to create a custom Identity provider ( don't want to use this provided by ASP.NET), with my own tables for user data, roles etc. I don't want store all important data required to fulfill the policy in JWT token (the amount of information stored in token matters, as well as security matters). Is it possible to store only basic claims (like user id, name etc) in JWT token, and then re-fetch other required data DB/ Cache? Along with that, I would like to use the standard mechanism for [Authorize] and the Policy mechanism.
How to make this all work: Custom User Identity + JWT + Standard ASP.NET policy-based authorization + claims fetched from DB/Cache on every request? How to achieve this?

Asp Net Core
First step is write the method that configure Jwt authentication:
// Configure authentication with JWT (Json Web Token).
public void ConfigureJwtAuthService(IServiceCollection services)
{
// Enable the use of an [Authorize(AuthenticationSchemes =
// JwtBearerDefaults.AuthenticationScheme)]
// attribute on methods and classes to protect.
services.AddAuthentication().AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = JwtController.SecurityKey,
ValidAudience = JwtController.Audience,
ValidIssuer = JwtController.Issuer,
// When receiving a token, check that we've signed it.
ValidateIssuerSigningKey = true,
// When receiving a token, check that it is still valid.
ValidateLifetime = true,
// This defines the maximum allowable clock skew when validating
// the lifetime. As we're creating the tokens locally and validating
// them on the same machines which should have synchronised time,
// this can be set to zero.
ClockSkew = TimeSpan.FromMinutes(0)
};
});
}
Now inside the ConfigureServices() method of the Startup.cs, we can call ConfigureJwtAuthService() method to configure the Jwt authentication.
This is the complete Startup.cs:
using System;
using Autofac;
using ExpertCodeBlogWebApp.Controllers;
using ExpertCodeBlogWebApp.Domain;
using ExpertCodeBlogWebApp.Domain.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SpaServices.Webpack;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
namespace ExpertCodeBlogWebApp
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add
// services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Configure jwt autenticazione
ConfigureJwtAuthService(services);
// Repositories
services.AddScoped<IUserRepository, UserRepository>();
// Create the Autofac container builder for dependency injection
var builder = new ContainerBuilder();
// Add any Autofac modules or registrations.
builder.RegisterModule(new AutofacModule());
// Return ServiceProvider
var serviceProvider = services.BuildServiceProvider();
return serviceProvider;
}
// Configure authentication with JWT (Json Web Token).
public void ConfigureJwtAuthService(IServiceCollection services)
{
// Enable the use of an [Authorize(AuthenticationSchemes =
// JwtBearerDefaults.AuthenticationScheme)]
// attribute on methods and classes to protect.
services.AddAuthentication().AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = JwtController.SecurityKey,
ValidAudience = JwtController.Audience,
ValidIssuer = JwtController.Issuer,
// When receiving a token, check that we've signed it.
ValidateIssuerSigningKey = true,
// When receiving a token, check that it is still valid.
ValidateLifetime = true,
// This defines the maximum allowable clock skew when validating
// the lifetime.
// As we're creating the tokens locally and validating them on the
// same machines which should have synchronised time, this can be
// set to zero.
ClockSkew = TimeSpan.FromMinutes(0)
};
});
}
// This method gets called by the runtime. Use this method to configure
// the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
{
HotModuleReplacement = true
});
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
}
}
// For dependency injection.
public class AutofacModule : Module
{
// Dependency Injection with Autofact
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<UserRepository>().As<IUserRepository>()
.SingleInstance();
}
}
}
The JwtController.cs
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using AutoMapper;
using ExpertCodeBlogWebApp.Domain;
using ExpertCodeBlogWebApp.Domain.Interfaces;
using ExpertCodeBlogWebApp.Domain.Models;
using ExpertCodeBlogWebApp.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
namespace ExpertCodeBlogWebApp.Controllers
{
[Route("api/[controller]")]
public class JwtController : Controller
{
#region Private Members
// JWT-related members
private TimeSpan TokenExpiration;
private SigningCredentials SigningCredentials;
// EF and Identity members, available through DI
private MyDbContext DbContext;
private IUserRepository _userRepository;
private readonly ILogger _logger;
#endregion Private Members
#region Static Members
private static readonly string PrivateKey = "my_PrivateKey";
public static readonly SymmetricSecurityKey SecurityKey =
new SymmetricSecurityKey(Encoding.ASCII.GetBytes(PrivateKey));
public static readonly string Issuer = "my_Issuer";
public static readonly string Audience = "my_Audience";
#endregion Static Members
#region Constructor
// I have used Autofac in the Startup.cs for dependency injection)
public JwtController(
MyDbContext dbContext,
IUserRepository userRepository,
ILogger<JwtController> logger)
{
_logger = logger;
_userRepository = userRepository;
// Instantiate JWT-related members
TokenExpiration = TimeSpan.FromMinutes(10);
SigningCredentials = new SigningCredentials(SecurityKey,
SecurityAlgorithms.HmacSha256);
// Instantiate through Dependency Injection with Autofact
DbContext = dbContext;
}
#endregion Constructor
#region Public Methods
// Manages the request for a new authentication or the refresh of an
// already established one
[HttpPost("token")]
public async Task<IActionResult>
Authentication([FromBody]JwtRequestViewModel jwt)
{
if (ModelState.IsValid)
{
string grantType = jwt.GrantType;
if (grantType == "password")
{
string userName = jwt.UserName;
string password = jwt.Password;
// Password check required
var user = await
_userRepository.GetUserInfoWithCheckPwd(userName, password);
// Check if user is expired (check the ExpireDate property)
if (UserExpired(user))
return BadRequest($"Account of {user.Name} expired!");
if (UserEnabled(user))
return await GenerateToken(user);
else
return BadRequest("User name or password invalid.");
}
}
else if (grantType == "refresh_token")
{
string userName = jwt.UserName;
// Refresh token (no password check required)
var user = await _userRepository.GetUserInfoByName(userName);
// Check if user is expired (check the ExpireDate property)
if (UserExpired(user))
return BadRequest($"Account of {user.Name} expired!");
string token = jwt.Token;
if (token == user.Token)
{
// Generate token and send it via a json-formatted string
return await GenerateToken(user);
}
else
{
return BadRequest("User token invalid.");
}
}
else
return BadRequest("Authentication type invalid.");
}
else
return BadRequest("Request invalid.");
}
#endregion Public Methods
#region Private Methods
private bool UserExpired(Users utente)
{
if (utente != null)
return utente.ExpireDate.CompareTo(DateTime.Now) < 0;
return true;
}
private bool UserEnabled(Users utente)
{
if (utente != null)
return utente.Enabled == true;
return false;
}
private JsonSerializerSettings DefaultJsonSettings
{
get
{
return new JsonSerializerSettings()
{
Formatting = Formatting.Indented
};
}
}
private async Task<IActionResult> GenerateToken(Users user)
{
try
{
if (user != null)
{
var handler = new JwtSecurityTokenHandler();
DateTime newTokenExpiration = DateTime.Now.Add(TokenExpiration);
ClaimsIdentity identity = new ClaimsIdentity(
new GenericIdentity(user.Name, "TokenAuth"),
new[] { new Claim("ID", user.Id.ToString())}
);
var securityToken = handler.CreateToken(new SecurityTokenDescriptor
{
Issuer = JwtController.Issuer,
Audience = JwtController.Audience,
SigningCredentials = SigningCredentials,
Subject = identity,
Expires = newTokenExpiration
});
string encodedToken = handler.WriteToken(securityToken);
// Update token data on database
await _userRepository.UpdateTokenData(user.Name, encodedToken,
newTokenExpiration);
// Build the json response
// (I use Automapper to maps an object into another object)
var jwtResponse = Mapper.Map<JwtResponseViewModel>(user);
jwtResponse.AccessToken = encodedToken;
jwtResponse.Expiration = (int)TokenExpiration.TotalSeconds;
return Ok(jwtResponse);
}
return NotFound();
}
catch(Exception e)
{
return BadRequest(e.Message);
}
}
#endregion
}
}
On my project I use Angular. For call JwtController method by Angular:
login(userName: string, password: string)
{
return this.getLoginEndpoint(userName, password)
.map((response: Response) => this.processLoginResponse(response));
}
getLoginEndpoint(userName: string, password: string): Observable<Response>
{
// Body
// JwtRequest is a model class that I use to send info to the controller
let jwt = new JwtRequest();
jwt.GrantType = "password";
jwt.UserName = userName;
jwt.Password = password;
jwt.ClientId = "my_Issuer";
// Post requiest (I use getAuthHeader that attach to the header the
// authentication token, but it can also be omitted because it is ignored
// by the JwtController
return this.http.post(this.loginUrl, JSON.stringify(jwt),
this.getAuthHeader(true))
}
protected getAuthHeader(includeJsonContentType?: boolean): RequestOptions
{
// Hera I use this.authService.accessToken that is a my service where
// I have store the token received from the server
let headers = new Headers({
'Authorization': 'Bearer ' + this.authService.accessToken });
if (includeJsonContentType)
headers.append("Content-Type", "application/json");
headers.append("Accept", `application/vnd.iman.v01+json,
application/json, text/plain, */*`);
headers.append("App-Version", "01");
return new RequestOptions({ headers: headers });
}
private processLoginResponse(response: Response)
{
// process the response..
}
On the controllers classes (or methods) that you want to be accessible only by authenticated users (not on your JwtController because its method must be accessible by all users) you can set:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
To call from Angular the controller method that require authentication, you need to attach the token into the header with the getAuthHeader() method.
I hope this post can help you.

yes it uses the claim stored in jwt token
look at the httpcontext object for claims that are stored in token when you created the token
this link can also be helpfull https://joonasw.net/view/adding-custom-claims-aspnet-core-2

Related

How do I get the JWT in a Blazor Server App with Microsoft Identity Platform (AAD) to make external API-Management call and authorize with the jwt

The situation I have:
Blazor Server App , .Net6.0.9 with Microsoft Identity Platform.
Blazor Server App is registered in the App Registration on Tenant-1
Client-API-1 is also resigered in the App Registration on Tenant-1
Login actions are done against/with the ClientId of the Client-API-1 registration and work fine.
In the API-Management I've added on the Inbound processing Polecies Validate-jwt like this:
(source of Microsoft)
<policies>
<inbound>
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid!!!">
<openid-config url="https://login.microsoftonline.com/11a14169-89cc-44e8-95d7-xxxxxxxxxxxx/v2.0/.well-known/openid-configuration" />
<required-claims>
<claim name="aud">
<value>{client-id-of-Client-API-1-on-App-Registration}</value>
</claim>
</required-claims>
</validate-jwt>
In Service looks like this:
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Identity.Web;
using System.Net.Http.Headers;
namespace BlazorSAAppJwt.Data
{
public class ApimService : IApimService
{
private AuthenticationStateProvider _authenticationStateProvider { get; set; }
private readonly ITokenAcquisition _tokenAcquisition;
public ApimService(AuthenticationStateProvider AuthenticationStateProvider, ITokenAcquisition tokenAcquisition)
{
_authenticationStateProvider = AuthenticationStateProvider;
_tokenAcquisition = tokenAcquisition;
}
//public async Task<string?> GetResponseAsync(string path, CancellationToken cancellationToken)
public async Task<string?> GetResponseAsync(string path)
{
try
{
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
if (authState?.User?.Identity?.IsAuthenticated ?? false)
{
using var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("https://apimanagement.azure-api.net/");
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.DefaultRequestHeaders.Add("email", authState.User.Identity.Name);
httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "{My APIM suvbscriptionkey}"); // APIM
httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Trace", "true");
// This gets the UserToken to get data from Microsoft Graph for the scopes: User.Read & Mail.Read
var token = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "User.Read", "Mail.Read" });
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(token);
var dataRequest = await httpClient.GetAsync("https://graph.microsoft.com/beta/me");
string? userDisplayName = "";
if (dataRequest.IsSuccessStatusCode)
{
var userData = System.Text.Json.JsonDocument.Parse(await dataRequest.Content.ReadAsStreamAsync());
userDisplayName = userData.RootElement.GetProperty("displayName").GetString();
}
//Remove the previous Authorization-header for the Microsoft Graph call
httpClient.DefaultRequestHeaders.Remove("Authorization");
//Add the Application token to the Authorization for APIM
//NOTE!!! Here is where the JWT token should be used!!!!
string jwt = "How do I get the jwt here to add and send to the APIM";
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");
//HttpResponseMessage response = await httpClient.GetAsync($"{path.ToLower()}", cancellationToken);
HttpResponseMessage response = await httpClient.GetAsync($"{path.ToLower()}");
if (response.IsSuccessStatusCode)
{
string clientApiResult = await response.Content.ReadAsStringAsync();
return clientApiResult;
}
else
{
throw new UnauthorizedAccessException($"(Graph) User Display Name: {userDisplayName}" +
$"{Environment.NewLine}Response from APIM call: {response}");
}
}
else
{
// "The user is NOT authenticated.";
throw new UnauthorizedAccessException();
}
return default;
}
catch (Exception ex)
{
var iets = ex.Message;
throw;
}
}
}
}
I receive the UserDisplayName from the Graph API-call.
My program.cs
using BlazorSAAppJwt.Data;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
var builder = WebApplication.CreateBuilder(args);
var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ') ?? builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' ');
var azureSection = builder.Configuration.GetSection("AzureAd");
var microsoftGraphSection = builder.Configuration.GetSection("MicrosoftGraph");
// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
//.AddMicrosoftGraph(microsoftGraphSection) // Nuget Microsoft.Identity.Web.MicrosoftGraph
.AddInMemoryTokenCaches();
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddTokenAcquisition();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddSingleton<ApimService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
What do I miss, and how do I setup my Blasor Server App to use the JWT token?
EDIT:
The API-calls on the APIM is not going to change and will call the Client-Api that is not exposed to the internet.
Who knows that the call:
var token = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "User.Read", "Mail.Read" });
Retruns the JWT... It does and what it is you need to use in the request header
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(token);
And make the call
HttpResponseMessage response = await httpClient.GetAsync($"{path.ToLower()}", cancellationToken);

Authorize attribute authorizing any JWT token for the controller in asp.net core?

I am trying to use JWT authentication in my ASP.NET CORE project.
Step-1: I have added the JWT service in ConfigureServices method of Starup.cs file.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"])),
RequireExpirationTime = true,
ValidateLifetime = true,
ValidateAudience = false,
ValidateActor = false,
ValidateIssuer = false
};
});
And added below code in the Configure method:
app.UseAuthentication();
Step-2: Sending the jwt token while login.
public class LoginRepository
{
public LoginRepository()
{
//TODO: Dependency to MongoDB will be initialized here
}
public LoginStatus Authenticate(string username, string password)
{
LoginStatus loginStatus = new LoginStatus();
string secretKey = ConfigurationManager.AppSetting["Jwt:SecretKey"];
int tokenExpirationHours = int.Parse(ConfigurationManager.AppSetting["Jwt:TokenExpirationHours"]);
//TODO: Need to add the userID in the payload. UserID will come from Database
Dictionary<string, string> payload = new Dictionary<string, string>() {
{ "UserName", username}
};
//TODO: Need to check the username and password in Database and then generate the token
loginStatus.Token = JwtTokenHelper.GenerateJwtToken(secretKey, payload, tokenExpirationHours);
return loginStatus;
}
}
Here is the JwtTokenHelper:
public class JwtTokenHelper
{
public static string GenerateJwtToken(string secretKey, IReadOnlyDictionary<string, string> payloadContents, int tokenExpirationHours)
{
JwtSecurityTokenHandler jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
var payloadClaims = payloadContents.Select(c => new Claim(c.Key, c.Value));
var payload = new JwtPayload("", "", payloadClaims, DateTime.Now, DateTime.Now.AddHours(tokenExpirationHours));
var header = new JwtHeader(signingCredentials);
var securityToken = new JwtSecurityToken(header, payload);
return jwtSecurityTokenHandler.WriteToken(securityToken);
}
}
Here, I am getting the JWT token successfully.
Step-3: Now, I tried to authorize a controller and it is working nice when I have given the token in the Authorization header from Postman.
namespace SampleAPI.Controllers
{
[Authorize]
[Produces("application/json")]
[Route("api/Test")]
public class TestController : Controller
{
[HttpGet]
[Route("Testing")]
public IActionResult Testing()
{
return Ok("Yes");
}
}
}
But, if I change something in the JWT token and again hit this endpoint, it is returning "Yes" means it is saying that the jwt token is valid. But I have changed some parts of that token before sending in the Authorization header.
What am I missing here? Can you please point me out what more steps should I follow?

Generating a JWT token using AuthenticateAsync

I am trying to login using ClaimsPrincipal and then fetch a JWT in .net core 2.0. With my current code, I get the error from the result of the SignInAsync function:
"No IAuthenticationSignInHandler is configured to handle sign in for the scheme: Bearer"
Here is the controller I am currently using:
[Route("Login/{username}")]
public async Task<IActionResult> Login(string username)
{
var userClaims = new List<Claim>
{
new Claim(ClaimTypes.Name, username)
};
var principal = new ClaimsPrincipal(new ClaimsIdentity(userClaims));
var sign = HttpContext.SignInAsync(principal);
await sign;
var res = await HttpContext.AuthenticateAsync();
var token = await HttpContext.GetTokenAsync("access_token");
return Json(token);
}
The login portion was tested and works well with cookies. However when I use the following code with JwtBearerDefaults.AuthenticationScheme in my startup.cs:
services.AddAuthentication(config => {
config.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
config.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(config =>
{
config.TokenValidationParameters = Token.tokenValidationParameters;
config.RequireHttpsMetadata = false;
config.SaveToken = true;
});
I get the error from the result of the SignInAsync function:
"No IAuthenticationSignInHandler is configured to handle sign in for the scheme: Bearer"
My Token class was created with the help of a code I found online (at JWT on .NET Core 2.0) and is defined as follows:
public static class Token
{
public static TokenValidationParameters tokenValidationParameters {
get
{
return new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = GetSignInKey(),
ValidateIssuer = true,
ValidIssuer = GetIssuer(),
ValidateAudience = true,
ValidAudience = GetAudience(),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
}
}
static private SymmetricSecurityKey GetSignInKey()
{
const string secretKey = "very_long_very_secret_secret";
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
return signingKey;
}
static private string GetIssuer()
{
return "issuer";
}
static private string GetAudience()
{
return "audience";
}
}
If I understand it correctly from looking at the source code for JwtBearerHandler, it does not implement IAuthenticationSignInHandler, which is why you are getting this error. Call to SignInAsync is designed to persist authentication information, such as created auth cookie which, for instance, is exactly what CookieAuthenticationHandler does. But for JWT there is no single well-known place to store the token, hence no reason to call SignInAsync at all. Instead of that, grab the token and pass it back to the browser. Assuming you are redirecting, you can tuck it into a query string. Assuming browser application is an SPA (i.e. Angular-based) and you need tokens for AJAX calls, you should store token in the SPA and send it with every API request. There are some good tutorials on how to use JWT with SPAs of different types, such as this: https://medium.com/beautiful-angular/angular-2-and-jwt-authentication-d30c21a2f24f
Keep in mind that JwtBearerHandler expects to find Authentication header with Bearer in it, so if your AJAX calls are placing token in query string, you will need to supply JwtBearerEvents.OnMessageReceived implementation that will take token from query string and put it in the header.
A signed token can be created using the JwtSecurityTokenHandler.
var handler = new JwtSecurityTokenHandler();
var jwt = handler.CreateJwtSecurityToken(new SecurityTokenDescriptor
{
Expires = DateTime.UtcNow.Add(Expiary),
Subject = new ClaimsIdentity(claims, "local"),
SigningCredentials = new SigningCredentials(SigningKey, SecurityAlgorithms.HmacSha256)
});
return handler.WriteToken(jwt);

Good algorithm for downloading and caching public signing key for token validation

In a katana web api, I'm using:
appBuilder.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://...",
ValidationMode = ValidationMode.Local,
RequiredScopes = new[] { "..." },
});
This appears to nicely find the public signing key(s) from the authority and (hopefully?) cache them, etc. Although I haven't tried it, I understand there's an equivalent for ASP.NET Core.
Now I need to do the same thing but not in a web api middleware. So I'm trying to find the code that IdentityServer3.AccessTokenValidation.IdentityServerBearerTokenValidationMiddleware uses to do this. All I can see is that it calls UseOAuthBearerAuthentication, which seems to be in Microsoft.Owin.Security.OAuth. I haven't been able to find a version of that source code that seems to match the signature.
It seems to me that under the covers, somebody is probably using System.IdentityModel.Tokens.JwtSecurityTokenHandler and putting a nice little snippet of code into the IssuerSigningKeyResolver of the TokenValidationParameters. That nice little snippet is getting the signing keys from the metadata address. Anybody know what that code is, or have a piece that works well? Obviously, I could write it but I hate to re-invent the wheel, plus mine would be un-tested.
We are using this class that plugs into the JWT handler:
https://github.com/IdentityServer/IdentityServer3.AccessTokenValidation/blob/master/source/AccessTokenValidation/Plumbing/DiscoveryDocumentIssuerSecurityTokenProvider.cs
Thanks, leastprivilege. Looking deeper at your DiscoverydocumentIssuerSecurityTokenProvider class, I found ConfigurationManager<OpenIdConnectConfiguration>. Using that, I have come up with the following helper class for access token validation outside of OWIN middleware.
Feedback solicited!
using Microsoft.IdentityModel.Protocols;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
namespace WpfClient
{
public class AccessTokenValidator
{
protected TokenValidationParameters _accessTokenValidationParameters;
public AccessTokenValidator(string stsRoot)
{
stsRoot = stsRoot.TrimEnd('/');
var discoveryEndpoint = stsRoot + "/.well-known/openid-configuration";
var webHandler = new WebRequestHandler();
var httpClient = new HttpClient(webHandler);
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(discoveryEndpoint, httpClient);
_accessTokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = stsRoot,
RequireSignedTokens = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeyResolver = (string token, SecurityToken securityToken, SecurityKeyIdentifier keyIdentifier, TokenValidationParameters validationParameters) => {
var signingTokens = configurationManager.GetConfigurationAsync().Result.JsonWebKeySet.GetSigningTokens();
foreach (var signingToken in signingTokens)
{
foreach (var clause in keyIdentifier)
{
var key = signingToken.ResolveKeyIdentifierClause(clause);
if (key != null)
{
return key;
}
}
}
return null;
},
RequireExpirationTime = true,
ValidateAudience = false, // See https://github.com/IdentityServer/IdentityServer3/issues/1365: "OAuth2 does not use the term 'audience' ... it instead uses the term 'scope' ... 'audience' and the 'aud' claim are JWT specific concepts."
ValidateLifetime = true,
};
}
public void ValidateAccessToken(string accessToken, IEnumerable<string> requiredScopes, IEnumerable<string> requiredRoles)
{
ClaimsPrincipal claimsPrincipal;
SecurityToken securityToken;
var handler = new JwtSecurityTokenHandler();
claimsPrincipal = handler.ValidateToken(accessToken, _accessTokenValidationParameters, out securityToken);
if (claimsPrincipal == null)
{
throw new NullReferenceException("ClaimsPrincipal object returned is null");
}
RequireClaims("scope", requiredScopes, claimsPrincipal);
RequireClaims("role", requiredRoles, claimsPrincipal);
}
private static void RequireClaims(string type, IEnumerable<string> requiredValues, ClaimsPrincipal claimsPrincipal)
{
if (requiredValues != null)
{
var haveClaims = claimsPrincipal.FindAll(type);
var missingRequiredValues = requiredValues.Where(s => !haveClaims.Any(c => c.Value.Equals(s, StringComparison.InvariantCultureIgnoreCase))).ToArray();
if (missingRequiredValues.Any())
{
var list = string.Join(", ", missingRequiredValues);
throw new InvalidDataException($"Missing required {type} claims: {list}");
}
}
}
}
}
I liked the idea of the helper class, but jon-brichoux's implementation still has a lot of code (e.g. iterating over signingTokens) you'd think would be handled by libraries. Microsoft's libraries also have a lot of solid code including refreshing the keys regularly. That implementation looks to be similar to what's recommended by Microsoft at Obtaining SecurityKeys for Validation Dynamically.
A blog post, Manually validating a JWT using .NET, is good but it hits the server and gets the metadata on every validation. Here's what I came up with that seems to work as expected.
You will need to reference the Microsoft.IdentityModel.Protocols.OpenIdConnect and Microsoft.IdentityModel.Protocols NuGet packages.
public class JwtTokenValidator
{
protected static TokenValidationParameters _validationParameters;
public JwtTokenValidator(string metadataAddress, string audience)
{
_validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = audience,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataAddress, new OpenIdConnectConfigurationRetriever()),
};
}
public void ValidateToken(string token)
{
var jsonWebTokenHandler = new JsonWebTokenHandler();
var tokenValidationResult = jsonWebTokenHandler.ValidateToken(token, _validationParameters);
if (!tokenValidationResult.IsValid)
{
// Handle each exception which tokenValidationResult can contain as appropriate for your service
// Your service might need to respond with a http response instead of an exception.
if (tokenValidationResult.Exception != null)
throw tokenValidationResult.Exception;
throw new InvalidOperationException("invalid token"); // TODO: throw an application-appropriate exception
}
}
}
Usage:
// cache single instance at application initialization
JwtTokenValidator jwtTokenValidator = new JwtTokenValidator($"https:.../.well-known/openid-configuration", "[audience]");
// live check in controller or wherever
jwtTokenValidator.ValidateToken("eyJ0eX...");

Two factor auth with IdentityServer3 - remember browser

I'm implementing 2fa with IdentityServer3 + Asp.Net Identity (2.2.1). I'm stuck on the 2fa implementation. I've looked at the "AspNetIdentity_2fa" sample, which helped a lot.
I have everything wired up, except for the cookie that indicates the browser has been successfully authenticated. I can set the cookie during the code confirmation, but I cannot get to the cookie in the PostAuthenticateLocalAsync() call to see whether or not to take the 2fa path.
protected override Task<AuthenticateResult> PostAuthenticateLocalAsync(User user, SignInMessage message)
{
if (user.TwoFactorEnabled) // && !TwoFactorCookieSet...
{
return Task.FromResult(new AuthenticateResult("/auth/sendcode", user.Id, user.DisplayName));
}
return base.PostAuthenticateLocalAsync(user, message);
}
I believe I'm taking the correct approach in using the partial logins, but how would I detect that the current browser has already been approved?
More detail: the /auth/sendcode is the standard Asp.Net Identity pages/flow for 2fa, combined with the partial login logic from the sample.
Okay, I found that OwinEnvironmentService can be injected into IdentityServer services. I can get the cookies via OwinEnvironmentService. I'd be interested to hear any opinions on this solution (this isn't meant to be production-ready, it's just a concept):
internal class UserService : AspNetIdentityUserService<User, string>
{
private readonly OwinEnvironmentService _owinEnvironmentService;
public UserService(UserManager userMgr, OwinEnvironmentService owinEnvironmentService) : base(userMgr)
{
_owinEnvironmentService = owinEnvironmentService;
DisplayNameClaimType = IdentityServer3.Core.Constants.ClaimTypes.Name;
}
protected override Task<AuthenticateResult> PostAuthenticateLocalAsync(User user, SignInMessage message)
{
if (user.TwoFactorEnabled)
{
var twoFactorNeeded = false;
object httpContext;
if (_owinEnvironmentService.Environment.TryGetValue("System.Web.HttpContextBase", out httpContext))
{
var cookies = (httpContext as HttpContext)?.Request.Cookies;
if (cookies != null && !cookies.AllKeys.Contains(IdentityConstants.CookieNames.TwoFactorCompleted)) twoFactorNeeded = true;
}
if (twoFactorNeeded)
return Task.FromResult(new AuthenticateResult("/auth/sendcode", user.Id, user.DisplayName));
}
return base.PostAuthenticateLocalAsync(user, message);
}
}
UPDATED
Based on Brock's comment, I think I have a better solution.
// custom User Service
internal class UserService : AspNetIdentityUserService<User, string>
{
private readonly OwinEnvironmentService _owinEnvironmentService;
public UserService(UserManager userMgr, OwinEnvironmentService owinEnvironmentService) : base(userMgr)
{
_owinEnvironmentService = owinEnvironmentService;
DisplayNameClaimType = IdentityServer3.Core.Constants.ClaimTypes.Name;
}
protected override async Task<AuthenticateResult> PostAuthenticateLocalAsync(User user, SignInMessage message)
{
if (user.TwoFactorEnabled)
{
var owinContext = new OwinContext(_owinEnvironmentService.Environment);
var result = await owinContext.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
if(result == null) return new AuthenticateResult("/auth/sendcode", user.Id, user.DisplayName);
}
return await base.PostAuthenticateLocalAsync(user, message);
}
}
// (in MVC controller) generate the 2FA security code and send it
public async Task<ActionResult> SendCode(SendCodeViewModel model)
{
// ...some code removed for brevity...
var token = await UserManager.GenerateTwoFactorTokenAsync(userId, model.SelectedProvider);
var identityResult = await UserManager.NotifyTwoFactorTokenAsync(userId, model.SelectedProvider, token);
if (!identityResult.Succeeded) return View("Error");
return RedirectToAction("VerifyCode", new { Provider = model.SelectedProvider, model.ReturnUrl, model.RememberMe });
}
// (in MVC controller) verify the code and sign in with 2FA
public async Task<ActionResult> VerifyCode(VerifyCodeViewModel model)
{
// ...some code removed for brevity...
var signInManager = new SignInManager<User, string>(UserManager, Request.GetOwinContext().Authentication);
if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, model.Provider, model.Code))
{
await UserManager.ResetAccessFailedCountAsync(user.Id);
await signInManager.SignInAsync(user, model.RememberMe, model.RememberBrowser);
var resumeUrl = await env.GetPartialLoginResumeUrlAsync();
return Redirect(resumeUrl);
}
else
{
await UserManager.AccessFailedAsync(user.Id);
ModelState.AddModelError("", "Invalid code.");
return View(model);
}
}
I implemented the same for remember browser requirement however following statement return always null when we logout and login again.so twofactory step is not skipped..
var result = await owinContext.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);