Runbook: Authentication end-to-end verification (Task 5.3)¶
| Field | Value |
|---|---|
| Authority | ADR-011; keycloak-and-auth.md |
| Task | 5.3 Authentication |
| Scope | Verify Keycloak login -> gateway JWKS validation -> identity propagation -> 401 on no token |
Per ADR-011 Keycloak is the identity provider and token issuer; identity-service does not mint JWTs.
This runbook proves the live flow. The deterministic CI proof lives in the api-gateway tests
(JwtClaimsFilterTest, GatewayAuthenticationIntegrationTest).
Prerequisites¶
cd infra
make auth # starts core services + Keycloak (compose profile: auth)
The telco-crm realm (roles, the telco-roles claim mapper, the telco-web / telco-gateway
clients, and the local demo users) is imported from infra/docker/keycloak/realm/realm-export.json
on startup. Realm import skips an already-existing realm, so after editing the realm file run
make destroy (drops the volume) before make auth to re-import.
Start the gateway (and config-server + discovery-server it depends on), e.g.:
mvn -f microservices/api-gateway/pom.xml spring-boot:run
Demo users (local only): admin@telco.local / admin (ADMIN),
agent@telco.local / agent (CALL_CENTER_AGENT), subscriber@telco.local / subscriber (SUBSCRIBER).
1. Obtain a token (AC 5.3.1)¶
TOKENS=$(curl -s http://localhost:8085/realms/telco-crm/protocol/openid-connect/token \
-d grant_type=password \
-d client_id=telco-gateway \
-d client_secret=local-dev-secret \
-d username=admin@telco.local \
-d password=admin)
echo "$TOKENS" | jq '{has_access: (.access_token != null), has_refresh: (.refresh_token != null)}'
ACCESS=$(echo "$TOKENS" | jq -r .access_token)
Expected: both has_access and has_refresh are true. (telco-gateway has
directAccessGrantsEnabled for this password-grant path.)
Decode the access token payload and confirm the required claims:
echo "$ACCESS" | cut -d. -f2 | base64 -d 2>/dev/null | jq '{sub, roles, iss, exp}'
Expected: sub (a UUID), a flat roles array containing ADMIN, iss ending in
/realms/telco-crm, and a numeric exp.
2. Reach a protected route with a valid token (AC 5.3.2)¶
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer $ACCESS" \
http://localhost:8080/api/v1/users/me
Expected: the gateway validates the token against the realm JWKS and forwards the request with
X-User-Id (from sub) and X-User-Roles (from roles) injected. The downstream identity-service
endpoint arrives in Sprint 5.5; until then a 404/503 from the gateway still confirms the token
was accepted (auth passed before routing). The 200-with-headers path is asserted today by
GatewayAuthenticationIntegrationTest.
3. Reject a request without a token (AC 5.3.2)¶
curl -s -w "\n%{http_code}\n" http://localhost:8080/api/v1/users/me
Expected: HTTP 401 with the ApiResult-shaped envelope
{"success":false,"error":{"code":"UNAUTHORIZED","message":"Authentication required"}}.
A malformed/expired token (-H "Authorization: Bearer not-a-jwt") likewise returns 401.
Acceptance criteria mapping¶
- 5.3.1: Step 1 - user obtains access + refresh tokens; access token carries
sub, flatroles, issuer, expiry. - 5.3.2: Step 2 - valid token accepted and identity propagated; Step 3 - missing/invalid token -> 401.