Tutorials / Build a REST API with C# and .NET · Lesson 36 of 38
Backend
On this page
The Login Endpoint
In what a JWT is, reception printed a badge after checking an identity card. Our API has no reception yet. This lesson builds it.
Our goal: an endpoint, POST /auth/login, that takes a user name and a password and, when they are right, answers with a signed token.
Here is the route:
- Install the package that makes tokens.
- Add the settings for the token: who makes it, who it is for, how long it lasts, and the secret key.
- Add two users, alice and bob, with their passwords stored safely.
- Write a small service that builds and signs the token.
- Write the login endpoint, and run it: a right password, a wrong one, and an empty form.
- Test it.
Left out: checking the token on the other endpoints. After this lesson every endpoint is still open to anyone; protect endpoints and roles closes them. Also left out: a users table, signing up, changing a password, and ASP.NET Core Identity, Microsoft's complete ready-made user system.
Where we are
This is the solution from the end of the report screen. A new folder, Auth, holds the pieces of logging in:
BookShop/
Directory.Packages.props <- changes (one package)
src/
BookShop.Api/
Program.cs <- changes
appsettings.json <- changes
appsettings.Development.json <- changes
Auth/
JwtSettings.cs <- NEW
AppUser.cs <- NEW
IUserService.cs <- NEW
DemoUserService.cs <- NEW
TokenService.cs <- NEW
Controllers/
AuthController.cs <- NEW
Dtos/
LoginRequest.cs <- NEW
LoginResponse.cs <- NEW
tests/
BookShop.Api.Tests/
DemoUserServiceTests.cs <- NEW
TokenServiceTests.cs <- NEW
AuthControllerTests.cs <- NEW
Only the files that change are listed. Run every command from the solution folder, BookShop/. No project is added, so there is no solution map.
Here is where the new pieces sit when a login arrives:
The controller only speaks HTTP, as in every earlier lesson. Checking a password and making a token are two separate jobs, so each gets its own small class.
Install the package
The package Microsoft.AspNetCore.Authentication.JwtBearer is Microsoft's support for JWTs in ASP.NET. It brings two halves: the library that makes tokens, which we use now, and the part that checks them on every request, which we use in the next lesson. One package for both.
Terminal:
dotnet add src/BookShop.Api package Microsoft.AspNetCore.Authentication.JwtBearer
Visual Studio: right-click BookShop.Api, choose Manage NuGet Packages, and install Microsoft.AspNetCore.Authentication.JwtBearer.
As before, the version goes into Directory.Packages.props. The whole file now looks like this (your numbers may be newer):
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.14" /> <!-- new -->
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.13">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.13" />
<PackageVersion Include="MSTest" Version="4.0.1" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.18.2" />
</ItemGroup>
</Project>
The settings for the token
Four values describe our tokens. Three of them are ordinary settings. The fourth is a secret.
| Setting | Our value | In the token | Meaning |
|---|---|---|---|
Issuer |
BookShop.Api |
iss |
Who makes the token. |
Audience |
BookShop |
aud |
Who the token is for. |
MinutesValid |
60 |
exp |
How long a token lasts. |
Key |
a long secret | (none) | The secret key that makes the signature. |
The key is the one thing that must never leak: whoever has it can make a token that says Manager. So it does not go in appsettings.json, the file every environment reads. On your machine it goes in appsettings.Development.json, which creating the solution showed is read only in Development. On a live server the key comes from outside the code, for example from an environment variable or a secret store that the hosting company provides. That setup belongs to deployment, and the course leaves it out.
Open src/BookShop.Api/appsettings.json. The whole file is:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"BookShop": "Data Source=bookshop.db"
},
"Jwt": { // new
"Issuer": "BookShop.Api", // new
"Audience": "BookShop", // new
"MinutesValid": 60 // new
} // new
}
(The // new marks are only for this page. JSON has no comments, so do not type them. Mind the comma after the ConnectionStrings block.)
Open src/BookShop.Api/appsettings.Development.json. The whole file is:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Jwt": { // new
"Key": "development-only-key-replace-me-at-least-32-bytes" // new
} // new
}
ASP.NET reads both files and merges them, so in Development the Jwt section has all four values. Make your own key if you like: any text of at least 32 characters. HS256 refuses a key that is too short. We tried short-key, and the login answered with a 500, with IDX10653: The encryption algorithm 'HS256' requires a key size of at least '128' bits in the log.
⚠️ This key is now in a file that goes into git with the rest of the code. That is acceptable for a key that only ever signs practice tokens on your own machine. A real key never goes into git. .NET has a tool for keeping even development secrets out of the project folder,
dotnet user-secrets, which we leave out.
Now a class to hold the four values, so the code reads settings.Issuer and not a string like "Jwt:Issuer". Create a folder Auth in src/BookShop.Api, and in it JwtSettings.cs:
public class JwtSettings
{
public string Issuer { get; set; } = "";
public string Audience { get; set; } = "";
public string Key { get; set; } = "";
public int MinutesValid { get; set; }
}
The names match the names in the Jwt section, which is how ASP.NET knows which value goes where. We fill it in Program.cs, later in this lesson.
Two users
A real shop keeps its users in a table, with a page to sign up and a way to reset a password. That is a lot of work that has nothing to do with tokens, so we keep two users in the code: alice, the manager, and bob, staff. This is demo code, and its name says so.
First, the user. Create Auth/AppUser.cs:
public record AppUser(string UserName, string Role);
A one-line record, as in records: a user only carries two values. It is called AppUser because .NET already has several classes called User.
Then the job, as an interface, because the demo users will one day be replaced by a table. Create Auth/IUserService.cs:
public interface IUserService
{
AppUser? FindUser(string userName, string password);
}
FindUser answers the user when the name and the password are right, and null otherwise. A wrong password is an ordinary outcome, not a crash, so it is a null and not an exception.
Passwords are never stored as they are
The tempting way is to keep each password next to its user and compare the two strings. If anyone ever reads that list (a leaked backup, a curious colleague, a bug that logs it), every password is out, and people use the same password in many places.
So a password is stored as a hash: the result of a one-way mixer. The mixer turns bob-pass-1 into a long string, and there is no way to turn the string back into the password. At login, the mixer runs again on what the user typed, and the two results are compared.
Do not write the mixer yourself. ASP.NET has one, PasswordHasher, and it adds a random part (a salt) to every hash. So the same password hashed twice gives two different strings. We ran it twice on bob-pass-1:
AQAAAAIAAYagAAAAEO/432cw+Q3K75/qrDmg9U8dqA6HyW/L+Ty9KlPTikZDlGpu36O6QogEX27r28jbPg==
AQAAAAIAAYagAAAAELJ1ZTexNus+a4nXZ2HxBHC+TEtKxD9lSoxNpEJMop3/abdma6AMEEINL5llUvaNtw==
The salt is stored inside the hash, and the hasher's own VerifyHashedPassword knows how to read it. Two people with the same password get different hashes, so a thief cannot spot them, nor look them up in a list of known hashes.
Now the demo users. Create Auth/DemoUserService.cs:
using Microsoft.AspNetCore.Identity;
public class DemoUserService : IUserService
{
private readonly PasswordHasher<AppUser> _hasher = new();
private readonly Dictionary<string, (AppUser User, string PasswordHash)> _users = [];
public DemoUserService()
{
Add(new AppUser("alice", "Manager"), "alice-pass-1");
Add(new AppUser("bob", "Staff"), "bob-pass-1");
}
private void Add(AppUser user, string password)
{
_users[user.UserName] = (user, _hasher.HashPassword(user, password));
}
public AppUser? FindUser(string userName, string password)
{
if (!_users.TryGetValue(userName, out var entry))
{
return null;
}
var result = _hasher.VerifyHashedPassword(entry.User, entry.PasswordHash, password);
if (result == PasswordVerificationResult.Failed)
{
return null;
}
return entry.User;
}
}
Line by line:
PasswordHasher<AppUser>is the hasher. It lives inMicrosoft.AspNetCore.Identity, which is part of ASP.NET, so there is nothing to install. The<AppUser>says which kind of user it hashes for; this hasher does not look inside it._usersis aDictionary: a list you look up by a key, here the user name. Each entry holds two values, the user and the hash, written(AppUser User, string PasswordHash). That pair is called a tuple: a quick way to keep two values together without writing a class.- The constructor adds the two users.
Addhashes the password at once and keeps only the hash. FindUserlooks up the name (TryGetValueanswersfalsewhen there is no such key, so an unknown user givesnull), then asks the hasher to compare the typed password with the stored hash.
Be honest about the shortcut: the two passwords are written in the code, so anyone who reads the code knows them. The hashing shows the right habit for the day the users move to a table, where only hashes are stored. Never ship a user list like this one.
The token service
Now the reception printer. Create Auth/TokenService.cs:
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
public class TokenService
{
private readonly JwtSettings _settings;
public TokenService(JwtSettings settings)
{
_settings = settings;
}
public string CreateToken(AppUser user)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.Key));
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(
[
new Claim("name", user.UserName),
new Claim("role", user.Role)
]),
Issuer = _settings.Issuer,
Audience = _settings.Audience,
Expires = DateTime.UtcNow.AddMinutes(_settings.MinutesValid),
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
};
return new JsonWebTokenHandler().CreateToken(descriptor);
}
}
Read it as filling in a badge form, then handing it to the printer:
SymmetricSecurityKeyturns the secret key text into bytes the signing code can use. "Symmetric" means the same key makes the signature and checks it, which is how HS256 works.SecurityTokenDescriptoris the form. Each property becomes part of the token:Subjectholds the claims, the facts about the user. AClaimis a name and a value. We use the short namesnameandrole, so the payload reads exactly as it did in the JWT lesson. (.NET also has long names for claims, which are web addresses; we leave them out.) The[ ... ]is the collection expression from small C# shortcuts.IssuerandAudiencebecomeissandaud.Expiresbecomesexp: now, plus 60 minutes.UtcNowbecause token times are always in UTC.SigningCredentialsnames the key and the way to sign: HMAC with SHA-256, which is what HS256 stands for.
JsonWebTokenHandler().CreateTokenis the printer: it writes the header and the payload, signs them, and returns the token as one string. It also addsiatandnbf(the time it was made) by itself.
The service takes JwtSettings in its constructor, like every other service takes what it needs. It does not read the settings files itself, which makes it easy to test with a test key.
The login endpoint
What the client sends and what it gets back are two small records, next to the other request and response classes. Create Dtos/LoginRequest.cs:
using System.ComponentModel.DataAnnotations;
public record LoginRequest([Required] string UserName, [Required] string Password);
And Dtos/LoginResponse.cs:
public record LoginResponse(string Token, string Role);
The response carries the role as well as the token. The role is inside the token already, but a web page would have to decode the token to find it. Sending it next to the token is simpler for the page, and it is not a secret: the page may see it either way.
Now the controller. Create Controllers/AuthController.cs:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("auth")]
public class AuthController : ControllerBase
{
private readonly IUserService _userService;
private readonly TokenService _tokenService;
public AuthController(IUserService userService, TokenService tokenService)
{
_userService = userService;
_tokenService = tokenService;
}
[HttpPost("login")]
[ProducesResponseType<LoginResponse>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult<LoginResponse> Login(LoginRequest request)
{
var user = _userService.FindUser(request.UserName, request.Password);
if (user == null)
{
return Problem(
statusCode: StatusCodes.Status401Unauthorized,
title: "Wrong user name or password.");
}
var token = _tokenService.CreateToken(user);
return new LoginResponse(token, user.Role);
}
}
Most of it is the controller shape you know. Three choices are new:
POST, notGET. The password travels in the body. AGETwould put it in the address, and addresses end up in server logs and in the browser's history. Logging in is not a read of a resource, so the "verb in the address" (/login) is the same kind of exception as the/selladdress of the sale.401 Unauthorizedis the status for "I do not know who you are". The name is old and misleading: it is about authentication, not authorization. The body is the usual Problem Details, with our owntitle.- One message for both mistakes. An unknown user and a wrong password get the same answer. If the API said "no such user", anyone could try names until they found the real ones, and then only guess passwords.
Login is not async. Nothing in it waits for a database or the network: the users and the key are in memory.
Register the services
Open src/BookShop.Api/Program.cs. The whole file is:
using BookShop.Data;
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
var connectionString = builder.Configuration.GetConnectionString("BookShop");
builder.Services.AddDbContext<BookShopDbContext>(options => options.UseSqlite(connectionString));
builder.Services.AddScoped<IBookService, BookService>();
builder.Services.AddScoped<IStockReportService, StockReportService>();
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>()!; // new
builder.Services.AddSingleton(jwtSettings); // new
builder.Services.AddSingleton<TokenService>(); // new
builder.Services.AddSingleton<IUserService, DemoUserService>(); // new
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapControllers();
app.Run();
GetSection("Jwt").Get<JwtSettings>()reads theJwtsection of the settings files and fills aJwtSettingswith it. It answersnullif the section is missing; the!at the end tells the compiler we know it is there. We keep the object in a variable because the next lesson needs it again in this file.AddSingleton(jwtSettings)registers that one object, soTokenServicecan ask for it in its constructor.- Singleton for all three. None of them holds anything that belongs to one request, and none of them changes after it is made: the settings, the key and the two users are the same for everyone. That is the case for a singleton from service lifetimes: singleton, and it means the passwords are hashed once, when the app starts, not on every login.
Run it
Start the app (dotnet run --project src/BookShop.Api). Each case below is one request, run on its own.
Case A: bob, with the right password
The request body:
{"userName":"bob","password":"bob-pass-1"}
curl -i -X POST http://localhost:5101/auth/login -H "Content-Type: application/json" -d '{"userName":"bob","password":"bob-pass-1"}'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 31 Oct 2026 09:52:23 GMT
Server: Kestrel
Transfer-Encoding: chunked
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJCb29rU2hvcCIsImlzcyI6IkJvb2tTaG9wLkFwaSIsImV4cCI6MTc5MzQ0Mzk0MywibmFtZSI6ImJvYiIsInJvbGUiOiJTdGFmZiIsImlhdCI6MTc5MzQ0MDM0MywibmJmIjoxNzkzNDQwMzQzfQ._dlpMAmhQU7RATNTOXovXe4H_gXos0snljjUYR9pyuw","role":"Staff"}
This is the very token that the JWT lesson took apart: its payload says bob, Staff, issued at 09:52:23 and valid for one hour, and the Date header shows the same 09:52:23. Your token will be different, because the times are different, and so is the signature.
Case B: bob, with a wrong password
{"userName":"bob","password":"guess"}
curl -i -X POST http://localhost:5101/auth/login -H "Content-Type: application/json" -d '{"userName":"bob","password":"guess"}'
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json; charset=utf-8
The body, one field per line (the real body is a single line):
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.2",
"title": "Wrong user name or password.",
"status": 401,
"traceId": "00-88fc31dd9f8ea53b4ad229b48d3bb581-71d17ec7bb4aa6a9-00"
}
A 401, our title, and no token. (Ignore the traceId, as before.) We also sent a user who does not exist, carol, and got exactly the same body, apart from the traceId.
Case C: an empty body
{}
curl -i -X POST http://localhost:5101/auth/login -H "Content-Type: application/json" -d '{}'
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Password": ["The Password field is required."],
"UserName": ["The UserName field is required."]
},
"traceId": "00-67546da7f97b78c9cc7c3b9e3a8c3b9b-e8fc198c814257f3-00"
}
The [Required] rules on LoginRequest stopped it before Login ran, the same gate as for a book. A 400 says "your request is malformed", a 401 says "your request is fine, and you are not who you say".
Still open
The books are still open to anyone, with or without a token:
curl -i http://localhost:5101/books
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
followed by the five books, as always. Making tokens changes nothing for the other endpoints yet. Nobody checks them. That is the next lesson.
In the test page
The Scalar page lists the new endpoint, POST /auth/login, with a body of userName and password. Send bob's body from case A and look for the 200 and the token. (We checked the OpenAPI document behind the page, not the clicks.)
Test it
Three small test files, one per new class, in tests/BookShop.Api.Tests. None of them needs a fake: the user service keeps its users in memory, and the token service needs only a settings object.
The users
Create DemoUserServiceTests.cs:
namespace BookShop.Api.Tests;
[TestClass]
public class DemoUserServiceTests
{
private readonly DemoUserService _service = new();
[TestMethod]
public void FindUser_RightPassword_ReturnsTheUserWithTheirRole()
{
var user = _service.FindUser("alice", "alice-pass-1");
Assert.IsNotNull(user);
Assert.AreEqual("Manager", user.Role);
}
[TestMethod]
public void FindUser_WrongPassword_ReturnsNull()
{
var user = _service.FindUser("alice", "guess");
Assert.IsNull(user);
}
[TestMethod]
public void FindUser_UnknownUser_ReturnsNull()
{
var user = _service.FindUser("carol", "alice-pass-1");
Assert.IsNull(user);
}
}
The third test uses alice's real password with a name that does not exist, so it proves the name matters, not only the password.
The token
Create TokenServiceTests.cs:
using Microsoft.IdentityModel.JsonWebTokens;
namespace BookShop.Api.Tests;
[TestClass]
public class TokenServiceTests
{
private static readonly JwtSettings Settings = new()
{
Issuer = "BookShop.Api",
Audience = "BookShop",
Key = "a-test-key-that-is-long-enough-for-hs256",
MinutesValid = 60
};
[TestMethod]
public void CreateToken_User_CarriesTheirNameAndRole()
{
var service = new TokenService(Settings);
var token = service.CreateToken(new AppUser("bob", "Staff"));
var read = new JsonWebToken(token);
Assert.AreEqual("bob", read.GetClaim("name").Value);
Assert.AreEqual("Staff", read.GetClaim("role").Value);
Assert.AreEqual("BookShop.Api", read.Issuer);
}
[TestMethod]
public void CreateToken_User_ExpiresAfterTheConfiguredMinutes()
{
var service = new TokenService(Settings);
var token = service.CreateToken(new AppUser("bob", "Staff"));
var read = new JsonWebToken(token);
var minutesLeft = (read.ValidTo - DateTime.UtcNow).TotalMinutes;
Assert.IsTrue(minutesLeft > 59 && minutesLeft <= 60);
}
}
new JsonWebToken(token)decodes the token, likeatobin the JWT lesson. It does not check the signature, and it does not need to: this test is about what goes into the token.- The test key is its own text, so the tests never depend on the key in the settings files.
- The expiry test allows a little time between making the token and reading it: more than 59 minutes and at most 60.
The endpoint
Create AuthControllerTests.cs:
using Microsoft.AspNetCore.Mvc;
namespace BookShop.Api.Tests;
[TestClass]
public class AuthControllerTests
{
private AuthController _controller = null!;
[TestInitialize]
public void Setup()
{
var settings = new JwtSettings
{
Issuer = "BookShop.Api",
Audience = "BookShop",
Key = "a-test-key-that-is-long-enough-for-hs256",
MinutesValid = 60
};
_controller = new AuthController(new DemoUserService(), new TokenService(settings));
}
[TestMethod]
public void Login_RightPassword_ReturnsATokenAndTheRole()
{
var result = _controller.Login(new LoginRequest("alice", "alice-pass-1"));
Assert.IsNotNull(result.Value);
Assert.IsFalse(string.IsNullOrEmpty(result.Value.Token));
Assert.AreEqual("Manager", result.Value.Role);
}
[TestMethod]
public void Login_WrongPassword_Returns401()
{
var result = _controller.Login(new LoginRequest("alice", "guess"));
var answer = result.Result as ObjectResult;
Assert.IsNotNull(answer);
Assert.AreEqual(401, answer.StatusCode);
}
}
The controller gets the real DemoUserService and TokenService. That breaks the habit of unit testing basics, where a unit test replaces the other classes with stand-ins. We allow it here because both classes are already stand-ins themselves (the users live in memory) and run in a blink. When the users move to a table, this test gets a fake IUserService, like FakeBookService.
Run the tests:
dotnet test
Passed! - Failed: 0, Passed: 27, Skipped: 0, Total: 27, Duration: 1 s - BookShop.Api.Tests.dll (net10.0)
Twenty earlier tests and seven new ones. The Data tests did not change, and still pass.
Where we ended
Compare with your solution:
BookShop/
Directory.Packages.props the JWT package added
src/
BookShop.Api/
Program.cs reads the Jwt settings, registers three singletons
appsettings.json Jwt: issuer, audience, minutes
appsettings.Development.json Jwt: the key, for your machine only
Auth/
JwtSettings.cs the four token settings, as a class
AppUser.cs a user: name and role
IUserService.cs "is this name and password right?"
DemoUserService.cs alice and bob, passwords kept as hashes
TokenService.cs builds and signs a token
Controllers/
AuthController.cs POST /auth/login: 200 with a token, or 401
Dtos/
LoginRequest.cs user name and password
LoginResponse.cs the token and the role
tests/
BookShop.Api.Tests/
DemoUserServiceTests.cs right, wrong, unknown
TokenServiceTests.cs the claims and the expiry
AuthControllerTests.cs 200 with a token, 401
The API now has a reception desk. It hands out badges, and none of the doors checks them yet.
Recap
POST /auth/logintakes a user name and a password and answers 200 with a token, 401 when they are wrong, and 400 when they are missing.- The token settings live in the
Jwtsection. The secret key stays out ofappsettings.json: in Development it is inappsettings.Development.json, and on a live server it comes from outside the code. - Passwords are stored as hashes, made and checked by ASP.NET's
PasswordHasher, which adds a salt so equal passwords give different hashes. TokenServicefills aSecurityTokenDescriptor(claims, issuer, audience, expiry, signing key) andJsonWebTokenHandlerturns it into a signed token.- A wrong password and an unknown user get the same answer, so the API does not reveal which user names exist.
- The settings, the token service and the users are singletons: nothing in them belongs to one request.
- Handing out tokens protects nothing by itself. The endpoints are still open until they check the token.
Questions to think about
Each question has its answer folded away underneath. Guess first, then open it to check.
- Why is the secret key not in
appsettings.json?
Show answer
Whoever has the key can sign any token, including one that says Manager. appsettings.json is read everywhere and lives with the code, so the real key comes from outside the code.
- Why does the API store a hash of each password, and not the password?
Show answer
If the stored list ever leaks, a hash cannot be turned back into the password. The API can still check a login by running the hasher on what the user typed.
- Why do a wrong password and an unknown user get the same answer?
Show answer
A different answer for an unknown user would let anyone find out which user names exist, which is half of breaking in.
- Why is the login a
POSTand not aGET?
Show answer
A GET puts its values in the address, and addresses are kept in logs and in the browser's history. The password belongs in the body.
- bob has a token. Can he read the stock report without it?
Show answer
Yes, at the moment. No endpoint checks tokens yet, so the report is open to anyone. The next lesson makes the API check them.
Next lesson: protect endpoints and roles, where the API checks the token and keeps the report for managers.