1 The Problem
We want to secure an API: users register with a password, log in to get a token, and must send that token to access protected routes. We will hash passwords properly and issue tokens. It teaches authentication — how real APIs verify who you are without storing passwords in plain text.
2 How to Think About It
Think about proving identity safely, before any code:
3 The Build — explained part by part
Here is the complete auth core. Each part is explained below.
import hashlib
import secrets
class Auth:
def __init__(self):
self.users = {} # username -> password_hash
self.tokens = {} # token -> username
def _hash(self, password):
# Hash the password so we never store it in plain text.
return hashlib.sha256(password.encode()).hexdigest()
def register(self, username, password):
if username in self.users:
raise ValueError("Username taken")
self.users[username] = self._hash(password)
def login(self, username, password):
stored = self.users.get(username)
if stored is None or stored != self._hash(password):
raise ValueError("Invalid credentials")
token = secrets.token_hex(16) # random, unguessable
self.tokens[token] = username
return token
def whoami(self, token):
# Return the user for a valid token, or None.
return self.tokens.get(token)
if __name__ == "__main__":
auth = Auth()
auth.register("alice", "hunter2")
token = auth.login("alice", "hunter2")
print("Token:", token[:12] + "...")
print("Authenticated as:", auth.whoami(token))
register — store the username with its password hash. Refuse duplicate usernames.
login — hash the supplied password and compare to the stored hash. If they match, the password was correct — without us ever un-hashing anything (you cannot reverse a hash).
secrets.token_hex(16) — issue a random, unguessable token on successful login. The client sends this token on future requests instead of the password.
whoami(token) — look up which user a token belongs to. Protected routes call this; a missing or unknown token means “not logged in”.
note: real systems add salting and use bcrypt/argon2 (slower hashes that resist cracking). This shows the shape; production needs those stronger tools.
secrets.token_hex for random, unguessable tokens.4 Test & Prove Each Part
We test the auth flow: registration, correct and incorrect login, and token validation.
import hashlib
import secrets
import pytest
class Auth:
def __init__(self):
self.users = {}
self.tokens = {}
def _hash(self, password):
return hashlib.sha256(password.encode()).hexdigest()
def register(self, username, password):
if username in self.users:
raise ValueError("Username taken")
self.users[username] = self._hash(password)
def login(self, username, password):
stored = self.users.get(username)
if stored is None or stored != self._hash(password):
raise ValueError("Invalid credentials")
token = secrets.token_hex(16)
self.tokens[token] = username
return token
def whoami(self, token):
return self.tokens.get(token)
def test_register_and_login():
"""Registering then logging in returns a token."""
auth = Auth()
auth.register("alice", "pw")
token = auth.login("alice", "pw")
assert auth.whoami(token) == "alice"
def test_wrong_password():
"""A wrong password is rejected."""
auth = Auth()
auth.register("alice", "pw")
with pytest.raises(ValueError):
auth.login("alice", "wrong")
def test_invalid_token():
"""An unknown token identifies no one."""
auth = Auth()
assert auth.whoami("fake-token") is None
def test_password_not_stored_plain():
"""The stored value is a hash, not the password."""
auth = Auth()
auth.register("alice", "secret")
assert auth.users["alice"] != "secret"
Run with pytest -v. The last test is the most important security check: it proves the password is not stored in plain text. Testing that sensitive data is protected is as vital as testing that login works.
5 API Documentation
Response
{"token": "a1b2c3d4..."}6 Run It & Automate It
python3 auth.pyRegister, log in, and see a token issued. Combine with the REST API project to protect its routes.
Jenkins runs the tests automatically — each line explained below.
Token: a1b2c3d4e5f6...
Authenticated as: alice// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Salting. Add a random salt per user before hashing. (Teaches: defeating rainbow tables.)
- Token expiry. Make tokens expire after a time. (Teaches: session lifetimes.)
- JWT. Switch to stateless JSON Web Tokens. (Teaches: modern auth.)