Skip to content

Module 5: Security and correctness

Add user authentication and task ownership to the service from Module 4. The objective is not to assemble a security feature checklist; it is to define trust boundaries, deny unauthorized behavior, preserve data invariants, and prove those properties with tests.

Use current primary guidance

Authentication guidance changes as attacks and implementations evolve. Check the current NIST Digital Identity Guidelines and OWASP Cheat Sheet Series before applying this module to a real system.

Learning outcomes

By the end of this module, you should be able to:

  • distinguish authentication from authorization;
  • identify assets, actors, entry points, and trust boundaries;
  • store passwords using a maintained password-hashing implementation;
  • issue, validate, expire, and revoke opaque sessions;
  • enforce resource ownership on every data operation;
  • keep secrets and credentials out of code, logs, and error responses;
  • design negative tests that demonstrate denied access and safe failure;
  • record security-relevant events without recording sensitive values.

Threat model the change first

Draw the system and mark where untrusted data crosses a boundary:

untrusted client
    → HTTP parsing and validation
    → authentication
    → authorization
    → task operation
    → parameterized SQL
    → database

operator → configuration and logs

Write a small threat table before implementation:

Asset Example threat Required property Evidence
Passwords Database is copied Offline guessing is expensive Password-hash inspection and tests
Session tokens Token leaks through logs Raw token is never stored or logged Log-capture test
Tasks User guesses another task ID Ownership checked on every operation Cross-user API tests
Database Invalid state bypasses API Constraints and transactions still hold Direct repository tests
Audit trail Attacker causes misleading log data Structured, sanitized event fields Injection and failure tests

Scope the actors to an unauthenticated client, a normal authenticated user, and an operator who can read configuration or logs. Do not invent administrator roles until the product requires them.

Extend the data model

Add these concepts in a numbered migration:

users(id, username, password_hash)
sessions(token_hash, user_id, created_at, expires_at, revoked_at)
tasks(..., owner_user_id)
task_creations(owner_user_id, request_key, ...)
audit_events(id, occurred_at, actor_user_id, event_type,
             target_type, target_id, outcome, request_id)

Enforce with database constraints:

  • username is unique and normalized by one documented rule;
  • every task has an owner referencing a user;
  • every session references a user and has a unique token hash;
  • an idempotency key is unique per owner, not globally;
  • timestamps use one unambiguous representation, such as UTC epoch seconds;
  • enumerated fields reject values outside the documented set.

SQLite often requires rebuilding a table to add or change constraints. Migrate existing tasks to one explicitly chosen owner inside a transaction, verify row counts and ownership, then replace the old table. Test rollback from a deliberately invalid row.

Register users safely

For this single-factor lab, accept passphrases from 15 through 128 characters, allow spaces and Unicode, do not require character-class mixtures, and do not force periodic changes. Reject commonly used or compromised values using a maintained blocklist appropriate to the real deployment. These choices follow current NIST password-verifier requirements; re-check them when implementing.

Do not trim, lowercase, log, or silently truncate a password. Normalize usernames independently from passwords.

Use a maintained library that implements Argon2id and stores its algorithm, salt, and work parameters in the encoded hash. Follow the library and current OWASP password-storage guidance rather than implementing the primitive yourself. Use the library's recommended parameters, benchmark them on deployment hardware, and rehash after a successful login when stored parameters are outdated.

A password database row should contain an encoded password hash, never plaintext, reversible encryption, or a fast general-purpose digest such as SHA-256.

Authenticate without exposing account state

Add only three auth operations:

Endpoint Behavior
POST /users Register username and password
POST /sessions Verify credentials and issue a session
DELETE /sessions/current Revoke the current session

Login returns the same external status and message for an unknown username and a wrong password. For an unknown user, verify against a fixed dummy password hash so the two paths do comparable expensive work. Avoid claiming that timing is identical; measure and prevent an obvious user-enumeration difference.

Throttle repeated failures by both account identifier and network source. Define bounded behavior, expiry, and recovery so an attacker cannot turn throttling into permanent account denial. Do not log attempted passwords.

Passwords are not phishing-resistant. For a real system with meaningful account risk, prefer a mature identity provider or add a phishing-resistant authenticator such as WebAuthn rather than treating password complexity as MFA. See the current NIST authentication guidance.

Use opaque server-side sessions

On successful login:

  1. generate 32 random bytes with Python's secrets module;
  2. return a URL-safe representation once as a bearer token;
  3. store only its SHA-256 digest in the sessions table;
  4. record creation and absolute expiry times;
  5. commit before returning success.

SHA-256 is appropriate here because the input is a high-entropy random token, not a human-chosen password. A copied session table should not contain immediately usable bearer credentials.

Authentication middleware should:

  1. require a syntactically valid Authorization: Bearer header;
  2. hash the presented token;
  3. find an unrevoked, unexpired session;
  4. attach only the authenticated user identity to request context;
  5. otherwise return 401 Unauthorized with a generic response and an appropriate WWW-Authenticate header.

Make expiry and revocation testable by injecting a clock into session logic rather than sleeping in tests. Never place bearer tokens in URLs, logs, error messages, or analytics fields.

Use TLS for every non-loopback deployment; otherwise credentials and bearer tokens can be observed in transit. This lab's explicit authorization header is not automatically attached by a browser, so cookie-specific CSRF defenses are outside its scope. A browser client that exposes the token to JavaScript must account for token theft through cross-site scripting. If you later use cookies, revisit secure cookie attributes and CSRF controls using the current OWASP session guidance.

Authorize by ownership

The policy is deliberately small:

An authenticated user may create tasks for themself and may read, update, or delete only tasks they own.

Enforce it in every repository operation, not only in a route that calls the repository:

SELECT id, title, done
FROM tasks
WHERE id = ? AND owner_user_id = ?;

Use the same owner predicate for listing, updates, and deletion. Derive owner_user_id from the authenticated session; never accept it from a request body.

Return 404 Not Found for both a missing task and another user's task so the endpoint does not reveal which guessed IDs exist. Affected-row counts must determine whether updates and deletes found an authorized record.

Deny by default and check every request, consistent with the current OWASP authorization guidance. CORS, hidden buttons, unguessable IDs, and client-side checks are not authorization controls.

Validate at each boundary

Keep validation responsibilities distinct:

Boundary Responsibility
HTTP parser Body size, syntax, content type, header shape
API contract Allowed fields, types, ranges, and unknown fields
Domain Title and state-transition rules
Authentication Credential and session validity
Authorization Actor may perform action on this resource
Database Final ownership, uniqueness, reference, and value constraints

Reject unexpected fields instead of binding an entire JSON object onto a model. Continue using parameterized SQL. Validation improves error quality, but it is not a substitute for authorization or database constraints.

Set request-body and header-size limits through the selected server. A valid but excessively large input is still a resource-exhaustion risk.

Handle secrets and configuration

This application should not require a hard-coded application secret for opaque sessions. It still handles sensitive runtime values: passwords in transit, issued tokens, database backups, and any deployment credentials added later.

  • keep local development configuration outside version control;
  • inject deployment secrets through the platform's secret mechanism;
  • fail startup when required configuration is absent or malformed;
  • grant the process only the permissions it needs;
  • document generation, rotation, revocation, and incident handling;
  • scan commits and build output for accidentally included credentials.

Environment variables can be convenient transport, but they are not a complete secrets-management system. Consult current OWASP secrets-management guidance when the service gains deployment credentials.

Create an audit trail

Record structured events for:

  • registration success and failure;
  • login success, failure, throttling, and logout;
  • authorization denial;
  • task creation, update, and deletion;
  • security-relevant configuration or migration failure.

Include event time, event type, outcome, request ID, actor ID when known, and target identifier when appropriate. Do not record passwords, raw session tokens, authorization headers, full request bodies, or unnecessary personal data.

Sanitize line breaks and delimiters at the logging boundary, or emit structured JSON through a logger that encodes values correctly. Decide whether a required audit-write failure rejects the associated mutation; if so, place both in the same transaction. Logging must not be able to crash the service from attacker-controlled text. Use the OWASP logging guidance as the living reference.

Prove the negative paths

Use two users and two tasks. Automate at least this matrix:

Scenario Expected result
No token accesses protected endpoint 401
Malformed, expired, or revoked token 401
User reads or mutates own task Success
User reads, updates, or deletes other's task 404, no state change
User lists tasks Only owned tasks returned
Body attempts to set owner_user_id Rejected
Same idempotency key used by different users Independent requests
Same user's key reused with changed input 409
SQL metacharacters appear in username or title Stored as data or rejected by contract, never executed
Audit field contains a newline One valid structured event
Sensitive value appears during failure Absent from response and captured logs

Also verify that:

  • no database row contains a plaintext password or raw session token;
  • a failed ownership migration rolls back completely;
  • database constraints reject a task without a valid owner;
  • revocation and expiry use controlled time in tests;
  • an audit event identifies each denied cross-user attempt without leaking sensitive data.

Evidence to keep

Your repository should now contain:

  • a one-page threat model and trust-boundary diagram;
  • numbered ownership and session migrations with rollback tests;
  • password-policy and session-lifecycle decisions linked to primary guidance;
  • automated authentication and cross-user authorization tests;
  • captured logs proving sensitive-field redaction;
  • a short list of deferred controls and the condition that would require them.

Reasonable deferrals for this local project include account recovery, email verification, MFA or passkeys, distributed throttling, and a managed secrets service. They become required when the application is exposed to real users or its threat model changes.

Completion check

You are ready for the next module when you can:

  • explain why password hashing and session-token hashing use different primitives;
  • demonstrate expiry and revocation without waiting for real time;
  • show that every task query is owner-scoped;
  • prove that guessed IDs cannot expose or mutate another user's task;
  • trace a failed request through authentication, authorization, transaction, response, and audit event;
  • identify every place credentials could leak and the control at that boundary;
  • name which production controls remain deferred and why.

Read only when needed

Continue with Module 6: Design and delivery after the completion check passes.