The threat model
You will read what Odudu defends against, so you can check it against what you need. Each defence names the decision that made it and how it is held in place, and each says where it stops. Odudu has not been reviewed by anyone outside the project, so treat this as the project's own account and test the parts you depend on.
What Odudu protects
- People's credentials: passwords, passkeys, authenticator secrets and recovery codes.
- The tokens it issues: authorization codes, access, ID and refresh tokens.
- Each tenant's data, against every other tenant on the same server.
- The keys it signs with, and the console's own access.
- The administrators' authority, so one cannot quietly widen their own.
Who it assumes the attacker is
- Someone who can reach the server over the network and send any request, including to endpoints that need no sign-in.
- Someone who controls a client app a tenant registered, or a web page a signed-in person visits.
- A tenant's administrator trying to reach beyond what they were given.
- A bug in Odudu's own code that lets a request reach data it should not.
It does not assume an attacker who already controls the server's machine, its database or its environment. That attacker holds the keys, and Odudu has nothing left to defend.
What stands in each attacker's way
| Threat | Defence | Where it stops |
|---|---|---|
| A tenant reads another tenant's rows | Two independent layers: code cannot build a query without a tenant, and the database forces row-level security on the serving role (ADR 0009). The project's rule is that each repository method is probed with another tenant's id; those probes are written per package, for example in packages/account/tests/action-tokens.int.test.ts and packages/authn-flows/tests/executions.int.test.ts, and no single check counts that every method has one. | The owner connection bypasses row-level security, because it must resolve which tenant a request names. A bug on that path is not caught by the database. The owner role is for migrations and that one read. |
| Credentials read in transit | The server refuses to start in production unless ODUDU_TLS=true, and cookies take the __Host- prefix and Secure when it is (ADR 0020). | Odudu ends no TLS. ODUDU_TLS is your statement that a proxy does, and the server cannot check it. |
| An authorization code intercepted and replayed | PKCE is required of every client with no exception (ADR 0016). A replayed code revokes the grant it produced. | An app that cannot do PKCE cannot use Odudu. |
| A stolen refresh token used | Refresh tokens rotate, and presenting a spent one revokes the whole family. The grant is decided again inside the rotating transaction (ADR 0019). | A thief who uses the token before its owner is not caught until the owner uses the older one. |
| Guessing a password | Passwords are hashed with Argon2id, accounts lock after repeated failures, and sign-in is throttled per origin, address and username (ADR 0023). | The throttle is a window in one process's memory. It is exact for the one instance Odudu runs and would not be shared between several. |
| A page framed by another site | Pages the server renders are sent with frame-ancestors 'none' and a strict policy, built by one function that packages/kernel/src/page.test.ts pins (ADR 0018, ADR 0029). | A page that is meant to be shown by a client in a frame, such as a front-channel logout page, declares the origins it frames (ADR 0034). |
| The server tricked into fetching an internal address | A client's jwks_uri or logout address must be https, and the address a lookup returns is checked, every one of them, before the connection is made to it (ADR 0028). | ODUDU_ALLOW_PRIVATE_CLIENT_URLS opens this on purpose for development, and production refuses to start with it on. The lookups have no deadline yet (P11 on the roadmap). |
| An administrator widening their own power | A change that alters who can sign in as a subject, or which capabilities a subject holds, is held to the caller's own capabilities, at every route that reaches it (ADR 0040). | The rule covers the routes the decision names. A new route that changes who can sign in must be added to it. |
| Tokens stolen from the browser through the console | The console keeps tokens on the server, encrypted, behind an HttpOnly cookie, and the browser never holds one (ADR 0038). | The encryption key is ODUDU_KEK, held in the server's environment. |
| The audit trail filled with noise to hide something | A refusal becomes a row only where something bounds how many a caller can cause (ADR 0037). | A refusal that names no known caller is a line in the server's log, not a row in the trail. Only the admin API and the console read the trail. Nothing delivers it to a monitoring system yet (P10). |
What is not covered
These are gaps, not choices, and each is placed on the roadmap.
- No outside review, and no penetration test.
- Secrets live in the environment. The encryption key and the database passwords are read from environment variables, with no secret store (P12).
- One instance. The throttles and the migration step are not safe to share between instances (P11).
- No deadline on two outbound lookups the token and user info endpoints make (P11).
- The deployment is yours. Odudu cannot see that TLS really ends in front of it, that the database is not reachable from the internet, or that the proxy overwrites the forwarding headers. Hardening lists every one.
- Features it does not have, such as DPoP, pushed authorization requests and SAML, are not partly done. They are absent (P13, P8).
How these claims are checked
Each row names a decision record that argues the defence and the alternatives it rejected. Some defences are also held by tests that attack them: the refresh-token reuse and code-replay paths in packages/protocol-oidc/tests/, the framing policy in packages/kernel/src/page.test.ts and packages/protocol-oidc/src/view/html-response.test.ts, and the start-up guards in apps/server. Not every row has such a test, and this page does not say which do. A guard in the server's start-up refuses the settings that would undo some of them. If you find a row that is not true of the code, that is a vulnerability, and Disclosure is how to tell us.