← All tutorials

What Is JWT?

Token-based authentication, explained simply — with a practical ASP.NET Core example

JWT lesson banner: the three token parts Header, Payload, and Signature, plus refresh-token notes.
A token has three parts: the algorithm, the claims, and a signature the server checks. Labels on this diagram are in Persian.

What we'll build

We have a small API. The user signs in with a username and password, the server issues a JWT, and the user uses that token to access protected routes.

The goal isn't to memorize jargon — we want to understand exactly what JWT is, how it works, and where security matters.

1. What is JWT?

JWT stands for JSON Web Token. In simple terms, a JWT is like a digital access card that the server gives the user after a successful login.

Imagine entering a company building. Security checks your identity and gives you a visitor badge. From then on, you don't show your ID every time you enter an authorized room — you show the badge.

In an API, roughly the same thing happens:

  • You send username and password once to the login endpoint.
  • The server verifies the credentials and creates a JWT.
  • In later requests, you send the JWT along with the request.
  • The server checks the token's validity and your access level.
JWT is commonly used for authentication in Web APIs, mobile apps, and service-to-service communication.

2. How does login work?

1. The user sends username and password to /login.
↓
2. The server checks the user against the database.
↓
3. If valid, the server returns a signed JWT.
↓
4. The app puts the token in the header of subsequent requests.
↓
5. The server validates the signature, expiry, and permissions.

A protected request header looks like this:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

The word Bearer means "whoever holds this token." Anyone with a valid token can use it — like cash. That's why a leaked token is dangerous.

3. What is a JWT made of?

A JWT has three parts separated by dots:

xxxxx.yyyyy.zzzzz
Header.Payload.Signature

Header — technical metadata

Specifies the token type and signing algorithm:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload — data or claims

Contains the information needed about the user:

{
  "sub": "42",
  "name": "Sara",
  "role": "Admin",
  "exp": 1784491200
}

Each piece of data in the Payload is called a Claim. For example, sub is the user ID, role is the role, and exp is the token expiry time.

Signature — the signature

The server signs the first two parts with a secret key. If someone changes the Payload, the signature no longer matches and the server rejects the token.

A JWT is not encrypted. Header and Payload are only Base64Url-encoded — anyone can read them. Never put passwords, card numbers, or sensitive data inside a JWT. The signature prevents tampering, not reading.

4. A practical ASP.NET Core example

Create a Minimal API project and install the JWT authentication package:

dotnet new webapi -n JwtDemo
cd JwtDemo
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Configure the key in appsettings.json

{
  "Jwt": {
    "Key": "A-DEMO-SECRET-KEY-AT-LEAST-32-CHARS!",
    "Issuer": "JwtDemoApi",
    "Audience": "JwtDemoClient"
  }
}
This key is for the demo only. In production, don't store the key in the project files or Git — use environment variables, Secret Manager, or a secrets service.

Full Program.cs code

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

var jwtKey = builder.Configuration["Jwt:Key"]
    ?? throw new InvalidOperationException("JWT key is missing");
var issuer = builder.Configuration["Jwt:Issuer"];
var audience = builder.Configuration["Jwt:Audience"];

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = issuer,
            ValidAudience = audience,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(jwtKey)),
            ClockSkew = TimeSpan.Zero
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapPost("/login", (LoginRequest request) =>
{
    // For demo only; in a real project, load the user from the database
    // and verify the hashed password with PasswordHasher.
    if (request.Username != "sara" || request.Password != "123456")
        return Results.Unauthorized();

    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, "42"),
        new Claim(ClaimTypes.Name, "Sara"),
        new Claim(ClaimTypes.Role, "Admin"),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
    };

    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
    var credentials = new SigningCredentials(
        key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: issuer,
        audience: audience,
        claims: claims,
        expires: DateTime.UtcNow.AddMinutes(15),
        signingCredentials: credentials);

    var tokenText = new JwtSecurityTokenHandler().WriteToken(token);
    return Results.Ok(new { accessToken = tokenText });
});

app.MapGet("/profile", (ClaimsPrincipal user) =>
{
    return Results.Ok(new
    {
        Name = user.Identity?.Name,
        Message = "You are signed in."
    });
}).RequireAuthorization();

app.MapGet("/admin", () => "Only an admin can see this message.")
   .RequireAuthorization(policy => policy.RequireRole("Admin"));

app.Run();

record LoginRequest(string Username, string Password);

What does this code do?

  • AddAuthentication tells ASP.NET Core that our auth method is Bearer Token.
  • TokenValidationParameters defines the token validation rules.
  • The /login route verifies the user and issues a 15-minute token.
  • RequireAuthorization blocks access without a valid token.
  • The /admin route requires a valid token and the Admin role.
The order of UseAuthentication and UseAuthorization matters: identity must be established first, then access is checked.

5. Testing the API

Run the application:

dotnet run

Replace https://localhost:7001 with the HTTPS URL shown in the terminal. Log in first:

curl -k -X POST https://localhost:7001/login \
  -H "Content-Type: application/json" \
  -d '{"username":"sara","password":"123456"}'

The response includes the token:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs..."
}

Now put the token value in the next request:

curl -k https://localhost:7001/profile \
  -H "Authorization: Bearer YOUR_TOKEN"

Without the Authorization header you get 401 Unauthorized. If the token is valid but you lack the required role, you typically get 403 Forbidden.

CodePlain meaning
401Identity unknown — no token, invalid token, or expired token.
403Identity known, but you don't have enough permission for this action.

6. Access Token and Refresh Token

Access Tokens should have a short lifetime — say 5 to 15 minutes. But you don't want users to re-enter their password every 15 minutes. That's where a Refresh Token comes in.

  • Access Token: short-lived; sent with every API request.
  • Refresh Token: longer-lived; used only to obtain a new Access Token.
The Access Token expires.
↓
The app sends the Refresh Token to a dedicated refresh endpoint.
↓
The server validates the Refresh Token against a secure database record.
↓
A new Access Token is issued and the old Refresh Token is rotated.

A Refresh Token is usually a random string, not necessarily a JWT. Store it hashed in the database and support revocation, expiry, and rotation.

7. Important security notes

  • Always use HTTPS: without HTTPS, tokens can be stolen in transit.
  • Short lifetime: a short-lived Access Token limits damage from a stolen token.
  • Strong key: for HS256, use a long random key and keep it out of code and Git.
  • Full validation: verify signature, issuer, audience, and expiry.
  • Minimal data: put only necessary claims in the token — a JWT is not a database.
  • Real passwords: never store passwords in plain text; use ASP.NET Core Identity or PasswordHasher.
  • Browser storage: keeping tokens in localStorage long-term is risky against XSS. For web apps, consider an architecture with HttpOnly, Secure, and SameSite cookies.
  • Revoking access: a JWT usually stays valid until it expires. For immediate logout, password changes, or blocking a user, you need revocation or very short-lived tokens.
For production systems — especially public-facing login — prefer standard solutions like ASP.NET Core Identity and OAuth 2.0 / OpenID Connect providers. Don't invent authentication from scratch.

8. Summary

  • A JWT is a signed digital access card.
  • It has three parts: Header, Payload, and Signature.
  • The Payload is readable — don't put secrets in it.
  • The token is sent to the API with the Authorization: Bearer ... header.
  • The server must verify signature, issuer, audience, and expiry.
  • Keep Access Tokens short-lived and use Refresh Tokens for secure renewal.

If you think of JWT as a "signed access card," most of its concepts become simple and intuitive.