Skip to content

Commit 577f469

Browse files
committed
feat: implement comprehensive SCIM security improvements from PR feedback
- Use secure password comparison with crypto/subtle.ConstantTimeCompare for SCIM basic auth - Create proper auth.scim_providers table with password hashes for provider management - Convert all map-based responses to proper Go structs (ServiceProviderConfig, ResourceTypes, Schemas) - Implement JSON parsing for filter values instead of manual string trimming - Add dedicated scim_external_id column with index instead of raw_app_meta_data storage - Implement comprehensive SCIM provider isolation system ensuring providers only manage their own users - Add database migrations for new scim_providers, scim_external_id, and scim_provider_id columns - Update User model with SCIMExternalID and SCIMProviderID fields using storage.NullString
1 parent 8766c56 commit 577f469

File tree

6 files changed

+611
-374
lines changed

6 files changed

+611
-374
lines changed

internal/api/middleware.go

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package api
33
import (
44
"bytes"
55
"context"
6+
"crypto/subtle"
67
"encoding/json"
78
"fmt"
89
"net/http"
@@ -295,38 +296,58 @@ func (a *API) requireSAMLEnabled(w http.ResponseWriter, req *http.Request) (cont
295296

296297
// requireSCIMEnabled ensures SCIM is enabled
297298
func (a *API) requireSCIMEnabled(w http.ResponseWriter, req *http.Request) (context.Context, error) {
298-
ctx := req.Context()
299-
if !a.config.SCIM.Enabled {
300-
return nil, apierrors.NewNotFoundError(apierrors.ErrorCodeValidationFailed, "SCIM is disabled")
301-
}
302-
return ctx, nil
299+
ctx := req.Context()
300+
if !a.config.SCIM.Enabled {
301+
return nil, apierrors.NewNotFoundError(apierrors.ErrorCodeValidationFailed, "SCIM is disabled")
302+
}
303+
return ctx, nil
304+
}
305+
306+
const scimProviderContextKey = contextKey("scim_provider_id")
307+
308+
func withSCIMProvider(ctx context.Context, providerID string) context.Context {
309+
return context.WithValue(ctx, scimProviderContextKey, providerID)
310+
}
311+
312+
func getSCIMProvider(ctx context.Context) string {
313+
if val := ctx.Value(scimProviderContextKey); val != nil {
314+
if providerID, ok := val.(string); ok {
315+
return providerID
316+
}
317+
}
318+
return "default"
303319
}
304320

305321
// requireSCIMAuth authenticates SCIM requests via Bearer token or Basic auth
306322
func (a *API) requireSCIMAuth(w http.ResponseWriter, req *http.Request) (context.Context, error) {
307-
ctx := req.Context()
308-
cfg := a.config.SCIM
309-
310-
// Bearer token
311-
authz := req.Header.Get("Authorization")
312-
if m := bearerRegexp.FindStringSubmatch(authz); len(m) == 2 {
313-
token := m[1]
314-
for _, t := range cfg.Tokens {
315-
if t != "" && t == token {
316-
return ctx, nil
317-
}
318-
}
319-
}
320-
321-
// Basic auth
322-
user, pass, ok := req.BasicAuth()
323-
if ok && cfg.BasicUser != "" && cfg.BasicPassword != "" {
324-
if user == cfg.BasicUser && pass == cfg.BasicPassword {
325-
return ctx, nil
326-
}
327-
}
328-
329-
return nil, apierrors.NewForbiddenError(apierrors.ErrorCodeInvalidCredentials, "Invalid SCIM credentials")
323+
ctx := req.Context()
324+
cfg := a.config.SCIM
325+
326+
// Bearer token
327+
authz := req.Header.Get("Authorization")
328+
if m := bearerRegexp.FindStringSubmatch(authz); len(m) == 2 {
329+
token := m[1]
330+
for i, t := range cfg.Tokens {
331+
if t != "" && t == token {
332+
// Use token index as provider ID for isolation
333+
providerID := fmt.Sprintf("token_%d", i)
334+
return withSCIMProvider(ctx, providerID), nil
335+
}
336+
}
337+
}
338+
339+
// Basic auth
340+
user, pass, ok := req.BasicAuth()
341+
if ok && cfg.BasicUser != "" && cfg.BasicPassword != "" {
342+
if subtle.ConstantTimeCompare([]byte(user), []byte(cfg.BasicUser)) == 1 &&
343+
subtle.ConstantTimeCompare([]byte(pass), []byte(cfg.BasicPassword)) == 1 {
344+
// Use basic auth username as provider ID
345+
providerID := fmt.Sprintf("basic_%s", user)
346+
return withSCIMProvider(ctx, providerID), nil
347+
}
348+
}
349+
350+
return nil, apierrors.NewForbiddenError(apierrors.ErrorCodeInvalidCredentials, "Invalid SCIM credentials")
330351
}
331352

332353
func (a *API) requireManualLinkingEnabled(w http.ResponseWriter, req *http.Request) (context.Context, error) {

0 commit comments

Comments
 (0)