An interactive Docker-based demo that shows how clock skew between a SAML Identity Provider (IdP) and Service Provider (SP) causes authentication failures, and how allowedClockDrift fixes them.
- Why SAML assertions carry
NotBeforeandNotOnOrAftertimestamps - How even a small clock difference between IdP and SP can break authentication
- How the
allowedClockDrift(clock skew tolerance) setting compensates for drift - A visual timeline that makes the math immediately obvious
- Docker and Docker Compose (v2+)
docker compose up --buildThen open http://localhost:8080 in your browser.
Keycloak takes ~30-40 seconds to start. The SP waits automatically.
Port conflict? If port 8080 is already in use, change the SP port mapping in
docker-compose.yml(e.g."8081:5000") and updateSP_BASE_URLaccordingly.
- Open http://localhost:8080 in your browser.
- You will see two cards:
- "Controls" — two live clocks (IdP / Real Time and SP Perceived Time), two sliders (SP Clock Offset and Allowed Clock Drift), an Apply Settings button, and a Login with SAML button.
- "What is Clock Skew?" — a brief explanation and an architecture diagram.
- Both sliders should already be at
0. No need to change anything for this first test. - Click the blue Login with SAML button. You will be redirected to the Keycloak login page at
localhost:8180. - Enter username
demoand passworddemo, then click Sign In. - Keycloak redirects you back to the SP. You should see a green "Authentication Succeeded" result page, with a timeline showing the SP's time (green marker) sitting inside the blue assertion validity window.
- Back on the SP page (http://localhost:8080), drag the SP Clock Offset slider to
+300(or type300in the number field). This simulates the SP's clock being 5 minutes ahead of the IdP. - Click Apply Settings to save the new offset.
- Click Login with SAML. Because you already authenticated in Step 1, Keycloak has an active session — it will skip the login form and immediately issue a new assertion.
- You should see a red "Authentication Failed" result. The SP rejected the assertion because, from its perspective, the assertion has already expired — look at the timeline: the red SP marker sits to the right of the blue validity window.
- Click Back — Try Different Settings to return to the controls page.
- Keep Clock Offset at
+300. - Set Allowed Drift to
300(drag the second slider or type the value). This tells the SP to tolerate up to 300 seconds of clock difference. - Click Apply Settings, then Login with SAML.
- This time you should see a green "Authentication Succeeded". The timeline now shows an orange extended window (
NotOnOrAfter + 300s) that covers the SP's perceived time.
- Click Back — Try Different Settings.
- Set Clock Offset to
-300(SP clock is 5 minutes behind the IdP) and Allowed Drift back to0. - Click Apply Settings, then Login with SAML.
- The SP rejects the assertion because, from its perspective, the assertion is not yet valid — the SP marker now sits to the left of the blue window.
- Set Allowed Drift to
300, click Apply Settings, and Login with SAML again — it succeeds because the extended window now starts atNotBefore − 300s.
Click Logout & Reset Demo to clear the SP session, reset sliders to zero, and log out of Keycloak. The next login will prompt for credentials again.
SAML Assertion
┌─────────────────────────┐
│ │
NotBefore NotOnOrAfter
│ │
───────────────┼─────────────────────────┼─────────────── time
│ valid window │
│ │
With allowedClockDrift = D:
NB − D NOA + D
│ │
───────┼──────────────────────────────────────┼───────── time
│ extended valid window │
The SP checks: (NotBefore − drift) ≤ now < (NotOnOrAfter + drift)
If the SP's clock is ahead or behind by less than the drift tolerance, the assertion is still accepted.
| Service | Internal port | External port | Image |
|---|---|---|---|
| Keycloak (IdP) | 8080 | 8180 | quay.io/keycloak/keycloak:26.5.6 |
| SP (Flask) | 5000 | 8080 | Custom (Python 3.11) |
The SP simulates clock drift by monkey-patching OneLogin_Saml2_Utils.now() — the function python3-saml uses for all timestamp validation. This is functionally equivalent to the SP's OS clock being offset.
| File | Purpose |
|---|---|
docker-compose.yml |
Service definitions and networking |
sp/app.py |
Flask SP — SAML endpoints, clock simulation, timestamp parsing |
sp/templates/index.html |
Controls — offset slider, drift input, live clocks |
sp/templates/result.html |
Result — success/failure, timeline visualization, assertion details |
idp/realm-export.json |
Keycloak realm with pre-configured SAML client and demo user |
All settings can be changed via environment variables in docker-compose.yml:
| Variable | Default | Description |
|---|---|---|
KEYCLOAK_URL |
http://keycloak:8080 |
Internal Keycloak URL |
KEYCLOAK_REALM |
demo |
Keycloak realm name |
SP_ENTITY_ID |
saml-sp-demo |
SAML SP entity ID |
SP_BASE_URL |
http://localhost:8080 |
SP URL as seen by the browser |
SECRET_KEY |
(set in compose) | Flask session secret |
Available at http://localhost:8180 with credentials admin / admin. You can inspect the SAML client configuration, view the signing certificate, and check event logs.
| Symptom | Fix |
|---|---|
| SP fails to start | Keycloak may still be booting — the SP retries for up to 120 seconds |
| "Invalid issuer" error | Make sure SP_BASE_URL matches the URL in your browser |
| Signature validation fails | Restart both services (docker compose down -v && docker compose up --build) |
| Port conflict on 8080 | Change ports in docker-compose.yml and update SP_BASE_URL |
In production, prefer fixing the root cause (NTP synchronization) over increasing clock drift tolerance. A reasonable allowedClockDrift is 30-120 seconds — enough to cover minor NTP jitter without opening a wide replay window.