auth: security audit fixes, rate limits on every route
ci / check (push) Successful in 1m3s
ci / images (push) Successful in 19s
ci / check (pull_request) Successful in 1m5s
ci / images (pull_request) Successful in 5s

Two separate reviews of the SSO branch and all 207 API routes (spec 12 §8.6). Every route was
guarded as meant. Fixed, each with a test:
- login: a directory 503 counts as a failed sign-in; a locked directory account is a refused
  password, not "unavailable"
- open redirect after OpenID sign-in (a tab in next)
- directory email matching only for non-admins without their own password; others connect on
  My settings (POST /api/me/identities/ldap); only admins disconnect a directory account
- OpenID session check reads the refreshed ID token's claims too
- connect is POST /api/auth/oidc/connect (CSRF header)
- stored bind password, client secret and SMTP password tied to their server
- 409 role_managed when a provider sets the role; empty Contrack PATCH is a 400
- @fastify/rate-limit on every route (RATE_LIMIT_PER_MINUTE), 10/min on Test buttons; a
  password asked again shares the login limiter
Live tests pass against lldap 0.6.3, Samba 4 AD and Keycloak 26.8.1.
This commit is contained in:
iamdoubz committed 2026-10-26 09:46:23 -05:00
1 parent 19610777a4
commit 97e67e347d
39 files changed
+1059 -186

No files matched your search

+15 -5
View File
@@ -128,12 +128,22 @@ describe.skipIf(!hasTestDb || !KC)('OpenID Connect with a real Keycloak (12 S3)'
/** start → Keycloak's login form, filled in like a person would → our callback. */
async function flow(cookie?: string, query = '') {
const start = await t.app.inject({
url: `/api/auth/oidc/start${query}`,
headers: cookie ? { cookie } : {},
});
const connecting = query === '?connect=1';
// Connect is a POST from My account (with the CSRF header); sign-in a GET from the button.
const start = connecting
? await t.app.inject({
method: 'POST',
url: '/api/auth/oidc/connect',
headers: { ...H, cookie: cookie! },
payload: {},
})
: await t.app.inject({
url: `/api/auth/oidc/start${query}`,
headers: cookie ? { cookie } : {},
});
const location = connecting ? (start.json() as { url: string }).url : start.headers.location;
const pending = start.cookies.find((c) => c.name === 'sp_oidc')!;
let res = await browser.get(start.headers.location as string);
let res = await browser.get(location as string);
if (res.status === 200) {
// Not signed in at Keycloak yet: post its login form.
const html = await res.text();