🔐 API Authentication Teaching Application

A beginner-friendly guide to the Flask backend test.py and animated frontend frontend.html.

1. What is this application?

This is a classroom application designed to demonstrate three common API authentication methods:

Basic Authentication
Username and password are sent in an Authorization header.
API Key Authentication
A fixed key is sent in a custom header.
JWT Authentication
The user logs in once and receives a signed token.
Authentication means: “Who are you?” The server checks the supplied information before allowing access.

2. Application architecture

Browser / Postman
HTTP Request
Flask Server
Credential Check

The server returns either a success response such as 200 OK or an error such as 401 Unauthorized.

3. Project structure and setup

authentication-demo/
├── test.py
├── frontend.html
└── README.html

Install dependencies:

pip install flask pyjwt flask-cors gunicorn

Run:

python test.py

Open:

http://127.0.0.1:5000/playground
http://127.0.0.1:5000/docs

4. Configuration in test.py

SECRET_KEY = os.environ.get("SECRET_KEY", "CHANGE_ME_TO_SOMETHING_LONG_RANDOM")
API_KEY = os.environ.get("API_KEY", "student123")

SECRET_KEY signs JWT tokens. API_KEY is the value checked by the API-key endpoint.

USERS = {
    "alice": "password123",
    "bob": "secret456",
}

This is only a fake in-memory user database for teaching.

Production warning: Do not store real passwords in plain text or hard-code secrets.

5. Basic Authentication

Basic Auth sends a username and password in this form:

Authorization: Basic <base64(username:password)>

Step 1: Create route

@app.route("/basic", methods=["GET"])
def basic_auth():

Step 2: Read header

auth_header = request.headers.get("Authorization")

Step 3: Check Basic prefix

if not auth_header or not auth_header.startswith("Basic "):
    return jsonify({"error": "Missing Basic auth header"}), 401

Step 4: Decode Base64

encoded = auth_header.split(" ")[1]
decoded = base64.b64decode(encoded).decode("utf-8")

Example result: alice:password123.

Step 5: Split username and password

username, password = decoded.split(":", 1)

Step 6: Verify

if USERS.get(username) != password:
    return jsonify({"error": "Invalid username or password"}), 401

Try it

curl -u alice:password123 http://127.0.0.1:5000/basic
curl -u alice:WRONG http://127.0.0.1:5000/basic
Remember: Base64 is encoding, not encryption. Always use HTTPS.

6. API Key Authentication

The client sends a custom header:

X-API-Key: student123

Backend breakdown

key = request.headers.get("X-API-Key")
if key != API_KEY:
    return jsonify({"error": "Invalid or missing API key"}), 401
return jsonify({
    "message": "Access granted using API Key!",
    "method": "API Key Auth"
})

Try it

curl -H "X-API-Key: student123" http://127.0.0.1:5000/apikey
curl -H "X-API-Key: wrong" http://127.0.0.1:5000/apikey

7. JWT Authentication

JWT uses two phases:

POST /login
Receive token
GET /jwt with Bearer token

Phase 1: Login

data = request.get_json(silent=True)
username = data.get("username")
password = data.get("password")
if USERS.get(username) != password:
    return jsonify({"error": "Invalid credentials"}), 401

Create expiry time

payload = {
    "user": username,
    "exp": datetime.datetime.now(datetime.timezone.utc)
           + datetime.timedelta(minutes=30)
}

Sign token

token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")

Return token

return jsonify({"token": token, "expires_in": "30 minutes"})

Phase 2: Protect endpoint

auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
    return jsonify({"error": "Missing Bearer token"}), 401
token = auth_header.split(" ")[1]
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return jsonify({
    "message": f"Hello {payload['user']}!",
    "method": "JWT Auth"
})

Try it

curl -X POST http://127.0.0.1:5000/login -H "Content-Type: application/json" -d '{"username":"alice","password":"password123"}'

curl -H "Authorization: Bearer <TOKEN>" http://127.0.0.1:5000/jwt

8. Understanding JWT parts

header.payload.signature
PartMeaning
HeaderToken type and algorithm.
PayloadClaims such as user and expiry.
SignatureProves the token was not changed.
JWT payloads can be read. A JWT is signed, not automatically encrypted. Never place passwords in the payload.

9. The reusable decorator

@require_jwt
def jwt_protected_v2():
    return jsonify({
        "message": f"Hello {request.user}!"
    })

The decorator performs token checking before the actual function runs. This avoids repeating the same validation code in every protected endpoint.

10. How frontend.html works

Important JavaScript snippets

const $ = id => document.getElementById(id);

This is a shortcut for finding an HTML element.

const sleep = ms => new Promise(r => setTimeout(r, ms));

This pauses an animation.

fetch("/apikey", {
  headers: { "X-API-Key": key }
});

This sends the actual HTTP request.

11. HTTP status codes

CodeMeaning
200Request succeeded.
400Client sent invalid or incomplete data.
401Credentials/token are missing or invalid.
404URL does not exist.
405Wrong HTTP method.
500Unexpected server error.

12. Suggested classroom experiments

  1. Run Basic Auth correctly.
  2. Use a wrong password.
  3. Remove the Authorization header.
  4. Use the correct API key.
  5. Change one character in the API key.
  6. Login and copy the JWT.
  7. Call /jwt without a token.
  8. Tamper with the JWT.
  9. Use /debug/jwt to inspect the token.
  10. Send GET to /login and observe 405.

13. Production limitations

14. One-page memory summary

Basic Auth
Send username and password every time.
API Key
Send a fixed application key.
JWT
Login once, then send a signed token.
Client
Credentials / Token
Server Verification
Response