Authentication with JWT: How a Token Is Created and Verified
We cover authentication with JWT: what a token is, what its three parts mean, why it's signed, why the server is stateless, and setting up token creation and verification in .NET step by step.
In the previous article we talked about how authentication asks the "who are you?" question. So what happens after you answer this question once? Does the user have to send their username and password again and again on every request? Of course not. This is exactly where JWT comes in. Today we talk about how we verify a user's identity once and then carry it securely in subsequent requests.
The Problem: How Does the Server Remember the User?
You need to remember one thing: HTTP is stateless by nature. That is, each request is unaware of the ones before it. When the server finishes processing a request, it forgets it. When the next request arrives, it doesn't automatically know that this person is the user who just logged in. So after the user logs in once, how will the server recognize them in subsequent requests?
There was a classic way: the server kept a record (a session) in its own memory for each logged-in user. But this approach has a cost; the server has to remember every user, and if you have more than one server, sharing this memory becomes difficult. JWT offers a completely different, elegant way: the server remembers nothing, the user carries the information themselves.
What Is a JWT?
JWT means "JSON Web Token." In short, it's a signed piece of text containing the user's identity. When the user logs in, the server gives them this token. And the user brings this token along on every subsequent request; like an entry ticket. The server checks the ticket, and if it's valid, says "okay, I recognized you" and processes the request.
Recall the hotel analogy from the previous article: the reception gave you a room card. A JWT is exactly that room card. You get it once, then show it at every door. The hotel doesn't need to confirm who you are from the reception every time; the card itself proves your identity. JWT works like this too: the token itself is the portable proof of your identity.
The Three Parts of a Token
A JWT consists of three sections separated by dots. Each section has a separate job.
Header: Says the type of the token and which signing method was used. A technical piece of metadata.
Payload: The essence of the matter is here. Information about the user is carried here: who they are, which roles they have, when the token will become invalid. This information is called claims.
Signature: The part that provides the token's security. The server signs the header and payload with a secret key. Thanks to this signature, whether the token's content has been tampered with can be understood.
Critical Point: JWT Is Signed, Not Encrypted
There's a very common misconception here, be sure to settle it correctly. The payload inside a JWT isn't encrypted; it's only encoded, and anyone who wants to can read its contents. So putting sensitive information (for example, a password) in the token is a big mistake, because everyone can see it.
So what does the signature do? The signature doesn't hide the content; it guarantees that the content hasn't been changed. If someone takes the "I'm a normal user" information inside the token and tries to change it to "I'm an administrator," the signature won't match, and the server rejects the token. Because producing a valid signature requires the server's secret key, and that key exists only on the server. In short: anyone can read the inside of a JWT, but no one can change it without being noticed. Security comes not from secrecy, but from this immutability.
JWT Setup in .NET
Now let's put this into code. First we add the necessary package:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Then we register JWT verification in Program.cs. This is where we tell the server "check incoming tokens according to these rules":
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
ValidateIssuer = false,
ValidateAudience = false
};
});
The most important line here is IssuerSigningKey: the secret key the server uses while signing and verifying tokens. We don't embed this key in the code, as we learned in the fifteenth article; we read it through Configuration and actually keep it in a secure place (User Secrets or an environment variable). If this key leaks, anyone can produce valid tokens; so it must be protected like a secret.
Creating a Token: The Login Endpoint
When the user logs in, we need to give them a token. We do this in a login endpoint. The logic is: check the username and password, and if correct, produce a token containing the user's identity and return it.
[HttpPost("login")]
public IActionResult Login(LoginDto dto)
{
// Note: the username/password check is done here.
// In reality the password is compared with the hash in the database.
var claims = new[]
{
new Claim(ClaimTypes.Name, dto.Username),
new Claim(ClaimTypes.Role, "User")
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(
key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: credentials);
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
return Ok(new { token = tokenString });
}
Let's look at what happens step by step. First we prepare the claims we'll put in the token; here we added the user's name and role. Then we create the signing information with our secret key. Next we build a token object, giving it the claims, an expiration date (here one hour), and the signing information. The expiration date is important: the token shouldn't be valid forever, it should have a lifespan so that if it's stolen, the damage stays limited. Finally we convert the token into text and return it to the user. The user will now store this text and bring it on every request.
Using the Token
After the user receives the token, they carry it in the Authorization header when making a request to a protected endpoint. The format of the header is: Authorization: Bearer <token>. The word "Bearer" means "the person carrying this token." The server sees this header, checks the token's signature with its secret key, and if it's valid and not expired, recognizes the user and processes the request.
At this point the [Authorize] attribute from the previous article gains meaning. When you put [Authorize] at the top of an endpoint, .NET automatically checks the incoming token; if there's no valid token it returns 401, if there is it lets them in. You don't write this check by hand; once you've done the JWT setup, [Authorize] handles the rest.
A Small Experiment
Add the JWT setup to your project and write a simple login endpoint; for now you can keep the username/password check fake, the aim is to see token creation. Make a request to login and get the returned token. Then copy this token somewhere and paste it into a site like jwt.io; see with your own eyes the three parts of the token and the claims inside it. Notice that the content is readable, that is, it's encoded, not encrypted. Then make a request to an endpoint protected with [Authorize], first without a token and then with one, and observe the difference between 401 and 200. This experiment makes the entire cycle of JWT concrete at once.
In the next article we'll deepen authorization: we'll move on to role- and policy-based authorization. Using that "role" information we put in the token today, we'll talk about giving different permissions to different user groups. The claims inside the token will be the very basis of those permission decisions.