mirror of
https://github.com/coder/coder.git
synced 2026-06-05 05:58:20 +00:00
4591212482
Rewrites the SCIM 2.0 user provisioning handler to be RFC 7644 compliant. Verified against an external IdP Okta. Behavior is OPT IN
139 lines
3.9 KiB
Go
139 lines
3.9 KiB
Go
package scim
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/subtle"
|
|
"encoding/json"
|
|
"net/http"
|
|
"sync/atomic"
|
|
|
|
"github.com/elimity-com/scim"
|
|
scimErrors "github.com/elimity-com/scim/errors"
|
|
"github.com/elimity-com/scim/optional"
|
|
"github.com/elimity-com/scim/schema"
|
|
|
|
"cdr.dev/slog/v3"
|
|
agpl "github.com/coder/coder/v2/coderd"
|
|
"github.com/coder/coder/v2/coderd/audit"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/idpsync"
|
|
)
|
|
|
|
// Handler wraps the elimity-com/scim library's Server to implement
|
|
// SCIM 2.0 endpoints. The library auto-serves /Schemas, /ResourceTypes,
|
|
// and /ServiceProviderConfig from schema definitions.
|
|
type Handler struct {
|
|
opts *Options
|
|
srv *scim.Server
|
|
}
|
|
|
|
// Options holds all the dependencies needed by SCIM resource handlers.
|
|
type Options struct {
|
|
DB database.Store
|
|
Auditor *atomic.Pointer[audit.Auditor]
|
|
IDPSync idpsync.IDPSync
|
|
Logger slog.Logger
|
|
|
|
// AGPL is needed for CreateUser.
|
|
AGPL *agpl.API
|
|
|
|
// SCIMAPIKey is the bearer token used to authenticate SCIM requests.
|
|
SCIMAPIKey []byte
|
|
}
|
|
|
|
func New(opts *Options) (*Handler, error) {
|
|
userHandler := &ResourceUser{
|
|
store: opts.DB,
|
|
opts: opts,
|
|
}
|
|
|
|
args := &scim.ServerArgs{
|
|
ServiceProviderConfig: &scim.ServiceProviderConfig{
|
|
DocumentationURI: optional.NewString("https://coder.com/docs/admin/users/oidc-auth#scim"),
|
|
AuthenticationSchemes: []scim.AuthenticationScheme{
|
|
{
|
|
Type: scim.AuthenticationTypeOauthBearerToken,
|
|
Name: "HTTP Header Authentication",
|
|
Description: "Authentication scheme using the Authorization header with the shared token",
|
|
// TODO: Add documentation links for these specific docs once they exist.
|
|
SpecURI: optional.String{},
|
|
DocumentationURI: optional.String{},
|
|
Primary: true,
|
|
},
|
|
},
|
|
MaxResults: 0,
|
|
// SupportFiltering is set to false, as all filtering operations are not
|
|
// supported. A minimal filtering syntax is supported because Okta seems to
|
|
// ignore this field and attempt to filter anyway.
|
|
SupportFiltering: false,
|
|
SupportPatch: true,
|
|
},
|
|
ResourceTypes: []scim.ResourceType{
|
|
{
|
|
ID: optional.NewString("User"),
|
|
Name: "User",
|
|
Description: optional.NewString("User Account"),
|
|
Endpoint: "/Users",
|
|
Schema: schema.CoreUserSchema(),
|
|
Handler: userHandler,
|
|
SchemaExtensions: nil,
|
|
},
|
|
},
|
|
}
|
|
|
|
srv, err := scim.NewServer(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Handler{
|
|
opts: opts,
|
|
srv: &srv,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Handler) authMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
if !s.verifyAuthHeader(r) {
|
|
scimUnauthorized(rw)
|
|
return
|
|
}
|
|
|
|
// All authenticated requests are treated as coming from the SCIM provisioner
|
|
//nolint:gocritic // auth header authenticates as this identity
|
|
ctx := dbauthz.AsSCIMProvisioner(r.Context())
|
|
r = r.WithContext(ctx)
|
|
|
|
next.ServeHTTP(rw, r)
|
|
})
|
|
}
|
|
|
|
func (s *Handler) Handler() http.Handler {
|
|
return s.authMiddleware(s.srv)
|
|
}
|
|
|
|
func (s *Handler) verifyAuthHeader(r *http.Request) bool {
|
|
bearer := []byte("bearer ")
|
|
hdr := []byte(r.Header.Get("Authorization"))
|
|
|
|
// Case-insensitive comparison of the "Bearer " prefix.
|
|
if len(hdr) >= len(bearer) && subtle.ConstantTimeCompare(bytes.ToLower(hdr[:len(bearer)]), bearer) == 1 {
|
|
hdr = hdr[len(bearer):]
|
|
}
|
|
|
|
return len(s.opts.SCIMAPIKey) != 0 && subtle.ConstantTimeCompare(hdr, s.opts.SCIMAPIKey) == 1
|
|
}
|
|
|
|
func scimUnauthorized(rw http.ResponseWriter) {
|
|
rw.Header().Set("Content-Type", "application/scim+json")
|
|
rw.WriteHeader(http.StatusUnauthorized)
|
|
// scim error spec:
|
|
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.12
|
|
_ = json.NewEncoder(rw).Encode(scimErrors.ScimError{
|
|
ScimType: "", // No scimType exists for unauthorized errors.
|
|
Detail: "invalid authorization",
|
|
Status: http.StatusUnauthorized,
|
|
})
|
|
}
|