.NET Core MVC RequestLocalization ignoring DefaultRequestCulture - localization

I've implemented RequestLocalization for es-ES with a single MVC view via the following (note: this code is condensed to only the most relevant pieces):
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix,
opts =>
{
opts.ResourcesPath = "Resources";
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
var english = "en-US";
var englishRequestCulture = new RequestCulture(culture: english, uiCulture: english);
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("es-ES")
};
var options = new RequestLocalizationOptions
{
DefaultRequestCulture = englishRequestCulture,
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
};
app.UseRequestLocalization(options);
app.UseMvc();
}
When passing culture=en-US or culture=es-ES as query string parameters, this works perfectly. My expectation is that the default culture should be en-US when no culture is provided. However, when I do not provide the culture parameter, my view is defaulting to es-ES. I have confirmed that all other Localization providers are also defaulted to en-US.
I should also note that I attempted Localization via ConfigureServices() but was unable to get this to function at all:
services.Configure<RequestLocalizationOptions>(
options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("es-ES")
};
options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});

I had the same problem myself. Take a look at your HTTP requests! Do they contain an Accept-Language header set to es-ES (or anything)? Then your localization middleware is working just fine. One of the three default RequestCultureProviders, namely AcceptLanguageHeaderRequestCultureProvider, tries to determine the culture by doing what you did - looking for the Accept-Language header.
So no, the localization middleware does not ignore DefaultRequestCulture, as you and a previous answer suggested.

After much trial and error, I determined that setting the DefaultRequestCulture property has no impact and, as a result, CookieRequestCultureProvider is actually defaulting to es-ES (though I am not entirely sure why, the machine this is running on is set to English and US locale).
As a workaround I modified my existing Configure() method to remove other (currently unused) providers:
private void ConfigureApplicationLocalization(IApplicationBuilder app)
{
var english = "en-US";
var englishRequestCulture = new RequestCulture(culture: english, uiCulture: english);
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("es-ES")
};
var options = new RequestLocalizationOptions
{
DefaultRequestCulture = englishRequestCulture,
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
};
//RequestCultureProvider requestProvider = options.RequestCultureProviders.OfType<AcceptLanguageHeaderRequestCultureProvider>().First();
//requestProvider.Options.DefaultRequestCulture = englishRequestCulture;
RequestCultureProvider requestProvider = options.RequestCultureProviders.OfType<CookieRequestCultureProvider>().First();
options.RequestCultureProviders.Remove(requestProvider);
app.UseRequestLocalization(options);
}

I had a similar problem to this. I was translating to Ukrainian language and I was using the country code ua instead of uk.
I changed that and it worked straight away.
Not saying this will fix the OP's problem but hopefully will help someone who comes across this question who has a similar problem.
Full list of codes here:
https://msdn.microsoft.com/en-gb/library/ee825488(v=cs.20).aspx

Related

Accessing Payload of JWT with mvc dotnet core

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.

MVC Core 3.0 options.LoginPath - add localization route parameter

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.

Asp.Net MVC Controller / Ressource Manager: globalization issue

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!

A localized scriptbundle solution

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));
}
}

Can MVC route resolution mechanism be re-configured?

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.

Resources