350

I'm trying to support JWT bearer token (JSON Web Token) in my web API application and I'm getting lost.

I see support for .NET Core and for OWIN applications.
I'm currently hosting my application in IIS.

How can I achieve this authentication module in my application? Is there any way I can use the <authentication> configuration similar to the way I use forms/Windows authentication?

Luke Girvin
  • 13,221
  • 9
  • 64
  • 84
Amir Popovich
  • 29,350
  • 9
  • 53
  • 99

7 Answers7

796

I answered this question: How to secure an ASP.NET Web API 4 years ago using HMAC.

Now, lots of things changed in security, especially that JWT is getting popular. In this answer, I will try to explain how to use JWT in the simplest and basic way that I can, so we won't get lost from jungle of OWIN, Oauth2, ASP.NET Identity, etc..

If you don't know about JWT tokens, you need to take a look at:

https://www.rfc-editor.org/rfc/rfc7519

Basically, a JWT token looks like this:

<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>

Example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

A JWT token has three sections:

  1. Header: JSON format which is encoded in Base64
  2. Claims: JSON format which is encoded in Base64.
  3. Signature: Created and signed based on Header and Claims which is encoded in Base64.

If you use the website jwt.io with the token above, you can decode the token and see it like below:

A screenshot of jwt.io with the raw jwt source and the decoded JSON it represents

Technically, JWT uses a signature which is signed from headers and claims with security algorithm specified in the headers (example: HMACSHA256). Therefore, JWT must be transferred over HTTPs if you store any sensitive information in its claims.

Now, in order to use JWT authentication, you don't really need an OWIN middleware if you have a legacy Web Api system. The simple concept is how to provide JWT token and how to validate the token when the request comes. That's it.

In the demo I've created (github), to keep the JWT token lightweight, I only store username and expiration time. But this way, you have to re-build new local identity (principal) to add more information like roles, if you want to do role authorization, etc. But, if you want to add more information into JWT, it's up to you: it's very flexible.

Instead of using OWIN middleware, you can simply provide a JWT token endpoint by using a controller action:

public class TokenController : ApiController
{
    // This is naive endpoint for demo, it should use Basic authentication
    // to provide token or POST request
    [AllowAnonymous]
    public string Get(string username, string password)
    {
        if (CheckUser(username, password))
        {
            return JwtManager.GenerateToken(username);
        }

        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    public bool CheckUser(string username, string password)
    {
        // should check in the database
        return true;
    }
}

This is a naive action; in production you should use a POST request or a Basic Authentication endpoint to provide the JWT token.

How to generate the token based on username?

You can use the NuGet package called System.IdentityModel.Tokens.Jwt from Microsoft to generate the token, or even another package if you like. In the demo, I use HMACSHA256 with SymmetricKey:

/// <summary>
/// Use the below code to generate symmetric Secret Key
///     var hmac = new HMACSHA256();
///     var key = Convert.ToBase64String(hmac.Key);
/// </summary>
private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";

public static string GenerateToken(string username, int expireMinutes = 20)
{
    var symmetricKey = Convert.FromBase64String(Secret);
    var tokenHandler = new JwtSecurityTokenHandler();

    var now = DateTime.UtcNow;
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, username)
        }),

        Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),
        
        SigningCredentials = new SigningCredentials(
            new SymmetricSecurityKey(symmetricKey), 
            SecurityAlgorithms.HmacSha256Signature)
    };

    var stoken = tokenHandler.CreateToken(tokenDescriptor);
    var token = tokenHandler.WriteToken(stoken);

    return token;
}

The endpoint to provide the JWT token is done.

How to validate the JWT when the request comes?

In the demo, I have built JwtAuthenticationAttribute which inherits from IAuthenticationFilter (more detail about authentication filter in here).

With this attribute, you can authenticate any action: you just have to put this attribute on that action.

public class ValueController : ApiController
{
    [JwtAuthentication]
    public string Get()
    {
        return "value";
    }
}

You can also use OWIN middleware or DelegateHander if you want to validate all incoming requests for your WebAPI (not specific to Controller or action)

Below is the core method from authentication filter:

private static bool ValidateToken(string token, out string username)
{
    username = null;

    var simplePrinciple = JwtManager.GetPrincipal(token);
    var identity = simplePrinciple.Identity as ClaimsIdentity;

    if (identity == null || !identity.IsAuthenticated)
        return false;

    var usernameClaim = identity.FindFirst(ClaimTypes.Name);
    username = usernameClaim?.Value;

    if (string.IsNullOrEmpty(username))
       return false;

    // More validate to check whether username exists in system

    return true;
}

protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
    string username;

    if (ValidateToken(token, out username))
    {
        // based on username to get more information from database 
        // in order to build local identity
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, username)
            // Add more claims if needed: Roles, ...
        };

        var identity = new ClaimsIdentity(claims, "Jwt");
        IPrincipal user = new ClaimsPrincipal(identity);

        return Task.FromResult(user);
    }

    return Task.FromResult<IPrincipal>(null);
}

The workflow is to use the JWT library (NuGet package above) to validate the JWT token and then return back ClaimsPrincipal. You can perform more validation, like check whether user exists on your system, and add other custom validations if you want.

The code to validate JWT token and get principal back:

public static ClaimsPrincipal GetPrincipal(string token)
{
    try
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;

        if (jwtToken == null)
            return null;

        var symmetricKey = Convert.FromBase64String(Secret);

        var validationParameters = new TokenValidationParameters()
        {
            RequireExpirationTime = true,
            ValidateIssuer = false,
            ValidateAudience = false,
            IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
        };

        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);

        return principal;
    }
    catch (Exception)
    {
        //should write log
        return null;
    }
}

If the JWT token is validated and the principal is returned, you should build a new local identity and put more information into it to check role authorization.

Remember to add config.Filters.Add(new AuthorizeAttribute()); (default authorization) at global scope in order to prevent any anonymous request to your resources.

You can use Postman to test the demo:

Request token (naive as I mentioned above, just for demo):

GET http://localhost:{port}/api/token?username=cuong&password=1

Put JWT token in the header for authorized request, example:

GET http://localhost:{port}/api/value

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

The demo can be found here: https://github.com/cuongle/WebApi.Jwt

cuongle
  • 74,024
  • 28
  • 151
  • 206
  • 8
    Well explained by @Cuong Le but i like to add more: If you are using OWIN check the UseJwtBearerAuthentication available in Microsoft.Owin.Security.Jwt you can use this owin middleware on the WebAPI to validate every incoming request automatically. use the owin startup class to register the middleware – Jek Oct 27 '16 at 13:13
  • 1
    I've actually implemented in pretty much the same way after I found this: https://www.asp.net/web-api/overview/security/authentication-filters Your answer is excellent. +1 + accept on it. I only have one open issue - Whats the best way to ping-pong (client-server) the bearer token? My client sends it - I go through the filter and either get a 401 or a valid authentication, do my stuff and want to set the token on the response. I can do it ugly with modifying the response header on the authentication filter, but is there a better way? – Amir Popovich Oct 27 '16 at 14:36
  • 6
    @AmirPopovich You don't need to set token on the response, token need to be stored somewhere else on the client side, for web, you can put in local storage, whenever you send HTTP request, put the token on the header. – cuongle Oct 27 '16 at 14:41
  • @CuongLe Great post! I followed it step by step now I ran into problem that all my API methods are requesting for token, i.e. login, register and reset password. How do I add them to exception list? What could go wrong? – Hitin Dec 31 '16 at 01:14
  • 1
    Sorry about my ignorance, just realised that there is a attribute [AllowAnonymous] – Hitin Dec 31 '16 at 13:21
  • @CuongLe Can I check on client-based? If I know the token, I can retrieve data from any client. – neer Feb 06 '17 at 07:19
  • @NEER: Not sure that I understand your question but you can add client Id into the token – cuongle Feb 06 '17 at 09:33
  • @CuongLe I give an example. Let's say we have two client computers (C1 and C2). I'm logged in with C1 and getting the token. Then I use C2 with token returning from c1. What I want to do is: If C2 is not logged in, C2 can not do anything. Can I make such a restriction? – neer Feb 06 '17 at 10:04
  • @NEER: Well, you can put the computer's IP or (or name) into the token and from backend, you can filter based on this info – cuongle Feb 07 '17 at 09:39
  • Instead of passing Username/Password to get a token, can't we generate an apikey ? what's the best way to generate it ? – Homam Feb 07 '17 at 12:55
  • 17
    Wow this is the most simple explication I have seen in a long time. +100 if I could – gyozo kudor Mar 24 '17 at 10:08
  • 5
    @Homam: Sorry fro this late answer, the best way to generate is: var `hmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);` – cuongle Mar 24 '17 at 12:41
  • 2
    @CuongLe: Thanks for this answer. It is brilliant. Just one question you are using expires in SecurityTokenDescriptor. What would happen after expiry time is passed? How would I generate new token? – Tim Liberty May 04 '17 at 11:23
  • 3
    @CuongLe - Regarding this ` public const string Secret = "856FECBA3B06519C8DDDBC80BB080553"; // your symetric`, what is the best way to generate and store the secret? – xaisoft May 11 '17 at 15:35
  • 2
    If I could up-vote this 1000 times I would. This is an amazingly good answer. Thank you! – Byron Whitlock May 16 '17 at 15:35
  • Struggling at setting this up with the Authentications filter. I'm using ASP.NET Mvc though – maracuja-juice Jun 21 '17 at 14:50
  • @CuongLe I now manually installed WebApi in my Project and used a Web Api Controller and used the JwtAuthenticationAttribute on the methods. But still I can access them without a token. Any idea what I may be doing wrong? – maracuja-juice Jun 27 '17 at 11:23
  • @CuongLe With a separate Web Api project the opposite happens. I can't access the method (even with a valid token provided) – maracuja-juice Jun 27 '17 at 11:59
  • @CuongLe How to delete the JWT token on any function like sign out? – DevSab Aug 10 '17 at 15:13
  • 1
    @DevSab JWT will be expired, so no need to delete it. The main point in here is we validate token on every request. If you need use provide Logout functionality, maybe you need to convert token to cookie local identity – cuongle Aug 10 '17 at 16:09
  • @xaisoft: I updated the answer how to generate the secret – cuongle Aug 10 '17 at 16:11
  • 1
    @TimLiberty: I think you need to call to the end point to get new token again – cuongle Aug 10 '17 at 16:31
  • Thanks so much for this example, it really helped but I am hitting a stumbling block regarding refreshing the token, is there a good example following this pattern? – user351711 Sep 02 '17 at 06:20
  • Thank you for the simple and concise example, yet extremely helpful – user1783490 Sep 10 '17 at 19:29
  • 2
    a really nice implementation step by step at http://www.decatechlabs.com/how-to-secure-webapi-using-json-web-tokenjwt – Sean Ch Sep 12 '17 at 14:48
  • Dear @CuongLe can you please elaborate your point here > This is naive action, in production you should use POST request or > Basic Authentication endpoint to provide JWT token – Jeet Oct 04 '17 at 14:17
  • This is a great explanation, can you do the same for RS256 and how to implement that from scratch? – Mark Redman Oct 25 '17 at 09:36
  • just saying :`simplePrinciple?.Identity as ClaimsIdentity;` since it can be null and hence exception.... – Royi Namir Dec 24 '17 at 13:00
  • @CuongLe - Is the secret key generated one time and then stored in a configuration or can it be generated every time a user logs in? – xaisoft Feb 12 '18 at 15:52
  • when we use JWT token then HMAC is not required? i heard HMAC prevent reply attack. so share your view. JWT token can prevent reply attack by hacker? – Monojit Sarkar Feb 28 '18 at 11:53
  • Great Answer. If anyone don't know how to call the autorized method, try this: HttpClient myclient = new HttpClient(); myclient.BaseAddress = new Uri(URL); myclient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", (token)); string urlParameters = ""; HttpResponseMessage response = myclient.GetAsync(urlParameters).Result; – humudu Mar 13 '18 at 12:28
  • 1
    Token expiration did not work for me, to fix it i added ClockSkew = TimeSpan.Zero, To the TokenValidationParameters – humudu Mar 13 '18 at 13:42
  • 1
    In production you will need some kind of refresh token mechanism. Care to add that to the above? Otherwise nice post! – Jonas Jakobsson Mar 28 '18 at 11:52
  • private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw=="; you describe this line, but with this token generate same value. – Brijesh Mavani Jun 07 '18 at 07:03
  • 2
    I opened a pull request on your code because I think I've found a bug when the Authorization header. If it's allright for you I suggest to edit this answer too! – magicleon94 Jul 04 '18 at 12:49
  • Is it necessary to check username - generated from token - existence from database/any storage, whenever we send API request with token ? – Bh00shan Jul 06 '18 at 13:31
  • 1
    Might be a stupid question, how do you create the secret key? – Imdad Jul 27 '18 at 17:40
  • How do I set this up in a fake http context? http://stackoverflow.com/questions/1981426/how-do-i-mock-fake-the-session-object-in-asp-net-web-forms – David Thielen Sep 30 '18 at 14:48
  • 2
    https://jwt.io/ says your sample key above has an invalid signature. Any idea why? – David Thielen Sep 30 '18 at 14:52
  • @Imdad see the comment in the code "Use the below code to generate symmetric Secret Key" – Chucky Nov 06 '18 at 11:04
  • 8
    Anyone using the demo code from CuongLe's repo will notice there is a bug where requests with no authorisation header are not handled, meaning any query without one can get through (not so secure an endpoint!). There is a pull request from @magicleon to fix this issue here: https://github.com/cuongle/WebApi.Jwt/pull/4 – Chucky Nov 06 '18 at 11:12
  • @Bh00shan see the comment `// More validate to check whether username exists in system` – Chucky Nov 06 '18 at 12:03
  • @cuongle I am not able to access the claim data in my controller. I tried a few different options and nothing is working for me. You can check the post here with what I tried [link to post](https://stackoverflow.com/questions/53296381/cannot-access-jwt-claim-in-webapi-authorization-token) – Hitin Nov 14 '18 at 09:05
  • github link is dead – SKLTFZ Dec 12 '18 at 03:06
  • @cuongle I know this kinda old. But when I generate token using your sample project and check to jwt.io the token is not valid. Even the sample token you provided. – Zach Feb 01 '19 at 03:09
  • Yeah, I'm noticing tokens don't always expire rightaway for a 20 second timeout. They eventually do, but I'm trying to debug timeouts in my system and they're not expiring when I tell them to. Wondering if it's what @humudu mentioned or something else. – Chucky Feb 12 '19 at 17:19
  • @Humudu's comment was correct. Microsoft's library compensates by setting a ClockSkew of five minutes. Which might be fine depending on what you are implementing. You can read about it in this answer: https://stackoverflow.com/a/46231102/812013 – Chucky Feb 14 '19 at 09:09
  • 1
    @Chucky: my tests show that the "default authorization" code suggested in the original code covers the case where no authorization header is provided. In this case the response body will contain: {"Message": "Authorization has been denied for this request."} – nevh Jul 16 '19 at 04:30
  • This will always create a new token within the expiration time. Could you please help me how to create token when and only when token is expired – Amardeep Kumar Agrawal Oct 11 '19 at 08:23
  • request.Headers.Authorization is always returning null for me can anyone help me with that. – harsha reddy Nov 14 '19 at 07:39
  • 1
    @cuongle Great, But what if someone steals your token from the header and sends the call to API from another **source**? How will you check the incoming request is from a valid ""source or authorized source** not from the third party or **unauthorized source**? – Arslan Afzal Jun 13 '20 at 11:24
  • what is the difference between this implementation (JWT) and the default web api 2.0 .net framework? – JobaDiniz Jun 29 '20 at 11:42
  • 1
    @SeanCh [Your link](https://stackoverflow.com/questions/40281050/jwt-authentication-for-asp-net-web-api#comment79321498_40284152), though [preserved on archive.org](https://web.archive.org/web/20170915060711/https://decatechlabs.com/how-to-secure-webapi-using-json-web-tokenjwt), est ded. – ruffin Sep 09 '20 at 14:13
  • 1
    @ruffin sorry there were some structural changes here is the right link https://decatechlabs.com/secure-webapi-using-jwt – Sean Ch Sep 15 '20 at 14:35
  • @ruffin Please do give feedback on article if you find any issues. decatechlabs.com/secure-webapi-using-jwt – Sean Ch Sep 15 '20 at 14:38
  • 1
    It allows http calls without authorization header, please accept the pull request https://github.com/cuongle/WebApi.Jwt/pull/4 thanks @magicleon94 – The One Sep 29 '20 at 18:29
  • This was exactly what I needed. We're doing authentication through a 3rd-party COM-based system, but still needed to pass JWTs to the client and back. No need for ASP.NET identity. This was great. Thanks! – FunkMonkey33 Mar 15 '21 at 22:11
  • @DavidThielen et al -- you get invalid signatures at jwt.io BY DESIGN _**because you didn't paste in his `private const string Secret` value there**_. If you go to jwt.io, cut the jwt in there by default, change the secret string from `your-256-bit-secret` to `fail` (or anything else), then paste the original back in over top of the new jwt it creates, guess what? The once valid jwt now has an Invalid signature. **Without the secret, it's [practically] impossible for someone to create jwts, _just as you want it_.** That's exactly what this stateless authentication is all about. – ruffin Jun 18 '21 at 20:28
  • How would you handle JWT in a WebForms app? I'm trying to migrate a legacy system from 2005 off ClearTrust ISAP filter. – Stephen York Oct 07 '22 at 01:46
  • For any one who is getting unauthorized error 401 after token generation add "config.Filters.Add(new JwtAuthenticationAttribute()); " before "config.Filters.Add(new AuthorizeAttribute())" – user3452210 Mar 30 '23 at 09:27
  • Excellent answer. Exactly what I was looking. I've been putting off moving to .net core for years, because I couldn't find a simple replacement for OAuthAuthorizationServerMiddleware (which they abandoned). – John May 29 '23 at 22:53
21

I've managed to achieve it with minimal effort (just as simple as with ASP.NET Core).

For that I use OWIN Startup.cs file and Microsoft.Owin.Security.Jwt library.

In order for the app to hit Startup.cs we need to amend Web.config:

<configuration>
  <appSettings>
    <add key="owin:AutomaticAppStartup" value="true" />
    ...

Here's how Startup.cs should look:

using MyApp.Helpers;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Jwt;
using Owin;

[assembly: OwinStartup(typeof(MyApp.App_Start.Startup))]

namespace MyApp.App_Start
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseJwtBearerAuthentication(
                new JwtBearerAuthenticationOptions
                {
                    AuthenticationMode = AuthenticationMode.Active,
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidAudience = ConfigHelper.GetAudience(),
                        ValidIssuer = ConfigHelper.GetIssuer(),
                        IssuerSigningKey = ConfigHelper.GetSymmetricSecurityKey(),
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true
                    }
                });
        }
    }
}

Many of you guys use ASP.NET Core nowadays, so as you can see it doesn't differ a lot from what we have there.

It really got me perplexed first, I was trying to implement custom providers, etc. But I didn't expect it to be so simple. OWIN just rocks!

Just one thing to mention - after I enabled OWIN Startup NSWag library stopped working for me (e.g. some of you might want to auto-generate typescript HTTP proxies for Angular app).

The solution was also very simple - I replaced NSWag with Swashbuckle and didn't have any further issues.


Ok, now sharing ConfigHelper code:

public class ConfigHelper
{
    public static string GetIssuer()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Issuer"];
        return result;
    }

    public static string GetAudience()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Audience"];
        return result;
    }

    public static SigningCredentials GetSigningCredentials()
    {
        var result = new SigningCredentials(GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
        return result;
    }

    public static string GetSecurityKey()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["SecurityKey"];
        return result;
    }

    public static byte[] GetSymmetricSecurityKeyAsBytes()
    {
        var issuerSigningKey = GetSecurityKey();
        byte[] data = Encoding.UTF8.GetBytes(issuerSigningKey);
        return data;
    }

    public static SymmetricSecurityKey GetSymmetricSecurityKey()
    {
        byte[] data = GetSymmetricSecurityKeyAsBytes();
        var result = new SymmetricSecurityKey(data);
        return result;
    }

    public static string GetCorsOrigins()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["CorsOrigins"];
        return result;
    }
}

Another important aspect - I sent JWT Token via Authorization header, so typescript code looks for me as follows:

(the code below is generated by NSWag)

@Injectable()
export class TeamsServiceProxy {
    private http: HttpClient;
    private baseUrl: string;
    protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;

    constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
        this.http = http;
        this.baseUrl = baseUrl ? baseUrl : "https://localhost:44384";
    }

    add(input: TeamDto | null): Observable<boolean> {
        let url_ = this.baseUrl + "/api/Teams/Add";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = JSON.stringify(input);

        let options_ : any = {
            body: content_,
            observe: "response",
            responseType: "blob",
            headers: new HttpHeaders({
                "Content-Type": "application/json", 
                "Accept": "application/json",
                "Authorization": "Bearer " + localStorage.getItem('token')
            })
        };

See headers part - "Authorization": "Bearer " + localStorage.getItem('token')

Alex Herman
  • 2,708
  • 4
  • 32
  • 53
  • `I replaced NSWag with Swashbuckle and didn't have any further issues.` Does Swashbuckle have the capability to generate typescript files or is that something you added to it yourself? – crush Feb 22 '19 at 22:50
  • @crush swashbucle is a backend library providing json, like nuget nswag library only better. In order to produce typescript file you should still use nswag package from npm. – Alex Herman Feb 23 '19 at 17:42
  • Right, I already have swashbuckle in my project for sometime, it sounded like you were suggesting it could generate the TypeScript models instead of nswag. I'm not a fan of nswag...it's heavy. I've created my own C#->TypeScript conversion that is hooked into Swashbuckle - generates the files as a post-build process, and publishes them to an npm feed for our projects. I just wanted to make sure I hadn't overlooked a Swashbuckle project that was already doing the same thing. – crush Feb 23 '19 at 18:51
14

Here's a very minimal and secure implementation of a Claims based Authentication using JWT token in an ASP.NET Core Web API.

first of all, you need to expose an endpoint that returns a JWT token with claims assigned to a user:

 /// <summary>
        /// Login provides API to verify user and returns authentication token.
        /// API Path:  api/account/login
        /// </summary>
        /// <param name="paramUser">Username and Password</param>
        /// <returns>{Token: [Token] }</returns>
        [HttpPost("login")]
        [AllowAnonymous]
        public async Task<IActionResult> Login([FromBody] UserRequestVM paramUser, CancellationToken ct)
        {

            var result = await UserApplication.PasswordSignInAsync(paramUser.Email, paramUser.Password, false, lockoutOnFailure: false);

            if (result.Succeeded)
            {
                UserRequestVM request = new UserRequestVM();
                request.Email = paramUser.Email;


                ApplicationUser UserDetails = await this.GetUserByEmail(request);
                List<ApplicationClaim> UserClaims = await this.ClaimApplication.GetListByUser(UserDetails);

                var Claims = new ClaimsIdentity(new Claim[]
                                {
                                    new Claim(JwtRegisteredClaimNames.Sub, paramUser.Email.ToString()),
                                    new Claim(UserId, UserDetails.UserId.ToString())
                                });


                //Adding UserClaims to JWT claims
                foreach (var item in UserClaims)
                {
                    Claims.AddClaim(new Claim(item.ClaimCode, string.Empty));
                }

                var tokenHandler = new JwtSecurityTokenHandler();
                  // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                var encryptionkey = Configuration["Jwt:Encryptionkey"];
                var key = Encoding.ASCII.GetBytes(encryptionkey);
                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Issuer = Configuration["Jwt:Issuer"],
                    Subject = Claims,

                // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                    Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(Configuration["Jwt:ExpiryTimeInMinutes"])),

                    //algorithm to sign the token
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)

                };

                var token = tokenHandler.CreateToken(tokenDescriptor);
                var tokenString = tokenHandler.WriteToken(token);

                return Ok(new
                {
                    token = tokenString
                });
            }

            return BadRequest("Wrong Username or password");
        }

now you need to Add Authentication to your services in your ConfigureServices inside your startup.cs to add JWT authentication as your default authentication service like this:

services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
             .AddJwtBearer(cfg =>
             {
                 cfg.RequireHttpsMetadata = false;
                 cfg.SaveToken = true;
                 cfg.TokenValidationParameters = new TokenValidationParameters()
                 {
                     //ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Encryptionkey"])),
                     ValidateAudience = false,
                     ValidateLifetime = true,
                     ValidIssuer = configuration["Jwt:Issuer"],
                     //ValidAudience = Configuration["Jwt:Audience"],
                     //IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"])),
                 };
             });

now you can add policies to your authorization services like this:

services.AddAuthorization(options =>
            {
                options.AddPolicy("YourPolicyNameHere",
                                policy => policy.RequireClaim("YourClaimNameHere"));
            });

ALTERNATIVELY, You can also (not necessary) populate all of your claims from your database as this will only run once on your application startup and add them to policies like this:

  services.AddAuthorization(async options =>
            {
                var ClaimList = await claimApplication.GetList(applicationClaim);
                foreach (var item in ClaimList)
                {                        
                    options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));                       
                }
            });

now you can put the Policy filter on any of the methods that you want to be authorized like this:

 [HttpPost("update")]
        [Authorize(Policy = "ACC_UP")]
        public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
        {
//your logic goes here
}

Hope this helps

Zeeshan Adil
  • 1,937
  • 5
  • 23
  • 42
7

In my case the JWT is created by a separate API so ASP.NET need only decode and validate it. In contrast to the accepted answer we're using RSA which is a non-symmetric algorithm, so the SymmetricSecurityKey class mentioned above won't work.

Here's the result.

using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Threading;
using System.Threading.Tasks;

    public static async Task<JwtSecurityToken> VerifyAndDecodeJwt(string accessToken)
    {
        try
        {
            var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{securityApiOrigin}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
            var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
            var validationParameters = new TokenValidationParameters()
            {
                ValidateLifetime = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                RequireSignedTokens = true,
                IssuerSigningKeys = openIdConfig.SigningKeys,
            };
            new JwtSecurityTokenHandler().ValidateToken(accessToken, validationParameters, out var validToken);
            // threw on invalid, so...
            return validToken as JwtSecurityToken;
        }
        catch (Exception ex)
        {
            logger.Info(ex.Message);
            return null;
        }
    }
Ron Newcomb
  • 2,886
  • 21
  • 24
5

I think you should use some 3d party server to support the JWT token and there is no out of the box JWT support in WEB API 2.

However there is an OWIN project for supporting some format of signed token (not JWT). It works as a reduced OAuth protocol to provide just a simple form of authentication for a web site.

You can read more about it e.g. here.

It's rather long, but most parts are details with controllers and ASP.NET Identity that you might not need at all. Most important are

Step 9: Add support for OAuth Bearer Tokens Generation

Step 12: Testing the Back-end API

There you can read how to set up endpoint (e.g. "/token") that you can access from frontend (and details on the format of the request).

Other steps provide details on how to connect that endpoint to the database, etc. and you can chose the parts that you require.

Community
  • 1
  • 1
Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
1

You no need to work with weird JwtSecurityTokenHandler API

Use JwtUtils Nuget package with simple API

var claims =  new Dictionary<string, object>
{
   { "exp", 1639942616 },
   { "uname", "i.a.ivanov" },
   { "claim1", "claim1_value" },   
   { "claims_array", new [] {"claim_item1", "claim_item2"}}
};
       
string token = JWT.HS256.Create(claims, "{TOKEN_SECRET}");
ZOXEXIVO
  • 920
  • 9
  • 21
0

You can follow this code for token controller or for more details you can visit here : How to Secure API using JWT Tokens. Building CRUD API using JWT Tokens with ASP.NET Core and Entity Framework Core and Swagger

From here you can learn to use JWT Token in a very easy way

using JWTToken.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace JWTToken.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TokenController : ControllerBase
    {
        public IConfiguration _configuration;
        private readonly InventoryContext _context;

        public TokenController(IConfiguration config, InventoryContext context)
        {
            _configuration = config;
            _context = context;
        }

        [HttpPost]
        public async Task<IActionResult> Post(UserInfo _userData)
        {

            if (_userData != null && _userData.Email != null && _userData.Password != null)
            {
                var user = await GetUser(_userData.Email, _userData.Password);

                if (user != null)
                {
                    //create claims details based on the user information
                    var claims = new[] {
                    new Claim(JwtRegisteredClaimNames.Sub, _configuration["Jwt:Subject"]),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
                    new Claim("Id", user.UserId.ToString()),
                    new Claim("FirstName", user.FirstName),
                    new Claim("LastName", user.LastName),
                    new Claim("UserName", user.UserName),
                    new Claim("Email", user.Email)
                   };

                    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));

                    var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

                    var token = new JwtSecurityToken(_configuration["Jwt:Issuer"], _configuration["Jwt:Audience"], claims, expires: DateTime.UtcNow.AddDays(1), signingCredentials: signIn);

                    return Ok(new JwtSecurityTokenHandler().WriteToken(token));
                }
                else
                {
                    return BadRequest("Invalid credentials");
                }
            }
            else
            {
                return BadRequest();
            }
        }

        private async Task<UserInfo> GetUser(string email, string password)
        {
            return await _context.UserInfos.FirstOrDefaultAsync(u => u.Email == email && u.Password == password);
        }
    }
}
Anik Saha
  • 37
  • 6