test(oidc): Keycloak integration test for MED-6 auto-refresh (Nit-5)

Audit 2026-05-10 Nit-5 closure.

WHAT.

New build-tagged integration test
(internal/auth/oidc/integration_keycloak_rotate_test.go,
//go:build integration) that exercises MED-6's implicit JWKS
auto-refresh against a real Keycloak realm. Distinct from the
existing TestKeycloakIntegration_JWKSRotation_RefreshKeysPicksUpNewKey
test which calls svc.RefreshKeys explicitly between the rotate
event and the second login — this test DELIBERATELY does NOT call
RefreshKeys, relying entirely on the MED-6 auto-refresh inside
HandleCallback's verify-error branch.

WHY.

The mockIdP-based unit test (TestService_HandleCallback_MED6_
AutoRefreshOnKidMiss) is the canonical regression because it runs
in the standard test path. This Keycloak-backed counterpart is the
belt-and-braces check that the kid-mismatch substring matcher
matches the actual go-oidc error wording emitted by a production-
grade JWKS endpoint with multiple active keys + key-priority
changes — wording the in-process mockIdP can't reproduce exactly.

HOW.

internal/auth/oidc/integration_keycloak_rotate_test.go (NEW):
  TestKeycloakIntegration_MED6_AutoRefreshOnKidMiss
    1. Baseline login under original key (primes JWKS cache).
    2. fx.RotateRealmKeys(t) — rotate via Keycloak admin REST API.
    3. Fresh login flow WITHOUT explicit RefreshKeys call.
    4. Assert callback succeeds (proves MED-6 auto-refresh fired).

internal/auth/oidc/integration_keycloak_test.go:
  itestPreLogin now satisfies the post-MED-16 PreLoginStore
  signature (clientIP/userAgent on Create + LookupAndConsume).
  Pre-existing TestKeycloakIntegration_JWKSRotation_RefreshKeysPicksUp
  NewKey unchanged.

VERIFY.

- go vet -tags=integration ./internal/auth/oidc/...           PASS
- go vet -tags='integration okta_smoke'
  ./internal/auth/oidc/...                                    PASS

Note: actual integration test run requires the Keycloak testcontainer
(invoked via 'make keycloak-integration-test'); not exercised in this
session because the sandbox lacks Docker. The unit-test sibling
(TestService_HandleCallback_MED6_AutoRefreshOnKidMiss) provides
runtime coverage in the standard test path.

Refs: cowork/auth-bundles-audit-2026-05-10.md Nit-5
      cowork/auth-bundles-fixes-2026-05-10/HANDOFF.md item 20
This commit is contained in:
shankar0123
2026-05-10 23:31:10 +00:00
parent 0df8bf5295
commit 5b8c8c06ff
2 changed files with 112 additions and 6 deletions
@@ -203,23 +203,27 @@ func (s *itestSessionMinter) Revoke(cookieValue string) {
type itestPreLogin struct {
rows map[string]itestPreLoginRow
}
type itestPreLoginRow struct{ providerID, state, nonce, verifier string }
type itestPreLoginRow struct {
providerID, state, nonce, verifier string
// Audit 2026-05-10 MED-16 — UA/IP binding capture.
clientIP, userAgent string
}
func newItestPreLogin() *itestPreLogin {
return &itestPreLogin{rows: make(map[string]itestPreLoginRow)}
}
func (s *itestPreLogin) CreatePreLogin(_ context.Context, providerID, state, nonce, verifier string) (string, string, error) {
func (s *itestPreLogin) CreatePreLogin(_ context.Context, providerID, state, nonce, verifier, clientIP, userAgent string) (string, string, error) {
cookieVal := fmt.Sprintf("pl-keycloak-itest-%d", len(s.rows)+1)
s.rows[cookieVal] = itestPreLoginRow{providerID, state, nonce, verifier}
s.rows[cookieVal] = itestPreLoginRow{providerID, state, nonce, verifier, clientIP, userAgent}
return cookieVal, "ses-" + cookieVal, nil
}
func (s *itestPreLogin) LookupAndConsume(_ context.Context, cookie string) (string, string, string, string, error) {
func (s *itestPreLogin) LookupAndConsume(_ context.Context, cookie string) (string, string, string, string, string, string, error) {
r, ok := s.rows[cookie]
if !ok {
return "", "", "", "", oidc.ErrPreLoginNotFound
return "", "", "", "", "", "", oidc.ErrPreLoginNotFound
}
delete(s.rows, cookie)
return r.providerID, r.state, r.nonce, r.verifier, nil
return r.providerID, r.state, r.nonce, r.verifier, r.clientIP, r.userAgent, nil
}
// ---------------------------------------------------------------------------