How to redirect to log in page on 401 using JWT authorization in ASP.NET Core - asp.net-core

I have this JWT authorization configuration in my Startup.cs:
services.AddAuthentication(opts =>
{
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opts =>
{
opts.RequireHttpsMetadata = false;
opts.SaveToken = true;
opts.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my_secret_key")),
ValidIssuer = "iss",
ValidAudience = "aud",
ValidateIssuerSigningKey = true,
ValidateLifetime = true
};
});
My HomeController has [Authorize] attribute. So upon access to Home/Index, I get a 401 response and I am presented with a blank page. I want to redirect to my Account/LogIn page but I am not sure how to do it.
I read that this shouldn't automatically redirect because it won't make sense to API calls if they are not authorized and then you redirect them, so what is the proper way on how I would get them to the login page on 401.
Please bear in mind that in this project, I have both Web API and Action methods with [Authorize] attributes so I need to redirect only when it is an action method.

You may use StatusCodePages middleware. Add the following inot your Configure method:
app.UseStatusCodePages(async context => {
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
// you may also check requests path to do this only for specific methods
// && request.Path.Value.StartsWith("/specificPath")
{
response.Redirect("/account/login")
}
});
I read that this shouldn't automatically redirect because it won't make sense to API calls
this relates to API calls, that returns data other than pages. Let's say your app do call to API in the background. Redirect action to login page doesn't help, as app doesn't know how to authenticate itself in background without user involving.

Thanks for your suggestion... after spending a good time on google i could find your post and that worked for me. You raised a very good point because it does not make sense for app API calls.
However, I have a situation where the Actions called from the app has a specific notation route (/api/[Controller]/[Action]) which makes me possible to distinguish if my controller has been called by Browser or App.
app.UseStatusCodePages(async context =>
{
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
var path = request.Path.Value ?? "";
if (response.StatusCode == (int)HttpStatusCode.Unauthorized && path.StartsWith("/api", StringComparison.InvariantCultureIgnoreCase))
{
response.Redirect("~/Account/Login");
}
});

This works for both Razor Pages and MVC Views as follows: response.Redirect("/Login"); for Razor Pages. response.Redirect("/Home/Login"); for MVC Views. In order for this to work, the Authorize filter has to be added to the Controller. The code block also has to be added between app.UseAuthentication(); and app.UseAuthorization(); methods.
app.UseStatusCodePages(async context =>
{
var response = context.HttpContext.Response;
if (response.StatusCode == (int)HttpStatusCode.Unauthorized ||
response.StatusCode == (int)HttpStatusCode.Forbidden)
response.Redirect("/Home/Login");
});

Related

Create Blazor Server Login Page to fetch Jwt

We have a Blazor server app which is packaged as an electron desktop app.
Identity cannot reside alongside the app it must be remote so I decided to use Jwt. I have created a Jwt server & have JwtBearer middleware enabled in the Blazor server app startup.cs (see code).
At present we can attach a Jwt to context.Token manually in 'JwtBearerEvents --> OnMessageReceived' and middleware picks this up so we can use [Authorize] & also include Policies [Authorize(Policy="account_write")] this works great.
The Jwt is created manually in Postman (calling our Jwt server and the pasting in the result - see code) as I cannot figure out how to have a login page that uses the Un/Pw to fetch a Jwt from the server.
I appreciate HTTP is only availabe for a few requests before SignalR kicks in, and that is the crux of the problem but im hoping there is a way around this.
I thought I could use a combination of Blazor and razor pages, ie login page is cshtml and saves token to LocalStorage or Cookie then we start blazor (Javascript autostart off for the blazor.server.js) & read the cookie - I have not been able to get this to work as I cannot get razor and cshtml working together.
Any ideas how can I create a login page to fetch the Jwt & attach it to the context?
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
RequireExpirationTime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// Is token in Querystring?
var accessToken = context.Request.Query["access_token"];
Debug.WriteLine($"JwtBearer - OnMessageReceived - access_token in QS: {ShorterJWT(accessToken)}");
//
// Attach the token manually
//
// Jwt fetched manually using Postman - 10 year
accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjNjMzIxMjI2LTFhNGYtNGQ3NS1iZjM1LTIwYjQ1NmVlYmYzOSIsImJvb2tlciI6InRydWUiLCJuYW1lIjoiQm9iIENyYXRjaGV0IiwibmJmIjoxNjI5NzM3MTk4LCJleHAiOjE5NDUyNjk5OTgsImlhdCI6MTYyOTczNzE5OH0.N9HUFM3zIl9BkoPJlPj_WQIUhhQ2Rk53ymEEm42LTD4";
context.Token = accessToken;
return Task.CompletedTask;
}
};
});````

In ASP Net Core 3.1 Expiration cookie is not redirecting to login page when using ajax

In my app, when my cookie expire, I'm redirect to my Account/Login page. But When I call ajax method and cookie is expired , the action return 401 and I'm not redirecting to my Account/login page...
I add [Authorize] attribute on my controller.
The xhr.status parameter return 401.
Example ajax method :
$(document).on('click', '.ajax-modal', function (event) {
var url = $(this).data('url');
var id = $(this).attr('data-content');
if (id != null)
url = url + '/' + id;
$.get(url)
.done(
function (data) {
placeholderElement.html(data);
placeholderElement.find('.modal').modal('show');
}
)
.fail(
function (xhr, httpStatusMessage, customErrorMessage) {
selectErrorPage(xhr.status);
}
);
});
My ConfigureServices method :
public void ConfigureServices(IServiceCollection services)
{
#region Session
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(1000);
options.Cookie.HttpOnly = true; // permet d'empecher à du code JS d'accèder aux cookies
// Make the session cookie essential
options.Cookie.IsEssential = true;
});
#endregion
#region Cookie
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "TestCookie";
options.ExpireTimeSpan = TimeSpan.FromSeconds(10);
options.LoginPath = "/Account/login";
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.Cookie.SameSite = SameSiteMode.Strict;
});
#endregion
Thanks for your help
I came across the issue where I am using cookie authentication in .NET Core 5, yet once the user is authenticated, everything BUT any initial AJAX request in the application works.
Every AJAX request would result in a 401. Even using the jQuery load feature would result in a 401, which was just a GET request to a controller with the [Authorize(Role = "My Role")]
However, I found that I could retrieve the data if I grabbed the URL directly and pasted it in the browser. Then suddenly, all my AJAX worked for the life of the cookie. I noticed the difference in some of the AJAX posts. The ones that didn't work used AspNetCore.AntiForgery in the headers, whereas the ones that did use AspNetCore.Cookies that authenticated.
My fix was to add a redirect in the OnRedirectToLogin event under cookie authentication. It works for all synchronous and asynchronous calls ensuring that AJAX redirects to the login page and authenticates as the current user. I don't know if this is the proper way to handle my issue, but here is the code.
EDIT: I should mention that all of the AJAX code worked perfectly in my .NET 4 web application. When I changed to 5, I experienced new issues.
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => {
o.LoginPath = "/Account/Login";
o.LogoutPath = "/Account/Logout";
o.AccessDeniedPath = "/Error/AccessDenied";
o.SlidingExpiration = true;
//add this to force and request to redirect (my purpose AJAX not going to login page on request and authenticating)
o.Events.OnRedirectToLogin = (context) => {
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});

ASP.NET Core 3.1 - AddJwtBearer, OnTokenValidated event not respecting custom message on Fail

I have a scenario where I need to recreate the principal if a bearer token is provided in the request. For this I use the OnTokenValidated event to execute some custom logic (if bearer is valid). I check if the user email is verified, if so I add custom claims to the user identity which I can then access later on during the same request and make use of the authorisation attributes on controllers and actions.
However I'm trying to return a custom message if the email is not verified, but I keep getting "Unauthorised" back, even though this code is being hit and using the preferred message.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = customDomain;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudiences = audiences,
ValidateIssuer = true,
ValidIssuers = issuers
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = async (context) =>
{
var user = context.Principal;
//Check if already restored during current request
if (user.GetDefaultUserPrincipal() == null)
{
var securityManager = context.HttpContext.RequestServices.GetRequiredService<ISecurityManager>();
var authResponse = await securityManager.AuthenticateMarketplaceFromBearerRequestAsync(user);
if(!authResponse.IsAuthenticated)
{
context.Fail(authResponse.Message);
}
}
}
};
});
Am I missing something here? I've also tried throwing an exception and handling that response in the AuthenticationFailed event, but I get the same thing.
Alternatively I'm playing with the idea of creating a custom policy to do this check as long I can still return a custom response message.
In order to display your custom message when failing the authentication, you can write it to the response of the AuthenticationFailedContext context object.
...
if (!authResponse.IsAuthenticated){
// set the content-type of the response
context.Response.ContentType = "application/json";
// prepare your custom data
var data = new { MyCustomMessage = authResponse.Message };
// user serializer to form your data to string. Here I used Newtonsoft.Json
var jsonResult = JsonConvert.SerializeObject(data);
// Write the jsonresult to the response. Make sure this returns a Task
context.Response.WriteAsync(jsonResult);
}
...

IsAuthenticated is always false in Custom Authorization Attribute (.NET Core 2.2 and JSON Web Token)

I'm trying to build my own custom authorization attribute using JSON Web Token JWT in .net core 2.2.
I'm calling the Authorized API using Postman and I'm facing two problems here:
The Claims in the JWT sent are not being received
IsAuthenticated property is always false in User.Identity.IsAuthenticated.
Please note that the part of JWT is working totally fine, a JWT is being created as I want with the correct Claims and I've checked it on https://jwt.io.
As for my Startup.cs I'm using app.UseAuthentication()
Here's how I'm adding the JWTAuthentication to services:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x=>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidateAudience = false
};
});
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
And here's a snippet of MyCustomAuthorizationAttribute.cs
public string Permissions { get; set; } //Permission string to get from controller
public void OnAuthorization(AuthorizationFilterContext context)
{
//Validate if any permissions are passed when using attribute at controller or action level
if (string.IsNullOrEmpty(Permissions))
{
//Validation cannot take place without any permissions so returning unauthorized
context.Result = new UnauthorizedResult();
return;
}
//The below line can be used if you are reading permissions from token
var permissionsFromToken = context.HttpContext.User.Claims.Where(x => x.Type == "Permissions").Select(x => x.Value).ToList();
var requiredPermissions = Permissions.Split(','); //Multiple permissiosn can be received from controller, delimiter "," is used to get individual values
foreach (var x in requiredPermissions)
{
if (permissionsFromToken.Contains(x))
return; //User Authorized. Wihtout setting any result value and just returning is sufficent for authorizing user
}
context.Result = new UnauthorizedResult();
return;
}
Note: I know that this question is asked a lot before, but I tried most of them and nothing worked for me.
I found out that the order of putting the middleware services in Startup.cs matters. As we can see in the code snippet above. I'm using AddIdentity() middleware after using AddAuthentication() and AddJwtBearer() which was somehow removing the JWT authentication provider.
The solution was simply to put AddIdentity() middleware with all of its sub-methods before AddAuthentication() and AddJWTBearer() middlware.

Where to store JWT Token in .net core web api?

I am using web api for accessing data and I want to authenticate and authorize web api.For that I am using JWT token authentication. But I have no idea where should I store access tokens?
What I want to do?
1)After login store the token
2)if user want to access any method of web api, check the token is valid for this user,if valid then give access.
I know two ways
1)using cookies
2)sql server database
which one is the better way to store tokens from above?
Alternatively, if you just wanted to authenticate using JWT the implementation would be slightly different
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var user = context.Principal.Identity.Name;
//Grab the http context user and validate the things you need to
//if you are not satisfied with the validation fail the request using the below commented code
//context.Fail("Unauthorized");
//otherwise succeed the request
return Task.CompletedTask;
}
};
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey("MyVeryStrongKeyHiddenFromAnyone"),
ValidateIssuer = false,
ValidateAudience = false
};
});
still applying use authentication before use MVC.
[Please note these are very simplified examples and you may need to tighten your security more and implement best practices such as using strong keys, loading configs perhaps from the environment etc]
Then the actual authentication action, say perhaps in AuthenticationController would be something like
[Route("api/[controller]")]
[Authorize]
public class AuthenticationController : Controller
{
[HttpPost("authenticate")]
[AllowAnonymous]
public async Task<IActionResult> AuthenticateAsync([FromBody]LoginRequest loginRequest)
{
//LoginRequest may have any number of fields expected .i.e. username and password
//validate user credentials and if they fail return
//return Unauthorized();
var claimsIdentity = new ClaimsIdentity(new Claim[]
{
//add relevant user claims if any
}, "Cookies");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);
return Ok();
}
}
in this instance I'm using cookies so I'm returning an HTTP result with Set Cookie. If I was using JWT, I'd return something like
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]LoginRequest loginRequest)
{
//validate user credentials and if they validation failed return a similar response to below
//return NotFound();
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("MySecurelyInjectedAsymKey");
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
//add my users claims etc
}),
Expires = DateTime.UtcNow.AddDays(1),//configure your token lifespan and needed
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey("MyVerySecureSecreteKey"), SecurityAlgorithms.HmacSha256Signature),
Issuer = "YourOrganizationOrUniqueKey",
IssuedAt = DateTime.UtcNow
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
var cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTimeOffset.UtcNow.AddHours(4);//you can set this to a suitable timeframe for your situation
cookieOptions.Domain = Request.Host.Value;
cookieOptions.Path = "/";
Response.Cookies.Append("jwt", tokenString, cookieOptions);
return Ok();
}
I'm not familiar with storing your users tokens on your back end app, I'll quickly check how does that work however if you are using dotnet core to authenticate with either cookies or with jwt, from my understanding and experience you need not store anything on your side.
If you are using cookies then you just need to to configure middleware to validate the validity of a cookie if it comes present in the users / consumer's headers and if not available or has expired or can't resolve it, you simply reject the request and the user won't even hit any of your protected Controllers and actions. Here's a very simplified approach with cookies.(I'm still in Development with it and haven't tested in production but it works perfectly fine locally for now using JS client and Postman)
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "yourCookieName";
options.Cookie.SameSite = SameSiteMode.None;//its recommended but you can set it to any of the other 3 depending on your reqirements
options.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents
{
OnRedirectToLogin = redirectContext =>//this will be called if an unauthorized connection comes and you can do something similar to this or more
{
redirectContext.HttpContext.Response.StatusCode = 401;
return Task.CompletedTask;
},
OnValidatePrincipal = context => //if a call comes with a valid cookie, you can use this to do validations. in there you have access to the request and http context so you should have enough to work with
{
var userPrincipal = context.Principal;//I'm not doing anything with this right now but I could for instance validate if the user has the right privileges like claims etc
return Task.CompletedTask;
}
};
});
Obviously this would be placed or called in the ConfigureServices method of your startup to register authentication
and then in your Configure method of your Startup, you'd hookup Authentication like
app.UseAuthentication();
before
app.UseMvc()