How to get [Authorize] attribute work in .NET Core 6 Web API?

4.9k views Asked by At

I am a noob, trying to do JWT the simplest possible way on a .NET Core 6 Web API project, but I can't even get it to work.

Requirement: You need to be logged in to call the GetProductList API.
(I am testing this on Swagger that comes with the project)

FYI, my Login controller: (working as intended)

    [HttpPost("login")]
    public async Task<ActionResult> Login(LoginDto request)
    {
        var user = GetUserFromRequest(request);

        if (user == null)
            return BadRequest("Invalid credentials.");

        string jwt = CreateJwtToken(user.Id.ToString());
        Response.Cookies.Append(COOKIE_JWT, jwt, _cookieOptions);

        return Ok();
    }

    [HttpGet("user")]
    public IActionResult GetUser() 
    {
        try
        {
            var jwt = Request.Cookies[COOKIE_JWT];
            var userId = VerifyJwtAndGetUserId(jwt);

            return Ok(GetUserById(userId));
        }
        catch(Exception ex)
        {
            return Unauthorized();
        }
    }

    public static string CreateJwtToken(string userId)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY));
        var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
        var token = new JwtSecurityToken(
            issuer: userId,
            expires: DateTime.Now.AddDays(365),
            signingCredentials: cred
        );
        var jwt = new JwtSecurityTokenHandler().WriteToken(token);
        return jwt;
    }

    public static string VerifyJwtAndGetUserId(string jwt)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        tokenHandler.ValidateToken(jwt, new TokenValidationParameters {
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY)),
            ValidateIssuerSigningKey = true,
            ValidateIssuer = false,
            ValidateAudience = false
        }, out SecurityToken validatedToken);

        string userId = validatedToken.Issuer;
        return userId;
    }

The question is, how can I make the [Authorize] attribute work?

    [HttpGet("list")]
    //[Authorize]
    public async Task<ActionResult<List<Product>>> GetProductList()
    {
        return Ok(GetProducts());
    }

The above works, but adding [Authorize] attribute gives a 401 with the following header: (while GetUser above is fine)

 content-length: 0 
 date: Mon,13 Jun 2022 23:27:32 GMT 
 server: Kestrel 
 www-authenticate: Bearer 

This is what's in my Program.cs: (maybe this is wrong?)

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options => {
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new TokenValidationParameters {  // similar to the one in controller
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY)),
        ValidateIssuerSigningKey = true,
        ValidateIssuer = false,
        ValidateAudience = false
    };
    options.Events = new JwtBearerEvents {  // https://spin.atomicobject.com/2020/07/25/net-core-jwt-cookie-authentication/
        OnMessageReceived = ctx => {
            ctx.Token = ctx.Request.Cookies["jwt"];
            return Task.CompletedTask;
        }
    };
  });

SOLUTION:

Move app.UseAuthentication(); above app.UserAuthorization();.

1

There are 1 answers

6
HsuTingHuan On

Summary

FrontEnd

According to JWT Introduction as below

Whenever the user wants to access a protected route or resource, the user agent should send the JWT, typically in the Authorization header using the Bearer schema. The content of the header should look like the following:

Authorization: Bearer [token]

So you need to add a header Authorization: Bearer <token> in your request on frontEnd.
a simple example at frontEnd. In this example the yourApiUrl should be your list api route.

   const token = getCookieValue(`COOKIE_JWT`);
   fetch(yourApiUrl, {
            headers: {
              "Authorization": `Bearer ${token}`,
            }
          })

getCookieValue function

const getCookieValue(name) =>{
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''
}

then your request will have the authorization header with a prefix [Bearer].
a picture from my browser's dev tool.
image

and then the [Authorize] attribute should work


BackEnd

Maybe it is not the problem at frontEnd, then you can check the backEnd's code.
Some code example at backEnd .NET 6 WebApi about generate or examine the JWT token

GenerateToken function
Use to generate token when sign-in controller

  public string GenerateToken(TokenModel tokenModel, int 
    expireMinutes = 30)
    {
        var issuer = this._configuration.GetValue<string> ("JwtSettings:Issuer");
        var signKey = this._configuration.GetValue<string> 
("JwtSettings:SignKey");

        // Configuring "Claims" to your JWT Token
        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Iss, issuer),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID
            new Claim(JwtRegisteredClaimNames.Sub, tokenModel.EmployeeNo), // User.Identity.Name
            new Claim(JwtRegisteredClaimNames.NameId, tokenModel.EmployeeNo),
            new Claim(JwtRegisteredClaimNames.Name, tokenModel.EmployeeName),
        };

        for (int i = 0; i < tokenModel.Roles.Length; i++)
        {
            claims.Add(new Claim("roles", tokenModel.Roles[i]));
        }

        var userClaimsIdentity = new ClaimsIdentity(claims);

        // Create a SymmetricSecurityKey for JWT Token signatures
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signKey));

        // HmacSha256 MUST be larger than 128 bits, so the key can't be too short. At least 16 and more characters.
        var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);

        // Create SecurityTokenDescriptor
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = userClaimsIdentity,
            NotBefore = DateTime.Now, // Default is DateTime.Now
            IssuedAt = DateTime.Now, // Default is DateTime.Now
            Expires = DateTime.Now.AddMinutes(expireMinutes),
            SigningCredentials = signingCredentials
        };

        // Generate a JWT securityToken, than get the serialized Token result (string)
        var tokenHandler = new JwtSecurityTokenHandler();
        var securityToken = tokenHandler.CreateToken(tokenDescriptor);
        var serializeToken = tokenHandler.WriteToken(securityToken);

        return serializeToken;
    }

Program.cs
.NET6 WebApi's setting in Program.cs about examine Jwt token.
[NOTICE]: the method builder.Services.AddAuthorization(); MUST below the builder.Services.AddAuthentication method, or will result error.

   builder.Services
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) //check the HTTP Header's Authorization has the JWT Bearer Token
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",

                RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",

                // varify Issuer
                ValidateIssuer = true,
                ValidIssuer = builder.Configuration.GetValue<string>("JwtSettings:Issuer"),

                //  Important!!! audience need to be set to false
                // Because the default is true. 
                // So if you didn't set this prop when generate token, then the api will always return a check token error
                ValidateAudience = false,

                ValidateLifetime = true,

                ValidateIssuerSigningKey = true,
                IssuerSigningKey =
                new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(builder.Configuration.GetValue<string>("JwtSettings:SignKey")))
            };
        });

    builder.Services.AddAuthorization();

Easy Test by thunder client of VsCode

  1. Use the thumder client VsCode to test the api route.
  2. Use the swagger's login controller to get the JWTToken.
  3. Then paste the token in thunder client, and add the prefix Bearer
    a simple image of thunder client enter image description here