// cat case-file.md
🏥

MedLink

HEALTH PORTAL · TEAM OF 6
A secure health portal that takes a three-clinic network (Sousse, Sfax & Tunis — ~40 practitioners, 15,000 patients) off a phone-only system: patients book appointments and read results online, practitioners get a unified multi-site agenda, and staff run the back-office — on a Spring Boot API behind Keycloak identity, with a React dashboard. I designed and built its authentication & authorization layer.
role: auth & security backend
team: 6 · 45 commits · 11 PRs
context: ESPRIM · Cyber Security labs
status: V1.0.0
api-MedLink ↗ medlink-dashboard ↗
3
roles · patient · practitioner · staff
0
tokens in the browser
5
security scanners in CI
244
automated tests · mostly functional
// scope --what-it-does
The brief: cut the switchboard load and give patients secure, remote access to their records — without weakening medical confidentiality (Tunisian INPDP, data kept in-country).
Patient space
Create an account, book / cancel / reschedule appointments, read results and documents, get reminders.
Practitioner space
A unified agenda across the three sites, the record of the patient in consultation, and consultation reports.
Back-office
Manage accounts across the three clinics, export anonymized activity stats, and read the access audit log.
// my-role --auth-and-security
On a six-person team I owned the identity and access layer of the API — the part a health portal lives or dies by — built test-first (the functional test goes in red, then the feature makes it green).
Session lifecycle
Login through the API (password grant, server to server), logout that ends both our session and Keycloak's, 15-minute idle expiry; a session also ends as soon as Keycloak refuses to renew its token (account disabled, signed out). Every refusal is the API's own JSON error, never a redirect.
Ownership guard
A user can read only their own record; staff can read any. Everyone else gets a 403 — the same answer whether the id exists or not.
Append-only audit trail
Every refused access is written to an append-only table (DB triggers), with the caller, the IP and a redacted path — personal data in the URL never reaches the log.
Access-matrix build gate
One test file holds a role × route matrix: every /api route × 4 callers (anonymous, patient, practitioner, staff). The route list is read from Spring itself, so a new endpoint without an access rule fails the build. An allowed cell must also not answer a server error or a redirect.
Architecture & threat model
Wrote the architecture and threat-model docs and the decision record for the password-grant BFF — trust boundaries and controls mapped to code and tests.
Privilege escalation found & fixed
While writing the threat model I found the staff role carried Keycloak admin rights — with only their own password a staff member could create accounts and read every user straight from Keycloak's admin API, bypassing the API's rules and audit trail. Proved it with a test (201 → 403 after the fix), removed the rights; only the API's service account keeps them.
Metrics off the public port (in review)
In production, Spring Boot Actuator (metrics, health) moves to a private management port that is never published; Prometheus scrapes it over the Docker network, and the public port keeps only /livez and /readyz ({"status":"UP"} only).
// auth --backend-for-frontend
The browser never talks to Keycloak. The API is the only component that does — so the front end holds an HttpOnly session cookie and a readable XSRF token, never an access token, a Keycloak page or a redirect.
Browser · React
Sends email + password and the CSRF token. Gets back the user and an HttpOnly session cookie — and nothing else.
→
api-MedLink
Does the password grant server-to-server, keeps the tokens in the server session, and maps everything to one JSON error shape.
→
Keycloak
Identity, roles (via groups) and per-account lockout after 5 failures. Reached only by the API's confidential clients.
// security --by-design
Deny by default
Routes are closed unless a role rule opens them — @PreAuthorize + an ownership guard for own-data access.
No account enumeration
Register, login and resend answer the same for any email; a locked account answers like a wrong password.
Rate limits
Per-IP on login/register/resend (429 + Retry-After, shared across instances via Postgres); per-account lockout in Keycloak.
CSRF + HttpOnly session
Every write echoes the XSRF token or gets 403; the session cookie is HttpOnly, SameSite=Strict, Secure in prod.
// pipeline --devsecops
Security is enforced on every push, not just reviewed. The team's pipeline gates each pull request before it can reach master.
CodeQL · SAST OWASP Dependency-Check · SCA gitleaks · secrets Trivy · container scan OWASP ZAP · DAST build + functional tests
Deploys are env-gated: one image per run, promoted by immutable digest through integration → staging → production over SSH, with a health check.
// process --how-we-work
Six people shipping to one protected master without stepping on each other — a ticket-driven, test-first flow with the rules enforced by CI, not goodwill.
YouTrack · MLink
Sprints split into sub-sprints, one ticket per task, with stories and epics. A ticket moves Open → In Progress → To Verify (with the PR link) → Done when merged. We comment on a teammate's ticket before touching a shared file.
Git flow
One branch per task, or a shared branch per epic — no stacked PRs. Every PR targets master, which is protected: it needs the code owner's review and a green CodeQL. Merge commits.
Tests first
The functional test is committed red, then made green, then a mutation check confirms it fails when the rule it guards is broken. Tests run in two class orders before every PR, so order can't hide a dependency.
Decisions & threat model
Decision records and a threat model live in the repo — decision 0001 keeps the OAuth password grant as a consciously accepted exception, with its conditions written down.
AI disclosure
Every PR declares whether an AI tool helped and confirms the author reviewed the change — a repo rule, checked by CI.
// stack --list
BACKEND
Java 17 Spring Boot 4.1 Spring Security Flyway
IDENTITY & DATA
Keycloak 26 PostgreSQL
TESTING
Testcontainers functional (over HTTP) Postman / newman
FRONTEND & OPS
React · Vite Docker Compose Terraform GitHub Actions
PROCESS
YouTrack GitHub PRs · code review pre-commit hooks