I have a problem with accessing the (payload of) JWT in a dotnet core controller. I don't know where I am wrong. I think I covered all the important points in the following description. If I have missed something, that could help, please let me know.
Thank you for your time.
Adding authentication service to the servicecollection
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = false,
ValidIssuer = null,
ValidAudience = null,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("testify"))
};
});
The token I used for the request:
The postman call:
The code of the controller action:
[HttpPost]
[ProducesResponseType(typeof(void), 201)]
[ProducesResponseType(typeof(void), 400)]
[ProducesResponseType(typeof(void), 401)]
[ProducesResponseType(typeof(void), 403)]
[ApiExplorerSettings(GroupName = "AuditLog")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Insert([Required] Dto.Log auditLog) => RunSafely(() =>
{
var log = _mapper.Map<Dto.Log, Log>(auditLog);
log.CorrelationId = _headerReader.GetCorrelationId(Request?.Headers);
_logRepository.AddLog(log);
return this.StatusCode((int)HttpStatusCode.Created);
});
The state of the controller:
Your question is a little unclear to me, too, and I am not sure about the creation process of your token. However, I have the exact scenario in one of my projects, which works fine. Here is the code for different puzzles:
Creating the Token:
if (user != null)
{
var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false);
if (result.Succeeded)
{
// Create the Token
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
_config["Tokens:Issuer"],
_config["Tokens:Audience"],
claims,
expires: DateTime.UtcNow.AddMinutes(120),
signingCredentials: creds);
var results = new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
};
return Created("", results);
}
}
For IServiceCollection :
services.AddAuthentication()
.AddCookie()
.AddJwtBearer(cfg =>
{
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = _config["Tokens:Issuer"],
ValidAudience = _config["Tokens:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]))
};
});
And config.json file: (Note that I changed real-world info due to security reasons)
"Tokens": {
"Key": "foo-foo-foo-foo-foo-foo-foo-foo",
"Issuer": "localhost",
"Audience": "localhost"
}
These settings are exact that I'm using in one of my small projects, and It's working fine. Check if something is missing in your project or not.
You can access payload of jwt by using User object inside of System.Security.Claims.ClaimsPrincipal namespace. For example:
var claims = User.Claims;
This claims contains payload of jwt. Also, you can access other informations like:
if (User.Identity.IsAuthenticated)
{ ....
Within a controller, you can take a dependency on IHttpContextAccessor and then, on the HttpContext, call the extension method GetTokenAsync(string), which will return the encoded string.
string encodedToken = _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
JwtSecurityToken decodedToken = new JwtSecurityToken(encodedToken);
string email = decodedToken.Payload["email"].ToString();
JwtSecurityToken is in the System.IdentityModel.Tokens.Jwt namespace, and GetTokenAsync extension method of HttpContext is in the Microsoft.AspNetCore.Authentication namespace.
RAW PAYLOAD
You can access the raw JWT access token to get its JWTPayload using the following type of code in a .NET API controller:
using System.IdentityModel.Tokens.Jwt;
[Route("api/contacts")]
public class ContactsController : Controller
{
[HttpGet("")]
public async Task<IEnumerable<Contact>> GetContactsAsync()
{
var authHeader = this.HttpContext.Request.Headers.Authorization.ToString();
var parts = authHeader.Split(' ');
var accessToken = parts[1];
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(accessToken);
var payload = token.Payload;
System.Console.WriteLine($"Access token expiry time is {payload.Exp}");
// Business code goes here
}
}
CLAIMS
Microsoft provide a higher level programming model, where by default you never get access to the JWT details. This is a good model to follow - business logic should only ever use a ClaimsPrincipal:
var expiry = this.User.Claims.First(c => c.Type == "exp");
System.Console.WriteLine($"Access token expiry time is {expiry.Value}");
CUSTOM JWT PROCESSING
If you want to implement custom behaviour related to JWTs then it is standard to use the extensibility features of the framework. The plumbing then goes into a middleware class, so that controller classes stay business focused. A couple of example classes of mine show how to extend the .NET framework:
CustomAuthenticationHandler
CustomJWTSecurityTokenHandler
For more info, see these resources of mine, which are focused on promoting understanding of the important standards based OAuth concepts in APIs:
.NET 6 API Code Sample
Blog Post
Easiest way to access payload is by using IHttpContextAccessor injected into your class like below.
public class MyClass: IEwocUser
{
private readonly IHeaderDictionary _headers;
public MyClass(IHttpContextAccessor httpContextAccessor)
{
_headers = httpContextAccessor.HttpContext?.Request.Headers;
}
}
From the headers you can filter for Auth token.
I have the following code inside Startup - ConfigureServices:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = new PathString("/en/Authentication/LogIn");
});
Everything works great, but I cannot find a way to make LoginPath being localisable using URL parameter (en/de/es etc.)
My MapControllerRoute looks like:
"{lang}/{controller=Home}/{action=Index}/{id?}"
Is it possible to redirect to appropriate lang for authentication like if user was accessing /de/NeedAuth/Index - it should be redirected to /de/Authentication/LogIn ?
Ok. I spent an hour and here is a solution - in case anyone would have similar use case.
Step 1:
Creating a new class that would dynamically get current http requests to determine redirection:
public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
public override Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
{
var httpContext = context.HttpContext;
var routePrefix = httpContext.GetRouteValue("lang");
context.RedirectUri = $"/{routePrefix.ToString()}/Authentication/LogIn";
return base.RedirectToLogin(context);
}
}
Step 2:
In Startup modifying cookie authentication declaration that relates to redirecting to authentication page:
services.AddScoped<CustomCookieAuthenticationEvents>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = new PathString("/Authentication/LogIn");
options.EventsType = typeof(CustomCookieAuthenticationEvents);
});
Pay attention to registering CustomCookieAuthenticationEvents as service above.
I experience some problems concerning globalization in asp.net.
Parts of the page are randomly in english instead of german.
See this code:
[HttpPost]
public async Task<JsonResult> InterestingControllerMethod(Guid offeringRateId, Guid trainerId, DateTime? selectedDate)
{
//de-DE
Thread.CurrentThread.CurrentUICulture = new CultureInfo(SlConst.DefaultCulture);
var dateAndUtcTime = GetSelectedDateTime(selectedDate);
var selectTimesTrainer = await QueryProcessor.ExecuteAsyncWithCache(new OfferingRateCourseSelectableTimesForTrainerQuery
{
BasketId = BasketId,
OfferingRateId = offeringRateId,
FirstBookableDateTime = dateAndUtcTime,
LastBookableDateTime = dateAndUtcTime.GetEndOfDay(),
CurrentDateTime = SlDateTime.CurrentDateTimeUtc(SlConst.DefaultTimeZoneTzdb),
ForTrainerId = trainerId,
CustomerId = CurrentUserId
}).ConfigureAwait(false);
// this method actually uses ResourceManager
var result = GetSelectTimesCourseForTrainer(selectTimesTrainer);
return Json(result);
}
But in the end I always got a mixed gui.
In debug mode it seems that the culture de-DE has not been assigned at all:
Any ideas?
Thx!
Hi I am currently using the asp.net MVC 4 rc with System.Web.Optimization. Since my site needs to be localized according to the user preference I am working with the jquery.globalize plugin.
I would very much want to subclass the ScriptBundle class and determine what files to bundle according to the System.Threading.Thread.CurrentThread.CurrentUICulture. That would look like this:
bundles.Add(new LocalizedScriptBundle("~/bundles/jqueryglobal")
.Include("~/Scripts/jquery.globalize/globalize.js")
.Include("~/Scripts/jquery.globalize/cultures/globalize.culture.{0}.js",
() => new object[] { Thread.CurrentThread.CurrentUICulture })
));
For example if the ui culture is "en-GB" I would like the following files to be picked up (minified of course and if possible cached aswell until a script file or the currentui culture changes).
"~/Scripts/jquery.globalize/globalize.js"
"~/Scripts/jquery.globalize/globalize-en-GB.js" <-- if this file does not exist on the sever file system so fallback to globalize-en.js.
I tried overloading the Include method with something like the following but this wont work because it is not evaluated on request but on startup of the application.
public class LocalizedScriptBundle : ScriptBundle
{
public LocalizedScriptBundle(string virtualPath)
: base(virtualPath) {
}
public Bundle Include(string virtualPathMask, Func<object[]> getargs) {
string virtualPath = string.Format(virtualPathMask, getargs());
this.Include(virtualPath);
return this;
}
}
Thanks
Constantinos
That is correct, bundles should only be configured pre app start. Otherwise in a multi server scenario, if the request for the bundle is routed to a different server other than the one that served the page, the request for the bundle resource would not be found.
Does that make sense? Basically all of your bundles need to be configured and defined in advance, and not dynamically registered on a per request basis.
take a look: https://stackoverflow.com/questions/18509506/search-and-replace-in-javascript-before-bundling
I coded this way for my needs:
public class MultiLanguageBundler : IBundleTransform
{
public void Process(BundleContext context, BundleResponse bundle)
{
var content = new StringBuilder();
var uicult = Thread.CurrentThread.CurrentUICulture.ToString();
var localizedstrings = GetFileFullPath(uicult);
if (!File.Exists(localizedstrings))
{
localizedstrings = GetFileFullPath(string.Empty);
}
using (var fs = new FileStream(localizedstrings, FileMode.Open, FileAccess.Read))
{
var m_streamReader = new StreamReader(fs);
var str = m_streamReader.ReadToEnd();
content.Append(str);
content.AppendLine();
}
foreach (var file in bundle.Files)
{
var f = file.VirtualFile.Name ?? "";
if (!f.Contains("localizedstrings"))
{
using (var reader = new StreamReader(VirtualPathProvider.OpenFile(file.VirtualFile.VirtualPath)))
{
content.Append(reader.ReadToEnd());
content.AppendLine();
}
}
}
bundle.ContentType = "text/javascript";
bundle.Content = content.ToString();
}
private string GetFileFullPath(string uicult)
{
if (uicult.StartsWith("en"))
uicult = string.Empty;
else if (!string.IsNullOrEmpty(uicult))
uicult = ("." + uicult);
return Kit.ToAbsolutePath(string.Format("~/Scripts/locale/localizedstrings{0}.js", uicult));
}
}
I have implemented custom VirtualPathProvider to serve customizable Views from a DB and when i put a breakpoint on the FileExists method I noticed that the framework does ton of unnecessary (for my project) requests. For example when I make a request for non-existing action (e.g. http://localhost/Example/Action) the framework looks for:
"~/Example/Action/5"
"~/Example/Action/5.cshtml"
"~/Example/Action/5.vbhtml"
"~/Example/Action.cshtml"
"~/Example/Action.vbhtml"
"~/Example.cshtml"
"~/Example.vbhtml"
"~/Example/Action/5/default.cshtml"
"~/Example/Action/5/default.vbhtml"
"~/Example/Action/5/index.cshtml"
"~/Example/Action/5/index.vbhtml"
"~/favicon.ico"
"~/favicon.ico.cshtml"
"~/favicon.ico.vbhtml"
"~/favicon.ico/default.cshtml"
"~/favicon.ico/default.vbhtml"
"~/favicon.ico/index.cshtml"
"~/favicon.ico/index.vbhtml"
When I make a request that matches an added route (e.g http://localhost/Test) the framework looks for:
"~/Test"
"~/Test.cshtml"
"~/Test.vbhtml"
"~/Test/default.cshtml"
"~/Test/default.vbhtml"
"~/Test/index.cshtml"
"~/Test/index.vbhtml"
before even initialising the controller. After the controller is initialised the framework looks for the view as defined in the custom RazorViewEngine that I have implemented.
This is my ViewEngine
AreaViewLocationFormats = new string[] { };
AreaMasterLocationFormats = new string[] { };
AreaPartialViewLocationFormats = new string[] { };
MasterLocationFormats = new string[] { };
ViewLocationFormats = new string[] {
"~/Views/Dynamic/{1}/{0}.cshtml",
"~/Views/Dynamic/Shared/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
PartialViewLocationFormats = new string[] {
"~/Views/Dynamic/{1}/Partial/{0}.cshtml",
"~/Views/Dynamic/Shared/Partial/{0}.cshtml",
"~/Views/{1}/Partial/{0}.cshtml",
"~/Views/Shared/Partial/{0}.cshtml"
};
FileExtensions = new string[] { "cshtml" };
So the question is can these default routes be removed and how?
Could they be related to the RouteCollection.RouteExistingFiles property? I doesn't make sense for it to check for lots of files rather than just one that matches, but it might be worth turning off to see if it makes any difference.