MVC Authorization with JWT

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
20,307
I've been working on setting up and MVC project which calls an API.

So far so good, everything has been working up till now but I've hit a snag implementing authorization. This is the JWT I get back from the API on Login. It's the usual crazy long string which translates to this.

Code:
{
  "sub": "administrator@hotmail.com",
  "jti": "871853ae-95a3-4e57-9b51-d5043dc812f7",
  "email": "administrator@hotmail.com",
  "id": "480b621b-f55c-43ad-b4d9-2734042b237d",
  "role": "Admin",
  "nbf": 1618694272,
  "exp": 1618694572,
  "iat": 1618694272
}

This is the middleware I have set up. The secret is the same as on the API project.

C#:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
    var signingKey = Encoding.ASCII.GetBytes(jwtSettings.Secret);
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(signingKey)
    };
});

C#:
app.UseAuthentication();

And this is in my AccountController

C#:
// api/v1/identity/login
[HttpPost]
public async Task<ActionResult> Login(SignIn signIn)
{
    var response = await _serviceClient.LoginUserAsync(_jwtSettings.WebServiceURL + ApiRoutes.Identity.Login, signIn.Email, signIn.Password);

    var responseMessage = response.StatusCode;
    if(responseMessage != HttpStatusCode.OK)
    {
        return Redirect("~/Account/Login");
    }

    return Redirect("~/Employee/Index");
}

On my Employee controller I have the following at the top:

C#:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles ="Admin")]
public class EmployeesController : Controller
{
.
.
.
.

What is happening is the Employee controller is simply returning a blank page. If I remove the authorize attribute above, the page works. This says to me that it is not reading the Admin role from the token.

Can someone please tell me where I have gone wrong? Been staring at this thing for a while now and would really appreciate some feedback.
 

DA-LION-619

Honorary Master
Joined
Aug 22, 2009
Messages
11,555
C#:
app.UseAuthentication();
You'd also need app.UseAuthorization(); but that won't fix your issue.

What is happening is the Employee controller is simply returning a blank page. If I remove the authorize attribute above, the page works. This says to me that it is not reading the Admin role from the token.

Can someone please tell me where I have gone wrong? Been staring at this thing for a while now and would really appreciate some feedback.
The client side(jQuery, mobile app etc.) needs to send the JWT on every request, there is no state.
Your MVC controller is getting the JWT but not passing it to the requester, you're probably seeing a blank page because you don't have a means to handle a 401/403 but that status code should be there in the response.

Also note typically JS in the browser won't follow a redirect.
 

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
20,307
You'd also need app.UseAuthorization(); but that won't fix your issue.


The client side(jQuery, mobile app etc.) needs to send the JWT on every request, there is no state.
Your MVC controller is getting the JWT but not passing it to the requester, you're probably seeing a blank page because you don't have a means to handle a 401/403 but that status code should be there in the response.

Also note typically JS in the browser won't follow a redirect.

For now what Ill do in the EmployeeController is just comment out any calls to the API. Good spot though I get what you are saying about calls needing to send the token back to the API to validate the user.

Even better I will just set up a quick TestController and have the login redirect to that with its index method simply returning some text to the view, have my auth header in place and see what happens.
 

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
20,307
You'd also need app.UseAuthorization(); but that won't fix your issue.


The client side(jQuery, mobile app etc.) needs to send the JWT on every request, there is no state.
Your MVC controller is getting the JWT but not passing it to the requester, you're probably seeing a blank page because you don't have a means to handle a 401/403 but that status code should be there in the response.

Also note typically JS in the browser won't follow a redirect.

I have put a bare bones app together quickly as an example of my issue. The HomeController simply logs in, retrieves the token and redirects to a dummy EmployeeController which has an authorization header.

What is happening is the authorization is failing and the employee controller is simply not displaying.

I have put this basic project into my Repo. If anybody could please point out why this is happening I would be most appreciative.

 
Last edited:

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
20,307
As was pointed out by @DA-LION-619 , I also don't see you sending the bearer token along with the redirect to your EmployeeController. Response.Redirect loses the bearer token.

Here's one way of doing where you use a HttpClient: https://stackoverflow.com/questions...ng-authorization-bearer-token-in-net-core-3-1 although I prefer using an HttpRequestMessage

You mean I actually have to send it to the controller? :unsure:

Yeah I'm getting the same issue as that guy:

My authorization header is {}

Just can't believe I have battled with this so much! I know this sounds like a copout but wow there's a lot of confusion and disinformation out there and those who are supposed to be teaching this stuff either haven't tested their code or just copied and pasted from some other guy who didn't test his code.
 
Last edited:

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
20,307
Don't thank us yet, first try the solution and let us know.

It seems a bit laborious to be passing the token to every controller. How to the headers know when to pick it up?

I say this because in this example this guy doesn't pass it back but sets the token value in the Httpcontext which to me is equally a not great way of doing it.


For me this is a career breaker. If I do not come right with this I am abandoning C#/.NET/Microsoft products all together as there is too much confusion and obscurity around doing simple tasks and after 4 days scouring everything from youtube to codeproject nobody has been able to advise me on the correct way.
 

DA-LION-619

Honorary Master
Joined
Aug 22, 2009
Messages
11,555
It seems a bit laborious to be passing the token to every controller. How to the headers know when to pick it up?

I say this because in this example this guy doesn't pass it back but sets the token value in the Httpcontext which to me is equally a not great way of doing it.


For me this is a career breaker. If I do not come right with this I am abandoning C#/.NET/Microsoft products all together as there is too much confusion and obscurity around doing simple tasks and after 4 days scouring everything from youtube to codeproject nobody has been able to advise me on the correct way.
You’re not suppose to pass it to the controller, you’re meant to provide it to the user(requester).

Passing it to the controller will do nothing as it’s ‘protected’, any request to it will fail in the middleware/pipeline.

The user’s app will put it into the header and call the controller, then it goes through the middleware/pipeline.

I can’t think of a single use case for using JWT in a MVC app, hence the lack of documentation because the responsibility is on the client for implementation in any technology.
 

DA-LION-619

Honorary Master
Joined
Aug 22, 2009
Messages
11,555
1. Setup your logging to see the flow.
Code:
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request starting HTTP/1.1 GET https://localhost:5001/
Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware: Debug: The request path / does not match a supported file type
Microsoft.AspNetCore.Routing.Matching.DfaMatcher: Debug: 1 candidate(s) found for the request path '/'
Microsoft.AspNetCore.Routing.Matching.DfaMatcher: Debug: Endpoint 'BASIC.Controllers.HomeController.Index (BASIC)' with route pattern '{controller=Home}/{action=Index}/{id?}' is valid for the request path '/'
Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware: Debug: Request matched endpoint 'BASIC.Controllers.HomeController.Index (BASIC)'
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Debug: AuthenticationScheme: Bearer was not authenticated.
Microsoft.AspNetCore.Routing.EndpointMiddleware: Information: Executing endpoint 'BASIC.Controllers.HomeController.Index (BASIC)'
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Route matched with {action = "Index", controller = "Home"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.IActionResult] Index() on controller BASIC.Controllers.HomeController (BASIC).
Microsoft.AspNetCore.Mvc.Infrastructure.RedirectResultExecutor: Information: Executing RedirectResult, redirecting to /Employee/Index.
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Executed action BASIC.Controllers.HomeController.Index (BASIC) in 3.7911ms
Microsoft.AspNetCore.Routing.EndpointMiddleware: Information: Executed endpoint 'BASIC.Controllers.HomeController.Index (BASIC)'
Microsoft.AspNetCore.Server.Kestrel: Debug: Connection id "0HM82PP8I1RJT" completed keep alive response.
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished in 28.3907ms 302
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request starting HTTP/1.1 GET https://localhost:5001/Employee/Index
Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware: Debug: The request path /Employee/Index does not match a supported file type
Microsoft.AspNetCore.Routing.Matching.DfaMatcher: Debug: 1 candidate(s) found for the request path '/Employee/Index'
Microsoft.AspNetCore.Routing.Matching.DfaMatcher: Debug: Endpoint 'BASIC.Controllers.EmployeeController.Index (BASIC)' with route pattern '{controller=Home}/{action=Index}/{id?}' is valid for the request path '/Employee/Index'
Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware: Debug: Request matched endpoint 'BASIC.Controllers.EmployeeController.Index (BASIC)'
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Debug: AuthenticationScheme: Bearer was not authenticated.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Debug: AuthenticationScheme: Bearer was not authenticated.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AuthenticationScheme: Bearer was challenged.
Microsoft.AspNetCore.Server.Kestrel: Debug: Connection id "0HM82PP8I1RJT" completed keep alive response.
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished in 13.5268ms 401
2. Use Postman, Fiddler etc.
Code:
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request starting HTTP/1.1 GET https://localhost:5001/
Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware: Debug: The request path / does not match a supported file type
Microsoft.AspNetCore.Routing.Matching.DfaMatcher: Debug: 1 candidate(s) found for the request path '/'
Microsoft.AspNetCore.Routing.Matching.DfaMatcher: Debug: Endpoint 'BASIC.Controllers.HomeController.Index (BASIC)' with route pattern '{controller=Home}/{action=Index}/{id?}' is valid for the request path '/'
Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware: Debug: Request matched endpoint 'BASIC.Controllers.HomeController.Index (BASIC)'
Microsoft.AspNetCore.Mvc.Infrastructure.RedirectResultExecutor: Information: Executing RedirectResult, redirecting to /Employee/Index.
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Executed action BASIC.Controllers.HomeController.Index (BASIC) in 4.3757ms
Microsoft.AspNetCore.Routing.EndpointMiddleware: Information: Executed endpoint 'BASIC.Controllers.HomeController.Index (BASIC)'
Microsoft.AspNetCore.Server.Kestrel: Debug: Connection id "0HM82PP8I1RJT" completed keep alive response.
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished in 35.8943ms 302
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request starting HTTP/1.1 GET https://localhost:5001/Employee/Index
Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware: Debug: The request path /Employee/Index does not match a supported file type
Microsoft.AspNetCore.Routing.Matching.DfaMatcher: Debug: 1 candidate(s) found for the request path '/Employee/Index'
Microsoft.AspNetCore.Routing.Matching.DfaMatcher: Debug: Endpoint 'BASIC.Controllers.EmployeeController.Index (BASIC)' with route pattern '{controller=Home}/{action=Index}/{id?}' is valid for the request path '/Employee/Index'
Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware: Debug: Request matched endpoint 'BASIC.Controllers.EmployeeController.Index (BASIC)'
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Failed to validate the token.

Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired. ValidTo: 'System.DateTime', Current time: 'System.DateTime'.
   at Microsoft.IdentityModel.Tokens.Validators.ValidateLifetime(Nullable`1 notBefore, Nullable`1 expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateLifetime(Nullable`1 notBefore, Nullable`1 expires, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
   at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Bearer was not authenticated. Failure message: IDX10223: Lifetime validation failed. The token is expired. ValidTo: 'System.DateTime', Current time: 'System.DateTime'.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Bearer was not authenticated. Failure message: IDX10223: Lifetime validation failed. The token is expired. ValidTo: 'System.DateTime', Current time: 'System.DateTime'.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AuthenticationScheme: Bearer was challenged.
Microsoft.AspNetCore.Server.Kestrel: Debug: Connection id "0HM82PP8I1RJT" completed keep alive response.
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished in 17.943ms 401
3. Profit
TgzO0VA.png
 

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
20,307
I found this series quite informative. From simple to building bit by bit

I've been making some headway here. Will link a github once I'm happy with it. It's a mixture of cookie authentication and JWT token.

Thanks Spacerat I will give that watch now!
 

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
20,307
I found this series quite informative. From simple to building bit by bit
Cookieauthenticationdefaults....

Wish I had seen your video a few days before it go to atomic frustration levels. This is a big help though!
 

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
20,307
Still going at it. So far I have the registration, email token confirmation and login. I have an Administration area and Account area with respective controllers. I am able to assign roles and claims to users. I am able to edit roles, add roles, delete roles (constraints are in place on everything so no oopsies).

At the moment I have a Guest, User and Admin role and playing the the Claims levels of each role type. A default role is Guest after registering. This is all running off of an API. As I said I will upload to github once I'm satisfied so you guys can have a gander.
 

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
20,307

Could be helpful on the API side thanks!. The front end is just a light weight UI with hardly any identity stuff in it and what little is there is used for authentication and authorization.

Edit: I am enjoying doing this myself. It is learning but also I have the freedom to be creative. That what you have linked also seems to be part of a massive core infrastructure project. It's overkill for what I am doing and I'd have to sift through there like a blind guy in a barn looking for what I need. Easier just to get examples from other sources! :giggle:
 
Last edited:
Top