Secure a Web API with a JWT Token
Last Updated: February 2021
This tutorial teaches how to secure an ASP.NET Core Web API with JSON Web Token (JWT) in SnapDevelop.
In this tutorial, you learn how to:
- Create a Web API Project
- Configure Authentication and JWT
- Enable HTTPS and Authentication
- Add a Service
- Add a Controller
- Enable Authentication for the Sample Controller
- Test the Web API
- Call the Web API from PowerBuilder
Prerequisites
- SnapDevelop 2019 R3
- PowerBuilder 2019 R3
Create a Web API Project
Start SnapDevelop and select Create New Project from the Start page. Or, from the File menu, select New and then Project....
In the New Project dialog box, select .NET Core, and in the list of project templates, select ASP.NET Core Web API. Name the project "WebAPI1", name the solution "WebAPI Tutorials" and click OK.
Test the Web API
A Web API is created from the project template.
Press Ctrl+F5 to run the app. SnapDevelop launches the browser and navigates to http://localhost:5000/api/sample/load
.
If it's the first time the Web API runs, you may need to wait several seconds for initiating .NET runtime after the browser is launched.
The following JSON is returned:
["value1","value2"]
Configure Authentication and JWT
Add the following using statements to Startup.cs:
Note: If your project targets .NET Core 3.1, install the NuGet package Microsoft.AspNetCore.Authentication.JwtBearer
(version between 2.0.0 and 3.1.11) first, because ASP.NET Core 3.0 removes some assemblies that were previously part of the Microsoft.AspNetCore.App package reference. For more information, refer to https://docs.microsoft.com/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio#add-package-references-for-removed-assemblies
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
Define a Secret Key in Startup.cs
public class Startup
{
//put secret here for simplicity, usually it should be in appsettings.json
public const string SECRET = "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING OF AT LEAST 128 BITS (16 BYTES)";
//other code
......
Add Configuration Code
Update the ConfigureServices(IServiceCollection services) method in Startup.cs. Add the following code to the end of the method:
var key = Encoding.ASCII.GetBytes(SECRET);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = true;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
Enable HTTPS and Authentication
In Startup.cs, uncomment the following line in the Configure(IApplicationBuilder app, IHostingEnvironment env) method:
app.UseHttpsRedirection();
If the project targets .NET Core 2.1, add the UseAuthentication method between app.UseHttpsRedirection() and app.UseMvc().
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseResponseCompression();
app.UseMvc();
If the project targets .NET Core 3.1, add the UseAuthentication method under UseRouting so that route information is available for authentication decisions, and before UseEndpoints so that users are authenticated before accessing the endpoints.
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseResponseCompression();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
Add a Service
Add a User Model
Right-click on the Web API project. Click Add > New Folder. Name it Models. Then right-click on the new folder and click Add > Class.
In the popup window, name it User.cs and click OK.
The User class represents the data for a user, and acts as a data model in the application. Replace the auto-generated code with the following code:
namespace WebAPI1.Models
{
public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string Role { get; set; }
public string Token { get; set; }
}
}
Add Service Interface and Implementation
Right-click on the Web API project. Click Add > New Folder. Name it Services. Now add a sub-folder to it and name it Impl. Next, right-click on the Services folder and click on Add > Interface.
Select Interface on the template list and name the interface IUserService, click OK. The interface defines the user service. Replace the auto-generated code with the following code:
namespace WebAPI1.Services
{
public interface IUserService
{
string Login(string userName, string password);
}
}
Right-click on the Services/Impl folder and click on Add > Class. Name the class UserService. The UserService class implements the IUserService interface. Replace the auto-generated code with the following code:
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using WebAPI1.Models;
namespace WebAPI1.Services.Impl
{
public class UserService : IUserService
{
// users hardcoded for simplicity, store in a db with hashed passwords in production applications
private List<User> _users = new List<User>
{
new User { Id = 1, UserName = "user1", Password = "password1", Role = "admin"},
new User { Id = 2, UserName = "user2", Password = "password2", Role = "guest"}
};
public string Login(string userName, string password)
{
var user = _users.SingleOrDefault(x => x.UserName == userName && x.Password == password);
// return null if user not found
if (user == null)
{
return string.Empty;
}
// authentication successful so generate jwt token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(Startup.SECRET);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Role, user.Role)
}),
Expires = DateTime.UtcNow.AddMinutes(5),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
return user.Token;
}
}
}
The Login
method checks user existence by the username and password passed to the method, and creates a token for the user if the user exists. We describe how to get information from the token later.
In ASP.NET Core, services must be registered with the dependency injection (DI) container. The container provides the service to controllers.
Add the following using statements to Startup.cs:
using WebAPI1.Services;
using WebAPI1.Services.Impl;
Add the following code to the end of the ConfigureServices(IServiceCollection services) method in Startup.cs:
services.AddScoped<IUserService, UserService>();
Add a Controller
Right-click on the Controllers folder and click on Add > New Item.... Select API Controller Class from the template list and name the controller UserController, click OK. Replace the auto-generated code with the following code:
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using WebAPI1.Services;
using WebAPI1.Models;
namespace WebAPI1.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class UserController : ControllerBase
{
private IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
// POST api/user/login
[AllowAnonymous]
[HttpPost]
public IActionResult Login([FromBody]User user)
{
var token = _userService.Login(user.UserName, user.Password);
if (token == null || token == String.Empty)
return BadRequest(new { message = "User name or password is incorrect" });
return Ok(token);
}
}
}
Enable Authentication for the Sample Controller
Add the following using statement to SampleController.cs:
using Microsoft.AspNetCore.Authorization;
Add an Authorize attribute with roles "admin,manager" property to Load() action. Only authorized users with the role admin or manager can access this API.
// GET api/sample/load
[HttpGet]
[Authorize(Roles = "admin,manager")]
public ActionResult<IEnumerable<string>> Load()
{
return new string[] { "value1", "value2" };
}
Add an Authorize attribute without roles property to LoadOne(int id) action. Any authorized users can access this API.
// GET api/sample/loadone/{id}
[HttpGet("{id}")]
[Authorize]
public ActionResult<string> LoadOne(int id)
{
return "value";
}
You can get the name and role information from the token created by the Login
method in the class UserService
. To do so, add the following code to the method where you need to get the token information in SampleController.cs:
//Get information from token
string name = User.FindFirstValue(ClaimTypes.Name);
string role = User.FindFirstValue(ClaimTypes.Role);
View of the Solution Tree:
Test the Web API
Press Ctrl+F5 to run the app. SnapDevelop launches the browser and navigates to https://localhost:5001/api/sample/load
. Please pay attention here, it's https://....
You will get HTTP ERROR 401 as expected.
Go to https://localhost:5001/api/sample/loadone/1
.
You will get HTTP ERROR 401 as expected too.
Call the Web API from PowerBuilder
Get a JWT Token
The RESTClient object provides the ability to access the RESTful Web APIs. It can get a JWT token using the POST method.
In the Declare Instance Variables of your window, add an instance variable of the RESTClient object:
RESTClient inv_RestClient
In the open event of your window, create the object variable and set the request header:
// Create the RESTClient object variable
inv_RestClient = CREATE RESTClient
// Set the Request Headers to tell the Web API you will send JSON data
inv_RESTClient.SetRequestHeader ("Content-Type", "application/json;charset=UTF-8")
In the clicked event of your Send Request
button, add the RESTClient.GetJWTToken() method to call the Web API to get a token, and add the RESTClient.SetJWTToken() method to set the JWT token string to the HTTP request header:
String ls_url_token = "https://localhost:5001/api/user/login"
String ls_user = '{"UserName":"user1", "Password":"password1"}'
String ls_token
// Get a JWT token.
If inv_RestClient.GetJWTToken(ls_url_token, ls_user, ls_token) = 1 Then
// Set the JWT token string to the HTTP request header.
inv_RestClient.SetJWTToken(ls_token)
Else
// Get JWT token failed.
Return
End If
Call the Web API with the JWT Token
In the clicked event of your Send Request
button, add the RESTClient.SendGetRequest() method to call your Web API to get data (with the JWT token in HTTP request header):
String ls_url = "https://localhost:5001/api/sample/load"
String ls_response
String ls_msg
// Request api/sample
inv_RestClient.SendGetRequest(ls_url, ls_response)
// Show response info.
ls_msg = "Status Code: " + String(inv_RestClient.GetResponseStatusCode()) + '~r~n' + &
"Status Text: " + String(inv_RestClient.GetResponseStatusText()) + '~r~n' + &
"Response Body: " + ls_response
Messagebox("Response Info", ls_msg)
Press Ctrl+R to run the app. Click Send Request and check the response. (Note: Your Web API needs to be running.)
Status Code: 200
Status Text: OK
Response Body: ["value1","value2"]
Call the Web API with Different Roles
There are two users created at server side (see Add Service Interface and Implementation): user1 has the admin role, and user2 has the guest role.
In the Sample Controller class at server side (see Enable Authentication for the Sample Controller), it only allows the users with admin or manager role to access the API GET api/sample/load
, and the API GET api/sample/loadone/{id}
is allowed to access by users with any roles.
Call the Web API with Admin Role
From the previous steps we see that user1 with the admin role is allowed to access the API GET api/sample/load
.
- In the clicked event of your
Send Request
button, change the request URL:
String ls_url = "https://localhost:5001/api/sample/loadone/1"
- Press Ctrl+R to run the app. Click Send Request and check the response. (Note: Your Web API needs to be running.)
Status Code: 200
Status Text: OK
Response Body: value
Notice that user1 is authorized to access both the GET api/sample/load
and GET api/sample/loadone/{id}
APIs.
Call the Web API with Guest Role
Close your application. In the clicked event of your Send Request
button, change user1 (admin) to user2 (guest):
String ls_user = '{"UserName":"user2", "Password":"password2"}'
In the clicked event of your Send Request
button, change the request URL to the APIs respectively and check the response.
GET api/sample/load
:
String ls_url = "https://localhost:5001/api/sample/load"
Press Ctrl+R to run the app. Click Send Request and check the response. (Note: Your Web API needs to be running.)
Status Code: 403
Status Text: Forbidden
Response Body:
Notice that user2 is forbidden to access the API GET api/sample/load
, because only admin and manager are allowed to access this API.
GET api/sample/loadone/{id}
:
String ls_url = "https://localhost:5001/api/sample/loadone/1"
Press Ctrl+R to run the app. Click Send Request and check the response. (Note: Your Web API needs to be running.)
Status Code: 200
Status Text: OK
Response Body: value
Notice that user2 can access the API GET api/sample/loadone/{id}
, because users with any roles are allowed to access this API.