ASP.Net Core CookieRequestCultureProvider not working - asp.net-core-mvc

I have an ASP.Net Core 3.1 app with the following startup.cs (I have tried various combinations of the below configuration based on web searches):
public void ConfigureServices(IServiceCollection services)
{
...
services.Configure<RequestLocalizationOptions>(options =>
{
options.RequestCultureProviders = new[] { new CookieRequestCultureProvider() };
});
and
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
And in my app's logon method, I'm setting the Culture Cookie as follows:
HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture([logged-in-user].CultureCode)));
In subsequent requests I can see the cookie in my browser dev tools Network tab:
Yet, on the server, any given request still maintains the default server culture (which is en-ZA) for my dev environment. (I'm seeing this by checking System.Threading.Thread.CurrentThread.CurrentCulture.Name in any breakpoint in my server action methods)
And I'm running into date conversion issues between my client and my server (e,g client with en-US culture as per screenshot above) sends a date of 3/5/2009 (March 5th) to the server, and the server is interpreting it as May 3rd.
Why is my server not honoring the CultureCookie? What am I missing?

As you mentioned, you have registered your localization service in your ConfigureServices method.
My suggested way is to use it like:
services.AddLocalization(options => options.ResourcesPath = "Resources");
services
.AddControllersWithViews()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
But don't forget to register the middleware:
// In StartUp.cs Configure method
var SupportedCultures = new CultureInfo[]
{
new CultureInfo("en"),
new CultureInfo("zh")
};
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(defaultLanguage),
SupportedCultures = SupportedCultures,
SupportedUICultures = SupportedCultures
});
As for your cookie end-time issue, please try to specify the end date of your cookie. Like this:
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
SameSite = SameSiteMode.None
});

I'm on asp.net core 6.0.
Thanks Anduin for the response, that helped me a lot! The official doc was missleading.
I found the key points for me were:
Service configuration:
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("zh-CN")
};
options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
App support:
var SupportedCultures = new[]
{
new CultureInfo("en"),
new CultureInfo("zh")
};
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(culture: "en", uiCulture: "en"),
SupportedCultures = SupportedCultures,
SupportedUICultures = SupportedCultures
});
Resource files with lang name, but not ISO code:
.\Resources\Controllers\HomeController.zh.resx
.\Resources\Views\Home\Index.zh.resx
Change culture programmatically using Ajax(I added an empty default):
#using Microsoft.AspNetCore.Builder
#using Microsoft.AspNetCore.Http.Features
#using Microsoft.AspNetCore.Localization
#using Microsoft.AspNetCore.Mvc.Localization
#using Microsoft.Extensions.Options
#inject IViewLocalizer Localizer
#inject IOptions<RequestLocalizationOptions> LocOptions
#{
var requestCulture = Context.Features.Get<IRequestCultureFeature>();
var cultureItems = new List<SelectListItem>();
cultureItems.Add(new SelectListItem { Value = "", Text = Localizer["Select Lang"].Value });
cultureItems.AddRange(LocOptions.Value.SupportedUICultures
.Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
.ToList());
var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : $"~{Context.Request.Path.Value}";
}
<div title="#Localizer["Request culture provider:"] #requestCulture?.Provider?.GetType().Name">
<form id="selectLanguage" asp-controller="Home"
asp-action="SetLanguage" asp-route-returnUrl="#returnUrl"
method="post" class="form-horizontal" role="form">
<label asp-for="#requestCulture.RequestCulture.UICulture.Name">#Localizer["Language:"]</label>
<select name="culture" onchange="this.form.submit();" asp-for="#requestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems">
</select>
</form>
</div>
[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)
{
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
);
return LocalRedirect(returnUrl);
}
The IStringLocalizer (I'm keeping the _logger, which is removable):
private readonly ILogger<HomeController> _logger;
private readonly IStringLocalizer<HomeController> _localizer;
public HomeController(ILogger<HomeController> logger, IStringLocalizer<HomeController> localizer)
{
_localizer = localizer;
_logger = logger;
}
public IActionResult Index()
{
ViewData["Message"] = _localizer["Test Text"];
return View();
}
IViewLocalizer in index.cshtml:
#using Microsoft.AspNetCore.Mvc.Localization
#inject IViewLocalizer Localizer
#{
ViewData["Title"] = Localizer["Title Text"];
}
And define the keys Test Text and Title Text in resource files.

Related

which TokenValidationParameters are needed with UseJwtBearerAuthentication

I am tying to do JwtBearerAuthentication on my .net WebAPI and it is just not working. The Authorize attribute is always claiming isAuthorized = false.
I am working with Okta as the SSO. I am authenticating on my client side and getting both an access token and id token. On a webapi get request I am providing the access token (i have also tried the id token) in the authorize header and I am able to see the authorize header with the token in the webapi actioncontext.
In my startup.cs I have the following
var clientID = WebConfigurationManager.AppSettings["okta:ClientId"];
var oidcIssuer = WebConfigurationManager.AppSettings["okta:OIDC_Issuer"];
TokenValidationParameters tvps = new TokenValidationParameters
{
ValidAudience = clientID,
ValidateAudience = true,
ValidIssuer = oidcIssuer,
ValidateIssuer = true
};
app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
{
TokenValidationParameters = tvps,
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new OpenIdConnectCachingSecurityTokenProvider(oidcIssuer + "/.well-known/openid-configuration")
}
});
Am i missing some TokenValidationParameters that I need?
As I was typing this, I see that twaldron was able to figure it out!
I also realized that he was asking about WebAPI, and not MVC. However, here is the code that I needed to get the following working with ASP.NET Core MVC, of particular interest might be this line, which is necessary to get access to the additional claims in the JWT:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
Here is how this code sample works from the command line, the $ID_TOKEN variable contains a valid JWT:
$ curl -H "Authorization: Bearer ${ID_TOKEN}" http://localhost:3000/test/test
sub: 01a23b4cd5eFgHI6j7k8 email:test#example.com
Setup.cs:
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
namespace WebApplication
{
public class Startup
{
readonly string clientId = string.Empty;
readonly string issuer = string.Empty;
readonly string audience = string.Empty;
public Startup(IHostingEnvironment env)
{
clientId = "A0b1CDef2GHIj3k4lm5n";
issuer = "https://example.okta.com";
audience = "A0b1CDef2GHIj3k4lm5n";
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddDebug();
// https://github.com/aspnet/Security/issues/1043#issuecomment-261937401
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
TokenValidationParameters tvps = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = audience,
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
MetadataAddress = issuer + "/.well-known/openid-configuration",
TokenValidationParameters = tvps
});
app.UseStaticFiles();
// Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715
app.UseMvc(routes =>
{
routes.MapRoute(
name: "test-controller",
template: "test/{action}",
defaults: new { controller = "Test", action = "Index" }
);
routes.MapRoute(
name: "default",
template: "{controller=Test}/{action=Index}/{id?}");
});
}
}
}
In Controllers/Test.cs:
[Authorize]
public IActionResult Test()
{
var contextUser = User.Identity as ClaimsIdentity;
Dictionary<string, string> claim = contextUser.Claims.ToDictionary(x => x.Type, x => x.Value);
var output = "sub: " + claim["sub"] + " email:" + claim["email"];
return Content(output);
}
My problem was not with the options.
It was 100% the need to move
app.UseWebApi(config);
below all the owin setup stuff.

Context Disposed Error before DropDownListFor() get populated

I have been trying to get a DropDownList to work:
My controller code is as follows:
public ActionResult Register()
{
var teams = new List<Team>();
using (var context = new TouristContext())
{
teams = (from x in context.Teams select x).ToList();
}
var model = new RegisterViewModel()
{
Teams = new SelectList(teams, "Value", "Text")
};
return View(model);
}
My DropDownListFor() code is as follows:
<div class="form-group">
#Html.LabelFor(m => m.Teams, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.DropDownListFor(m => m.SelectedTeamId, Model.Teams, "Value", "Text")
</div>
</div>
When I try and access the page I get the error:
The operation cannot be completed because the DbContext has been disposed.
I understand the error, but I have no idea how to overcome it.
It turns out the reason it was failing was because I had nothing called Value and Text!
My properties were Id and Name, so I changed my code to:
public ActionResult Register()
{
var teams = new List<Team>();
using (var context = new TouristContext())
{
teams = (from x in context.Teams select x).ToList();
}
var model = new RegisterViewModel()
{
Teams = new SelectList(teams, "Id", "Name")
};
return View(model);
}
And now all works fine.

ASP.Net Web Api Url.Link not returning a UriString in unit test

I Have the following test:
[Test]
public void Add_New_Group_Should_Return_StatusCode_Created_And_A_Header_Location_To_The_New_Group()
{
var newGroup = new GroupData { ID = 1, UserID = 1, Name = "Group 1", Description = "Description 1" };
var fakeGroupDAL = A.Fake<IGroupDAL>();
var contactGroupsController = new ContactGroupsController(fakeGroupDAL);
SetupControllerForTests(contactGroupsController, HttpMethod.Post);
var response = contactGroupsController.AddGroup(new ContactGroupApiRequest(), newGroup);
Assert.IsTrue(response.StatusCode == HttpStatusCode.Created, "Should have returned HttpStatusCode.Created");
}
Which calls the following configuration method:
private static void SetupControllerForTests(ApiController controller, HttpMethod httpMethod)
{
var config = new HttpConfiguration();
var request = new HttpRequestMessage(httpMethod, "http://localhost/contactgroups");
var route = config.Routes.MapHttpRoute("ContactGroupsApi", "{controller}/{action}/{request}", new { request = RouteParameter.Optional });
var routeData = new HttpRouteData(route, new HttpRouteValueDictionary { { "controller", "contactgroups" } });
controller.ControllerContext = new HttpControllerContext(config, routeData, request);
controller.Request = request;
controller.Request.Properties[HttpPropertyKeys.HttpConfigurationKey] = config;
}
I'm trying to test the following action method:
[HttpPost]
public HttpResponseMessage AddGroup([FromUri]ApiRequest req, [FromBody] GroupData contactGroup)
{
if(ModelState.IsValid && contactGroup !=null)
{
_groupDal.AddGroup(contactGroup);
contactGroup.Name = HttpUtility.HtmlEncode(String.Format("{0} - {1}", contactGroup.Name, contactGroup.Description));
var response = new HttpResponseMessage(HttpStatusCode.Created) { Content = new StringContent(contactGroup.Name) };
var uriString = Url.Link("ContactGroupsApi", new { controller = "contactgroups", action = "Group", UserId = contactGroup.UserID, GroupId = contactGroup.ID});
response.Headers.Location = new Uri(uriString);
return response;
}
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
The action method works perfectly well when called normally, but fails under test because the call to Url.Link returns null.
var uriString = Url.Link("ContactGroupsApi", new { controller = "contactgroups", action = "Group", UserId = contactGroup.UserID, GroupId = contactGroup.ID});
All this code is based very closely on the following article: Unit test ASP.NET Web Api
I suspect that when running under test there is insufficient route table info. Any idea what I'm doing wrong?
I fixed my tests by adding the HttpRouteData to the HttpRouteDataKey property of the controller's HttpRequestMessage. Like this:
controller.Request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;

Workaround for missing whitelist in phonegap for windows phone

In my other question I found out that there is no whitelist for windows phones.
Now I am looking for a native code workaround but I have never written a line of native code for windows phones. So it's not easy for me. I think I can download a page like this:
void GetAirportData()
{
var url = new Uri("http://server.example.com/data.php", UriKind.Absolute);
var webClient = new WebClient();
webClient.OpenReadCompleted += new OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
webClient.OpenReadAsync(url, url);
}
But how can a get this data to my javascript app?
Here is a workaround. The following code is a Phonegap command that implements Cross Domain Call functionality.
using System;
using System.IO;
using System.Net;
using System.Runtime.Serialization;
using WP7CordovaClassLib.Cordova;
using WP7CordovaClassLib.Cordova.Commands;
using WP7CordovaClassLib.Cordova.JSON;
namespace Cordova.Extension.Commands //namespace is predefined, don't change it!
{
public class Cdc : BaseCommand //Cross domain call
{
[DataContract]
public class CdcOptions
{
[DataMember(Name = "path")]
public string Path { get; set; }
}
public void Call(string args)
{
CdcOptions options = JsonHelper.Deserialize<CdcOptions>(args);
var url = new Uri(options.Path, UriKind.Absolute);
var webClient = new WebClient();
webClient.OpenReadCompleted += (s, e) =>
{
if (e.Error != null)
{
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Error"));
return;
}
//Stream -> string
var sr = new StreamReader(e.Result);
var result = sr.ReadToEnd();
DispatchCommandResult(
new PluginResult(PluginResult.Status.OK, result));
};
webClient.OpenReadAsync(url, url);
}
}
}
Test on the client side:
<script type="text/javascript">
function cdc(path, success, fail) {
PhoneGap.exec(
success, //success
fail, //fail
"Cdc", //service
"Call", //action
path //args
);
};
function onDeviceReady(e) {
cdc(
{
path: "http://stackoverflow.com/questions/9291809/workaround-for-missing-whitelist-in-phonegap-for-windows-phone"
},
function (arg) {
document.getElementById('test').innerHTML = arg;
}, function (arg) {
document.getElementById('test').innerHTML = arg;
});
}
document.addEventListener("deviceready", onDeviceReady, false);
</script>
</head>
<body>
<div id="test"></div>
</body>
</html>

Deployed Mvc app won't work

I'm having some problems publishing an asp.net mvc3 application. When deployed, the application fails with "The controller for path '/Dashboard/Alarmes' was not found or does not implement IController" where Alarmes is an action at DashboardController. Not sure if it has something to do with it, but Alarmes return an Json result.
Another thing I noticed is that some assemblies, that are referenced by another project in the same solution, are not deployed (only if I reference them in the mvc project itself).
Any tips on these?
Update:
The routes registration:
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{*allpng}", new { allpng = #".*\.png(/.*)?" });
routes.IgnoreRoute("{*favicon}", new { favicon = #"(.*/)?favicon.ico(/.*)?" });
routes.IgnoreRoute("{directory}/{resource}.asmx/{*pathInfo}");
routes.MapRoute("Default", "{controller}/{action}/{id}", new {
controller = "Dashboard",
action = "Index",
id = UrlParameter.Optional
});
}
the action:
public ActionResult Alarmes() {
var alarmesPorPonto = new Dictionary<string, List<Ponto>>();
var alarmes = _repositorioDeAlarmes.Pesquise(ObtenhaInicio(), DateTime.Today);
foreach (var alarme in alarmes) {
var tipo = alarme.Tipo;
var ponto = alarme.Ponto;
if (!alarmesPorPonto.ContainsKey(tipo.Nome)) {
alarmesPorPonto.Add(tipo.Nome, new List<Ponto>());
}
if (!alarmesPorPonto[tipo.Nome].Contains(ponto)) {
alarmesPorPonto[tipo.Nome].Add(ponto);
}
}
return Json(alarmesPorPonto.Select(a => new { Tipo = a.Key, a.Value.Count }), JsonRequestBehavior.AllowGet);
}
Another missing info: I'm deploying this application to a virtual directory.
Update 2
the full controller class:
public class DashboardController : Controller {
private readonly IRepositorioDeAlarmes _repositorioDeAlarmes;
private readonly bool _enableMap;
public DashboardController(IRepositorioDeAlarmes repositorioDeAlarmes) {
_repositorioDeAlarmes = repositorioDeAlarmes;
_enableMap = Convert.ToBoolean(ConfigurationManager.AppSettings["EnableMap"]);
}
public ActionResult Index() {
ViewBag.EnableMap = _enableMap;
return View();
}
public ActionResult Alarmes() {
var alarmesPorPonto = new Dictionary<string, List<Ponto>>();
var alarmes = _repositorioDeAlarmes.Pesquise(ObtenhaInicio(), DateTime.Today);
foreach (var alarme in alarmes) {
var tipo = alarme.Tipo;
var ponto = alarme.Ponto;
if (!alarmesPorPonto.ContainsKey(tipo.Nome)) {
alarmesPorPonto.Add(tipo.Nome, new List<Ponto>());
}
if (!alarmesPorPonto[tipo.Nome].Contains(ponto)) {
alarmesPorPonto[tipo.Nome].Add(ponto);
}
}
return Json(alarmesPorPonto.Select(a => new { Tipo = a.Key, a.Value.Count }), JsonRequestBehavior.AllowGet);
}
}
I suspect that you have hardcoded the url in your javascript when invoking the action instead of using an url helper.
So you wrote:
<script type="text/javascript">
$.getJSON('/Dashboard/Alarmes', function(result) {
...
});
</script>
instead of:
<script type="text/javascript">
$.getJSON('#Url.Action("Alarmes", "Dashboard")', function(result) {
...
});
</script>
which would have generated the correct url in the case when your application is hosted in a virtual directory which would be:
<script type="text/javascript">
$.getJSON('/MyAppName/Dashboard/Alarmes', function(result) {
...
});
</script>
What if you try to clean the ASP.Net temporary files and restart the web server?
[{windows-path}\Microsoft.NET\Framework{framework-version}\Temporary ASP.NET Files]

Resources