Goal: Set up a Flask application and create basic routes. This program will ensure Flask is installed, start a minimal app, and demonstrate simple routing (including a dynamic route).
# app1.py - Basic Flask setup and routing
from flask import Flask
app = Flask(__name__) # Initialize Flask application
# Define a simple route for the home page
@app.route('/')
def home():
return "Welcome to the Flask API Lecture Series!"
# Define another route that accepts a dynamic segment <name>
@app.route('/hello/<name>')
def greet(name):
# Return a greeting for the provided name
return f"Hello, {name}!"
# Run the app in debug mode (for development convenience)
if __name__ == '__main__':
app.run(debug=True)- We first import
Flaskfrom theflaskpackage and create anappinstance usingapp = Flask(__name__). The__name__argument tells Flask to use the current module as the application – this is needed to locate resources and templates relative to the app. - Using the
@app.route()decorator, we bind URL paths to view functions. For example,@app.route('/')associates the URL / (root) with thehome()function. When a client requests the root URL, Flask will invokehome()and send its return value back as the response. - The
home()function returns a simple welcome string. Flask will convert this string into an HTTP response sent to the client. - The second route
'/hello/<name>'demonstrates a dynamic route. The<name>part in the URL path means this route will match any URL of the form/hello/something, and thesomethingwill be passed into thegreet(name)function as the variablename. For example, requesting/hello/Johnwill callgreet("John")and return"Hello, John!". Flask automatically URL-encodes/decodes the<name>segment, so it can handle spaces or special characters in the name if properly encoded. - The
if __name__ == '__main__':block ensures the Flask development server runs only when we execute this script directly.app.run(debug=True)starts the server on the default port 5000 in debug mode, which auto-reloads on code changes and provides an interactive debugger for errors (useful during development).
-
Run the application: Make sure you have Flask installed (
pip install Flask). Save the code asapp1.pyand run it withpython app1.py. You should see Flask’s startup message indicating the server is running onhttp://127.0.0.1:5000/(orlocalhost:5000). -
Test the home route: In Postman (or your web browser), send a GET request to
http://localhost:5000/. You should receive a 200 OK response with the body "Welcome to the Flask API Lecture Series!".- In Postman, you can create a new GET request, enter the URL, and hit Send. The response body will appear in the output section.
-
Test the dynamic route: Send a GET request to
http://localhost:5000/hello/Flask. You should get "Hello, Flask!" in the response. Try changing the name in the URL (for example,/hello/Alice) to see the response update accordingly. This confirms our dynamic routing works for any name provided. -
Edge case: If you omit the name (e.g., just
/hello/), Flask will return a 404 Not Found error because the route expects a<name>segment. You can see the 404 status in Postman’s response. (Flask’s routing requires the dynamic part since we didn’t provide a default.)
This basic setup verifies that Flask is installed and routing is functioning. In the next program, we will extend this app to handle different HTTP methods and JSON data.
Goal: Introduce handling of GET vs POST methods in Flask routes, and how to accept and return JSON data. We will create two routes: one that processes URL query parameters (for a GET request) and one that accepts a JSON body in a POST request.
# app2.py - Handling GET query parameters and POST JSON payloads
from flask import Flask, request, jsonify
app = Flask(__name__)
# GET route that reads query parameters
@app.route('/square', methods=['GET'])
def square_number():
# Get the 'number' query parameter from the URL
num_str = request.args.get('number') # Returns None if 'number' not provided
if num_str is None:
# If no number is provided, return a 400 Bad Request
return jsonify({"error": "Missing 'number' query parameter"}), 400
try:
num = int(num_str)
except ValueError:
# If the value is not an integer, return 400 with an error message
return jsonify({"error": "Invalid 'number' parameter, must be an integer"}), 400
result = num * num
# Return the result as JSON
return jsonify({"number": num, "square": result}) # Flask will send 200 OK by default
# POST route that accepts a JSON payload and echoes it
@app.route('/echo', methods=['POST'])
def echo():
# Parse JSON from request body
data = request.get_json()
if data is None:
# If request body is not valid JSON or empty, return 400
return jsonify({"error": "Request body must be JSON"}), 400
# For demonstration, just return the received JSON data with a message
response = {
"received": data,
"message": "JSON received successfully"
}
return jsonify(response), 200
if __name__ == '__main__':
app.run(debug=True)-
We import
requestandjsonifyfrom Flask. Therequestobject gives us access to the incoming HTTP request (including query parameters, headers, body data, etc.), andjsonifyis a helper to create JSON responses easily. -
We define a GET route
/squarethat expects a query parameternumber. We explicitly setmethods=['GET']to ensure this route only handles GET requests (Flask defaults to GET if not specified).- Inside
square_number(), we userequest.args.get('number')to retrieve the query parameter named "number" from the URL.request.argsis a dictionary-like object (ImmutableMultiDict) containing query string keys and values. Using the.get()method is safer than direct indexing because it returnsNoneif the key is missing instead of raising an error. - If
numberis missing, we return a JSON error message and a 400 Bad Request status. In Flask, you can return a tuple(<response_body>, <status_code>)directly from a view function. We usejsonify({"error": ...})to create a JSON response for the error, and provide400as the status code. - If
numberis provided, we attempt to convert it to an integer. If conversion fails (e.g.,?number=abc), we return another 400 error with a message indicating the parameter must be an integer. - If all is well, we compute the square of the number and return a JSON response containing both the original number and its square. For example, if
number=5, our response JSON will be{"number": 5, "square": 25}. We usejsonifywhich will set theContent-Type: application/jsonheader and by default return a 200 OK status (since we didn’t specify a different status).
- Inside
-
We also define a POST route
/echothat expects a JSON payload in the request body.- We call
request.get_json()to parse the incoming JSON data. Flask’sget_json()method will return a Python dictionary (or list) parsed from the JSON, orNoneif the content is not valid JSON or if theContent-Typeis notapplication/json. - If
data is None, we return a 400 error indicating the body must be JSON. (In Postman, forgetting to set the body to raw JSON or a typo in JSON can trigger this.) - If JSON is received, we prepare a response dictionary containing the received data and a confirmation message. This shows how to echo back data or further process it as needed.
- We return the response dictionary as JSON with a 200 OK status. (Using
jsonify(response), 200explicitly sets the status, but in this case it would be 200 even if we omitted the status since 200 is default for a successful return.)
- We call
-
Note: We did not specify
methodsfor/echo, but provided onlymethods=['POST']. Flask by default will respond with 405 Method Not Allowed if someone tries to GET/echosince we restricted it to POST.
Setup: Run app2.py with python app2.py. The server will run on port 5000 as before.
-
Testing the GET route (
/square):- In Postman, create a GET request to
http://localhost:5000/square. - First, send it without any query parameter. You should receive a 400 Bad Request response with JSON body:
{"error": "Missing 'number' query parameter"}. This confirms our validation for missing params works. - Now add a query parameter. In Postman, you can use the Params tab: enter key as
numberand value as, say,7. (This will append?number=7to the URL.) Send the request again. You should get a 200 OK response with JSON:{"number": 7, "square": 49}. - Test an invalid input: set
numbertoabcand send. The response should be 400 with{"error": "Invalid 'number' parameter, must be an integer"}. - This shows the route reading query parameters and returning JSON with appropriate status codes.
- In Postman, create a GET request to
-
Testing the POST route (
/echo):-
In Postman, create a POST request to
http://localhost:5000/echo. -
Under the Body tab, select raw and choose JSON (application/json) from the dropdown. This ensures the
Content-Typeheader is set toapplication/json. -
Now provide a JSON body. For example:
{ "course": "Flask API", "lesson": 2, "items": ["GET", "POST", "JSON"] } -
Send the request. The response should be 200 OK with a JSON body that contains the same data under
"received"and a message, for example:{ "message": "JSON received successfully", "received": { "course": "Flask API", "lesson": 2, "items": ["GET", "POST", "JSON"] } }This confirms the server correctly parsed and returned the JSON.
-
Test sending an improper request: for example, change the body to plain text or remove the JSON header. If the body isn’t valid JSON, you should get the
{"error": "Request body must be JSON"}message with 400 status. In Postman, also try not selecting the JSON type for raw body to see the error handling in action.
-
By completing Program 2, you’ve learned how to handle query parameters in GET requests and process JSON in POST requests, responding with JSON and proper HTTP status codes.
Goal: Dive a bit deeper into query parameters and returning custom HTTP status codes. In this program, we’ll reinforce the concept of reading query strings and explicitly setting various response statuses including success (200), bad request (400), and not found (404).
# app3.py - Query parameters and custom status codes
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
# A GET route that uses multiple query parameters to filter a result
@app.route('/divide', methods=['GET'])
def divide():
# Expecting two query params: a and b
a = request.args.get('a')
b = request.args.get('b')
if a is None or b is None:
# Missing one of the parameters -> 400 Bad Request
return jsonify({"error": "Please provide both 'a' and 'b' parameters"}), 400
try:
a = float(a)
b = float(b)
except ValueError:
# Non-numeric query values -> 400
return jsonify({"error": "Parameters 'a' and 'b' must be numbers"}), 400
if b == 0:
# Division by zero is not allowed -> 400
return jsonify({"error": "Division by zero is not allowed"}), 400
result = a / b
return jsonify({"a": a, "b": b, "result": result}), 200
# A route to demonstrate 404 Not Found for an invalid path parameter
@app.route('/item/<int:item_id>', methods=['GET'])
def get_item(item_id):
# Suppose we only have items 1-3 for demo
if item_id not in [1, 2, 3]:
# Use Flask's abort() to send a 404 error if item not found
abort(404)
# If valid, return a dummy item
item = {"id": item_id, "name": f"Item {item_id}"}
return jsonify(item), 200
if __name__ == '__main__':
app.run(debug=True)-
We import
abortfrom Flask to help in sending HTTP errors easily. -
The
/divideroute demonstrates reading multiple query parameters:- We expect two numbers,
aandb, provided as query parameters. For example:/divide?a=10&b=2. We retrieve them withrequest.args.get('a')andrequest.args.get('b'). - If either is missing, we return a 400 with an error JSON, similar to Program 2.
- We attempt to convert both to floats (to allow decimal division). If conversion fails (inputs aren't numeric), return 400 with an error message.
- If
b == 0, we detect an attempt to divide by zero and return a 400 error as well (with an explanatory message). This is a business logic validation resulting in a bad request response. - Otherwise, we compute the division and return a JSON containing
a,b, and the result ofa/b. We explicitly include, 200in the return to emphasize the success status code.
- We expect two numbers,
-
The
/item/<int:item_id>route shows how to handle a case where a requested resource might not exist:- The route uses a dynamic path segment
<int:item_id>. By specifying<int:...>, Flask will automatically convert that part of the URL to an integer and pass it to the function (or return 404 if it cannot convert). Here,item_idwill be anintinside the function. - We simulate a scenario where only item IDs 1, 2, 3 exist. If the client requests an item outside this range, our code calls
abort(404).abort()is a Flask function that immediately stops the request and returns an HTTP error response with the given status code (404 Not Found in this case). Flask will return a default 404 HTML page if running in debug, but since we called it explicitly, it knows to send a 404 status. - If the item ID is valid (1–3), we return a dummy item dictionary (in real cases, you would fetch from a database or data structure). The response is JSON with a 200 status.
- The route uses a dynamic path segment
-
Why explicitly handle status codes? In a Flask API, you’ll often need to return specific HTTP status codes based on conditions:
- 200 OK for successful requests (Flask does this by default if you return normally).
- 400 Bad Request for client-side errors like missing or invalid parameters.
- 404 Not Found for resources that cannot be located (Flask will also do this automatically if no route matches or if a path converter fails, but using
abort(404)is useful for application-level missing resources). - You can also use
abort(401)for unauthorized,abort(403)for forbidden, etc., when we handle authentication (coming up in later programs).
Run app3.py and test the routes:
-
Testing
/divide:- Try a request without parameters: GET
http://localhost:5000/divide. Response should be 400 with JSON error about providing both parameters. - Try with non-numeric values: e.g.
?a=foo&b=bar. Still 400 with error "must be numbers". - Try with
b=0: e.g.?a=5&b=0. 400 with "division by zero not allowed". - Finally, try a valid one: e.g.
?a=9&b=3. You should get{"a": 9.0, "b": 3.0, "result": 3.0}with a 200 OK. Note that we converted to float, so even if you passed integers, the JSON shows9.0etc. - Postman tip: You can add these query params in the Params section or manually append
?a=...&b=...to the URL. - Each test verifies a different branch of our code (missing params, invalid type, invalid operation, success).
- Try a request without parameters: GET
-
Testing
/item/<item_id>:- Try
GET http://localhost:5000/item/5. Since 5 is not in [1,2,3], our code callsabort(404). Postman will show a 404 Not Found status. The body might be an HTML error page by default. (If you prefer JSON errors, you could catch this and return JSON, but by default Flask’s 404 is HTML.) - Try
GET http://localhost:5000/item/2. You should get 200 OK with JSON{"id": 2, "name": "Item 2"}. - If you try a non-integer like
GET /item/test, Flask itself will return 404 because it couldn’t convert "test" to an int for<int:item_id>. - This demonstrates path parameter conversion and using
abort()for returning errors.
- Try
At this point, you know how to validate inputs and return appropriate HTTP status codes in your Flask API. Next, we will move on to implementing authentication mechanisms to secure certain routes.
Goal: Implement HTTP Basic Authentication in a Flask route by examining the Authorization header. In Basic Auth, clients send a header with a username and password encoded in Base64. We will simulate a protected route that only allows access if the correct credentials are provided via this header.
# app4.py - Basic Authentication via Authorization header
import base64
from flask import Flask, request, jsonify, make_response
app = Flask(__name__)
# For demonstration, define a single valid username and password
VALID_USERNAME = "admin"
VALID_PASSWORD = "secret123"
@app.route('/protected-basic', methods=['GET'])
def protected_basic():
auth_header = request.headers.get('Authorization')
if not auth_header:
# No credentials provided, request authentication
# Send a 401 with WWW-Authenticate header as per Basic Auth spec
response = make_response(jsonify({"error": "Authentication required"}), 401)
response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
return response
# Authorization header expected format: "Basic <base64_credentials>"
parts = auth_header.split()
if len(parts) != 2 or parts[0].lower() != 'basic':
# Malformed header
return jsonify({"error": "Invalid Authorization header format"}), 400
# Decode the Base64 credentials part
encoded_credentials = parts[1]
try:
decoded_bytes = base64.b64decode(encoded_credentials)
credentials = decoded_bytes.decode('utf-8')
except Exception as e:
return jsonify({"error": "Invalid Base64 credentials"}), 400
# Credentials format is "username:password"
if ':' not in credentials:
return jsonify({"error": "Invalid credential format"}), 400
username, password = credentials.split(':', 1)
# Check against our valid credentials
if username == VALID_USERNAME and password == VALID_PASSWORD:
# Successful authentication
return jsonify({"message": f"Welcome, {username}! You have accessed a protected resource."}), 200
else:
# Wrong credentials -> 401 Unauthorized
response = make_response(jsonify({"error": "Invalid username or password"}), 401)
# Prompt again for correct credentials
response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
return response
if __name__ == '__main__':
app.run(debug=True)-
We import
base64to decode the credentials and alsomake_responseto attach headers to our responses. -
We set
VALID_USERNAMEandVALID_PASSWORDas the only acceptable credentials for simplicity. In real cases, you would check against a user database. -
The route
/protected-basicrequires Basic Auth:- We read the
Authorizationheader from the incoming request:Authorization: Basic <credentials>. If the header is missing, we return a 401 Unauthorized with aWWW-Authenticateheader. TheWWW-Authenticateheader instructs the client that Basic authentication is needed. We set it toBasic realm="Login Required"which is a standard way to prompt the user (the realm can be any description). - If the header is present, we split it on whitespace. The expected format is two parts: "Basic" and the Base64-encoded credentials. We check that the first part (scheme) is "Basic" (case-insensitive) and that we have two parts.
- We take the second part which is the Base64 string. We attempt to decode it using
base64.b64decode. This should give us a bytes string likeb'username:password'. We decode that to UTF-8 to get the stringusername:password. - We verify the decoded format contains a colon. If not, it’s not in the expected "user:pass" format.
- We then split the credentials into
usernameandpassword. - We compare these against our known valid credentials. If they match, we return a success message JSON with 200 OK.
- If they do not match, we return 401 Unauthorized with an error message and again include
WWW-Authenticateto allow the client to retry with correct credentials.
- We read the
-
This mimics HTTP Basic Auth flow: the first request typically has no auth and gets a 401 with
WWW-Authenticate, then the client sends credentials. In Postman, we can send the header preemptively. -
Security considerations: Basic Auth sends credentials in Base64 (which is just an encoding, not encryption). This means it’s not secure on its own—typically you would use HTTPS to encrypt the entire connection if doing this. Also, storing plaintext passwords (like our
VALID_PASSWORD) is not secure; later we’ll use hashing for stored passwords. But for a demo, this is fine.
Run app4.py. In Postman:
-
Without credentials: Make a GET request to
http://localhost:5000/protected-basicwithout any Authorization header.- You should receive a 401 Unauthorized. In Postman’s response, you’ll see the status 401. Check the headers in the response (in Postman, switch to the Headers tab of the response): there should be
WWW-Authenticate: Basic realm="Login Required". - The body is a JSON:
{"error": "Authentication required"}.
- You should receive a 401 Unauthorized. In Postman’s response, you’ll see the status 401. Check the headers in the response (in Postman, switch to the Headers tab of the response): there should be
-
With valid credentials: In Postman, go to the Authorization tab for your request. Choose "Basic Auth" from the dropdown. Enter username
adminand passwordsecret123(our valid pair). Postman will automatically add the correctAuthorization: Basic ...header to the request.- Send the GET request again. This time, you should get 200 OK and a JSON message:
{"message": "Welcome, admin! You have accessed a protected resource."}. - This shows that the server decoded and verified your credentials successfully.
- Send the GET request again. This time, you should get 200 OK and a JSON message:
-
With invalid credentials: Test by changing the password or username in Postman’s Basic Auth fields. For example, use
admin : wrongpass. The response should be 401 Unauthorized with{"error": "Invalid username or password"}. The response will still includeWWW-Authenticate: Basic realm="Login Required", meaning the server is saying "that was not correct, please try again or provide valid credentials". -
Manual header: Alternatively, you can manually set the
Authorizationheader. Foradmin:secret123, the Base64 encoding isYWRtaW46c2VjcmV0MTIz(you can verify this by using an online Base64 encoder or Python). You would then setAuthorizationtoBasic YWRtaW46c2VjcmV0MTIz. Postman’s Basic Auth does this for you, but it’s good to understand the underlying format.
This program demonstrated how to secure a route with Basic Auth. However, sending credentials with every request is not ideal for a modern API. Instead, token-based authentication (JWT) is preferred for stateless authentication. We will explore JWTs next.
Goal: Learn how to create a JWT (JSON Web Token) after a user logs in. JWTs are self-contained tokens that clients can send to prove their authentication, instead of sending username/password each time. We will use the PyJWT library to encode a token. This program focuses on generating a JWT – the next will focus on using it to protect routes.
# app5.py - JWT token creation example
import jwt # PyJWT library
from datetime import datetime, timedelta, timezone
from flask import Flask, request, jsonify
app = Flask(__name__)
app.config['SECRET_KEY'] = 'supersecretkey' # secret key for signing JWT
# Dummy user data (in real life, you would query a database)
USER_DATA = {
"alice": {"password": "alice123"}, # username: alice, password: alice123
"bob": {"password": "bobpassword"} # username: bob, password: bobpassword
}
@app.route('/login', methods=['POST'])
def login():
# Expect JSON with username and password
data = request.get_json()
if not data or 'username' not in data or 'password' not in data:
return jsonify({"error": "Username and password required"}), 400
username = data['username']
password = data['password']
# Check if user exists and password matches
user = USER_DATA.get(username)
if user and user['password'] == password:
# Create JWT payload
payload = {
"user": username,
# Set token to expire in 1 hour for example
"exp": datetime.now(timezone.utc) + timedelta(hours=1)
}
# Encode JWT using our secret key and HS256 algorithm
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm="HS256")
# PyJWT's jwt.encode returns a token (in PyJWT>=2 it returns a byte string or str depending on version)
token_str = token if isinstance(token, str) else token.decode('utf-8')
return jsonify({"token": token_str}), 200
else:
# Unauthorized if login fails
return jsonify({"error": "Invalid username or password"}), 401
if __name__ == '__main__':
app.run(debug=True)-
We use the
jwtmodule from PyJWT (install it viapip install PyJWT). JWT (JSON Web Token) is a standard for tokens that contain claims (payload data) that are signed (not encrypted) by the server’s secret so they cannot be forged. -
We set
app.config['SECRET_KEY']to a secret value. This key is used to sign the JWT (and also generally used by Flask for sessions and security-related needs). In a real application, never hardcode the secret key in code – you’d load it from an environment variable or config file, but for our demonstration it’s here. -
We have a dummy
USER_DATAdictionary simulating user accounts. In practice, you’d verify user credentials from a database. Here, we have two users: “alice” and “bob” with their passwords in plaintext for simplicity. -
The
/loginroute accepts POST requests with JSON containing"username"and"password".-
We parse the JSON with
request.get_json(). If JSON is missing or doesn’t have both fields, return 400. -
We then check if the provided username exists in
USER_DATAand if the password matches the stored password. -
If the credentials are valid, we proceed to create a JWT payload. Typically, you’d include a user identifier and maybe some other claims:
- We include
"user": usernameas an identifier in the token payload. - We include an
"exp"(expiration) claim set to current time + 1 hour. Thedatetime.now(timezone.utc) + timedelta(hours=1)gives an aware datetime one hour from now in UTC. Theexpclaim is standard in JWTs to indicate token expiration time.
- We include
-
We call
jwt.encode(payload, app.config['SECRET_KEY'], algorithm="HS256")to encode the token.- HS256 means HMAC-SHA256 signing algorithm, which uses our secret key to sign the token so we can verify it later. This returns a token (in PyJWT v2, it returns a byte string; we ensure it’s a string with the
decodelogic). - The token is essentially three base64 parts (header, payload, signature) separated by dots. It might look like a long string of characters (e.g.,
eyJ0eXAiOiJKV1QiLCJhbGci...).
- HS256 means HMAC-SHA256 signing algorithm, which uses our secret key to sign the token so we can verify it later. This returns a token (in PyJWT v2, it returns a byte string; we ensure it’s a string with the
-
We return the token as JSON:
{"token": "<token_str>"}. -
If credentials are invalid, we return 401 Unauthorized with an error.
-
-
Important: We included the
"exp"claim, which PyJWT will interpret and enforce on decode (it will raise an exception if the token is expired when we verify it later). If we omitted"exp", the token would never expire by itself. Always set an expiration for JWTs so they eventually become invalid. -
Also note: In a real scenario, you wouldn’t store plaintext passwords or even have them in a dictionary. We will handle password hashing in a later program. Here it’s plain for simplicity.
-
Get a JWT token: Start
app5.py. In Postman, create a POST request tohttp://localhost:5000/login. Set the body to JSON (raw, application/json) and provide a JSON object with a valid username and password. For example:{ "username": "alice", "password": "alice123" }Send the request. If the credentials are correct, you should get 200 OK with a JSON response containing a
tokenfield. The token will be a long string of characters (JWTs typically have two dots in them, separating three parts). It will look something like:{"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWxpY2UiLCJleHAiOi...etc"}.- Copy this token value (without the quotes) to use in the next step.
- If you send wrong credentials (e.g. username exists but wrong password, or unknown username), you’ll get a 401 with
{"error": "Invalid username or password"}. - If you omit username or password, you’ll get 400 with the appropriate error message.
-
Examine the token (optional): JWTs are not encrypted, they are just encoded. If you paste the token into a tool like jwt.io (or use a JWT library to decode without verifying), you can actually see the payload. You should see the
userclaim (e.g. "alice") and anexptimestamp. This is just to illustrate that JWT payload is visible to clients (so never put secret info in JWT payload). The important thing is the signature (generated using SECRET_KEY) which clients cannot fake. -
Token expiration test (optional): Our token expires in 1 hour. You can test an expired token by manually setting a short expiration (for instance, change
timedelta(minutes=1)for a quick test, or even a past time to simulate expiration). The actual usage ofexpwill be shown when we decode the token in the next program.
So far, we have a way to generate tokens for valid users. Next, we will create routes that require this token (JWT) to be provided, demonstrating route protection using JWT verification.
Goal: Use JWTs to secure certain API routes. We will implement a middleware-like check in protected routes that verifies the JWT from the Authorization header. This is commonly done with a "Bearer" token scheme where the header is Authorization: Bearer <JWT>.
# app6.py - JWT-protected routes
import jwt
from functools import wraps
from flask import Flask, request, jsonify
app = Flask(__name__)
app.config['SECRET_KEY'] = 'supersecretkey' # same secret used for token encoding
# Decorator for routes that need JWT authentication
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header:
return jsonify({"error": "Authentication token is missing"}), 401
# Expect header format: "Bearer <JWT>"
parts = auth_header.split()
if parts[0].lower() != 'bearer' or len(parts) != 2:
return jsonify({"error": "Invalid token header. Use Bearer <Token>"}), 401
token = parts[1]
try:
# Decode the token using the secret; raises exceptions if invalid or expired
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token has expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Token is invalid"}), 401
# Token is valid; payload is available. You could attach user info to request here if needed.
return f(*args, **kwargs, user_payload=payload)
return decorated
# Unprotected route (for comparison)
@app.route('/public', methods=['GET'])
def public():
return jsonify({"message": "Anyone can access this public route."}), 200
# Protected route - requires a valid JWT
@app.route('/protected', methods=['GET'])
@token_required
def protected(user_payload):
# user_payload is injected by the decorator, contains decoded token data
user = user_payload.get("user")
return jsonify({"message": f"Hello, {user}. This is a protected route."}), 200
# Protected admin-only route
@app.route('/admin-area', methods=['GET'])
@token_required
def admin_area(user_payload):
user = user_payload.get("user")
# For demonstration, suppose 'alice' is an admin and 'bob' is not
if user != 'alice':
return jsonify({"error": "Admin access required"}), 403
return jsonify({"message": f"Welcome to the admin area, {user}!"}), 200
if __name__ == '__main__':
app.run(debug=True)-
We define a decorator
token_requiredto avoid repeating token verification logic for each protected route. Thefunctools.wrapsdecorator ensures the wrapped function’s metadata is preserved (optional but good practice).-
The decorator function checks the
Authorizationheader in the request. We expect it to contain a bearer token in the form "Bearer <JWT>". -
If the header is missing, we return 401 immediately.
-
If the header format is wrong (e.g., missing "Bearer" or token part), we also return 401 with an error. The client should send
Authorization: Bearer YOUR_TOKEN_HERE. -
If we have a token, we attempt to decode it using
jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"]). This will verify the signature and, if the token has an"exp"claim, also check expiration.- On success, we get the
payload(which we set in Program 5 to include"user"and"exp"). - If the token is expired,
jwt.ExpiredSignatureErroris raised; we catch it and return 401 with a specific message. - If any other issue occurs (wrong signature, malformed token, etc.),
jwt.InvalidTokenErroris caught and we return 401.
- On success, we get the
-
If the token is valid, we call the actual route function
f, passing through any original arguments and addinguser_payload=payload. This allows the protected route to know the content of the token (like who the user is).
-
-
We have a
/publicroute that is accessible by anyone (no decorator). -
We have a
/protectedroute with@token_required. The function signature now includesuser_payloadbecause our decorator will pass it. This route simply greets the user by name using the token’s data. -
We also add an
/admin-arearoute to demonstrate role-based access control. In this simple scenario, we decide that user "alice" is an admin, others are not. (We don't have roles explicitly stored yet, but we're simulating.)- We use
@token_requiredas well, so only a valid token gets this far. - We then check the
userfrom the token payload. If it’s not "alice", we return 403 Forbidden. If it is "alice", we allow access. - In a real app, you might include a
"role"claim in the JWT or look up the user’s role from the database. We’ll refine this when we integrate a database.
- We use
-
Why Bearer? "Bearer" is just a convention indicating the type of auth token. The header
Authorization: Bearer <token>is a common scheme for JWTs in REST APIs. It's not enforced by Flask, but by using it we follow standards. -
With this setup, any route we decorate with
@token_requiredwill require a valid JWT. We can easily extend the decorator to handle roles, but for clarity we did role check inside the route.
Before testing, ensure you have a token from the previous login program (Program 5). Alternatively, we can integrate the login route into this app for a seamless test, but assuming you have app5.py running or can copy the token. For testing, let's use the token for "alice" to test admin route and "bob" to see the 403.
-
Public route: Send GET to
http://localhost:5000/public. This should return 200 with{"message": "Anyone can access this public route."}. No token needed. -
Protected route (without token): Send GET to
http://localhost:5000/protectedwith no Authorization header. You should get 401 Unauthorized, and the JSON{"error": "Authentication token is missing"}. -
Protected route (with token): Now add the token. In Postman, under Authorization, choose Bearer Token and paste your JWT (or manually add header
Authorization: Bearer <token>).- Use the token for alice (from Program 5, username "alice"). Send GET to
/protected. Expected result: 200 OK,{"message": "Hello, alice. This is a protected route."}. - Try with bob’s token (if you have one from login as bob): It should say "Hello, bob..." accordingly, as long as token is valid.
- Try altering the token (e.g., change one character) and send: You should get 401 Invalid token.
- If you wait until the token expires (1 hour) or manually alter the
"exp"to a past time before encoding, you will get{"error": "Token has expired"}with 401.
- Use the token for alice (from Program 5, username "alice"). Send GET to
-
Admin-only route:
- Using bob’s token, send GET to
http://localhost:5000/admin-area. Bob is not an admin in our logic, so even though the token is valid, the route should return 403 Forbidden with{"error": "Admin access required"}. - Using alice’s token, send GET to
/admin-area. Alice should get 200 OK with{"message": "Welcome to the admin area, alice!"}. - If you send no token or invalid token to
/admin-area, you’ll get the same 401 responses as other protected routes because the token check happens first in the decorator.
- Using bob’s token, send GET to
Now we have a basic token-based auth system: a login route issues a JWT, and protected routes require the JWT. We have also implemented a simple role check. The next step is to integrate a database for storing users and roles, instead of our in-memory USER_DATA.
Goal: Understand the significance of Flask’s SECRET_KEY. We’ve already been using SECRET_KEY for JWT signing. Here, we’ll briefly demonstrate its role in Flask (session signing) and reaffirm its importance in keeping tokens secure.
# app7.py - Demonstrating SECRET_KEY usage and secure sessions
from flask import Flask, session, jsonify
app = Flask(__name__)
# Set a secret key for secure sessions and JWT signing
app.config['SECRET_KEY'] = 'myverystrongsecretkey'
@app.route('/set-session')
def set_session():
# Example of using Flask session (which requires SECRET_KEY)
session['framework'] = 'Flask'
return jsonify({"message": "Session value set. (Check /get-session)"}), 200
@app.route('/get-session')
def get_session():
framework = session.get('framework')
return jsonify({"framework": framework}), 200
# (In a real app, JWT creation would also use this SECRET_KEY as shown in previous programs.)
if __name__ == '__main__':
app.run(debug=True)-
We configure
app.config['SECRET_KEY']with a random-looking string. The secret key is used by Flask for securely signing the session cookie (among other things). If you do not set a secret key, Flask’s session functionality will not work (it will complain or not allow setting session data). -
In previous programs, we used this key for JWT. In fact, if using extensions like Flask-JWT-Extended, it would use Flask’s
SECRET_KEYby default to sign tokens. -
In the code above, we show a simple usage of Flask’s session:
- The
/set-sessionroute puts a value into thesessionobject. The Flask session is a secure cookie stored on the client; data is cryptographically signed (not encrypted) using theSECRET_KEY. Settingsession['framework'] = 'Flask'will send a cookie to the client. - The
/get-sessionroute readssession.get('framework')and returns it. - This is just to illustrate one use of
SECRET_KEY. The actual output is trivial (just returning"Flask"), but what's important is that this wouldn't work at all ifSECRET_KEYwas not set. Flask would not set a session cookie without a secret to sign it (to prevent tampering).
- The
-
In the context of our API, the
SECRET_KEYsecures:- Session Cookies: If we were using session-based auth or storing anything in session, it’s signed with this key. If an attacker stole or guessed the
SECRET_KEY, they could forge session cookies. - JWT Signing: We manually used it in
jwt.encode. The strength of JWT’s security is directly tied to keeping this key secret. If leaked, someone could generate valid JWTs and impersonate users. - Other extensions: Some Flask extensions (e.g., CSRF protection in forms, Flask-Login cookies) also use
SECRET_KEYto sign tokens or cookies.
- Session Cookies: If we were using session-based auth or storing anything in session, it’s signed with this key. If an attacker stole or guessed the
-
Choosing a SECRET_KEY: It should be a long, random string of bytes. The documentation even suggests using
secrets.token_hex()to generate one. For development, you might hardcode it, but in production, load it from an environment variable and keep it secret. -
We won't extensively test session behavior via Postman (since it’s more of a web/browser concept with cookies), but this program serves as a conceptual lecture about
SECRET_KEY.
You can still test the above routes in Postman, though it’s slightly artificial:
- GET
http://localhost:5000/set-session. The response will be a JSON message. More importantly, check the response headers in Postman. You should see a header called Set-Cookie. It will set something likesession=<long_value>; HttpOnly; Path=/. This is Flask sending the signed session cookie to the client. - In Postman, by default, it will not automatically send that cookie on the next request unless you handle it. For a quick test, copy the Set-Cookie value.
- Now do GET
http://localhost:5000/get-session. In Postman, go to the Headers tab of the request and add a header:Cookie: session=<the value you copied>. This simulates the browser sending the session cookie back. - The response should be
{"framework": "Flask"}confirming that the session data was stored and retrieved using the secret key to validate the cookie. - If you modify the cookie value (simulate tampering), Flask will reject it and
session.get('framework')would returnNonebecause the signature check fails.
This demonstrates the importance of SECRET_KEY in maintaining session integrity and any signed data. Always keep your secret key safe and never expose it. Next, we’ll integrate a MySQL database to store users and illustrate a more persistent authentication system.
Goal: Connect our Flask app to a MySQL database using the Flask-MySQLdb extension, and perform basic operations. We will set up a MySQL connection, create a user table (if not exists), and demonstrate a simple query. This lays the groundwork for storing user credentials in a database.
# app8.py - MySQL integration example with Flask-MySQLdb
from flask import Flask, jsonify
from flask_mysqldb import MySQL
app = Flask(__name__)
# MySQL configuration (adjust with your MySQL credentials and database name)
app.config['MYSQL_HOST'] = 'localhost'
app.config['MYSQL_USER'] = 'root'
app.config['MYSQL_PASSWORD'] = 'your_mysql_password'
app.config['MYSQL_DB'] = 'flaskapi'
app.config['MYSQL_CURSORCLASS'] = 'DictCursor' # optional: to get results as dicts
mysql = MySQL(app)
# Initialize database table for users (if not exists)
with app.app_context():
cur = mysql.connection.cursor()
# Create users table with id, username, password, role
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
role VARCHAR(20) NOT NULL
)
""")
# Insert a demo user if not exists
cur.execute("SELECT * FROM users WHERE username=%s", ("alice",))
result = cur.fetchone()
if not result:
# Insert a default user 'alice' with password 'alice123' and role 'admin'
cur.execute("INSERT INTO users (username, password, role) VALUES (%s, %s, %s)",
("alice", "alice123", "admin"))
mysql.connection.commit()
cur.close()
@app.route('/users', methods=['GET'])
def get_users():
cur = mysql.connection.cursor()
cur.execute("SELECT id, username, role FROM users")
rows = cur.fetchall()
cur.close()
# `rows` will be a list of dicts because we set cursorclass to DictCursor
return jsonify(rows), 200
if __name__ == '__main__':
app.run(debug=True)-
We use
flask_mysqldb.MySQLextension to integrate with a MySQL database. Make sure to install it:pip install flask-mysqldb. (Also ensure MySQL server is running and accessible.) -
We configure database connection details:
MYSQL_HOST,MYSQL_USER,MYSQL_PASSWORD,MYSQL_DBare set to connect to our MySQL. Adjust these to your environment (e.g., if you have a different user or password, and create a database namedflaskapifor this exercise).MYSQL_CURSORCLASS = 'DictCursor'is optional but convenient – it means fetch operations will return dictionaries (column name to value) instead of tuples.
-
We create a
mysql = MySQL(app)which sets up the connection. This extension will handle connections for us, letting us usemysql.connection.cursor()to get a cursor. -
Using an application context (
with app.app_context():), we run some startup SQL to ensure a table and a default user:-
We execute a SQL statement to create a
userstable if it doesn’t exist, with columns:id: auto-increment primary key.username: unique, up to 50 chars.password: up to 100 chars (we'll later store hashed passwords which can be longer than plain).role: e.g., "admin" or "user".
-
We then check if a user "alice" exists. If not, we insert a default user:
- Username "alice", password "alice123", role "admin".
- We commit the transaction to save changes.
-
This block ensures we have at least one user to work with for testing. In a real application, you'd manage migrations or have separate setup scripts.
-
-
We define a GET route
/userswhich will retrieve all users (id, username, role) from the database:- We get a cursor with
mysql.connection.cursor(), execute a SELECT query, and fetch all results. - After fetching, we close the cursor.
- We then
return jsonify(rows). If usingDictCursor, each row is a dict like{"id": ..., "username": ..., "role": ...}. Flask’s jsonify can handle a list of dicts, converting it to a JSON array. - If not using DictCursor,
rowswould be a list of tuples; we’d have to manually format them or specify columns.
- We get a cursor with
-
This program doesn’t yet involve JWT or auth – it’s focusing on making sure our Flask app can talk to MySQL and that the users table is in place for the next steps.
Before running this, ensure:
- MySQL server is installed and running.
- A database named
flaskapi(as used in config) exists. You can create one via MySQL client:CREATE DATABASE flaskapi;. - The user in config (here 'root' with
your_mysql_password) has access to this database. Adjust if needed (for example, you might create a specific user for the app). - The flask-mysqldb extension actually uses the MySQLdb (mysqlclient) driver under the hood. On some systems, you may need to install OS-level MySQL client libraries. If you encounter an error on installing, refer to Flask-MySQLdb docs for dependency instructions.
-
Start the application: Run
app8.py. Watch the console for any errors on connecting to the database. If everything is correct, the app will start and on first run create the table and insert the user. -
Test the
/usersendpoint: In Postman, send a GET request tohttp://localhost:5000/users. You should get a JSON array of users. For example, after first run it might return:[ { "id": 1, "username": "alice", "role": "admin" } ]If you run it again (without resetting the DB), it will still show "alice" once because we only insert if not exists.
-
Verify data in MySQL (optional): Connect to your MySQL database (using CLI or a tool) and run
SELECT * FROM users;. You should see the "alice" user. You can also add more users via SQL if you want to test (just ensure unique usernames). -
If you see any errors related to MySQL connection, double-check your host/user/password, and that MySQL service is running. Also ensure the
flaskapidatabase exists.
Now that we have database integration, we can modify our login and role checks to use real database data instead of the dummy dictionary. We will do that in the next program by creating a login endpoint that verifies user credentials from the database and generates a JWT.
Goal: Create a /login endpoint that authenticates a user against the MySQL database and returns a JWT token. This combines our previous JWT creation logic with the database of users. We’ll also update the token payload to include the user’s role for authorization purposes.
# app9.py - Login with MySQL database and JWT token response
import jwt
from datetime import datetime, timedelta, timezone
from flask import Flask, request, jsonify
from flask_mysqldb import MySQL
from werkzeug.security import check_password_hash
app = Flask(__name__)
app.config['SECRET_KEY'] = 'myverystrongsecretkey'
app.config['MYSQL_HOST'] = 'localhost'
app.config['MYSQL_USER'] = 'root'
app.config['MYSQL_PASSWORD'] = 'your_mysql_password'
app.config['MYSQL_DB'] = 'flaskapi'
app.config['MYSQL_CURSORCLASS'] = 'DictCursor'
mysql = MySQL(app)
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
if not data or 'username' not in data or 'password' not in data:
return jsonify({"error": "Username and password required"}), 400
username = data['username']
password = data['password']
cur = mysql.connection.cursor()
cur.execute("SELECT id, username, password, role FROM users WHERE username=%s", (username,))
user = cur.fetchone()
cur.close()
if not user:
# Username not found
return jsonify({"error": "Invalid username or password"}), 401
# user['password'] is the stored password (hashed or plain depending on earlier setup)
stored_password = user['password']
# We'll assume passwords are stored in plain for now or already hashed appropriately.
# If hashed (which we will do in next program), use check_password_hash
if stored_password == password or check_password_hash(stored_password, password):
# Build token payload
payload = {
"user_id": user['id'],
"username": user['username'],
"role": user['role'],
"exp": datetime.now(timezone.utc) + timedelta(hours=1)
}
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm="HS256")
token_str = token if isinstance(token, str) else token.decode('utf-8')
return jsonify({"token": token_str}), 200
else:
return jsonify({"error": "Invalid username or password"}), 401
# (We could include protected routes here as in Program 6, but focusing on login for this example.)
if __name__ == '__main__':
app.run(debug=True)-
We import
check_password_hashfromwerkzeug.security. We will use this in anticipation of storing hashed passwords. For now, our database has plaintext"alice123", socheck_password_hashwon’t be used (it will return False since the stored password isn’t a hash). We include it to show where it fits when we improve security. -
The app is configured with the same
SECRET_KEYand MySQL connection as before (pointing to theflaskapidatabase with theuserstable). -
The
/loginroute:-
Expects JSON with username and password, similar to Program 5. We validate presence of both fields.
-
We query the database for a user with the given username: selecting
id, username, password, rolefromusers. We fetch one result (because username is unique, either one or none). -
If
useris None, that means the username doesn’t exist, so we return 401 (don’t reveal which part is wrong for security – just a generic invalid credentials message). -
If a user is found, we get the stored password. In our current setup (from Program 8), we stored
"alice123"in plain text. In future (Program 11) we will store hashed, at which pointstored_passwordwill be a hash string. -
We then check the provided password:
- If we have stored plaintext (like "alice123"),
stored_password == passwordwill be True for a correct password. - If we have a hashed password,
check_password_hash(stored_password, password)will return True if the hash matches the given password. - We combine these conditions with OR, assuming that either the password is correct in plaintext or when hashed. (This is for smooth transition – in practice, once you hash passwords, you’d only use the hash check.)
- If we have stored plaintext (like "alice123"),
-
If the password check passes, we prepare the JWT payload:
- We include
user_id,username, androlefrom the database. Now the token carries the user’s role too. - We include
expfor 1 hour expiration.
- We include
-
We encode the token with
jwt.encodeand return it as before. -
If password check fails, return 401.
-
-
Essentially, we replaced the dummy user dictionary with a real database lookup. Also, the token now has
rolewhich we can use in protected endpoints to differentiate admin vs regular user (no role check here, but we will adjust in the next program). -
Notice: We did not create the table or default user here because we expect Program 8 already created them. If running this standalone, ensure the table and a user exist in the DB (from previous step). For integration, one could combine app8 and app9 logic or ensure to run app8 first. In a real app, you'd have a consistent single app with both table creation and routes.
Make sure you have the MySQL database running with the users table and at least one user (e.g., "alice" as inserted previously).
-
Test correct credentials: Send a POST to
http://localhost:5000/loginwith JSON body:{ "username": "alice", "password": "alice123" }You should get 200 OK and a JSON response like
{"token": "<JWT_TOKEN_HERE>"}. Copy the token for later. The token payload (if decoded) should have alice’s id, username, and role "admin". -
Test incorrect password: Change the password in the JSON to something else. You should get 401 Unauthorized with
{"error": "Invalid username or password"}. -
Test non-existent user: Try a username that’s not in the DB. Also should yield 401 with the same message.
-
Omitted fields: Send an empty JSON or missing username or password. You should get 400 Bad Request with
{"error": "Username and password required"}. -
Token usage: Although this program doesn’t define protected routes, you can manually test the returned token by decoding it using a JWT tool or by writing a small decode snippet. It should contain the fields we included. We will use it in next program.
Now we have a robust login that checks the database. Next, we will enforce role-based access using the role stored in the token and also improve security by hashing passwords in the database.
Goal: Use the user’s role to control access to certain endpoints (admin vs user), and introduce Flask Blueprints to organize our routes. We will create separate blueprints for authentication and for main API functionality, demonstrating modularity.
# app10.py - Role-based access control and using Blueprints
import jwt
from datetime import datetime, timedelta, timezone
from functools import wraps
from flask import Flask, request, jsonify, Blueprint
from flask_mysqldb import MySQL
app = Flask(__name__)
app.config['SECRET_KEY'] = 'myverystrongsecretkey'
app.config['MYSQL_HOST'] = 'localhost'
app.config['MYSQL_USER'] = 'root'
app.config['MYSQL_PASSWORD'] = 'your_mysql_password'
app.config['MYSQL_DB'] = 'flaskapi'
app.config['MYSQL_CURSORCLASS'] = 'DictCursor'
mysql = MySQL(app)
# Blueprint for auth-related routes
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
# Blueprint for API routes
api_bp = Blueprint('api', __name__, url_prefix='/api')
# Decorator to require valid JWT for protected routes
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header:
return jsonify({"error": "Authentication token is missing"}), 401
parts = auth_header.split()
if len(parts) != 2 or parts[0].lower() != 'bearer':
return jsonify({"error": "Invalid token header. Use Bearer <token>"}), 401
token = parts[1]
try:
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token has expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
# Attach payload to keyword arguments for use in the route
return f(*args, **kwargs, token_payload=payload)
return decorated
@auth_bp.route('/login', methods=['POST'])
def login():
data = request.get_json()
if not data or 'username' not in data or 'password' not in data:
return jsonify({"error": "Username and password required"}), 400
username = data['username']
password = data['password']
cur = mysql.connection.cursor()
cur.execute("SELECT id, username, password, role FROM users WHERE username=%s", (username,))
user = cur.fetchone()
cur.close()
if not user or user['password'] != password:
return jsonify({"error": "Invalid username or password"}), 401
# Create token payload with role
payload = {
"user_id": user['id'],
"username": user['username'],
"role": user['role'],
"exp": datetime.now(timezone.utc) + timedelta(hours=1)
}
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm="HS256")
token_str = token if isinstance(token, str) else token.decode('utf-8')
return jsonify({"token": token_str}), 200
@api_bp.route('/data', methods=['GET'])
@token_required
def get_data(token_payload):
# A protected route accessible to any logged-in user (regardless of role)
user = token_payload['username']
return jsonify({"message": f"Hello, {user}. Here is the confidential data."}), 200
@api_bp.route('/admin', methods=['GET'])
@token_required
def admin_area(token_payload):
# A protected route accessible only to admins
if token_payload.get('role') != 'admin':
return jsonify({"error": "Admin access required"}), 403
user = token_payload['username']
return jsonify({"message": f"Welcome to the admin panel, {user}."}), 200
# Register blueprints with the app
app.register_blueprint(auth_bp)
app.register_blueprint(api_bp)
if __name__ == '__main__':
app.run(debug=True)-
We create two Blueprints:
auth_bpfor authentication-related routes, with URL prefix/auth. This will group routes like login (and in the future maybe register, logout, etc.).api_bpfor our main API routes, with prefix/api. This could group various endpoints that the API provides.
-
The
token_requireddecorator is similar to Program 6, but we place it at the top-level of this file (outside of blueprint definitions, which is fine). -
We define the
/auth/loginroute inside theauth_bpblueprint. It’s basically the same login logic as Program 9 (without hashing yet, assuming plaintext match for now). It returns a token containing the user’s role.- Note: The route is
@auth_bp.route('/login')with prefix/auth, so full URL will be/auth/login.
- Note: The route is
-
Under
api_bp, we define two routes:/api/datawhich is a generic protected resource accessible to any valid token (any logged-in user). It just returns a hello message with the user’s name./api/adminwhich is protected and also checkstoken_payload['role']. If role isn’t "admin", it returns 403 Forbidden. If role is admin, returns a welcome message.
-
We register the blueprints:
app.register_blueprint(auth_bp)andapp.register_blueprint(api_bp). The blueprint prefixes (/authand/api) ensure these routes are mounted under those paths. -
Why use Blueprints? In a larger application, you might separate your routes into different files or logical groups. For example, an
auth.pyfile with anauth_bpblueprint for auth routes, aapi.pyfor main API, etc. This keeps the code organized. Here we show the structure in one file for clarity, but one can imagine splitting them. -
The logic inside is straightforward and similar to earlier programs, just organized differently. We still use the global
app.configandmysqlin the blueprint routes, which is fine because they have access to those (since we createdmysql = MySQL(app)at the top in the app context). -
This program ties together login, JWT, role checking, and blueprint usage.
Ensure the MySQL DB has users (especially an admin user like "alice" as we inserted). Run app10.py.
-
Login (auth blueprint): Send POST to
http://localhost:5000/auth/loginwith a valid user (e.g., alice). You should get a token as before.- If you use "alice" with "alice123", you get a token which encodes role "admin".
- If you use a non-admin user (if you added one, say "bob"), you get a token with role "user".
- Bad credentials should yield 401, missing fields 400.
-
Access /api/data: Using a token from either user, send GET to
http://localhost:5000/api/datawithAuthorization: Bearer <token>.- For a valid token, you should get a message greeting that user. Admin or not doesn’t matter here, any logged user can see it. If token missing or invalid, you get 401.
-
Access /api/admin:
- Using admin’s token (alice’s token): GET
http://localhost:5000/api/adminwith the token. Should get 200 and welcome message. - Using a non-admin token: GET
/api/adminwith that token. Should get 403 Forbidden with{"error": "Admin access required"}. - Without token or invalid token: will be caught by
token_requiredfirst, resulting in 401.
- Using admin’s token (alice’s token): GET
-
Blueprint route prefixes: Notice that in Postman we had to include
/author/api. If you tryhttp://localhost:5000/login(without /auth), you’ll get 404 because the login route is under the blueprint prefix. Blueprints basically prepend that prefix to all its routes, organizing the URL space.
At this stage, our application is well-structured and secure for basic needs. The final improvement will be to hash user passwords in the database instead of storing plaintext, and update our login check accordingly.
Goal: Enhance our user authentication by storing hashed passwords instead of plaintext. We will use werkzeug.security.generate_password_hash to hash passwords when creating users, and check_password_hash to verify. We’ll add a user registration endpoint to demonstrate storing a hashed password, and modify login to use hash check.
# app11.py - Password hashing and verification with Werkzeug
from flask import Flask, request, jsonify
from flask_mysqldb import MySQL
from werkzeug.security import generate_password_hash, check_password_hash
import jwt
from datetime import datetime, timedelta, timezone
from functools import wraps, partial
app = Flask(__name__)
app.config['SECRET_KEY'] = 'myverystrongsecretkey'
app.config['MYSQL_HOST'] = 'localhost'
app.config['MYSQL_USER'] = 'root'
app.config['MYSQL_PASSWORD'] = 'your_mysql_password'
app.config['MYSQL_DB'] = 'flaskapi'
app.config['MYSQL_CURSORCLASS'] = 'DictCursor'
mysql = MySQL(app)
# (Reusing token_required decorator from previous program)
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header:
return jsonify({"error": "Authentication token is missing"}), 401
parts = auth_header.split()
if len(parts) != 2 or parts[0].lower() != 'bearer':
return jsonify({"error": "Invalid token header. Use Bearer <token>"}), 401
token = parts[1]
try:
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token has expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
return f(*args, **kwargs, token_payload=payload)
return decorated
@app.route('/register', methods=['POST'])
def register():
data = request.get_json()
if not data or 'username' not in data or 'password' not in data or 'role' not in data:
return jsonify({"error": "Username, password, and role are required"}), 400
username = data['username']
password = data['password']
role = data['role']
# Basic validation: ensure role is either 'admin' or 'user'
if role not in ('admin', 'user'):
return jsonify({"error": "Role must be 'admin' or 'user'"}), 400
# Check if username already exists
cur = mysql.connection.cursor()
cur.execute("SELECT id FROM users WHERE username=%s", (username,))
existing = cur.fetchone()
if existing:
cur.close()
return jsonify({"error": "Username already exists"}), 409 # Conflict
# Hash the password
hashed_pw = generate_password_hash(password) # default method=pbkdf2:sha256
# Insert new user
cur.execute("INSERT INTO users (username, password, role) VALUES (%s, %s, %s)",
(username, hashed_pw, role))
mysql.connection.commit()
cur.close()
return jsonify({"message": f"User {username} registered successfully"}), 201
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
if not data or 'username' not in data or 'password' not in data:
return jsonify({"error": "Username and password required"}), 400
username = data['username']
password = data['password']
cur = mysql.connection.cursor()
cur.execute("SELECT id, username, password, role FROM users WHERE username=%s", (username,))
user = cur.fetchone()
cur.close()
if not user:
return jsonify({"error": "Invalid username or password"}), 401
# user['password'] is now a hashed password
if not check_password_hash(user['password'], password):
return jsonify({"error": "Invalid username or password"}), 401
# Create token payload
payload = {
"user_id": user['id'],
"username": user['username'],
"role": user['role'],
"exp": datetime.now(timezone.utc) + timedelta(hours=1)
}
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm="HS256")
token_str = token if isinstance(token, str) else token.decode('utf-8')
return jsonify({"token": token_str}), 200
# Example protected route (as demonstration, requires any logged-in user)
@app.route('/profile', methods=['GET'])
@token_required
def profile(token_payload):
return jsonify({
"user_id": token_payload['user_id'],
"username": token_payload['username'],
"role": token_payload['role']
}), 200
if __name__ == '__main__':
app.run(debug=True)-
We added
generate_password_hashandcheck_password_hashfrom Werkzeug.generate_password_hash(password)will create a secure hash (with salt) using a strong algorithm (default is PBKDF2-HMAC-SHA256 with many iterations, or even scrypt in latest Werkzeug).check_password_hash(hashed, plain)returns True if the plain password matches the given hash. -
The
/registerendpoint:- Expects JSON with
username,password, androle. - It ensures none of these are missing and that role is either "admin" or "user" (for simplicity, we enforce only these two roles).
- It checks if the username already exists in the DB. If yes, returns 409 Conflict error.
- If new, it calls
generate_password_hash(password)to hash the plaintext password. This returns a string like"pbkdf2:sha256:260000$<salt>$<hash>"(format may vary) which includes the method and salt and hashed value. Each call produces a different hash even for the same password due to random salt, butcheck_password_hashknows how to verify it. - It inserts the new user into the
userstable with the hashed password and given role, then commits and returns a 201 Created status with a success message.
- Expects JSON with
-
The
/loginendpoint:- Fetches the user by username as before.
- If user not found, return 401.
- If found, it uses
check_password_hash(user['password'], password)to verify the provided password against the stored hash. If this returns False, credentials are wrong. - Only if the hash check passes do we generate the JWT token (with user_id, username, role, exp) and return it.
-
We included a sample protected route
/profilethat simply returns the info fromtoken_payloadto show that login+token works with hashed password too. -
Note: If you have existing users in the DB from previous steps with plaintext passwords, their
passwordfield is not hashed. Thecheck_password_hashon a plaintext stored password will fail (since it expects a hash format). One approach is to re-register those users or update their passwords. Since we introduced a register route, you can add users properly. Or manually update the DB to replace plaintext with a generated hash for that password. -
For learning purposes, it’s okay to just register a new user via the new endpoint.
If continuing from previous steps, consider clearing the users table or at least know that "alice" currently has a plaintext password. You can re-register her with the same username to update the password hash (our code would currently block duplicate usernames, so you might need to delete the old record or use a new username).
Let's test fresh:
-
Register a new user: POST
http://localhost:5000/registerwith body:{ "username": "charlie", "password": "charlie123", "role": "user" }Expected: 201 Created,
{"message": "User charlie registered successfully"}.- If you try the same username again, you should get 409 conflict.
- Try with missing fields or an invalid role to test the 400 responses.
- In the database,
userstable now has charlie with a hashed password (you can select to see it).
-
Login with the new user: POST
http://localhost:5000/loginwith body:{ "username": "charlie", "password": "charlie123" }Expected: 200 OK with a token. This proves that our
check_password_hashworked for the correct password. If you use a wrong password, you get 401.- If you still have "alice" in DB with plaintext "alice123", login will fail for her now because our code tries to treat that as a hash. You should update her entry. For now, use the users created properly via register.
-
Use the token: Copy the token from login. GET
http://localhost:5000/profilewithAuthorization: Bearer <token>. You should get 200 and a JSON with youruser_id,username, androle. This confirms the whole flow: registration with hashed password, login, token generation, and token verification on protected route. -
Security check: If you inspect the database, the password is stored as a long hash string, not the original. Even if someone got read access to your DB, they can’t reverse these hashes (they are one-way and salted). This is critical for security best practices in real applications.
Finally, you can test an admin scenario: register an admin user and try using their token on an admin-only route (if you carry over the admin route logic from Program 10, or adapt a similar check in a route here). The principle remains the same.