Three separate questions
- Lesson 1
- 45 min
- Module 1: Identity and permission basics
Lesson 1 of 19 · Authentication, Permissions and Multi-Tenant ApplicationsThree separate questions
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
Ask three separate questions of every request to the ticket API: who is calling, in which organization, and what may they do there. Set up the course project, and see that its one shared key answers none of them well.
Before you startWhat to know and to have ready
What you need to know
- The ticket API and its API key in the
X-API-Keyheader, from Basic protection in Build and Deploy Your First API. - The project of Databases and Storage for AI Applications: PostgreSQL in Docker, migrations,
python -m scripts.loadandpsql. This course starts from its last version. - A terminal,
cd, and a virtual environment.
What you need on your computer
- Python 3.12 or later, and Docker Desktop, as in the databases course. Install and verify in the Docker course shows the Docker install.
- About 2 GB of free disk space. No account, no key and no money.
You start a new project folder, ~/projects/ticket-api, from the snapshot start. It is the databases course’s project at its end, with no change.
A door code is not a name badge
Section titled “A door code is not a name badge”Think of an office building with one door code. Everybody who works there knows the code. The door opens for anyone who types it.
The code works well while one small company uses the building. Then a second company rents the second floor. Now the code has three problems. The door cannot say who came in: the cleaner, the manager and a visitor all type the same digits. It cannot say which company the person visits. And it cannot say what they may do inside: the code opens every room on every floor.
A good reception desk asks three separate questions instead. Who are you? Show your badge. Which company are you visiting? Is your name on their list? What may you do there? A visitor may sit in the meeting room, but not open the cash box.
The ticket API has a door code. In this lesson, you see exactly what it can and cannot answer.
Larkfield gets a second shop
Section titled “Larkfield gets a second shop”You know Larkfield’s help desk from the earlier courses. Grace leads the support agents, and Sam answers tickets. Omar, the product manager, reads tickets to plan the next features. Amira builds the ticket API. Tomás is an AI engineer who likes shortcuts.
Larkfield now offers its help-desk service to a second shop, Bramble Books, an independent bookshop. Its owner is Ines. Bramble Books will keep its customers, tickets and files in the same API and the same database. Camille, an experienced Larkfield agent, will help Ines in her first weeks.
Kwame joins the team as solution architect. He guides the design in this course. Before any code, he asks one thing of every request: “Who is the caller, what may they do, and in which organization?”
Set up the project
Section titled “Set up the project”The project is ticket-api as the databases course left it. If you finished that course, your folder is the same as start. Make a new copy anyway: this course changes it step by step, and the lessons expect the same files as yours.
-
Get the files of the course. If you cloned the labs repository for an earlier course, update it in
~/projects:Terminal window cd ~/projects/nextia-learning-labsgit pullcd ..If you do not have it yet, clone it in
~/projectswithgit clone https://github.com/Nextia-Intelligence/nextia-learning-labs.git. Without Git, download the ZIP file and unzip it in~/projects. -
If a folder
ticket-apiexists already, from the databases course, first stop its containers and delete their data. Docker names the containers and the data volumes after the folder. Your new folder has the same name, so it would reuse the old database, with its old password and its old data:Terminal window cd ticket-apidocker compose down --volumescd ..Your old files stay, and the course can load the data again at any time. Then rename the old folder, for example to
ticket-api-databases. Copy the snapshotstart, go into it, and make your.env:Terminal window cp -R nextia-learning-labs/C23/snapshots/start ticket-apicd ticket-apicp .env.example .envTerminal window Copy-Item -Recurse nextia-learning-labs\C23\snapshots\start ticket-apicd ticket-apiCopy-Item .env.example .env -
Open the folder in VS Code. In
.env, replacechange-me-local-onlywith a password of your own, in all three places. -
Start Docker Desktop. Then start PostgreSQL and Azurite, the object storage for files:
Terminal window docker compose up -d db azuriteRun
docker compose psuntil thedbline says(healthy). -
Make the virtual environment and install the packages:
Terminal window python3 -m venv .venvsource .venv/bin/activatepython -m pip install -r requirements.txtTerminal window python -m venv .venv.venv\Scripts\Activate.ps1python -m pip install -r requirements.txt -
Load the data:
Terminal window python -m scripts.loadapplied 001_create_classificationsapplied 002_add_score…applied 012_erasureDatabase is up to date (12 migrations).Loaded the small data in 1.9 s:customers 40tickets 200messages 331attachments 43documents 36ai_runs 294ticket_embeddings 20043 files uploaded to the object storage.3 messages have no ticket: they are in orphaned_messages.Lines 003 to 011 are left out here. The twelve migrations are the databases course’s. Your time differs.
-
Run the tests:
Terminal window python -m pytestThe last line is
108 passed, 1 deselected. The deselected test needs Azurite and runs only with-m azurite, as in the databases course.
If you see these two results, your setup works. The course tested every step on macOS (Apple silicon). The Windows and Linux commands are written, not run.
Ask the API the three questions
Section titled “Ask the API the three questions”Now put Kwame’s three questions to the API itself. Start it with an API key, as in the API course. Open a second terminal in the project folder, activate the virtual environment, and run:
API_KEY=local-dev-key fastapi dev$env:API_KEY = "local-dev-key"fastapi devIn the first terminal, ask for ticket T-30002 without the key. On Windows, type curl.exe instead of curl:
curl -i http://127.0.0.1:8000/v1/tickets/T-30002HTTP/1.1 401 Unauthorizedcontent-type: application/jsonx-request-id: 7bbff66891b8
{"error":{"code":"unauthorized","message":"A valid X-API-Key header is required.","request_id":"7bbff66891b8","fields":[]}}Some headers are left out. Send the same request with the key:
curl -H "X-API-Key: local-dev-key" http://127.0.0.1:8000/v1/tickets/T-30002{"ticket_id":"T-30002","customer_id":"C-0022","customer_name":"Hugo Garcia", "subject":"Charged twice for order LK-182074","team":"billing","priority":1,"status":"open", "message_count":1,"messages":[{"message_id":2,"author":"agent","body":"Could you send the receipt …"}], …}The answer is shortened here. Now add a message to the ticket, as an agent does:
curl -X POST -H "X-API-Key: local-dev-key" -H "Content-Type: application/json" -d '{"author": "agent", "body": "We refunded one payment."}' http://127.0.0.1:8000/v1/tickets/T-30002/messages{"message_id":332,"author":"agent","body":"We refunded one payment.","created_at":"2026-10-10T06:09:21.800162Z"}In PowerShell, quotes inside a string work differently. Put the body in a file, msg.json, and send it with -d "@msg.json".
Look at the API’s terminal. The API wrote one line for each request:
INFO ticket_api: GET /v1/tickets/T-30002 401 10ms queries=0 id=7bbff66891b8INFO ticket_api: GET /v1/tickets/T-30002 200 27ms queries=3 id=3b9d4f4fea83INFO ticket_api: POST /v1/tickets/T-30002/messages 201 11ms queries=3 id=89aa010fe214Now ask the three questions of the last request.
Who wrote the message? The log says POST, the path, 201, a time and a request ID. The database row says author: agent. Grace, Sam and Omar all send the same key, so the request is the same, byte for byte, whoever sent it.
Text description of the diagram
Grace, Sam and Omar each send POST /v1/tickets/T-30002/messages with the header X-API-Key: local-dev-key. The API compares the key with its setting, and it matches, so it saves the message and answers 201 Created. It records a log line and a message whose author is “agent”. Nothing in the request or in the record names the person.
In which organization? The question has no answer yet. There is one shop, so the API has no word for “organization”. Look in psql: docker compose exec db psql -U tickets -d tickets, then \d tickets. The table has ticket_id, customer_id, subject and ten more columns. No column says which shop a ticket belongs to.
What may the caller do? Everything that the key opens. Omar only needs to read tickets. With the key, he can also write messages, upload files and read every ticket. The API cannot give him less, because it does not know that the caller is Omar.
Type \q to leave psql. Stop the API with Ctrl+C.
The idea: three separate questions
Section titled “The idea: three separate questions”Kwame writes the three questions on the whiteboard. Each one has a name.
Authentication answers “Who is the caller?” The caller proves who they are, and the API checks the proof. A shared key proves only “this caller has the key”. From Module 2, a person signs in, and the API receives a user ID: a stable name for that one person, such as usr-sam.
The tenant answers “In which organization?” A tenant is one customer organization whose data lives in a shared application. The word comes from a building: many tenants rent space in it, and each one keeps its own rooms. Larkfield and Bramble Books are the two tenants of the ticket API. “Multi-tenant” means that one application serves several of them.
Authorization answers “What may they do there?” The API checks whether this person may do this action in this organization. Authentication comes first: you cannot decide what a person may do before you know who they are.
Text description of the diagram
The three questions, in the order the API asks them. First, who is the caller: authentication. Second, in which organization: the tenant. Third, what may they do there: authorization. Today’s API answers “someone who has the key”, “no answer” and “everything”. At the end of the course, the first answer is “usr-sam, from a checked token”. The second is “larkfield: the path names it, and Sam is a member there”. The third answer is “staff: read and change tickets, upload files”.
Sam asks the obvious question: “Why three? Why not one check, ‘may Sam do this?’”
Kwame points at Camille’s name on the plan. Camille is staff at Larkfield, so Camille reads and answers Larkfield’s tickets. At Bramble Books, Camille only helps Ines, so they may read but not change anything. The same person gets two different answers to “What may they do?” The answer depends on the organization. So the API must ask “in which organization?” before “what may they do?” That is why the diagram numbers the questions in that order.
Tomás has a shortcut ready: “The browser knows which shop the person works for. Let it send the shop’s ID in a header.” Kwame shakes his head. A person can change anything that the browser sends. The API must find the organization in its own data. The request names an organization, and the API checks that the caller is a member there. You build that check in Module 4.
What the shared key answers, and what it cannot
Section titled “What the shared key answers, and what it cannot”| Question | The start’s API | Why it is not enough for two shops |
|---|---|---|
| Who is the caller? | “Someone who has the key.” | Every person and every program has the same key. A log cannot show who did what. |
| In which organization? | No answer. | Bramble’s tickets will sit next to Larkfield’s, and nothing keeps them apart. |
| What may they do? | Everything the key opens. | Omar may only read, but the key lets him write. |
The key is not wrong. It is the right tool for a different question. The API course used it for one program that calls another: a service-to-service check. The question there is “Is this one of our programs?”, and a key answers that well. At the end of this course, the key still protects /v1/classify and /v1/history, which other programs call. Every route that a person uses moves to signed-in users.
People need more. Each person needs their own identity, a list of the organizations they belong to, and a role in each one. That costs real work, and this course does it one step at a time. You add an identity provider, which signs people in and issues tokens. You add token checks in the API, a membership table, and a check on every route. Then you write tests that try every person against every route.
In the next lesson, you build the data that answers the second and third questions: people, organizations, memberships and roles. Then Threat examples shows what goes wrong while the API still has only the key. It tries five made-up attempts against your own practice API, and all five get what they should not.
Common mistakes
Section titled “Common mistakes”Common mistake
docker compose up: port is already allocated
Symptom: docker compose up -d db azurite prints this, and db does not start:
Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint ticket-api-db-1 (…): Bind for 127.0.0.1:5432 failed: port is already allocatedCause: another container already uses port 5432 on your computer. Usually it is the database of another project folder, for example from another course.
Diagnose: run docker ps. Look for a container with 127.0.0.1:5432->5432/tcp. The first part of its name is the name of its project folder.
Fix: in that folder, run docker compose down. Its data stays in its volume. Then run docker compose up -d db azurite again in ticket-api.
Common mistake
Every request with the key gets 401
Symptom: curl -H "X-API-Key: local-dev-key" … answers 401 and A valid X-API-Key header is required.
Cause: the API runs with another key, or the header name is wrong. If the API runs with no key at all, a request without a key gets 200. Then the API’s terminal shows API_KEY is not set: /v1/classify accepts requests without a key.
Fix: stop the API with Ctrl+C, and start it again with API_KEY=local-dev-key fastapi dev (PowerShell: set $env:API_KEY first). Check the header name: X-API-Key, with a colon after it.
Reset. python -m scripts.load --reset deletes the help-desk data, with your new message, and loads it again. docker compose down --volumes deletes the whole database and the files. After it, run docker compose up -d db azurite and python -m scripts.load.
Knowledge check
Section titled “Knowledge check”Check your understanding
Name the three questions that the API must answer for every request, and the technical name of each. Why does the API ask about the organization before it decides what the caller may do?
Show the answer
Who is the caller? That is authentication. In which organization? That is the tenant. What may they do there? That is authorization. The organization comes before the permission, because one person can have different roles in different organizations. Camille is staff at Larkfield and read-only at Bramble Books. So “may Camille add a message?” has no answer until the API knows the organization.
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
Predict: Omar only reads tickets in his work. He sends POST /v1/tickets/T-30002/messages with X-API-Key: local-dev-key to the start's API. What does the API answer, and what does the database record about who wrote the message?
Show the answer
201 Created. The key is right, and the key is the only thing that the API checks. The message row says author: agent, and the log line says POST /v1/tickets/T-30002/messages 201. Neither one names Omar. The API cannot refuse him, because it cannot tell his request from Sam’s.
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: show that your project works. Which commands do you run, and what must they print?
Show the answer
python -m scripts.load --reset ends with the counts (customers 40, tickets 200, messages 331, attachments 43, documents 36, ai_runs 294, ticket_embeddings 200) and 43 files uploaded to the object storage. python -m pytest ends with 108 passed, 1 deselected. With API_KEY=local-dev-key fastapi dev running, curl http://127.0.0.1:8000/v1/tickets/T-30002 answers 401, and the same request with -H "X-API-Key: local-dev-key" answers with the ticket.
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.
Case studies in this course
At the end of the course, you use what you learn on a realistic project, from the data to the recommendation. Complete at least one for the certificate; the others are optional.
- Audit access control in an open-source multi-tenant appRun a permissively licensed open-source multi-tenant application on your computer, map its users, organizations and roles, and test its isolation with the course's access matrix.
- Add sign-in and roles to an existing APIAdd the mock identity provider, token validation and roles to the data API of the API course's case study, test it with an access matrix, then map the design to a real identity provider.
Recap
- Every request must answer three questions: who is the caller (authentication), in which organization (the tenant), and what may they do there (authorization).
- The API asks about the organization before the permission, because a person’s role depends on the organization.
- The start’s shared key says only “the caller has the key”. Grace, Sam and Omar send the same bytes, and the API records
author: agent. - A key stays right for one program that calls another. People need their own identity, memberships and roles.
- Your project
~/projects/ticket-apiis the snapshotstart:108 passed, 1 deselected.
Next, you add Bramble Books, the people and their memberships to the database, and you write down who may do what.
Next: Users and organizations