Module 3 — Identity and access management¶
Why it matters to a software engineer¶
Almost every serious web incident is an identity incident: a session stolen,
a token over-scoped, a missing authorization check, a service account shared
by twelve microservices. Authentication answers “who is this?” Authorization
answers “what may they do to this object, now?” Mixing them up produces
if (loggedIn) return note.
Visual overview¶
sequenceDiagram
User->>API: username + password
API->>IdentityStore: verify slow password hash
IdentityStore-->>API: identity + attributes
API-->>User: short-lived signed token
User->>API: GET /notes/2 + token
API->>API: authenticate token
API->>Policy: authorize actor=alice, object=note:2, action=read
Policy-->>API: deny (owner=bob)
Intuition
Notice the diagram deliberately ends in a deny. Authentication proved Alice is Alice — that part succeeded. The request still fails, because authentication only answers "who are you," never "are you allowed to touch this object." Treating a valid token as if it were a yes is the single most common access-control bug in real APIs (it has its own name: BOLA/IDOR, covered in Module 4).
stateDiagram-v2
[*] --> Issued
Issued --> Active: signature, issuer, audience, time valid
Active --> Expired: exp reached
Active --> Revoked: response / compromise
Expired --> [*]
Revoked --> [*]
Hint
A token's lifecycle has two exits, not one. Teams routinely build the
Expired path (just let exp pass) and forget the Revoked path
(actively invalidate a token before it would have expired, e.g. on
logout or after a suspected leak). If your system has no revocation
story, a stolen long-lived token stays valid until its natural expiry no
matter what you do.
sequenceDiagram
participant U as User
participant C as Client
participant AS as Authorization server / OIDC provider
participant API as Resource API
U->>C: choose sign in
C->>AS: authorization request + PKCE challenge
AS->>U: authenticate + consent
AS-->>C: authorization code
C->>AS: code + PKCE verifier
AS-->>C: access token + ID token
C->>API: access token
Intuition
This is the "log in with..." button, unrolled. The client (your app) never sees the user's password — it only ever gets a short-lived authorization code, which it then exchanges for a token. PKCE exists specifically so that even if the authorization code leaks in transit (e.g. via a mobile deep link), the code alone is useless without the verifier secret the client generated for itself.
| Authentication | Authorization |
|---|---|
| Who/what is calling? | May this identity perform this action on this object now? |
| Password, MFA, certificate, signed token | RBAC, ABAC, ownership, policy |
| Failure: forged/stolen identity | Failure: valid Alice reads Bob's note |
Human identity usually begins with an interactive login and MFA; workload identity begins with attested runtime context and receives a short-lived, audience-bound credential. Both need lifecycle, least privilege, audit, and revocation. A valid token is input to authorization, not proof of permission.
Learning objectives¶
- Separate authentication, authorization, sessions, and tokens.
- Explain cookies, bearer tokens, OAuth 2.0 concepts, OIDC concepts, API keys, MFA, RBAC, and ABAC well enough to review a design.
- Identify common implementation failures in the lab JWT flow.
- Describe service-to-service identity better than a shared static key.
Key concepts¶
Authentication (AuthN). Establishing identity: password, passkey, OIDC
id_token, mTLS certificate, workload identity. In the lab, /login returns
a JWT after verifying the seeded lab password (unsalted SHA-256 in
LAB_MODE — weak, not fictional).
Authorization (AuthZ). A decision: allow or deny an action on a resource. Must happen on the server for every object. UI hiding a button is not AuthZ.
Session vs token. A session is server-side state (session id in a cookie). A token is typically client-held claims (JWT). Both can be stolen. Both need expiry, revocation strategy, and transport security.
Cookies. Automatically sent by browsers for a site. Need Secure,
HttpOnly, SameSite in real browser apps. This lab uses Authorization:
Bearer instead (typical of APIs and SPAs).
JWT (JSON Web Token). Three segments: header, payload, signature. Signed
(JWS) tokens are integrity-protected claims, not automatically confidential.
Anyone who has the token can read claims. alg=none, HS256 with a leaked
secret, missing exp, and confusing aud are classic failures.
OAuth 2.0 is a delegation framework: an authorization server grants an access token so a client can call an API as a user or as itself. It is not “login.” OpenID Connect adds an identity layer (id_token) on top of OAuth. You do not need to implement either in this course; you need to stop treating a random JWT your app minted as “we use OAuth.”
API keys. Bearer secrets, often long-lived, often pasted into frontends by accident. Prefer scoped, rotatable keys or workload identity.
Secrets. Anything that grants access: passwords, tokens, signing keys. Not “the config file.”
MFA. Additional factor. Phishing-resistant MFA (passkeys, hardware) beats OTP that can be phished. MFA does not fix IDOR.
RBAC vs ABAC.
| RBAC | ABAC | |
|---|---|---|
| Decision inputs | Roles (admin, user) | Attributes (owner, tenant, clearance, time) |
| Strength | Simple ops | Fine-grained, policy-as-data |
| Failure | Role explosion; “admin” means everything | Policy complexity; slow reviews |
| Lab | role=admin for /admin/users |
note.owner == token.sub |
You usually need both: roles for functions, attributes for objects.
Service-to-service identity. Shared API_KEY=... in six repos is not
identity. Prefer short-lived credentials: cloud workload identity,
SPIFFE/SPIRE (an open standard that issues each workload a short-lived
cryptographic identity document instead of a shared secret), mTLS with
rotation, or OIDC from CI. The mock IMDS in this lab exists because cloud
SDKs historically fetched instance role keys from a link-local service —
a powerful identity that SSRF can steal.
Common implementation failures.
- AuthN without object AuthZ (IDOR / BOLA).
- Missing function AuthZ (
/admin/*if logged in). - Tokens without
exp/aud/iss, or acceptingalg=none. - Session fixation, cookie without
Secure. - Storing passwords with SHA-256 or reversible encryption.
- Logging tokens.
- Confused deputy: API fetches a URL the user supplied (SSRF) using the service’s identity.
Architecture connection¶
user --password--> /login --JWT--> client
client --Bearer JWT--> /notes/2
|
+-- must check: token valid AND owner==sub
Zero-trust service communication looks the same for machines: each request carries a verifiable identity; the callee authorizes.
Hands-on lab — review the authentication flow¶
AUTHORIZED LAB USE ONLY.
Prerequisites¶
Lab running with LAB_MODE=true (default).
Before you run this¶
Predict: (1) which evidence appears (2) which does not (3) why.
Then run the steps. Compare with the prediction. If the result differs, which assumption was wrong?
Steps¶
- Login as Alice; decode the JWT payload (it is base64, not encryption):
TOKEN=$(curl -s http://127.0.0.1:8080/login -H 'Content-Type: application/json' \
-d '{"username":"alice","password":"alice-lab-password"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
export TOKEN
python3 - <<'PY'
import os, base64, json
def b64url(seg: str) -> dict:
pad = "=" * (-len(seg) % 4)
return json.loads(base64.urlsafe_b64decode(seg + pad))
token = os.environ["TOKEN"]
header, payload, _sig = token.split(".")
print("header:", json.dumps(b64url(header), indent=2))
print("payload:", json.dumps(b64url(payload), indent=2))
PY
Decoding is not verifying. Anyone with the token can read sub and
role. This lab mints its own HS256 JWT; that is not OAuth/OIDC.
- Observe missing
expin lab mode (weak session). There is still no revocation path — see the unusedRevokedstate in the diagram. - Call
/whoamiand/admin/userswith Alice’s token. In LAB_MODE the admin route succeeds — broken function-level authorization.
curl -s http://127.0.0.1:8080/whoami -H "Authorization: Bearer $TOKEN"
curl -s http://127.0.0.1:8080/admin/users -H "Authorization: Bearer $TOKEN"
Expect username/role from whoami, and a user list (alice, bob,
admin) from the admin route.
4. Login as Bob; confirm Bob cannot be Alice by comparing sub.
5. Read issue_token and current_user in labs/notes-api/app.py. List
production fixes (exp, aud/iss, bcrypt, owner checks) — more than
three is fine.
Do not try to brute-force real passwords or reuse this token outside localhost.
Expected observations¶
Payload contains sub and role, likely no exp in LAB_MODE. Alice can
hit /admin/users while LAB_MODE is true. Logs show login_success and
possibly broken_function_authz.
Security lessons¶
A valid token is the start of authorization, not the end. Role in a JWT is a claim you signed; still check it on sensitive functions, and still check object owners.
Common mistakes¶
- Putting PII in JWT and assuming it is secret.
- Checking roles only in the frontend.
- Using the same signing secret across all environments.
- Infinite-lived service tokens “for convenience.”
Cleanup¶
If you copied tokens into a scratch file, delete it. lab-down optional.
Knowledge check¶
- User is logged in and the UI omits the delete button. Is that authorization?
- Difference between OAuth access token and OIDC id_token (conceptually)?
- Why does MFA not fix later DET-002 (cross-user note access)?
- Why is IMDS a service-identity concern?
- Name one RBAC check and one ABAC check in notes-api when LAB_MODE=false.
Answers: (1) No. (2) Access token is for an API; id_token asserts
authentication to the client. (3) Alice is already authenticated; the bug is
AuthZ. (4) It hands out the workload’s cloud credentials to anything that can
HTTP to it. (5) RBAC: admin role for /admin/users. ABAC: owner equals
subject for GET note.
Engineering assignment¶
Document the AuthN/AuthZ path of a service you know: identity provider, token type, expiry, where object checks live, how services authenticate. Note one failure mode. No production credential dumps.
Self-check¶
Answer before expanding. These are the assessment moves on this module's Acme Notes lab, not trivia.
Explain: Alice presents a valid JWT and GET /notes/2 (Bob's). Which question did authentication answer, and which did it not?
Authentication: this caller is Alice (sub). Authorization: may Alice
read note 2 now? A missing owner check is an AuthZ failure after
AuthN succeeded.
Predict: In LAB_MODE, what is in the JWT payload, and what is missing?
You should see sub and role (and the algorithm in the header).
Decoding is not verifying. Expect no exp in lab mode — a weak
session, and still no revocation path.
Diagnose: Alice's token lists /admin/users successfully. Which check failed?
Function-level authorization: a role=user token was treated as
enough for an admin route. That is DET-004's later evidence
(broken_function_authz), not proof Alice became admin.
Design: Name a prevention and a detection for missing exp.
Prevention: mint short-lived tokens with exp (and a revocation story
for theft before expiry). Detection: alert on use of tokens with no
exp, or on reuse after logout if you have a denylist. Logging the
raw token is not a detection.
Defend: Why does MFA not contain a DET-002-class bug?
MFA raises the cost of becoming Alice. Once a valid Alice session exists, object AuthZ still has to run. Residual risk of "MFA everywhere, no owner check" is still Bob's note in the response body.
Before you leave¶
- Predict — write expected evidence (what appears, what does not, and why) before the next observation.
- Diagnose — name the failed invariant from
/whoamivs/admin/usersvs a note read. - Build — complete the identity-flow lab (decode a lab JWT, list production fixes, do not reuse the token off localhost).
- Defend — state containment and residual risk in one sentence each.
- Exit criteria — the course pass bar: Explain → Predict → Diagnose → Design → Defend.