- client side application, developed in JavaScript, to send request to server. We will use AJAX or fetch API to make requests to the server
- server application, ASP.NET Core Web API, which will receive the request, process it and respond back to the client application
- Later we will modify the client application to make it React app which will make requests to the Web API. It will be rudimentary application which will just show the AJAX based authorization.
- Open Visual Studio and create ASP.NET Core Web API application using Web API template with latest SDK.
- Add DataController.cs class in Controllers folder. Add two endpoints - secure and public as given below in DataController.
DataController.cs:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[Authorize(Policy = "AdminOnly")]
[Route("api/[controller]")]
[ApiController]
public class DataController : ControllerBase
{
[HttpGet("secure")]
public IActionResult GetSecureData()
{
return Ok(new { message = "This is secured data" });
}
[AllowAnonymous]
[HttpGet("public")]
public IActionResult GetPublicData()
{
return Ok(new {message = "This data is publicly available" });
}
}
In above DataController, one endpoint is secure and another is public.
The secure endpoint can be accessed only by authorized user. For authorization, we can use username and password based login. After successuful login, JWT token will be generated which can be used throughout the login session. For authentication and authorization, we modify the Program class as done below:
Program.cs:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace AJAX_Authorization;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Configure JWT authentication
var key = "appliedk.blogspot.com_secret_key_2024"; // Replace with your own secret key
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
builder =>
{
builder.WithOrigins("http://127.0.0.1:5500")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
app.UseCors("AllowSpecificOrigin");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
Notes
- JWT is used for authentication.
- The authorization service is added in the Web API application so that only authenticated and authorized users will be able to access any secured endpoint.
- Authorization service is added with the name called AdminOnly so that only users who are in role of Admin will be able to access secured endpoints.
- The CORS is used so that any other origin application, e.g. JavaScript based client, could access the Web API application.
Next step, Create a Login Endpoint to Issue JWT
The question is how is JWT token generated. An endpoint is needed at server which will generate the token.
- Add AuthenticationController in Controllers folder.
- Add Login endpoint to this controller. The Login method validates user credentials and issues a JWT token if successful. In this example, We use hardcoded username and password. When username and password matches then JWT token generator method is called. This method uses the secret key defined in Program class.
- You can use Login form to authenticate user. This is skipped for simplicity.
- You can keep the secret key in appsettings.json file etc. This is skipped for simplicity.
- The UserCredentials model class is used to map username and password. This class can be placed inside Models folder.
AuthenticationController.cs:
using AJAX_Authorization.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace AJAX_Authorization.Controllers;
[Route("api/[controller]")]
[ApiController]
public class AuthenticationController : ControllerBase
{
private readonly string _key = "appliedk.blogspot.com_secret_key_2024"; // Use the same secret key as in Program.cs
[HttpPost("login")]
public IActionResult Login([FromBody] UserCredentials credentials)
{
// Validate credentials (use a proper user validation in production)
if (credentials.Username == "admin" && credentials.Password == "password")
{
var token = GenerateJwtToken();
return Ok(new { token });
}
return Unauthorized();
}
private string GenerateJwtToken()
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_key);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, "admin"),
new Claim(ClaimTypes.Role, "Admin")
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
UserCredentials:
namespace AJAX_Authorization.Models;
// Model to hold user credentials
public class UserCredentials
{
public string? Username { get; set; }
public string? Password { get; set; }
}
We have done the basic setup for authentication in Web API application. Next, we will create client app which will send requests to Web API for authentication and resources at two endpoints in DataController.
Client Application: We need a client side application which will be used to authenticate user and access different endpoints of Web API application. This is usually a front-end app built with JavaScript, HTML, and CSS (or frameworks like React, Vue, Angular, etc.). It makes AJAX calls to the Web API for data and processes responses. We will use this client app to provide login to user. After successful login, users will get JWT token to access secured data.
In our case, simple JavaScript application will be used which will
- authenticate user using login system
- send request to different endpoints
Login System: In the client app (e.g., an HTML/JavaScript app):
- Login: Send an AJAX request to the Web API’s login endpoint to authenticate the user and receive a token.
- Store the Token: After receiving the token, store it in localStorage or sessionStorage.
- Authorized Requests: For each subsequent AJAX request to a protected Web API endpoint, include the token in the headers.
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JavaScript API Client</title>
</head>
<body>
<h1>AJAX Authorization Test</h1>
<button onclick="getPublicData()">Get Public Data</button>
<button onclick="getSecuredData()">Get Secured Data</button>
<button onclick="getLoginToken()">Get JWT token</button>
<h2>Response:</h2>
<pre id="response"></pre>
<script src="app.js"></script>
</body>
</html>
In the index HTML page, we create three buttons for: 1. public endpoint 2. secure endpoint and 3. endpoint for jwt authentication
- When user clicks public endpoint then corresponding event handler function, getPublicData, in script file, app.js, is executed.
- When user clicks secure endpoint then corresponding event handler function, getSecuredData, is executed. The web API sends back message to the user to first login.
- When user clicks third button then jwt authentication token is generated and is stored in local storage of the browser. Then and thereafter, when the second button is again clicked by the user then the API returns the data from the secure endpoint.
- Note that when any event handler function is executed then fetch API function is executed which sends the request at the URL of the Web API at which the resource is and the API is running. The fetch function takes URL as first argument and an optional second argument, JSON object. Look at fetch function in this regard in below script:
app.js:
// Base URL of your Web API
const apiUrl = "https://localhost:5000/api/data";
// Function to display response
function displayResponse(data) {
document.getElementById("response").textContent = JSON.stringify(
data,
null,
2
);
}
// Function to fetch public data
function getPublicData() {
fetch(`${apiUrl}/public`)
.then((response) => {
if (!response.ok) throw new Error("Network response was not ok");
return response.json();
})
.then((data) => displayResponse(data))
.catch((error) => displayResponse({ error: error.message }));
}
// Function to fetch secured data
function getSecuredData() {
const token = localStorage.getItem("jwtToken"); // Retrieve the token from localStorage
if (!token) {
displayResponse({ error: "Token not found. Please log in first." });
return;
}
fetch(`${apiUrl}/secure`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}` // Use the stored token here
},
})
.then((response) => {
if (response.status === 401) {
throw new Error("Unauthorized: You need to log in as an Admin.");
}
if (!response.ok) throw new Error("Network response was not ok");
return response.json();
})
.then((data) => displayResponse(data))
.catch((error) => displayResponse({ error: error.message }));
}
// Login function to retrieve the token
function getLoginToken() {
// Check if the JWT token already exists in local storage
const existingToken = localStorage.getItem("jwtToken");
if (existingToken) {
console.log("Token already exists. Skipping login.");
displayResponse("Token already exists. Skipping login.");
return;
}
// Proceed with login if no token is found
fetch("https://localhost:5000/api/authentication/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "admin", password: "password" })
})
.then(response => response.json())
.then(data => {
if (data.token) {
localStorage.setItem("jwtToken", data.token); // Save token in local storage
console.log("Logged in successfully!");
displayResponse("Logged in successfully!");
} else {
console.error("Login failed");
displayResponse("Login failed");
}
})
.catch(error => console.error("Error:", error));
}
Now run the Web API and client app separately.
This approach separates concerns between your API and the client app, making it easy to maintain and secure each part individually.
Related Post:
The application will be modified; React app will be used as client app and Login form will be used for authentication in React app.
No comments:
Post a Comment