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?
This question is related to
c#
security
asp.net-web-api
jwt
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.
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;
}
}
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
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')
Source: Stackoverflow.com