Authentication & Passwords
Learning Objectives
- You can distinguish authentication evidence from caller-controlled identity claims.
- You can use a password-hashing library to create a hash and verify a submitted password.
- You can explain how an authentication boundary provides a trusted account identity without exposing credentials.
- You can distinguish rejected credentials from an unavailable operation.
In Part 4, we checked a proposed change before storing it. A valid title tells us what the user wants to save, but it does not tell us who is making the change or whether the note belongs to that account. Let’s follow those questions through a notes application in which signed-in users create and edit their own notes.
Authentication establishes which account a request represents. Authorization decides whether that account may perform an operation. Putting userId: 7 in a request body answers neither question: a caller can choose that number.
The application’s authentication subsystem verifies credentials and manages sessions; the rest of the application uses its public interface. In this example, a session is server-side state that associates later requests with an account. A cookie is a name/value pair that the browser stores and sends automatically on matching requests. We will first follow the identity that these requests establish, then examine cookies more closely in the next chapter.
Registration, login, and session endpoints
To create a note for a particular account, we first need to register and sign in. The example API accepts JSON bodies for registration and login and validates them against strict schemas. Keep the request and its returned account fields together as you follow the interaction:
| Request | Accepted result |
|---|---|
POST /api/auth/register with {email, displayName, password} | 201 with {user:{id,displayName}}. The account exists, but registration does not log in. |
POST /api/auth/login with {email, password} | 200 with {user:{id,displayName}} and a session cookie set by the server. |
GET /api/session with the browser’s cookie | 200 with account fields {user:{id,displayName}} for a valid session, or {user:null} without one. |
An account view such as {"user":{"id":7,"displayName":"Example user"}} exposes only public account fields. The API keeps the password and session token out of JSON responses. We will return to the choice of public output fields in Chapter 5.5.
The note service uses the acting account’s ID from the resolved session, never an identity claimed in a form, query parameter, browser store or caller-supplied header. It also checks that the account owns the note before allowing an edit.
Figure 1 separates the session check that identifies an account from the ownership check that permits a note edit.
What happens to a password
Signing in asks the server to verify a password. Before tracing the rest of the request, let’s look at what the server needs to keep for that check.
Send passwords over HTTPS outside local development. Do not log them, place them in URLs, return them from the API or persist them in browser storage. Pass the entered password unchanged to the authentication subsystem; do not add trimming or normalization in the form. Any password normalization policy must be consistent at registration and verification. Email normalization is a separate account-lookup policy.
Plaintext is the original readable password. Storing it would expose every password if someone obtained the account records. Store a password hash instead: a one-way representation used to verify a submitted password without keeping the original.
Password hashing deliberately requires computing time and memory. That cost is manageable for one login but makes an attacker trying many guesses do more work for each guess. A salt is a random value included when hashing each password, so hashing the same password twice produces different stored values. Use a maintained password-hashing library to handle the algorithm, salt generation and verification. The OWASP password-storage guidance explains these protections.
Create and verify a hash
The course uses scrypt-kdf. Save this example as password-example.js. Predict the two verification results, then run it with deno run password-example.js; Deno downloads the pinned package on the first run.
import Scrypt from "npm:scrypt-kdf@4.0.0";
const parameters = { logN: 15, r: 8, p: 3 };const storedHash = await Scrypt.kdf("hamburger with extra pickles", parameters);
console.log(await Scrypt.verify(storedHash, "hamburger with extra pickles")); // trueconsole.log(await Scrypt.verify(storedHash, "hamburger without pickles")); // falseScrypt.kdf(password, parameters) creates the stored hash. The supplied parameters control the computing and memory cost; use these course settings for this example. Its result is a Uint8Array, a collection of bytes containing the hash, salt and parameters needed for later verification. Keep that complete result as the password record.
Scrypt.verify(storedHash, password) uses the stored salt and parameters to check a candidate password and returns a Boolean. The stored hash is its first argument. Both operations return promises, so the example uses await. Verification does not recover the original password. Creating another fresh hash and comparing the two stored values would fail because each creation uses a new salt.
Use these two library operations in the exercise below. Its editor supplies the import, parameters and function signatures, so you can concentrate on creating and verifying a hash.
Create and verify password hashes
0 / 10 points
Files in the editor
Edit src/passwords.ts. The scrypt-kdf import, parameter constant, and two
function signatures are supplied.
Task
Complete these two asynchronous functions using the library introduced above:
createPasswordHash(password)callsScrypt.kdfwith the password and the suppliedpasswordParameters, then returns the resulting hash. AUint8Arrayis a collection of bytes; return the library’s result without converting it. Each call creates a fresh salted hash, even for the same password.verifyPassword(password, storedHash)callsScrypt.verifyand returns its Boolean result:truefor the correct password andfalsefor an incorrect one. The library expects the stored hash first and the password second.
Use the password exactly as received, including any spaces. Assume storedHash
is a valid hash created by this library. The functions work with any supplied
password, not just the example below.
const storedHash = await createPasswordHash("hamburger");console.log(await verifyPassword("hamburger", storedHash)); // trueconsole.log(await verifyPassword("cheeseburger", storedHash)); // falseTwo 5-point checks cover creating fresh library hashes with the supplied parameters, and verifying correct and incorrect passwords.
Check and submit
Choose Submit solution below the editor to run the grading checks and read
the feedback. The editor submits src/passwords.ts.
From login to authenticated note requests
We can now return to the notes workflow. Suppose the server assigns account ID 7 at registration. Follow that account from login to the creation and reading of a note. This trace focuses on account identity; Chapter 5.6 adds the checks that protect state-changing requests.
| Step | Browser and request | Authentication boundary and application |
|---|---|---|
| Log in | Send email/password once to POST /api/auth/login, from the configured origin. | Verify the credential, create a server session, and return a Set-Cookie header for its token. |
| Discover the account | The browser stores the HttpOnly cookie and sends it with GET /api/session. Page JavaScript does not read the cookie. | Resolve that token to the current session and return account fields {user:{id:7,displayName:"Example user"}}. |
| Create a note | Send {title,body}; the browser also attaches its session cookie. | Resolve the session again and pass trusted actor ID 7 to the note operation. The new note stores owner ID 7. |
| Read the new note | Request the signed-in user’s notes. | Use the resolved actor to select that account’s notes. A later edit checks the current note’s owner. |
In Hono, session middleware can place a safe user in the request context as currentUser. The handler reads c.get("currentUser"), rejects a missing identity with 401, and passes the user’s ID to the note service. If the session has expired, the displayed account name cannot authorize the request.
Follow the signed-in request
0 / 5 points
A user logs in, reloads the page, then creates a note. Which path gives the note service a trusted author ID?
Rejection is different from unavailability
The successful trace assumes the server could check the credentials. Now consider two failures: the password does not match, or the database cannot be reached. As with rejected and uncertain mutations in Part 4, these results give us different information.
Missing accounts and incorrect passwords receive the same public rejection message to avoid revealing account existence. If the database is unavailable, the service cannot decide whether the credentials matched. Report an unavailable operation without exposing internal details.
This type describes the credential-verification interface:
type SafeUser = { id: number; displayName: string;};
type CredentialResult = | { kind: "accepted"; user: SafeUser } | { kind: "rejected" } | { kind: "unavailable" };The login handler creates a session only for accepted. It maps rejected to 401 with {"error":{"code":"INVALID_CREDENTIALS","message":"Invalid email or password"}}, and a temporarily unavailable lookup to 503 with {"error":{"code":"UNAVAILABLE","message":"Sign-in is temporarily unavailable"}}. Returning 401 for every failure would erase this distinction.
A failed account lookup
0 / 5 points
The account repository throws because the database is unavailable. Why should the login service not label this Invalid email or password?
We have followed how the server establishes an account identity. The next chapter follows what happens after login: how the browser carries the cookie, how a reload rediscovers the account, and what changes when the session expires or the user logs out.
Check Your Understanding
- Why does a caller-supplied user ID fail to establish identity?
- Which user fields may a resource handler receive from the trusted session boundary?
- Why does successful login not authorize editing another person’s note?
- Why are rejected credentials and an unavailable account lookup different results?
- Why can two stored hashes differ even when they were created from the same password?