From 83738f03fd248961b995dcfc902efa2f6bcfea86 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Wed, 16 Sep 2026 22:21:50 +0530 Subject: [PATCH] Add DeleteOIDCAuthRequest so SSO state tokens can be single-use Storage had CreateOIDCAuthRequest and GetOIDCAuthRequest but no delete for any SSO auth request type; upstream relies on TTL expiry alone. An OIDC implementation needs to delete the request on read so a state token cannot be replayed within its TTL window. Adds DeleteOIDCAuthRequest to IdentityService and to the services.Identity interface, using the same backend key construction and error conventions as the neighbouring Get. local.IdentityService is the only concrete implementer of services.Identity, so no other type required changes. Co-Authored-By: Claude Opus 5 --- lib/services/identity.go | 3 ++ lib/services/local/users.go | 8 ++++ lib/services/local/users_test.go | 67 ++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/lib/services/identity.go b/lib/services/identity.go index a152020e2787..af38d53babbc 100644 --- a/lib/services/identity.go +++ b/lib/services/identity.go @@ -226,6 +226,9 @@ type Identity interface { // GetOIDCAuthRequest returns OIDC auth request if found GetOIDCAuthRequest(ctx context.Context, stateToken string) (*types.OIDCAuthRequest, error) + // DeleteOIDCAuthRequest deletes OIDC auth request + DeleteOIDCAuthRequest(ctx context.Context, stateToken string) error + // CreateSAMLConnector creates a new SAML connector. CreateSAMLConnector(ctx context.Context, connector types.SAMLConnector) (types.SAMLConnector, error) // UpdateSAMLConnector updates an existing SAML connector diff --git a/lib/services/local/users.go b/lib/services/local/users.go index 3c6fad42fb18..6e4228cba4c6 100644 --- a/lib/services/local/users.go +++ b/lib/services/local/users.go @@ -1715,6 +1715,14 @@ func (s *IdentityService) GetOIDCAuthRequest(ctx context.Context, stateToken str return req, nil } +// DeleteOIDCAuthRequest deletes OIDC auth request +func (s *IdentityService) DeleteOIDCAuthRequest(ctx context.Context, stateToken string) error { + if stateToken == "" { + return trace.BadParameter("missing parameter stateToken") + } + return trace.Wrap(s.Delete(ctx, backend.NewKey(webPrefix, connectorsPrefix, oidcPrefix, requestsPrefix, stateToken))) +} + // UpsertSAMLConnector upserts SAML Connector func (s *IdentityService) UpsertSAMLConnector(ctx context.Context, connector types.SAMLConnector) (types.SAMLConnector, error) { if err := services.ValidateSAMLConnector(connector, nil); err != nil { diff --git a/lib/services/local/users_test.go b/lib/services/local/users_test.go index 8097a9c05500..f673bc6ff2f8 100644 --- a/lib/services/local/users_test.go +++ b/lib/services/local/users_test.go @@ -1785,3 +1785,70 @@ func TestIdentityService_SSOMFASessionDataCRUD(t *testing.T) { _, err = identity.GetMFASessionData(ctx, sd.RequestID) require.True(t, trace.IsNotFound(err)) } + +func TestIdentityService_OIDCAuthRequestCRUD(t *testing.T) { + t.Parallel() + ctx := context.Background() + clock := clockwork.NewFakeClock() + identity := newIdentityService(t, clock) + + req := types.OIDCAuthRequest{ + ConnectorID: "google", + StateToken: "test-state-token", + CSRFToken: "test-csrf-token", + } + + // Verify create. + err := identity.CreateOIDCAuthRequest(ctx, req, time.Minute) + require.NoError(t, err) + + // Verify read. + got, err := identity.GetOIDCAuthRequest(ctx, req.StateToken) + require.NoError(t, err) + require.Equal(t, req.ConnectorID, got.ConnectorID) + require.Equal(t, req.StateToken, got.StateToken) + + // Verify delete. + err = identity.DeleteOIDCAuthRequest(ctx, req.StateToken) + require.NoError(t, err) + + // Verify get-after-delete returns NotFound. + _, err = identity.GetOIDCAuthRequest(ctx, req.StateToken) + require.True(t, trace.IsNotFound(err)) + + // Deleting again should also return NotFound. + err = identity.DeleteOIDCAuthRequest(ctx, req.StateToken) + require.True(t, trace.IsNotFound(err)) + + // Deleting or getting with an empty state token should be a BadParameter. + err = identity.DeleteOIDCAuthRequest(ctx, "") + require.True(t, trace.IsBadParameter(err)) +} + +func TestIdentityService_OIDCAuthRequestTTLExpiry(t *testing.T) { + t.Parallel() + ctx := context.Background() + clock := clockwork.NewFakeClock() + identity := newIdentityService(t, clock) + + req := types.OIDCAuthRequest{ + ConnectorID: "google", + StateToken: "expiring-state-token", + CSRFToken: "test-csrf-token", + } + + ttl := time.Minute + err := identity.CreateOIDCAuthRequest(ctx, req, ttl) + require.NoError(t, err) + + // Still readable before TTL elapses. + _, err = identity.GetOIDCAuthRequest(ctx, req.StateToken) + require.NoError(t, err) + + // Advance the clock past the TTL; the request should expire on its own, + // independent of any explicit delete. + clock.Advance(ttl + time.Second) + + _, err = identity.GetOIDCAuthRequest(ctx, req.StateToken) + require.True(t, trace.IsNotFound(err)) +}