In this lab, you'll learn how JSON Web Tokens (JWT) provide a modern, stateless alternative to traditional session-based authentication. You'll build a simple API service that issues and verifies JWTs for user authentication, and you'll see how this approach differs from classic session IDs.
- Older web apps used session IDs stored in cookies.
- When a user logged in, the server generated a random session ID, stored it in memory, Redis, or a database, and sent it to the client as a cookie.
- On each request, the client sent the session ID cookie; the server checked its session store to see if the session was valid and which user it belonged to.
- Drawback: The server must keep state for every active session, which doesn't scale well and requires extra infrastructure.
- JWTs embed authentication claims (like username, expiration) directly in a signed token.
- When a user logs in, the server creates a JWT, signs it with a secret, and sends it to the client (usually as an HttpOnly cookie).
- On each request, the client sends the JWT; the server verifies the signature and expiration, and grants access if valid.
- No session IDs or server-side session storage required!
- JWT does NOT eliminate authentication logic—it just eliminates the need to store session state on the server.
- Use a small in-memory user list (usernames and passwords; plaintext for this lab only).
- When a user logs in successfully:
- Create a JWT with claims: username, expiration time (e.g., 5 minutes from now).
- Sign the JWT with a shared server secret.
- Return the JWT to the client in an HttpOnly cookie.
- For protected endpoints:
- Read the JWT from the cookie.
- Verify the signature and expiration.
- If valid, grant access; if not, reject the request.
- No session IDs or token records are stored on the server.
# ---
# Example curl commands for login and using JWT cookie:
# curl is not a browser, so cookies must be explicitly saved and reused using -c and -b.
#
# Login and save JWT cookie:
# curl -i -X POST http://localhost:5000/login \
# -H "Content-Type: application/json" \
# -d '{"username":"alice","password":"password1"}' \
# -c cookies.txt
#
# Access a protected endpoint using the saved cookie:
# curl -i http://localhost:5000/protected \
# -b cookies.txt
# ---
import jwt, datetime
from flask import Flask, request, jsonify, make_response
app = Flask(__name__)
SECRET = 'dev-secret-change-me'
users = {'alice': 'password1', 'bob': 'password2'}
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
if users.get(username) == password:
payload = {
'username': username,
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=5)
}
token = jwt.encode(payload, SECRET, algorithm='HS256')
resp = make_response({'message': 'Logged in'})
resp.set_cookie('jwt', token, httponly=True, samesite='Lax')
return resp
return jsonify({'error': 'Invalid credentials'}), 401
@app.route('/protected')
def protected():
token = request.cookies.get('jwt')
if not token:
return jsonify({'error': 'Missing token'}), 401
try:
payload = jwt.decode(token, SECRET, algorithms=['HS256'])
return jsonify({'message': f'Hello, {payload["username"]}!'}), 200
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 401
@app.route('/logout')
def logout():
resp = make_response({'message': 'Logged out'})
resp.set_cookie('jwt', '', expires=0)
return resp
if __name__ == '__main__':
app.run(debug=True)- JWTs are not encrypted by default—just base64-encoded and signed. Anyone can read the contents, but only the server with the secret can create/validate them.
- Security comes from the signature, not secrecy of the contents.
- JWTs are stateless: tokens cannot be instantly revoked. Use short expirations and delete the cookie on logout.
- Since cookies are used, CSRF protections (like SameSite cookies) are important.
- Tradeoffs:
- Stateless: no server-side session storage, but no instant revocation.
- Short-lived tokens are safer.
- Logout = delete the cookie.
- Out of scope for this lab:
- OAuth, refresh tokens, public/private key JWTs, multi-service trust, advanced claims.
For this challenge, modify your previous API key-based Flask app to support both JWT and API keys, just like real services such as Dockerhub or GitHub:
-
JWT Login and Management:
- Users log in with username and password.
- On successful login, issue a JWT (as above) with claims like username, permissions, and expiration.
- Use a function for handling JWT instead of doing it each requests
- JWT is sent to the client in an HttpOnly cookie.
- Authenticated users (with a valid JWT) can:
- Generate new API keys (long, random, unique).
- List, update (label/permissions), or revoke (delete) their API keys.
- Change their username or password.
- API keys should be stored in-memory (dictionary) for this lab, with fields: value, username, permissions, creation date, label/description.
-
API Key Usage for CRUD:
- For app CRUD operations, allow authentication via API key (sent in a header, e.g.,
X-API-Key). - When an API key is used, check its validity and permissions.
- No JWT is required for API key-authenticated endpoints.
- For app CRUD operations, allow authentication via API key (sent in a header, e.g.,
-
JWT vs API Key:
- JWT is for user login and managing API keys (interactive, short-lived).
- API keys are for automation/scripts (long-lived, can be revoked/rotated).
- This is how real services like Dockerhub work: you log in with a password, then create/manage API keys for automation.
| Feature | JWT (Cookie-based) | API Keys |
|---|---|---|
| Typical Use | Interactive login | Automation/scripts |
| Lifetime | Short-lived | Long-lived |
| Revocation | Not easily revoked | Revocable/rotatable |
| Usage Context | Browser-friendly | CLI, CI/CD, integrations |
| Permissions | Usually user-wide | Permission-scoped |
JWTs are for interactive, browser-based sessions, while API keys are for automation, can be scoped, and are easier to manage for long-term integrations.
JWTs are not ideal for automation because they expire quickly, are harder to rotate safely, and are tied to user identity rather than application permissions.
When you set a cookie with the HttpOnly flag (as in the JWT example), it tells the browser not to make this cookie accessible to JavaScript running on the page. This is a key security feature:
- Protection from XSS: If an attacker manages to inject JavaScript into your site (via a cross-site scripting vulnerability), they cannot read or steal cookies marked as
HttpOnly. This helps protect sensitive tokens like JWTs from being exfiltrated. - Safer than localStorage/sessionStorage: Data stored in
localStorageorsessionStorageis always accessible to JavaScript, so XSS can easily leak tokens stored there.HttpOnlycookies are not exposed to JavaScript at all. - Automatic sending: Browsers automatically send cookies (including
HttpOnlyones) with every request to the server for the relevant domain/path, so the client code doesn't need to manually attach the token. - Not a silver bullet:
HttpOnlydoes not protect against all attacks (e.g., CSRF), but it is a strong defense against token theft via XSS.
Summary:
HttpOnlycookies are more secure for storing authentication tokens than regular cookies or browser storage, because browsers do not give JavaScript access to them.
Security Reminders:
- API keys should be long, random, and never stored in plaintext in production (hashing is best, but plaintext is OK for this lab).
- JWTs are short-lived and for interactive use; API keys are for automation and can be revoked/rotated.
- Use HttpOnly and SameSite cookies for JWTs.
- Never expose secrets in URLs or logs.
- What happens if you delete a user but their JWT is still valid?
- Logout deletes the cookie but does not invalidate already-issued JWTs server-side.
This challenge will help you understand how modern platforms combine JWT and API keys for secure, flexible authentication!