Response.RedirectToRoute(RouteData.Values) redirects to the Area controller - asp.net-mvc-routing

I am trying to setup a BaseController to handle culture as part of the url (based on ASP.NET MVC 5 Internationalization). My implementation works properly as long as I disable my Areas' registration.
When One of my Area is registered, if I try to input a wrong/not supported culture (http://localhost:52639/zz/), I experience a 404 error with a request URL: http://localhost:52639/fr/Test/Post.
I have checked my routes are properly registered.
If I do the same while disabling the Areas registration, the base controller and routing behave correctly if I type the following URL: http://localhost:52639/zz/ I am redirected to http://localhost:52639/fr/ (default culture).
Those are my routes:
public static void RegisterRoutes(RouteCollection routes)
{
var namespaces = new[]{typeof(PostController).Namespace};
routes.IgnoreRoute("favicon.ico");
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("PostToHack", "{culture}/Post/{idAndSlug}", new { culture = "", Controller = "Post", Action = "Show" }, namespaces);
routes.MapRoute("Post", "{culture}/Post/{id}-{slug}", new { culture = "", Controller = "Post", Action = "Show" }, namespaces);
routes.MapRoute("TagToHack", "{culture}/Tag/{idAndSlug}", new { culture = "", Controller = "Post", Action = "Tag" }, namespaces);
routes.MapRoute("Tag", "{culture}/Tag/{id}-{slug}", new { culture = "", Controller = "Post", Action = "Tag" }, namespaces);
routes.MapRoute("Logout", "{culture}/Logout", new { culture = "", Controller = "Authentication", Action = "Logout" }, namespaces);
routes.MapRoute("Login", "{culture}/Login", new { culture = "", Controller = "Authentication", Action = "Login" }, namespaces);
//Error routes
routes.MapRoute("Error404", "{culture}/errors/404", new { culture = "", Controller = "Errors", Action = "NotFound" }, namespaces);
routes.MapRoute("Error500", "{culture}/errors/500", new { culture = "", Controller = "Errors", Action = "Error" }, namespaces);
routes.MapRoute("Home", "{culture}", new { culture = "", Controller = "Post", Action = "Index"},namespaces);
//Never to be called by user which is why it comes after MapRoute Home so it is always overwritten by it
routes.MapRoute("Sidebar", "{culture}", new { culture = "", Controller = "Layout", Action = "Sidebar"},namespaces);//This is a "child-only" controller
routes.MapRoute("NavigationBar", "{culture}", new { culture = "", Controller = "Layout", Action = "NavigationBar"},namespaces);//This is a "child-only" controller
Area Route
public override void RegisterArea(AreaRegistrationContext context)
{
var namespaces = new[] { typeof(PostsController).Namespace };
context.MapRoute(
"admin_default",
"{culture}/admin/{controller}/{action}/{id}",
new { culture = "", action = "Index", id = UrlParameter.Optional }, namespaces
);
}
Base Controller:
public class BaseController : Controller
{
protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
{
var cultureName = RouteData.Values["culture"] as string;
// Attempt to read the culture cookie from Request
if (cultureName == null)
cultureName = (Request.UserLanguages != null) && (Request.UserLanguages.Length > 0)
? Request.UserLanguages[0]
: null; // obtain it from HTTP header AcceptLanguages
// Validate culture name
cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe
if (RouteData.Values["culture"] as string != cultureName)
{
// Force a valid culture in the URL
RouteData.Values["culture"] = cultureName.ToLowerInvariant(); // lower case too
// Redirect user
Response.RedirectToRoute(RouteData.Values);
}
// Modify current thread's cultures
Thread.CurrentThread.CurrentCulture = new CultureInfo(cultureName);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
return base.BeginExecuteCore(callback, state);
}
}

After some more digging I have found a solution that work for me. My issue was coming from the order in which I was registering my routes. I was registering my Area's routes first:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
I inverted the order and made sure that I was only registering my Area's routes after:
protected void Application_Start()
{
RouteConfig.RegisterRoutes(RouteTable.Routes);
AreaRegistration.RegisterAllAreas();
}

Related

Invalid JWT bearer token on .net 6.0 web api

I am trying to implement security to my app and used Identity and JWT bearer token to authenticate but I always get the invalid token response in my swagger. Tried many solutions but I still get the same response.
[HttpPost("[action]")]
public async Task<IActionResult> Login(LoginBindingModel login)
{
IActionResult actionResult;
var user = await userManager.FindByEmailAsync(login.Email);
if (user == null)
{
actionResult = NotFound(new {errors = new[] {$"User with email {login.Email} is not found."}});
}
else if (await userManager.CheckPasswordAsync(user, login.Password))
{
if(!user.EmailConfirmed)
{
actionResult = BadRequest(new { errors = new[] { $"Email not confirmed." } });
}
else
{
var token = GenerateTokenAsync(user);
actionResult = Ok(token);
}
}
else
{
actionResult = BadRequest(new { errors = new[] { $"Password is not valid." } });
}
return actionResult;
}
private string GenerateTokenAsync(IdentityUser user)
{
IList<Claim> userClaims = new List<Claim>
{
new Claim("UserName", user.UserName),
new Claim("Email", user.Email)
};
var x = jwtOptions.SecurityKey;
return new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(
claims: userClaims,
expires: DateTime.UtcNow.AddMonths(1),
signingCredentials: new SigningCredentials(jwtOptions.SecurityKey, SecurityAlgorithms.HmacSha256)));
}
Program.cs
using ASPNetCoreMasterAPIAssignment2.Filters;
using DomainModels;
using Infrastructure.Data.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Repositories;
using Services;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddSwaggerGen(x =>
{
x.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please insert JWT token with bearer into field",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
});
x.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});
builder.Services.AddScoped<IItemService, ItemService>();
builder.Services.AddScoped<IItemRepository, ItemRepository>();
builder.Services.AddDbContext<ItemDbContext>(opt =>
{
opt.UseSqlServer(builder.Configuration.GetConnectionString("default"));
});
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ItemDbContext>()
.AddDefaultTokenProviders();
SecurityKey key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(builder.Configuration["jwt:secret"]));
builder.Services.Configure<JWTOptions>(_ => _.SecurityKey = key);
builder.Services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = "Bearer";
opt.DefaultChallengeScheme = "Bearer";
opt.DefaultScheme = "Bearer";
})
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKey = key
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseExceptionHandler("/error");
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"default": "Server=localhost;Database=EfCoreDb;Trusted_Connection=True;"
},
"jwt": {
"secret": "5a927360-790b-4ba5-bae1-09aa98364090"
}
}
when i add the [Authorized] attribute to a controller, i get the following error
invalid_token can be caused by a lot of cases.
Try to view the root cause by handling the OnChallenge method (in particular the content of context.AuthenticateFailure):
return builder.AddJwtBearer(options =>
{
options.Authority = issuer;
options.Audience = audience;
options.TokenValidationParameters = new TokenValidationParameters()
{
ClockSkew = new System.TimeSpan(0, 0, 30)
};
options.Events = new JwtBearerEvents()
{
OnChallenge = context =>
{
context.HandleResponse();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json";
// Ensure we always have an error and error description.
if (string.IsNullOrEmpty(context.Error))
context.Error = "invalid_token";
if (string.IsNullOrEmpty(context.ErrorDescription))
context.ErrorDescription = "This request requires a valid JWT access token to be provided";
// Add some extra context for expired tokens.
if (context.AuthenticateFailure != null && context.AuthenticateFailure.GetType() == typeof(SecurityTokenExpiredException))
{
var authenticationException = context.AuthenticateFailure as SecurityTokenExpiredException;
context.Response.Headers.Add("x-token-expired", authenticationException.Expires.ToString("o"));
context.ErrorDescription = $"The token expired on {authenticationException.Expires.ToString("o")}";
}
return context.Response.WriteAsync(JsonSerializer.Serialize(new
{
error = context.Error,
error_description = context.ErrorDescription
}));
}
};
});
source: https://sandrino.dev/blog/aspnet-core-5-jwt-authorization#configuring-jwt-bearer-authentication
In my case, it was resolved by updating some nuget, specially Microsoft.IdentityModel.Tokens : https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1792

Unsupported Grant Type with CustomGrantValidator with IdentityServer 3

I'm trying to set up our IdentityServer solution to accept a custom Grant Validator. Our API project is accessed by to UIs, one that uses Password authentication (which is working) and now one that will use a 3rd party authentication.
In our API I've set up IdentityServer like so:
Startup.cs
public void Configuration(IAppBuilder app)
{
var factory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
var userService = new IdentityUserService();
factory.UserService = new Registration<IUserService>(resolver => userService);
factory.CustomGrantValidators.Add(
new Registration<ICustomGrantValidator, MyGrantValidator>());
var options = new IdentityServerOptions
{
SiteName = "My App Name",
SigningCertificate = Certificate.Get(),
Factory = factory
};
app.Map("/identity", identityServerApp =>
{
identityServerApp.UseIdentityServer(options);
});
}
MyGrantValidator.cs:
public class MyGrantValidator : ICustomGrantValidator
{
public async Task<CustomGrantValidationResult> ValidateAsync(ValidatedTokenRequest request)
{
// For now I just want a basic response. More logic will come later.
var authResult = new AuthenticateResult(
subject: "1234", // user.AccountId.ToString(),
name: "bob" //context.UserName
);
var grantResult = new CustomGrantValidationResult
{
IsError = authResult.IsError,
Error = authResult.ErrorMessage,
ErrorDescription = authResult.ErrorMessage,
Principal = authResult.User
};
return await Task.FromResult(grantResult);
}
public string GrantType => "myGrantType";
}
In my UI, I setup a client like this:
var owinContext = HttpContext.GetOwinContext();
var token = owinContext.Authentication.User.FindFirst(c => c.Type == "myToken")?.Value;
var tokenId = owinContext.Authentication.User.FindFirst(c => c.Type == ClaimTypes.Sid)?.Value;
var client = new TokenClient(
ConfigurationManager.AppSettings["IdentityServerBaseUrl"] + "/connect/token",
"MyUser",
ConfigurationManager.AppSettings["MyClientSecret"],
AuthenticationStyle.Custom
);
var tokenResponse = client.RequestCustomGrantAsync(
"myGrantType",
"read write",
new Dictionary<string, string>
{
{ "token", token },
{ "tokenId", tokenId }
}
).Result;
return Redirect(returnUrl);
When the Request is triggered, I get: unsupported_grant_type
What am I missing?
You're using a client called "MyUser" (weird name for a client, but ok). Is that client registered as one of the in-memory clients with grant type set to "custom"?

Can't get email or public profile facebook by ASP MVC5

I use MVC5 to get public profile user, but I just get ID and DisplayName of user. I have researched multi ways but have no result. This my setup snippet:
var options = new FacebookAuthenticationOptions
{
AppId = "xxx",
AppSecret = "xxx",
SignInAsAuthenticationType = "ExternalCookie",
Provider = new FacebookAuthenticationProvider
{
OnAuthenticated = async context =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));
}
}
};
options.Scope.Add("email");
options.Scope.Add("public_profile");
app.UseFacebookAuthentication(options);
And I call request result in action
public async Task<ActionResult> Index()
{
var userDisplayModel = new UserDisplayModel();
var authenticateResult = await AuthenticationManager.AuthenticateAsync("ExternalCookie");
if (authenticateResult != null)
{
userDisplayModel = new UserDisplayModel()
{
DisplayName = authenticateResult.Identity.Claims.FirstOrDefault(t => t.Type == ClaimTypes.Name).Value
};
}
return View(userDisplayModel);
}

ASP.NET MVC 3 - Custom SEO friendly routes

I've defined the following route:
routes.MapRoute(
null,
"foo/{id}/{title}",
new { controller = "Boo", action = "Details" }
);
When I call this method:
Url.Action("Details", "Boo", new { id = article.Id, title = article.Title })
I get the following URL:
http://localhost:57553/foo/1/Some%20text%20Š
I would like to create a new route that will lowercase all characters and replace some of them.
e.g.
http://localhost:57553/foo/1/some-text-s
Rules:
Uppercase -> lowercase
' ' -> '-'
'Š' -> 's'
etc.
Any help would be greatly appreciated!
Seems like a perfect candidate for a custom route:
public class MyRoute : Route
{
public MyRoute(string url, object defaultValues)
: base(url, new RouteValueDictionary(defaultValues), new MvcRouteHandler())
{
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
values = new RouteValueDictionary(values);
var title = values["title"] as string;
if (!string.IsNullOrEmpty(title))
{
values["title"] = SEOify(title);
}
return base.GetVirtualPath(requestContext, values);
}
private string SEOify(string title)
{
throw new NotImplementedException();
}
}
which will be registered like this:
routes.Add(
"myRoute",
new MyRoute(
"foo/{id}/{title}",
new { controller = "Boo", action = "Details" }
)
);
Now all you have to do is to implement your SEO requirements in the SEOify function that I left. By the way you could get some inspiration from the way StackOverflow does it for the question titles.

Querystring Route in MVC2

I'm trying to create a route to a specific controller/action which needs to accept optional querystring parameters.
the urls i'd like to accept are:
/Products/ProductsListJson
/Products/ProductsListJson?productTypeId=1
/Products/ProductsListJson?productTypeId=1&brandId=2
/Products/ProductsListJson?productTypeId=1&brandId=2&year=2010
I have an action like this:
public JsonResult ProductsListJson(int productTypeId, int brandId, int year)
And a route like this:
routes.MapRoute(
null, "Products/ProductsListJson",
new { controller = "Products", action = "ProductsListJson", productTypeId = 0, brandId = 0, year = 0 }
);
I assumed that the action "ProductsListJson" would simply see the querystring urls and map them to the appropriate arguments however this is not happening.
Anyone know how this could be achived?
You don't need to specify their values in the route if those parameters are passed in the query string:
routes.MapRoute(
null, "Products/ProductsListJson",
new { controller = "Products", action = "ProductsListJson" }
);
and your action:
public ActionResult ProductsListJson(int? productTypeId, int? brandId, int? year)
{
...
}
but you probably don't need a specific route for this as the default route will handle it just fine:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);