ASP.NET Core 1.0. Bearer Token, cannot access custom claims - authentication

I'm trying to setup Bearer authentication for an SPA using ASP.NET Core 1.0. I've almost got it working for JwtToken with OpenIdConnect Server but have an issue that my custom claims are not returned with the token.
My Startup.cs logic for authentication is the following:
private void ConfigureAuthentication(IApplicationBuilder app)
{
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.Authority = "http://localhost:53844";
options.Audience = "http://localhost:53844";
options.RequireHttpsMetadata = false;
});
app.UseOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/api/v1/token";
options.AllowInsecureHttp = true;
options.AuthorizationEndpointPath = PathString.Empty;
options.Provider = new OpenIdConnectServerProvider
{
OnValidateClientAuthentication = context =>
{
context.Skipped();
return Task.FromResult<Object>(null);
},
OnGrantResourceOwnerCredentials = async context =>
{
var usersService = app.ApplicationServices.GetService<IUsersService>();
User user = usersService.getUser(context.Username, context.Password);
var identity = new ClaimsIdentity(new List<Claim>(), OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
identity.AddClaim(new Claim(ClaimTypes.Name, user.Id.ToString()));
identity.AddClaim(new Claim("myclaim", "4815162342"));
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
ticket.SetResources(new[] { "http://localhost:53844" });
ticket.SetAudiences(new [] {"http://localhost:53844"});
ticket.SetScopes(new [] {"email", "offline_access" });
context.Validated(ticket);
}
};
});
}
Both access_token and refresh_token are generating succesfully and when passing access_token in Authorization header system treats request as authorized.
The only issue is that all claims except NameIdentifier are not passed.
I use the following code to receive my claims for authenticated request:
public class WebUserContext : IUserContext
{
private readonly IHttpContextAccessor contextAccessor;
public WebUserContext(IHttpContextAccessor contextAccessor)
{
this.contextAccessor = contextAccessor;
}
public long UserId
{
get
{
ClaimsIdentity identity = Principal?.Identity as ClaimsIdentity;
if (identity == null)
{
return -1;
}
Claim claim = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name); // There is no such claim in claims collection
return long.Parse(claim.Value);
}
}
private ClaimsPrincipal Principal => contextAccessor.HttpContext.User as ClaimsPrincipal;
}
What can be the reason my claims are not passed or extracted from the token?

What can be the reason my claims are not passed or extracted from the token?
Security.
Unlike OAuthAuthorizationServerMiddleware, ASOS doesn't assume access tokens are always consumed by your own resource servers (though I agree it's a common scenario) and refuses to serialize claims that don't explicitly specify a "destination" to avoid leaking confidential data to unauthorized parties.
With JWT being the default format in ASOS beta4 (but not in the next beta), you must also keep in mind that even client applications (or users) can read your access tokens.
For this reason, you must explicitly attach a "destination" to your claims:
identity.AddClaim(ClaimTypes.Name, "Pinpoint", destination: "id_token token");
Specify id_token to serialize the claim in the identity token, token to serialize it in the access token or both to serialize it in both tokens (there's no equivalent for authorization codes or refresh tokens as they are always encrypted and only readable by the authorization server itself)

Related

Call WebApi with JWT Token from Blazor WebAssembly

I am getting an unexpected Unauthorized response from an Api when using JWT in Blazor WebAssembly. Note, I am not trying to secure anything on the WebAssembly client; just the API endpoint. I have deliberately left out expiry validation.
Server
appsettings.json
{
"JwtSecurity": {
"Key": "RANDOM_KEY_MUST_NOT_BE_SHARED",
"Issuer": "https://localhost",
"Audience": "https://localhost",
"ExpiryDays": 1
}
}
Program.cs
// Service registration
builder.Services
.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["JwtSecurity:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["JwtSecurity:Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtSecurity:Key"])),
RequireExpirationTime = false,
ValidateLifetime = false
};
});
// Configure the HTTP request pipeline.
// SignalR Compression
app.UseResponseCompression();
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
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();
// Logs the received token
app.UseJwtTokenHandler();
//explicitly only use blazor when the path doesn't start with api
app.MapWhen(ctx => !ctx.Request.Path.StartsWithSegments("/api"), blazor =>
{
blazor.UseBlazorFrameworkFiles();
blazor.UseStaticFiles();
blazor.UseRouting();
blazor.UseEndpoints(endpoints =>
{
endpoints.MapHub<Cosmos.App.Server.Hubs.TillSiteHub>("/tradingsessionhub");
endpoints.MapFallbackToFile("index.html");
});
});
//explicitly map api endpoints only when path starts with api
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), api =>
{
api.UseStaticFiles();
api.UseRequestLogging();
api.UseRouting();
api.UseAuthentication();
api.UseAuthorization();
// HAVE ALSO TRIED
// api.UseAuthentication();
// api.UseRouting();
// api.UseAuthorization();
api.UseErrorHandling();
api.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
});
app.Run();
This link suggests UseRouting should come before
UseAuthentication and UseAuthorisation.
This link suggests UseRouting should come between them.
Have tried both to no avail.
Token Generation on Login
Helper Class
public class JwtHelper
{
public static JwtSecurityToken GetJwtToken(
string username,
string signingKey,
string issuer,
string audience,
TimeSpan expiration,
Claim[] additionalClaims = null)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub,username),
// this guarantees the token is unique
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
if (additionalClaims is object)
{
var claimList = new List<Claim>(claims);
claimList.AddRange(additionalClaims);
claims = claimList.ToArray();
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
return new JwtSecurityToken(
issuer: issuer,
audience: audience,
expires: DateTime.UtcNow.Add(expiration),
claims: claims,
signingCredentials: creds
);
}
}
Controller Method for Login
Guid userGid = await loginManager.LoginAsync(request.Email!, request.Password!);
if (userGid == default)
{
return base.NotFound();
}
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, userGid.ToString()));
claims.Add(new Claim(ClaimTypes.Name, userGid.ToString()));
string key = configuration["JwtSecurity:Key"];
string issuer = configuration["JwtSecurity:Issuer"];
string audience = configuration["JwtSecurity:Audience"];
string expiryDays = configuration["JwtSecurity:ExpiryDays"];
TimeSpan expiry = TimeSpan.FromDays(Convert.ToInt32(expiryDays));
var token = JwtHelper.GetJwtToken(
userGid.ToString(),
key,
issuer,
audience,
expiry,
claims.ToArray());
LoginResponse response = new(new JwtSecurityTokenHandler().WriteToken(token));
return base.Ok(response);
The token string response is stored in Local Storage.
Client
Adding header to Http Client
// GetTokenAsync retrieve the string from Local Storage
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"bearer",
(await _accessControlStateManager.GetTokenAsync())!.Write());
Server Logging of Received Token by middleware
public class JwtTokenHandlerMiddleware
{
readonly RequestDelegate _next;
readonly ILogger _logger;
public JwtTokenHandlerMiddleware(
RequestDelegate next,
ILoggerFactory loggerFactory)
{
_next = next;
_logger = loggerFactory.CreateLogger(typeof(JwtTokenHandlerMiddleware).FullName!);
}
public async Task Invoke(HttpContext context)
{
JwtSecurityToken? jwt = context.GetJwtTokenFromAuthorizationHeader();
if (jwt != null)
{
_logger.LogInformation("Request received with token: {uri}", context.Request.GetDisplayUrl());
_logger.LogInformation("Token: {token}", jwt.Write());
}
else
{
_logger.LogInformation("Request received from ANONYMOUS: {uri}", context.Request.GetDisplayUrl());
}
await _next(context);
}
}
public static JwtSecurityToken? GetJwtTokenFromAuthorizationHeader(this HttpContext httpContext)
{
string text = httpContext.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
return new JwtSecurityTokenHandler().ReadJwtToken(text);
}
I have confirmed from the logs that the JWT is being received
Using jwt.io I can confirm that the token logged in the request middleware can be read
jwt.io output
Given all of the above, it seems that:
I am generating JWT correcly.
JWT is being stored and retrieved correctly from Local Storage
JWT is being received in the Authorization header in the request
The controller is secured at Controller level using Authorize
attribute (no roles mentioned)
But still I get Unauthorized.
Any advice?
Attempts to Isolate
Disable validation parameters
Tried:
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = false,
RequireExpirationTime = false,
ValidateLifetime = false
};
And added simple test controller:
using Microsoft.AspNetCore.Authorization;
namespace Cosmos.App.Server.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class TestController : ControllerBase
{
[HttpGet]
[Route("test")]
public async Task<IActionResult> TestAsync()
{
await Task.Delay(1);
return Ok("Hello");
}
}
But same issue.
Found it!
The issue was that I was caching the returned string from login as a Token so I could quickly access claims in the client (whilst saving the string received from login in LocalStorage).
Then, when putting the token on the HttpClient, if I had a cached token, I was writing it out to string to populate the Authorization on the Http Request.
The problem is that the string received from the initial login, e.g.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGU2YWZhNy00NGYwLTRlNTUtODgxMy0xMTRmNGY1OWE2NzIiLCJqdGkiOiI2MWYwYTRiMi0wNjQwLTRiMjgtYmM2Mi0zMDZlYTVmYmJiM2UiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6Ijk0ZTZhZmE3LTQ0ZjAtNGU1NS04ODEzLTExNGY0ZjU5YTY3MiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiI5NGU2YWZhNy00NGYwLTRlNTUtODgxMy0xMTRmNGY1OWE2NzIiLCJleHAiOjE2NjM5NjEwMjMsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0IiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3QifQ.5l9LRYIx3wXruW7BMa1DDbEoltVgP6Fbfkc2O03XAAY
was truncated when reading back into a token for caching. It appears to have truncated what I presume is the signing key. The following was missing:
5l9LRYIx3wXruW7BMa1DDbEoltVgP6Fbfkc2O03XAAY
This meant I had the full string stored in local storage but a cached token without the signing key.
When then used the cached token to write to string for the authorization header I got:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGU2YWZhNy00NGYwLTRlNTUtODgxMy0xMTRmNGY1OWE2NzIiLCJqdGkiOiI2MWYwYTRiMi0wNjQwLTRiMjgtYmM2Mi0zMDZlYTVmYmJiM2UiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6Ijk0ZTZhZmE3LTQ0ZjAtNGU1NS04ODEzLTExNGY0ZjU5YTY3MiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiI5NGU2YWZhNy00NGYwLTRlNTUtODgxMy0xMTRmNGY1OWE2NzIiLCJleHAiOjE2NjM5NjEwMjMsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0IiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3QifQ.
without the suffix of the signing key.
This meant that the validation of that string failed authorization on the server, even though jwt.io would happily read it.
Using the full string that I'd stored in Local Storage instead of a 'written' string from the cached token solved the problem.

Pattern for get user's informations in all my services from JWT token

I have an api with many controllers. One of these controllers is the authentication one. It use to get a JWT token and call others paths, from this controller or another.
My problem is, for any paths, I need to get the user id in the JWT token, and ask to the database to get informations about user, like check if the user exist or datas linked to him. So in each method, I have to call a specific method to retrieve informations.
In fact, the authentication layer is only used in order to check if the JWT Token is valid. I don't know if is it possible to add some logic in this layer. And how.
Can I implement any pattern in order to retrieve automatically user's information ?
I thought about singleton, but I'm not sure about the scope of the object. The goal is to stay in the request scope.
I thought to create an User Service, but I think it is not a good way because services are about treatment, not keep datas in these.
I thought to implement a custom middleware, but I'm not sure about the way to do it.
The objective is to implement all the user logic in the same place, and each service can call it in order to deal with it :
Request (with JWT Token) --> Controller --> Service --> Call the object with all user's stuff and logic and treatment
or
Request (with JWT token) --> middleware (get all user's informations) --> Controller --> Call the object created in middleware with all user's stuff
For information, there are my login method and the way I create the JWT token :
public async Task<ServiceResponse<UserLoginResponseDto>> Login(ServiceRequest<UserLoginRequestDto> request)
{
ServiceResponse<UserLoginResponseDto> response = new ServiceResponse<UserLoginResponseDto>();
User user = await _context.Users.FirstOrDefaultAsync(x => x.Username.ToLower().Equals(request.Data.Username.ToLower()));
if (user == null)
{
response.Success = false;
response.Message = "User not found";
}
else if (user.IsLocked)
{
response.Success = false;
response.Message = "User locked";
}
else if (!user.IsActivated)
{
response.Success = false;
response.Message = "User not activated";
}
else if (!VerifyPasswordHash(request.Data.Password, user.PasswordHash, user.PasswordSalt))
{
response.Success = false;
response.Message = "Wrong password";
}
else
{
response.Data = _mapper.Map<UserLoginResponseDto>(user);
response.Data.JWtToken = CreateToken(user);
}
return response;
}
private string CreateToken(User user)
{
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
};
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config.GetSection("AppSettings:Token").Value));
SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(1),
SigningCredentials = creds
};
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}

Where to store JWT Token in .net core web api?

I am using web api for accessing data and I want to authenticate and authorize web api.For that I am using JWT token authentication. But I have no idea where should I store access tokens?
What I want to do?
1)After login store the token
2)if user want to access any method of web api, check the token is valid for this user,if valid then give access.
I know two ways
1)using cookies
2)sql server database
which one is the better way to store tokens from above?
Alternatively, if you just wanted to authenticate using JWT the implementation would be slightly different
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var user = context.Principal.Identity.Name;
//Grab the http context user and validate the things you need to
//if you are not satisfied with the validation fail the request using the below commented code
//context.Fail("Unauthorized");
//otherwise succeed the request
return Task.CompletedTask;
}
};
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey("MyVeryStrongKeyHiddenFromAnyone"),
ValidateIssuer = false,
ValidateAudience = false
};
});
still applying use authentication before use MVC.
[Please note these are very simplified examples and you may need to tighten your security more and implement best practices such as using strong keys, loading configs perhaps from the environment etc]
Then the actual authentication action, say perhaps in AuthenticationController would be something like
[Route("api/[controller]")]
[Authorize]
public class AuthenticationController : Controller
{
[HttpPost("authenticate")]
[AllowAnonymous]
public async Task<IActionResult> AuthenticateAsync([FromBody]LoginRequest loginRequest)
{
//LoginRequest may have any number of fields expected .i.e. username and password
//validate user credentials and if they fail return
//return Unauthorized();
var claimsIdentity = new ClaimsIdentity(new Claim[]
{
//add relevant user claims if any
}, "Cookies");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);
return Ok();
}
}
in this instance I'm using cookies so I'm returning an HTTP result with Set Cookie. If I was using JWT, I'd return something like
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]LoginRequest loginRequest)
{
//validate user credentials and if they validation failed return a similar response to below
//return NotFound();
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("MySecurelyInjectedAsymKey");
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
//add my users claims etc
}),
Expires = DateTime.UtcNow.AddDays(1),//configure your token lifespan and needed
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey("MyVerySecureSecreteKey"), SecurityAlgorithms.HmacSha256Signature),
Issuer = "YourOrganizationOrUniqueKey",
IssuedAt = DateTime.UtcNow
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
var cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTimeOffset.UtcNow.AddHours(4);//you can set this to a suitable timeframe for your situation
cookieOptions.Domain = Request.Host.Value;
cookieOptions.Path = "/";
Response.Cookies.Append("jwt", tokenString, cookieOptions);
return Ok();
}
I'm not familiar with storing your users tokens on your back end app, I'll quickly check how does that work however if you are using dotnet core to authenticate with either cookies or with jwt, from my understanding and experience you need not store anything on your side.
If you are using cookies then you just need to to configure middleware to validate the validity of a cookie if it comes present in the users / consumer's headers and if not available or has expired or can't resolve it, you simply reject the request and the user won't even hit any of your protected Controllers and actions. Here's a very simplified approach with cookies.(I'm still in Development with it and haven't tested in production but it works perfectly fine locally for now using JS client and Postman)
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "yourCookieName";
options.Cookie.SameSite = SameSiteMode.None;//its recommended but you can set it to any of the other 3 depending on your reqirements
options.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents
{
OnRedirectToLogin = redirectContext =>//this will be called if an unauthorized connection comes and you can do something similar to this or more
{
redirectContext.HttpContext.Response.StatusCode = 401;
return Task.CompletedTask;
},
OnValidatePrincipal = context => //if a call comes with a valid cookie, you can use this to do validations. in there you have access to the request and http context so you should have enough to work with
{
var userPrincipal = context.Principal;//I'm not doing anything with this right now but I could for instance validate if the user has the right privileges like claims etc
return Task.CompletedTask;
}
};
});
Obviously this would be placed or called in the ConfigureServices method of your startup to register authentication
and then in your Configure method of your Startup, you'd hookup Authentication like
app.UseAuthentication();
before
app.UseMvc()

Azure AD B2C with Angular4 and WebAPI Core2 token validation issue [duplicate]

This question already has an answer here:
Azure AD B2C error - IDX10501: Signature validation failed
(1 answer)
Closed 5 years ago.
Doesn't seem Azure documentation can give a clear example hot to do it right.
There are Angular4 (WebApp) and WebAPI Core 2.0 back-end.Two application configured in Azure B2C. WebApp has WebAPI app in its API access.
Web app gets redirected to https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize. There, credentials provided and then AAD B2C calls back WebApp page with access_token, token_type, expires_in, id_token url parameters.
Then, WebApp makes a request to a protected endpoint of the back-end with access_token in Authorization header. MessageReceivedAsync is called when request hits the back-end and goes all the way through validating the token.
However, when process exits the method next step it goes into is AuthenticationFailed with error.
"IDX10501: Signature validation failed. Unable to match 'kid': 'Base64_kid',
token: '{"alg":"RS256","typ":"JWT","kid":"Base64_kid"}.{"iss":"number of claims"}'."
My understanding that Audience is the WebAPI application id. I have only a SingIn/Up policy.
What am I missing here to complete jwt manual validation w/o errors? Another question, when claimsPrincipal is created when token validated, how does it go into request context to be able to access protected endpoint?
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.Authority = string.Format("https://login.microsoftonline.com/{0}/v2.0/",
Configuration["Authentication:AzureAd:ida:Tenant"], Configuration["Authentication:AzureAd:ida:Policy"]);
options.Audience = Configuration["Authentication:AzureAd:ida:ClientId"];
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = AuthenticationFailed,
OnMessageReceived = MessageReceivedAsync,
OnChallenge = Challenge,
OnTokenValidated = TokenValidated
};
});
...
}
private Task MessageReceivedAsync(MessageReceivedContext arg)
{
string jwtToken = null;
var aadInstance = Configuration["Authentication:AzureAd:ida:AADInstance"];
var tenant = Configuration["Authentication:AzureAd:ida:Tenant"];
var audience = Configuration["Authentication:AzureAd:ida:Audience"];
var policy = Configuration["Authentication:AzureAd:ida:Policy"];
var authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
string _issuer = string.Empty;
List<SecurityKey> _signingTokens = null;
var authHeader = arg.HttpContext.Request.Headers["Authorization"];
// 7 = (Bearer + " ").Length
var token = authHeader.ToString().Substring(7);
try
{
string stsDiscoveryEndpoint = string.Format("{0}/v2.0/.well-known/openid-configuration?p={1}", authority, policy);
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint,
new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration config = null;
var openIdConfigTask = Task.Run(async () => {
config = await configManager.GetConfigurationAsync();
});
openIdConfigTask.Wait();
_issuer = config.Issuer;
_signingTokens = config.SigningKeys.ToList();
}
catch(Exception ex)
{
...
}
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidAudience = audience,
ValidIssuer = _issuer,
IssuerSigningKeys = _signingTokens
};
var claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken);
//Thread.CurrentPrincipal = claimsPrincipal; ?
//var ticket = new AuthenticationTicket(claimsPrincipal, arg.Scheme.Name); ?
//arg.HttpContext.User = claimsPrincipal; ?
return Task.FromResult(0);
}
The options.Audience property is correct (i.e. the application identifier for the Web API application) but the JWT bearer authentication middleware is downloading the wrong signing keys because you don't seem to be setting the options.Authority property to the right value.
It must include the Azure AD B2C policy.
You should be setting it to:
https://login.microsoftonline.com/tfp/{tenant}/{policy}/v2.0/'
such as:
https://login.microsoftonline.com/tfp/{Configuration["Authentication:AzureAd:ida:Tenant"]}/{Configuration["Authentication:AzureAd:ida:Policy"]}/v2.0/.
As result of the token validation, the HttpContext.User object contains the claims from the token, so you can control access for example via scopes.

Authorize via JWT Token

ASP.NET Core 5 with ASP.NET Identity 3.0, I'm using both web pages and apis. I am using OpenIddict to issue a JWT token and to authenticate. My code looks as such:
X509Certificate2 c = new X509Certificate2(#"tokensign.p12", "MyCertificatePassword");
services.AddOpenIddict<WebUser, IdentityRole<int>, WebDbContext, int>()
.EnableTokenEndpoint("/api/customauth/login")
.AllowPasswordFlow()
.UseJsonWebTokens()
.AddSigningCertificate(c);
If I disable UseJsonWebTokens(), I can generate a token and authorise successfully. However, I am not sure that my certificate is validating the returned tokens.
And when enable UseJsonWebTokens, I am able to issue a JWT token at this end point. However, I can't authenticate any request!
I am using the following code in the app configuration:
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
RequireHttpsMetadata = false,
Authority = "http://localhost:60000/",
Audience = "http://localhost:60000/",
});
app.UseOAuthValidation();
app.UseIdentity();
app.UseOpenIddict();
app.UseMvcWithDefaultRoute();
How can I enforce the request to be validated with my certificate to make sure the JWT token is not tampered with.
What are the correct settings that will allow validation and authorisation of my JWT token, given that if I am not using JWT, I am getting authorised successfully.
If I disable UseJsonWebTokens(), I can generate a token and authorise successfully. However, I am not sure that my certificate is validating the returned tokens.
In ASOS (the OpenID Connect server framework behind OpenIddict), there are 2 different built-in serialization mechanisms to create and protect tokens:
One that uses IdentityModel (a library developed by Microsoft) and produces standard tokens verifiable by third parties:
Identity tokens (JWT by definition) are always created using this process and you can call UseJsonWebTokens() to force OpenIddict to issue access tokens that use the same serialization process.
The certificate you specify when calling AddSigningCertificate() is always used to sign these tokens.
One that uses the ASP.NET Core Data Protection stack (also developed by Microsoft):
This stack exclusively produces "proprietary" tokens that are not meant to be read or verified by a third-party, as the token format is not standard and necessarily relies on symmetric signing and encryption.
It's the mechanism we use for authorization codes and refresh tokens, that are only meant to be consumed by OpenIddict itself. It's also used for access tokens when you use the default token format.
In this case, the certificate you specify when calling AddSigningCertificate() is not used.
Instead, these tokens are always encrypted by the Data Protection stack using an Authenticated Encryption algorithm (by default, AES-256-CBC with HMACSHA256), that provides authenticity, integrity and confidentiality. For that, 2 keys (one for encryption, one for validation) are derived by the Data Protection stack from one of the master keys stored in the key ring.
How can I enforce the request to be validated with my certificate to make sure the JWT token is not tampered with.
What are the correct settings that will allow validation and authorisation of my JWT token, given that if I am not using JWT, I am getting authorised successfully.
To answer these questions, it would help if you enabled logging and shared your traces.
Creating JWT Token based authentication in ASP.NET Core is very very simple. Please follow below link you will get more idea.
How to Create JWT Token in Asp NET Core
Sample Code
public static class AuthenticationConfig
{
public static string GenerateJSONWebToken(string user)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("730F046B1ADF1555FF0C80149B47B38CD7C0A146AAFA34870E863CAA25B585C3"));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[] {
new Claim("UserName", user),
new Claim("Role", "1"),
};
var token = new JwtSecurityToken("http://localhost:30972",
"http://localhost:30972",
claims,
DateTime.UtcNow,
expires: DateTime.Now.AddMinutes(10),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
//ConfigureJwtAuthentication
internal static TokenValidationParameters tokenValidationParams;
public static void ConfigureJwtAuthentication(this IServiceCollection services)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("730F046B1ADF1555FF0C80149B47B38CD7C0A146AAFA34870E863CAA25B585C3"));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
tokenValidationParams = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
ValidIssuer = "http://localhost:30972",
ValidateLifetime = true,
ValidAudience = "http://localhost:30972",
ValidateAudience = true,
RequireSignedTokens = true,
// Use our signing credentials key here
// optionally we can inject an RSA key as
//IssuerSigningKey = new RsaSecurityKey(rsaParams),
IssuerSigningKey = credentials.Key,
ClockSkew = TimeSpan.FromMinutes(10)
};
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = tokenValidationParams;
#if PROD || UAT
options.IncludeErrorDetails = false;
#elif DEBUG
options.RequireHttpsMetadata = false;
#endif
});
}
}
Add this line in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureJwtAuthentication();
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build();
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
Add these lines in Authentication Controller
[Route("api/[controller]")]
public class AuthenticationController : Controller
{
// GET: api/<controller>
[HttpGet]
public string Get(string user, string pass)
{
if (user == "admin")
{
return AuthenticationConfig.GenerateJSONWebToken(user);
}
else
{
return "";
}
}
// POST api/<controller>
[Authorize]
[HttpPost]
public string Post()
{
var identity = HttpContext.User.Identity as ClaimsIdentity;
IEnumerable<Claim> claim = identity.Claims;
var UserName = claim.Where(c => c.Type == "UserName").Select(c => c.Value).SingleOrDefault();
return "Welcome to " + UserName + "!";
}
}