Start learning free

LESSON 94 OF 173 · BACKEND: USERS AND SECURITY

Change the Password

Practice

Users must be able to change their password, for instance when they think someone saw it. The route is PUT /me/password: PUT because it replaces a value the user already has, and /me because it is about the logged-in user. It sits on loggedIn, so LoginRequired runs first and keeps the user in context.Items["user"].

Changing a value of "me": the same pattern works for any value of the logged-in user. Here is a route that changes the user's email:

loggedIn.MapPut("/me/email", (JsonElement data, HttpContext context) => {
    var user = (Dictionary<string, object>)context.Items["user"];
    var email = data.GetProperty("email").GetString();
    var cmd = new SqliteCommand("UPDATE users SET email = $email WHERE id = $id", db);
    cmd.Parameters.AddWithValue("$email", email);
    cmd.Parameters.AddWithValue("$id", user["id"]);
    cmd.ExecuteNonQuery();
    return Results.Json(new { email = email });
});

A password needs three more safety rules. Ask for the old password and check it against the stored hash with CheckPasswordHash: a stolen token, or a laptop left logged in, must not be enough to take the account, so a wrong old password gets 403. Store only the hash of the new password, made with GeneratePasswordHash. Then delete all the user's tokens and clear the session: every device, this one too, must log in again with the new password.

Ana is logged in twice: on her phone with the token <phone> and on her laptop with <token>. Your code must give these answers, in this order:

PUT /me/password  {"old_password": "guess123", "new_password": "newsecret456"}   (Bearer <token>)
    →  403  {"error": "Wrong password"}
PUT /me/password  {"old_password": "secret123", "new_password": "newsecret456"}  (Bearer <token>)
    →  200  {"message": "Password changed, please log in again"}
GET /me  (Bearer <phone>)  →  401  {"error": "Login required"}
GET /me  (Bearer <token>)  →  401  {"error": "Login required"}
POST /login  {"email": "ana@example.com", "password": "secret123"}     →  401  {"error": "Wrong email or password"}
POST /login  {"email": "ana@example.com", "password": "newsecret456"}  →  200  {"token": "<new>"}
GET /me  (the new login's session)  →  200  {"email": "ana@example.com", "id": 2, "role": "customer"}

Your task: below /logout, add PUT /me/password on loggedIn. Answer {"error": "Wrong password"} with 403 when the old password doesn't match. Otherwise store the new password's hash, delete all the user's tokens, clear the session and answer {"message": "Password changed, please log in again"}.


Assignment
Follow the Coding Tutorial to build the PUT /me/password route, one safety rule at a time.


Hint
user has no password hash in it: fetch it with SELECT password_hash FROM users WHERE id = $id. One command can run several SQL texts: change CommandText, and $id keeps its value. Delete the tokens with WHERE user_id = $id, not by token, so every device is logged out. Once the if returns the 403, C# won't compile the route until its last line returns an answer too.


STEP 1 OF 12
    Ctrl + Enter to check