Handoff
- Lesson 19
- 45 min
- Module 6: Validate and document the design
Lesson 19 of 19 · Authentication, Permissions and Multi-Tenant ApplicationsHandoff
1. Identity and permission basics
2. Understand login and tokens
3. Sessions and application integration
4. Enforce resource access
5. Background work and administration
6. Validate and document the design
What you will be able to do
Hand the design to another team in a form that they can check, not only read. Generate the security test report from the test results, collect the access matrix and the identity configuration, and write down what is still open. Every claim in the handoff has a command that proves it.
Before you startWhat to know and to have ready
What you need to know
- The test matrix, from Negative testing, and the audit trail, from Auditability.
- The identity configuration and the real provider, from Provider configuration.
- How long access lasts after a change, from Lifecycle changes.
What you need on your computer
- The project after Auditability:
python -m pytestprints452 passed, 1 deselected. - PostgreSQL and Azurite running, and the data loaded. The provider and the API are not needed for this lesson.
At the end of this lesson, your project equals the snapshot end-of-m06, the end of the course. Tested on macOS (Apple silicon); the Windows commands are written but not tested.
Handing over a house
Section titled “Handing over a house”You sell a house. The new owner gets the keys, but keys are not enough. A good seller adds a list. It says which key opens which door, and how to change the alarm code. It has the boiler’s last safety check, with its date. It also names the things not done yet: “the roof window leaks a little; we did not choose a repair”. Each line can be checked. The buyer tries each key, reads the certificate, and looks at the window.
A bad handover is a bag of keys and “it all works”. The first winter shows what “all” meant.
The problem: the design lives in one head
Section titled “The problem: the design lives in one head”Amira built the access control of the ticket API. Larkfield’s operations team will now run it. Two short stories show what goes wrong without a handoff. Both use real results of this course.
The team moves sign-in to Keycloak, the real provider of Provider configuration. Every sign-in fails: “This is not an access token”. Keycloak’s access tokens say typ: JWT, and the API expects at+jwt unless ACCESS_TOKEN_TYPES=JWT. Amira knows. The team spends a day finding out.
Later, a developer sees that the API reads the membership table on every request. “Let us cache it for 30 seconds.” In Lifecycle changes, that setting kept a removed member reading for 30.3 s. Nobody wrote down why the cache is off, so the next person turns it on.
The same problem shows in the evidence. “We tested it” is a claim. Which tests, on which code, with which result? Kwame wants each answer from a command, not from a memory.
The idea: claims with evidence, and open questions
Section titled “The idea: claims with evidence, and open questions”A handoff package is a small set of documents that answers four questions, each with a command that proves the answer:
Text description of the diagram
The first question, who may do what in which organization, is answered by docs/access-matrix.md. The second, how sign-in is set up, by docs/identity-config.md. The third, does the design hold, by docs/test-report.md, which a script writes from the test results. The fourth, how long access lasts and what is still open, by docs/handoff.md. Each document ends in commands: pytest, matrix_report, attempts, audit --check and lifecycle all.
You wrote two of the four documents already: the access matrix in Users and organizations, and the identity configuration in Provider configuration. This lesson adds the test report and the handoff itself.
The test report: generated, never typed
Section titled “The test report: generated, never typed”A report typed by hand can say “all passed” when they did not. So the report is written by a script, from the results of the test run. pytest writes its results as JUnit XML, a standard file that CI tools read. scripts/matrix_report.py runs the matrix with --junitxml, reads each test case’s result, and writes two files: docs/test-report.md for people and reports/access-matrix.json for programs.
-
Copy the report script and the handoff from the snapshot
end-of-m06:Terminal window S=../nextia-learning-labs/C23/snapshots/end-of-m06cp "$S/scripts/matrix_report.py" scripts/cp "$S/docs/handoff.md" docs/Terminal window $S = "..\nextia-learning-labs\C23\snapshots\end-of-m06"Copy-Item "$S\scripts\matrix_report.py" scripts\Copy-Item "$S\docs\handoff.md" docs\ -
Run it:
Terminal window python -m scripts.matrix_report213 of 213 checks passed. Wrote docs/test-report.md and reports/access-matrix.json.It takes some seconds, because it runs the whole matrix.
-
Open
docs/test-report.md. Its start:docs/test-report.md (start) # Security test report: access control**213 of 213 checks passed.** Generated by `python -m scripts.matrix_report` from the results of `tests/test_access_matrix.py` (made-up people and data).Then one table row per person and organization, the four cases with the other shop’s record ID, and the bad identities:
docs/test-report.md (end) ## Bad identities on every route- no identity: 11 of 11 routes answered 401- expired: 11 of 11 routes answered 401- wrong audience: 11 of 11 routes answered 401- ID token: 11 of 11 routes answered 401- no scope: 11 of 11 routes answered 403The table has the same rows and columns as the heat map in Negative testing.
The script exits with code 0 only when every check passed. A CI job can run it and stop a release on code 1. With the member-list bug of Negative testing, it printed 211 of 213 checks passed., exited with 1, and the table marked the cells: | Omar | larkfield | read_only | … | 403 **FAIL** | … |.
A report that lied, and its fix
Section titled “A report that lied, and its fix”While writing this lesson, the author ran the report with PostgreSQL stopped, by accident. The first version of the script printed this:
213 of 213 checks passed. Wrote docs/test-report.md and reports/access-matrix.json.All 213 tests had been skipped, and the script counted “not failed” as “passed”. That is the worst kind of report: green, and false. The script in the snapshot now records a skipped check as SKIPPED, never as passed. With the database stopped, it prints:
0 of 213 checks passed. Wrote docs/test-report.md and reports/access-matrix.json.213 checks did not run (skipped). Is PostgreSQL running?and exits with code 1. Try it: stop the database (docker compose stop db), run the report, and start the database again (docker compose start db). The lesson for every report: count only what passed, and say what did not run.
The identity configuration: what was tested
Section titled “The identity configuration: what was tested”docs/identity-config.md says what each side must know: the provider’s registration (client ID, redirect URI, PKCE, scopes, audience) and the API’s .env. The handoff must also say how much of it is proven. Three providers, three levels of evidence:
Text description of the diagram
The practice provider in idp/ is tested by every test and every lesson. Keycloak 26.8.0 in Docker was tested once: it needed ACCESS_TOKEN_TYPES=JWT, an audience mapper and its basic scope. Microsoft Entra ID is described from its documentation and not tested. Its audience is an api:// address, its scopes are in scp, and the person’s ID is oid or sub. Some choices stay open for any real provider.
| Provider | Evidence | What changed in the API’s settings |
|---|---|---|
Practice provider (idp/) |
every test; every lesson | none: the defaults |
| Keycloak 26.8.0 | one run: sign-in 302 to /app/, Larkfield 200, Bramble 404, check_identity 0 problems |
ACCESS_TOKEN_TYPES=JWT; an audience mapper and the basic scope at Keycloak; a membership row for Keycloak’s own sub, a UUID |
| Microsoft Entra ID | described, not tested | ACCESS_TOKEN_TYPES=JWT; issuer https://login.microsoftonline.com/<directory ID>/v2.0 with version 2 tokens; audience such as api://ticket-api |
Never write “works with Entra ID” when you only read its documentation. Write “described, not tested”, as the course does. The next team then knows where the first real run must happen.
Open choices: write them down
Section titled “Open choices: write them down”Some questions have no answer yet, because they depend on a decision that is not Amira’s. A handoff lists them, so that nobody mistakes silence for a decision. From docs/identity-config.md:
- Which claim is the person’s stable ID. This API uses
sub. Entra ID’ssubdiffers per application; itsoidis the same in every application. If the ID changes, every membership row must move. - Scope names. Some providers do not allow
:in a scope, and some need an audience mapper before the token hasaud: ticket-apiat all. - Logout. Whether the provider supports RP-initiated logout and back-channel logout. Without back-channel logout, a session at the API ends at its next refresh, within 10 minutes.
- Multi-factor sign-in and account recovery. The provider’s job, not the API’s: the API sees only the result.
docs/handoff.md adds the choices of the design itself. The membership cache is off, and the handoff says why. The audit trail needs a retention rule, and a rule for who outside an organization may read it. And somebody must decide what a deleted organization’s AI-run costs need.
Open the handoff and read its five sections: the model, the identity configuration, sessions and tokens over time, tests and evidence, and open choices. Its table of lifetimes is the short form of Lifecycle changes:
| Access token | 10 minutes (+30 s leeway) | nothing: the API cannot recall a token. Membership checks make it useless for an organization at once. || Refresh token | 8 hours, one use each (rotation) | logout (revoked at the provider), a disabled account, a reused old token (the whole family ends) |Check the whole course
Section titled “Check the whole course”The outline’s completion check: pass all cross-tenant and privilege-escalation checks. Run the evidence commands of the handoff, in a fresh state:
-
Load the data again:
python -m scripts.load --reset. -
Run every test:
Terminal window python -m pytest -qThe last line:
452 passed, 1 deselected in 43.50sOne test needs Azurite and is deselected by default. Run it with
python -m pytest -m azurite:1 passed, 452 deselected. -
The cross-tenant and privilege-escalation checks, as a report:
python -m scripts.matrix_reportprints213 of 213 checks passed. -
The made-up attempts:
Terminal window python -m scripts.attempts 2>/dev/null | tail -10 of 10 attempts got what they should not get. -
The audit trail, after the attempts:
python -m scripts.audit --checkprintsChecked 4 audit events: 0 with a token or a secret.. -
Compare your project with the snapshot. Files that are not in git (
.venv,.env,.idp,.tokens,reports) and caches may differ:Terminal window diff -rq . ../nextia-learning-labs/C23/snapshots/end-of-m06 \-x .venv -x .env -x .idp -x .tokens -x reports -x __pycache__ -x .pytest_cache -x .ruff_cacheTerminal window git diff --no-index --stat . ..\nextia-learning-labs\C23\snapshots\end-of-m06diffprints nothing: your project is the course’s end. In PowerShell, the list also shows the ignored folders; only they may differ.
If a check fails, the course is not complete, even if every page works. A working happy path with one failing cross-tenant cell is a data leak with a nice front.
Your turn: a two-tenant demo and a test report
Section titled “Your turn: a two-tenant demo and a test report”Prepare a 10-minute demo for Larkfield’s operations team, as if you hand the API over today. Use only made-up people and your own computer.
- Two tenants, live. Sign in at
http://127.0.0.1:8000/app/as Camille. Show their Larkfield tickets, with a “Reply” button. Switch to Bramble Books: the list has no “Reply” button, because Camille is read-only there. Then send the reply yourself with Camille’s token andcurl, as Omar did in Authorization in the API, and show the 403. Sign in as Tomás: show that he sees nothing. - The evidence. Run
python -m scripts.matrix_reportand showdocs/test-report.md. Runpython -m scripts.attempts. Show the denied events withpython -m scripts.audit --result denied. - The report. Write one page,
docs/demo-notes.md. Name the three questions and where the API answers each. List the test results with their commands, and the evidence level of each provider. End with your two most important open choices, and the person who must decide each.
A good demo shows refusals, not only successes. The audience should leave knowing what fails, how they know, and what is still open.
Common mistakes
Section titled “Common mistakes”Common mistake
The report says 0 of 213
Symptom: python -m scripts.matrix_report prints 0 of 213 checks passed. and 213 checks did not run (skipped). Is PostgreSQL running?
Cause: the test database cannot be reached, so every test skipped itself.
Fix: start the database (docker compose up -d db azurite) and run the report again. Never commit a report with skipped checks.
Common mistake
ModuleNotFoundError: No module named 'tests'
Symptom: python -m scripts.matrix_report stops at once with this error.
Cause: the command runs outside the project folder. The script imports the routes and people from tests/test_access_matrix.py.
Fix: run it from the project folder, with the virtual environment active.
Check your understanding
Why is the test report generated from pytest's JUnit results, and not typed by hand or built from the expected values?
Show the answer
A typed report, or one built from the expected values, can say “passed” for a check that failed or never ran. The JUnit file records what each test case did. The report must also count skipped checks as not passed. The first version counted them as passed, and printed 213 of 213 with the database stopped.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
The handoff says Keycloak was 'tested once' and Entra ID 'described, not tested'. Give one reason why the difference matters to the next team.
Show the answer
A real run finds what documentation hides. The Keycloak run found two bugs that the practice provider had hidden: a PKCE verifier that was too short, and a unique email. Entra ID may have surprises of its own: the next team must plan its first real run, and must not trust the description as proof.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Module check: for each request, give the status and say whether it makes an audit event, with or without an organization. (a) Omar adds a message at Larkfield; (b) Tomás reads Larkfield's T-30002; (c) Sam reads Bramble's T-40001 through Larkfield's path; (d) an expired token lists Larkfield's tickets.
Show the answer
(a) 403, an event with organization larkfield, reason role_cannot. (b) 404, an event without organization, reason not_a_member, the path in its details. (c) 404 “Ticket T-40001 does not exist.”, no event: Sam passes the checks at Larkfield and the lookup finds nothing; only the log has it. (d) 401, an event token.check with reason expired, no actor, no organization and no token. The test matrix checks all four statuses; test_auditability.py checks (a), (b) and (d).
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Practical check (completion): which four commands prove that your project passes all cross-tenant and privilege-escalation checks, and what must each print?
Show the answer
python -m pytest: 452 passed, 1 deselected. python -m scripts.matrix_report: 213 of 213 checks passed. python -m scripts.attempts: 0 of 10 attempts got what they should not get. python -m scripts.audit --check: 0 events with a token or a secret. Your project then equals the snapshot end-of-m06.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Recap
- A handoff package answers four questions with documents and commands. It has the access matrix, the identity configuration, the test report, and the handoff with lifetimes and open choices.
- The test report is generated from the test results:
213 of 213 checks passed.A skipped check is never counted as passed. - Write the evidence level of each provider: the practice provider and Keycloak tested, Entra ID described, not tested.
- Write the open choices down, with who must decide. Examples: the stable person ID, scope names, logout, multi-factor sign-in, the membership cache, the audit’s retention.
Module recap. You tested what must fail for every person, organization and route, recorded every refusal without a secret, and handed the design over with its evidence. Your project equals end-of-m06, the end of the course.
Course recap. The API now answers three questions on every request. Who: a checked token or session. In which organization: the path and the caller’s own membership. What: the membership’s role and the token’s scopes. Every query, file link, search and cache key carries the organization, and the database checks it again.
Next: Audit access control in an open-source multi-tenant app