Complete "Authentication, Tokens, and Secure Credential Handling" first to unlock this lesson.Go to previous lesson

Project Overview

In this capstone lesson, you will build a complete AI agent that performs four real-world tasks on behalf of an authenticated user: verifying identity through authentication, reading and writing records to a database, calling external APIs to fetch or send data, and enforcing role-based access control (RBAC) so users only see what they are allowed to see. This is the integration point where every previous concept in the course connects.

The agent you build follows a request-response pattern: a user sends a request, the agent checks who the user is, decides what the user is allowed to do, then either queries the database, calls an API, or both, and returns a result. We will use Python for the agent logic, a SQL database for persistence, a mock external API for external data, and a simple JWT (JSON Web Token) mechanism for authentication.

Architecture and Setup

Before writing code, let us sketch the four moving parts and how they connect. The user sends a request with a JWT token. The agent validates the token, extracts the user ID and role, then routes the request to either a database handler or an API caller depending on the user's intent. The database handler uses parameterized SQL queries to prevent injection attacks. The API caller attaches the same JWT to outbound requests so the external service can independently verify the caller.

python
from dataclasses import dataclass
@dataclass
class UserContext:
user_id: int
role: str
token: str

This UserContext object travels with every internal call. Once a request enters the agent, the context is stamped on it and every downstream function receives the same context, so authorization checks happen at every step, not only at the entry point.

Implementing Authentication

Authentication means proving who the user is. We will use a simple JWT-style token: a base64-encoded string containing the user ID, role, and an expiry timestamp, signed with a shared secret. In production you would use a library like PyJWT, but here we keep it minimal so you can see the mechanics.

python
import base64, json, hmac, hashlib, time
SECRET = "change-me-in-production"
def create_token(user_id: int, role: str) -> str:
payload = {"uid": user_id, "role": role, "exp": int(time.time()) + 3600}
body = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()
sig = hmac.new(SECRET.encode(), body.encode(), hashlib.sha256).hexdigest()
return f"{body}.{sig}"
def verify_token(token: str) -> UserContext:
body, sig = token.split(".")
expected = hmac.new(SECRET.encode(), body.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
raise PermissionError("Invalid token signature")
payload = json.loads(base64.urlsafe_b64decode(body))
if payload["exp"] < int(time.time()):
raise PermissionError("Token expired")
return UserContext(user_id=payload["uid"], role=payload["role"], token=token)

The verify_token function is the gatekeeper. Every protected endpoint calls it first. If the token is tampered, expired, or missing, the request is rejected before any database or API code runs.

Querying the Database Safely

With authentication done, the next step is letting the agent read user records. The critical rule here is: never build SQL by string concatenation. Always use parameterized queries so user input is treated as data, not as executable SQL.

python
import sqlite3
def get_user_orders(ctx: UserContext, target_user_id: int) -> list:
conn = sqlite3.connect("app.db")
cur = conn.cursor()
cur.execute("SELECT id, product, total FROM orders WHERE user_id = ?", (target_user_id,))
rows = cur.fetchall()
conn.close()
return rows

Notice how the user ID is passed as a tuple parameter, not interpolated into the string. This single habit blocks SQL injection, which is the most common database attack.

Calling External APIs

Many agent tasks need data the database does not have, such as shipping status from a courier or weather for a delivery route. The agent calls an external HTTP API, passing the same JWT so the external service can verify the caller.

python
import urllib.request
def get_shipping_status(ctx: UserContext, order_id: int) -> dict:
req = urllib.request.Request(
f"https://api.courier.example/orders/{order_id}",
headers={"Authorization": f"Bearer {ctx.token}"}
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())

The token is forwarded, not a new one generated. This lets the courier service log who asked and apply its own rate limits per user.

Enforcing Role-Based Access Control

Authentication tells you who the user is. Authorization (RBAC) tells you what they are allowed to do. The cleanest place to enforce this is a small policy function called at every entry point.

python
def require_role(ctx: UserContext, allowed: list[str]):
if ctx.role not in allowed:
raise PermissionError(f"Role '{ctx.role}' not permitted")
python
def list_all_orders(ctx: UserContext) -> list:
require_role(ctx, ["admin"])
conn = sqlite3.connect("app.db")
cur = conn.cursor()
cur.execute("SELECT id, user_id, product, total FROM orders")
rows = cur.fetchall()
conn.close()
return rows

A regular user calling list_all_orders gets rejected. An admin calling it gets the full table. The policy check is one line, but it is the difference between a secure system and a data breach.

Tying It All Together

The main agent function ties authentication, database access, API calls, and RBAC into one flow.

python
def handle_request(token: str, intent: str, params: dict):
ctx = verify_token(token)
if intent == "my_orders":
return get_user_orders(ctx, params["user_id"])
elif intent == "all_orders":
return list_all_orders(ctx)
elif intent == "shipping":
return get_shipping_status(ctx, params["order_id"])
else:
raise ValueError("Unknown intent")

Every path starts with verify_token. Every path that touches sensitive data calls require_role. Every database query is parameterized. Every outbound API call carries the user's token. These four habits, repeated everywhere, are what make the agent production-ready.

flowchart showing user request entering the agent, branching through verify_token, then routing to require_role check, then splitting into database query path or external API call path, with both paths returning to the user

Summary

You now have a working end-to-end agent. Authentication proves identity through signed tokens. Parameterized SQL queries read and write data without injection risk. Outbound API calls reuse the same token so external services can audit and rate-limit per user. Role-based access control sits at every entry point, not just the front door, so privilege escalation requires bypassing multiple checks. When you build real agents, the pattern is the same: authenticate once, authorize everywhere, parameterize always, and propagate context through every call.

Lesson practice

Lesson Checkpoint

Review what you learned and get feedback on your work.

0/1
Answered · 0/1
End-to-End Project: Building a Secure, Database-Backed Agent