Localization chooses wrong language based on accept-language header - asp.net-core

I´am trying to add translations for some error messages, but I'am having a hard time figuring out some .net black magic.
Everything works in firefox, it seems to be because it sends the nb-No field in accept-language header.
Firefox header:
nb-NO,nb;q=0.9,no-NO;q=0.8,no;q=0.6,nn-NO;q=0.5,nn;q=0.4,en-US;q=0.3,en;q=0.1
but it doesn't work in chrome, this seems to be because it sends this accept-lanuage header:
no,sv;q=0.9
I have added support for both nb-NO and no (I thought). I have tried to add a file for each of the languages. This is how it looks:
[![Image of resx file layout in solution][1]][1]
So I have figured out that if the accept-language header is noit goes to Controllers.ErrorController.resx, why doesnt it go to Controllers.ErrorController.no.resx?
I'am using netcoreapp 2.1
I have added relevant code below:
So in startup it looks like this:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddLocalization(options => options.ResourcesPath = "Resources/Languages");
services.Configure<RequestLocalizationOptions>(Language.ConfigureRequestLanguage());
...
}
public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
....
app.UseExceptionHandler("/api/errors");
app.UseRequestLocalization(Language.SupportedCultures());
...
}
The helper functions:
public static class Language
{
private const string DefaultLanguage = "en-US";
private static readonly CultureInfo[] SupportedUiCultures = {
new CultureInfo(DefaultLanguage),
new CultureInfo("no"),
new CultureInfo("nb-NO"),
new CultureInfo("sv"),
new CultureInfo("sv-SE"),
};
public static Action<RequestLocalizationOptions> ConfigureRequestLanguage()
{
return options =>
{
options.DefaultRequestCulture = new RequestCulture(culture: DefaultLanguage, uiCulture: DefaultLanguage);
options.SupportedCultures = SupportedUiCultures;
options.SupportedUICultures = SupportedUiCultures;
options.RequestCultureProviders.Insert(0, new CustomRequestCultureProvider(context =>
{
var userLanguages = context.Request.GetTypedHeaders().AcceptLanguage
.OrderByDescending(x => x.Quality ?? 1);
var match = userLanguages.FirstOrDefault(r => SupportedUiCultures.Any(s => s.Name == r.Value.ToString()));
var prioritizedLanguage = match == null ? DefaultLanguage : match.Value.ToString();
var language = string.IsNullOrEmpty(prioritizedLanguage) ? DefaultLanguage : prioritizedLanguage;
return Task.FromResult(new ProviderCultureResult(language, language));
}));
};
}
public static RequestLocalizationOptions SupportedCultures()
{
return new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(DefaultLanguage),
SupportedCultures = SupportedUiCultures,
SupportedUICultures = SupportedUiCultures
};
}
}
[1]: https://i.stack.imgur.com/3W58R.png

Related

Redirect to same page with an extra parameter

I am using Request Localization in a NET Core 7 and Razor Pages application:
builder.Services.AddRazorPages();
builder.Services.Configure<RequestLocalizationOptions>(options => {
options.DefaultRequestCulture = new RequestCulture("pt");
options.SupportedCultures = new List<CultureInfo> { new CultureInfo("en"), new CultureInfo("pt") };
options.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider {
RouteDataStringKey = "culture",
UIRouteDataStringKey = "culture",
Options = options
});
});
WebApplication application = builder.Build();
application.UseRouting();
application.MapRazorPages();
application.UseRequestLocalization();
// ...
A few of my razor pages are:
Index: #page "/{culture?}" Example: /pt
About: #page "/{culture?}/about" Example: /pt/about
When the culture is null or invalid I want to redirect to same page with default culture.
I am trying to do this using a middleware:
public class RedirectUnsupportedCulturesMiddleware {
private readonly RequestDelegate _next;
private readonly string _routeDataStringKey;
public RedirectUnsupportedCulturesMiddleware(
RequestDelegate next,
IOptions<RequestLocalizationOptions> monitor) {
RequestLocalizationOptions options = monitor.Value;
_next = next;
var provider = options.RequestCultureProviders
.Select(x => x as RouteDataRequestCultureProvider)
.Where(x => x != null)
.FirstOrDefault();
_routeDataStringKey = provider.RouteDataStringKey;
}
public async Task Invoke(HttpContext context) {
var requestedCulture = context.GetRouteValue(_routeDataStringKey)?.ToString();
var cultureFeature = context.Features.Get<IRequestCultureFeature>();
var actualCulture = cultureFeature?.RequestCulture.Culture.Name;
if (string.IsNullOrEmpty(requestedCulture) ||
!string.Equals(requestedCulture, actualCulture, StringComparison.OrdinalIgnoreCase)) {
var newCulturedPath = GetNewPath(context, actualCulture);
context.Response.Redirect(newCulturedPath);
return;
}
await _next.Invoke(context);
}
private string GetNewPath(HttpContext context, string newCulture) {
var routeData = context.GetRouteData();
var router = routeData.Routers[0];
var virtualPathContext = new VirtualPathContext(
context,
routeData.Values,
new RouteValueDictionary { { _routeDataStringKey, newCulture } });
return router.GetVirtualPath(virtualPathContext).VirtualPath;
}
}
When I access the home page, e.g "/", I get an exception in the following:
var router = routeData.Routers[0];
Questions
How to solve this?
Can I use a 301 redirect from thee middleware?
Can I use the Net Core UseRewriter to accomplish the same objective?
Here is a whole working demo you could follow:
1.Custom IPageRouteModelConvention to map the route and then no need modify page route like #page "/{culture?}/xxx"
public class CustomCultureRouteRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
List<SelectorModel> selectorModels = new List<SelectorModel>();
foreach (var selector in model.Selectors.ToList())
{
var template = selector.AttributeRouteModel.Template;
selectorModels.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel
{
Template = "/{culture}" + "/" + template
}
});
}
foreach (var m in selectorModels)
{
model.Selectors.Add(m);
}
}
}
2.Add this page conventions in Program.cs
builder.Services.AddRazorPages().AddRazorPagesOptions(opts =>
{
opts.Conventions.Add(new CustomCultureRouteRouteModelConvention()); //add this...
});
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.Configure<RequestLocalizationOptions>(
opt =>
{
var supportCulteres = new List<CultureInfo>
{
new CultureInfo("pt"),
new CultureInfo("fr")
};
opt.DefaultRequestCulture = new RequestCulture("pt");
opt.SupportedCultures = supportCulteres;
opt.SupportedUICultures = supportCulteres;
opt.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider()); //add this...
});
3.The order of the middleware
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.UseRequestLocalization();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseRequestLocalization(); //be sure add here......
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Result:

Why are my swagger docs showing 'additionalProperties = false' for my custom schema filter?

I have this SchemaFilter in my swagger config
public class SmartEnumSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (!TryGetSmartEnumValues(context.Type, out var values))
{
return;
}
var openApiInts = new OpenApiArray();
openApiInts.AddRange(values.Select(x => new OpenApiInteger(x.Value)));
schema.Type = "integer";
schema.Enum = openApiInts;
schema.Properties = null;
schema.Description = string.Join(", ", values.Select(v => $"{v.Value}: {v.Name}"));
}
}
It is working well, but for some reason this "additional properties" always appears in the docs:
But only for this particular schema - all the other schemas don't have it. Is there some way of removing it?
I tried setting both
schema.AdditionalProperties = null;
schema.AdditionalPropertiesAllowed = false;
but it made no difference

Mocking ControllerBase.HttpContext.AuthenicateAsync() to return success using SAML2.0

I am trying to understand why HttpContext.AuthenticateAsync() method fails when I set the httpContext using the following code
public static void SetMockAuthenticatedControllerContext(this Controller controller,string userName)
{
var httpContext = MockHttpContext(userName);
var controllerContext = new ControllerContext
{
HttpContext= httpContext,
RouteData=new Microsoft.AspNetCore.Routing.RouteData()
};
controller.ControllerContext = controllerContext;
}
public static HttpContext MockHttpContext(string userName)
{
var context = new Mock<HttpContext>();
var request = new Mock<HttpRequest>();
var response = new Mock<HttpResponse>();
var session = new Mock<ISession>();
var user = new Mock<ClaimsPrincipal>();
var identity = new Mock<ClaimsIdentity>();
var features = new Mock<IFeatureCollection>();
var authService = new Mock<IAuthenticationService>();
var prodvierServiceMock = new Mock<IServiceProvider>();
var authTicket = new AuthenticationTicket(new ClaimsPrincipal(), "External");
authService.Setup(c => c.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
.Returns(Task.FromResult(AuthenticateResult.Success(authTicket)));
prodvierServiceMock.Setup(c => c.GetService(typeof(IAuthenticationService))).Returns(authService);
context.Setup(ctx => ctx.Request).Returns(request.Object);
context.Setup(ctx => ctx.Response).Returns(response.Object);
context.Setup(ctx => ctx.Session).Returns(session.Object);
//context.Setup(ctx=>ctx.GetServerVariable(It.IsAny<string>())).Returns()
context.Setup(ctx => ctx.User).Returns(user.Object);
context.Setup(x => x.RequestServices).Returns(prodvierServiceMock.Object);
context.Setup(ctx => ctx.Features).Returns(features.Object);
context.Setup(ctx => ctx.User.Identity).Returns(identity.Object);
identity.Setup(x => x.IsAuthenticated).Returns(true);
identity.Setup(x => x.Name).Returns(userName);
return context.Object;
}
and I am calling this in my controller test class as
follows:
HttpContextFactory.SetMockAuthenticatedControllerContext(mycontrollerInstance,userName);
then
call
var result = controller.Index();
which throws the following error:
Unable to cast object of type 'Moq.Mock`1[Microsoft.AspNetCore.Authentication.IAuthenticationService]' to type 'Microsoft.AspNetCore.Authentication.IAuthenticationService
Please help me understand what i am doing wrong.
Thanks for the help.

IdentityServer 4 - Custom IExtensionGrantValidator always return invalid_grant

My app requirements is to authenticate using client credentials AND another code (hash).
I followed this link to create and use custom IExtensionGrantValidator.
I manged to invoke the custom IExtensionGrantValidator with approved grant, but client always gets invalid_grant error.
For some reason the set operation ofd Result (property of ExtensionGrantValidationContext) always fails (overriding the Error value returns the overrided value to client).
This is CustomGrantValidator Code:
public class CustomGrantValidator : IExtensionGrantValidator
{
public string GrantType => "grant-name";
public Task ValidateAsync(ExtensionGrantValidationContext context)
{
var hash = context.Request.Raw["hash"]; //extract hash from request
var result = string.IsNullOrEmpty(hash) ?
new GrantValidationResult(TokenRequestErrors.InvalidRequest) :
new GrantValidationResult(hash, GrantType);
context.Result = result
}
}
Startup.cs contains this line:
services.AddTransient<IExtensionGrantValidator, CustomGrantValidator>();
And finally client's code:
var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:5000") };
var disco = await httpClient.GetDiscoveryDocumentAsync("http://localhost:5000");
var cReq = await httpClient.RequestTokenAsync(new TokenRequest
{
GrantType = "grant-name",
Address = disco.TokenEndpoint,
ClientId = clientId,// client Id taken from appsetting.json
ClientSecret = clientSecret, //client secret taken from appsetting.json
Parameters = new Dictionary<string, string> { { "hash", hash } }
});
if (cReq.IsError)
//always getting 'invalid_grant' error
throw InvalidOperationException($"{cReq.Error}: {cReq.ErrorDescription}");
The below codes works on my environment :
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var hash = context.Request.Raw["hash"]; //extract hash from request
var result = string.IsNullOrEmpty(hash) ?
new GrantValidationResult(TokenRequestErrors.InvalidRequest) :
new GrantValidationResult(hash, GrantType);
context.Result = result;
return;
}
Don't forget to register the client to allow the custom grant :
return new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = { "grant-name" },
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
I got the same issue and found the answer from #Sarah Lissachell, turn out that I need to implement the IProfileService. This interface has a method called IsActiveAsync. If you don't implement this method, the answer of ValidateAsync will always be false.
public class IdentityProfileService : IProfileService
{
//This method comes second
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//IsActiveAsync turns out to be true
//Here you add the claims that you want in the access token
var claims = new List<Claim>();
claims.Add(new Claim("ThisIsNotAGoodClaim", "MyCrapClaim"));
context.IssuedClaims = claims;
}
//This method comes first
public async Task IsActiveAsync(IsActiveContext context)
{
bool isActive = false;
/*
Implement some code to determine that the user is actually active
and set isActive to true
*/
context.IsActive = isActive;
}
}
Then you have to add this implementation in your startup page.
public void ConfigureServices(IServiceCollection services)
{
// Some other code
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddAspNetIdentity<Users>()
.AddInMemoryApiResources(config.GetApiResources())
.AddExtensionGrantValidator<CustomGrantValidator>()
.AddProfileService<IdentityProfileService>();
// More code
}

Route localization in ASP.NET Core 2.2

I am developing application using ASP.NET Core 2.2 and I am struggling with how to implement route localization, ex. depending on request I need to redirect to route /en/products, if language is not specified in the route.
If language is not specified then get locale from accept-language header.
Below demo is applied to use twoLetterLanguageName.Refer to this tutorial
1.Create a RouteDataRequestCultureProvider class:
public class RouteDataRequestCultureProvider : RequestCultureProvider
{
public int IndexOfCulture;
public int IndexofUICulture;
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext == null)
throw new ArgumentNullException(nameof(httpContext));
string culture = null;
string uiCulture = null;
var twoLetterCultureName = httpContext.Request.Path.Value.Split('/')[IndexOfCulture]?.ToString();
var twoLetterUICultureName = httpContext.Request.Path.Value.Split('/')[IndexofUICulture]?.ToString();
if (twoLetterCultureName == "de")
culture = "de-DE";
else if (twoLetterCultureName == "en")
culture = uiCulture = "en-US";
if (twoLetterUICultureName == "de")
culture = "de-DE";
else if (twoLetterUICultureName == "en")
culture = uiCulture = "en-US";
if (culture == null && uiCulture == null)
return NullProviderCultureResult;
if (culture != null && uiCulture == null)
uiCulture = culture;
if (culture == null && uiCulture != null)
culture = uiCulture;
var providerResultCulture = new ProviderCultureResult(culture, uiCulture);
return Task.FromResult(providerResultCulture);
}
}
2.And a LanguageRouteConstraintclass
public class LanguageRouteConstraint : IRouteConstraint
{
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.ContainsKey("culture"))
return false;
var culture = values["culture"].ToString();
return culture == "en" || culture == "de";
}
}
3.startup.cs ConfigureServices:
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en"),
new CultureInfo("de"),
};
options.DefaultRequestCulture = new RequestCulture(culture: "en", uiCulture: "en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders = new[]{ new RouteDataRequestCultureProvider{
IndexOfCulture=1,
IndexofUICulture=1
}};
});
services.Configure<RouteOptions>(options =>
{
options.ConstraintMap.Add("culture", typeof(LanguageRouteConstraint));
});
4.startup.cs Configure
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
//other middlewares
app.UseMvc(routes =>
{
routes.MapRoute(
name: "LocalizedDefault",
template: "{culture:culture}/{controller}/{action}/{id?}",
defaults: new {controller = "Home", action = "Index" });
//constraints: new { culture = new CultureConstraint(defaultCulture: "en", pattern: "[a-z]{2}") });
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
});
}
Then you could change culture in browser url directly using /en/Home/Privacy.
If language is not specified then get locale from accept-language header
You could use Url Rewriting Middleware to check the route value and redirect it to a new route with default culture.
1.Create a Redirect rule:
public class RewriteRules
{
public static void RedirectRequests(RewriteContext context)
{
//Your logic
var request = context.HttpContext.Request;
var path = request.Path.Value;
var userLangs = request.Headers["Accept-Language"].ToString();
var firstLang = userLangs.Split(',').FirstOrDefault();
var defultCulture = string.IsNullOrEmpty(firstLang) ? "en" : firstLang.Substring(0,2);
//Add your conditions of redirecting
if ((path.Split("/")[1] != "en") && (path.Split("/")[1] != "de"))// If the url does not contain culture
{
context.HttpContext.Response.Redirect($"/{defultCulture}{ request.Path.Value }");
}
}
}
2..Use the middleware in Startup Configure method:
app.UseAuthentication();//before the Rewriter middleware
app.UseRewriter(new RewriteOptions()
.Add(RewriteRules.RedirectRequests)
);
//MVC middleware
Then if your input /Home/Privacy in browser it will redirect to url like /en/Home/Privacy.