What you'll learn: how identity travels to the Python service and why the service verifies it again, authentication (who) versus authorisation (what), how the capstone's dev token works and what a real identity provider adds, and how to scope every lookup to the caller's tenant.
At a glance
Figure 1. Where each check happens
How to read this: identity is set at the gateway, carried inward as a token, verified again by the service, then used by code, not the model, to allow actions and filter data.
- Authentication answers "who is this"; authorisation answers "what may they do". They fail as 401 and 403.
- Identity comes from a verified token, never the body, a header or the model.
- The HMAC dev token teaches the idea; a real identity provider adds keys, claims, rotation and revocation.
- Every lookup is scoped to the caller's tenant, and "not yours" looks like "not found".
- The service verifies the token itself, so it stays safe if the gateway is bypassed.
Scenario
Composite scenario, not a real company. At Lumen Industrial Supply, the fictional distributor, the copilot serves public visitors and partners, who may see partner-only products. Two partner companies share the service. A reviewer asks: where does the service learn who the caller is, what stops partner A seeing partner B's records, and what if the gateway is bypassed? Numbers are illustrative.
What you will be able to do
- Explain authentication and authorisation and map each to a status code.
- Read
sign_tokenandverify_tokenand say what each line protects against. - State what the dev token lacks compared with a real identity provider.
- Scope a store to the caller's tenant and write the tests that prove it.
Prerequisites
Metadata filters and tenant-aware access control, Permissions the application enforces, Where the Python service sits and Security and threat modeling.
Core ideas
Two questions, two failures
| Authentication | Authorisation | |
|---|---|---|
| Question | Who is calling? | May they do this? |
| Failure | 401, unauthorized |
403, forbidden |
| Capstone code | identity_from_header |
permissions.require |
| Checked | At the edge and again in the service | In the service, close to the action |
| Never taken from | The request body or a header the caller invented | The model |
RFC 6750 says an expired, revoked or malformed token (invalid_token) gets HTTP 401. A known caller refused an action gets 403.
The dev token, line by line
The capstone's token is an HMAC dev token, not a real identity provider. The service signs a JSON payload with a shared secret; anyone holding it can mint any identity.
Verified: capstone/fde-product-discovery-copilot/app/security/identity.py
def sign_token(identity: Identity, secret: str, ttl_s: int = 3600, now: float | None = None) -> str:
exp = int((time.time() if now is None else now) + ttl_s)
payload = json.dumps({"sub": identity.user_id, "tenant": identity.tenant, "role": identity.role, "exp": exp})
body = _b64(payload.encode())
return f"{body}.{_sign(body, secret)}"
def verify_token(token: str, secret: str, now: float | None = None) -> Identity:
body, _, signature = token.partition(".")
if not body or not signature:
raise AuthError("Malformed bearer token.")
if not hmac.compare_digest(signature, _sign(body, secret)): # constant-time compare
raise AuthError("Invalid bearer token.")
Two lines matter most. The signature is checked before the payload is trusted, so a forged role never reaches code that uses it. And hmac.compare_digest is designed to prevent timing analysis, says Python's documentation.
A real identity provider adds:
| Dev token | A real identity provider adds |
|---|---|
| One shared secret signs and verifies | A private key signs; services verify with a public key and cannot mint tokens |
sub, tenant, role, exp only |
Standard claims such as issuer and audience, so a token for another service is refused |
No revocation before exp |
Sessions, revocation and rotation of signing keys |
| A dev endpoint hands out tokens | Login, single sign-on and multi-factor, owned by the identity team |
The dev token has two dot-separated parts where a signed JWT (RFC 7519) has three, so it is not a JWT. RFC 7519 says a token must not be accepted on or after its exp; the dev token is (see the worked example).
A bearer token works for anyone who holds it (RFC 6750), so it travels only over TLS. RFC 6750 says to issue short-lived tokens (one hour or less); the capstone's default is 3600 seconds.
Propagating identity
Figure 2. One token, verified at each hop
How to read this: the same token is checked twice, then the service passes an Identity object, never the raw request, to everything that decides access.
identity_from_header turns the Authorization header into an Identity, and the pipeline receives that object. A caller can add X-Role: partner; nothing reads it.