Keycloak Auth¶
Current auth architecture and Keycloak integration status.
Status (March 2026)
Phases 3–4 of Keycloak migration are deferred indefinitely. ShivaVPN continues using custom JWT auth with a Keycloak bridge enabled. The bridge is ON — all clients receive Keycloak tokens, but via the legacy V1 endpoint with auto-fallback.
Current Architecture¶
Four systems coexist:
Client → POST /api/auth/login (header: AuthToken)
↓
AuthServiceImpl.loginV2()
├─ IF keycloakBridgeEnabled (= true in prod):
│ ├─ Try mintUserV2ViaKeycloak() ← Keycloak path
│ │ ├─ KeycloakUserService.authenticateUserOrCreate()
│ │ ├─ DeviceUnificationService.migrateRedisDevicesToKeycloak()
│ │ ├─ KeycloakDeviceService.getOrCreateDeviceClient()
│ │ └─ KeycloakDeviceService.authenticateDevice() → KC JWT
│ │
│ └─ CATCH → mintUserV2() ← Legacy fallback
│ └─ JwtUtils.generateAccessToken() → custom HMAC JWT
│
└─ ELSE: mintUserV2() ← Legacy only
| System | File | Status |
|---|---|---|
| Custom JWT (JwtUtils.java, 809 lines) | security/JwtUtils.java |
@Deprecated |
| Redis Sessions (RedisTokenService.java, 1598 lines) | security/RedisTokenService.java |
Partial legacy |
| Keycloak (KeycloakDeviceService + KeycloakUserService) | security/keycloak/ |
TARGET |
| Migration Bridge (DeviceUnificationService) | security/DeviceUnificationService.java |
@Deprecated |
Keycloak Architecture¶
Realm: shiva-users
- Users = VPN accounts (username = 16-digit account number)
- Password = DEFAULT_PASSWORD (same for all — KC used for identity, not authentication)
- Attributes:
account_id(UUID),primary_device_id
Realm: shiva-devices
- Clients = device sessions
- ClientId format:
dev_{accountId}.{sha256(deviceId)} - Auth:
client_credentialsgrant - Tokens: access (30 min) + refresh (90 days via
offline_accessscope) - JWKS endpoint:
http://10.99.87.249:8180/realms/shiva-devices/protocol/openid-connect/certs
Admin clients:
| Client | Realm | Purpose |
|---|---|---|
shiva-backend |
shiva-users | User CRUD |
shiva-device-manager |
shiva-devices | Device CRUD |
Client Auth Summary¶
| Client | Endpoint | Header | Notes |
|---|---|---|---|
| Android | /api/auth/login |
AuthToken |
V1, gets KC token via bridge |
| iOS | /api/auth/login |
authToken |
V1, gets KC token via bridge |
| Admin | /auth/login/admin |
authToken |
V1, separate admin flow |
| Desktop | RPC (daemon) | N/A | No direct HTTP auth |
All clients receive Keycloak tokens because bridge is ON — but they still use the legacy V1 endpoints.
Migration Phases¶
| Phase | Status | Description |
|---|---|---|
| Phase 1: Metrics | Done | auth.login{path=keycloak} / auth.login{path=legacy} counters |
| Phase 2: Deprecation | Done | @Deprecated on JwtUtils, AuthTokenFilter, DeviceUnificationService |
| Phase 3: Remove Fallback | Deferred | Disable legacy JWT fallback (auth.legacy.fallback.enabled=false) |
| Phase 4: Client Migration + Cleanup | Deferred | Move clients to V2 endpoints, delete ~3300 lines |
Monitoring Auth Traffic¶
# Login distribution (Grafana/Prometheus)
rate(auth_login_total{path="keycloak"}[5m])
rate(auth_login_total{path="legacy"}[5m])
# Refresh distribution
rate(auth_refresh_total{path="keycloak"}[5m])
rate(auth_refresh_total{path="legacy"}[5m])
# Legacy ratio (should trend toward 0 as migration progresses)
auth_login_total{path="legacy"} / auth_login_total
Config¶
Relevant application-prod.properties settings:
# Bridge is ON
auth.keycloak.bridge.enabled=true
# Migration speed (currently conservative)
keycloak.migration.auto.batch-size=3
keycloak.migration.auto.max-users-per-run=5
keycloak.migration.auto.fixed-delay-ms=1800000 # 30 min
# To disable legacy fallback (Phase 3, not yet enabled):
# auth.legacy.fallback.enabled=false
What Will Be Deleted After Migration (Phase 4)¶
| File | Lines | Reason |
|---|---|---|
| JwtUtils.java | 809 | Custom JWT → Keycloak JWT |
| AuthTokenFilter.java | ~150 | Legacy JWT filter → OAuth2 Resource Server |
| DeviceUnificationServiceImpl.java | 265 | Redis↔KC bridge → all devices in KC |
| KeycloakMigrationServiceImpl.java | 337 | One-time migration → done |
| AuthServiceImpl.java (simplify) | -958 | Remove fallback paths |
| RedisTokenService.java (simplify) | -798 | Remove token storage (keep locks/cache) |
| Total | ~3300 |
Keycloak Admin Access¶
Keycloak admin console runs on 10.99.87.249:8180. Access via ssh-internal tunnel: