mirror of
https://github.com/fleetdm/fleet
synced 2026-05-18 14:38:53 +00:00
134 lines
4.9 KiB
Go
134 lines
4.9 KiB
Go
// Package sso wraps SSO functionality to be used by Fleet's service layer.
|
|
// It uses https://github.com/crewjam/saml for SAML parsing and validation.
|
|
//
|
|
// Initiate SSO:
|
|
// - Fleet generates a random session ID, and SAML AuthnRequest with a random Request ID.
|
|
// - Fleet stores the session in Redis with the session ID as key and "Request ID" + "Original URL" + configured Metadata as value.
|
|
// - Fleet returns a URL that redirects the user to the IdP with AuthnRequest and the session ID as a HTTP cookie.
|
|
//
|
|
// Callback SSO:
|
|
// - Fleet receives SAMLResponse in the request.
|
|
// - Fleet loads the session ID from a HTTP cookie and loads the session from Redis.
|
|
// - Fleet uses the Request ID + Metadata loaded from Redis to verify the SAMLResponse.
|
|
// - If verification succeeds, Fleet redirects the user to the Original URL loaded from Redis.
|
|
//
|
|
// IdP-initiated Callback SSO (if enabled by the admin):
|
|
// - Fleet receives SAMLResponse without session ID or "Request ID".
|
|
// - Fleet uses the configured metadata to verify the SAMLResponse.
|
|
//
|
|
// PS: We use a HTTP cookie for the session ID to prevent CSRF attacks (outcome of a pentest).
|
|
package sso
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
|
|
"github.com/crewjam/saml"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
)
|
|
|
|
// SAMLProviderFromConfiguredMetadata creates a SAML provider that can validate SAML responses
|
|
// from the configured SSO metadata.
|
|
func SAMLProviderFromConfiguredMetadata(
|
|
ctx context.Context,
|
|
entityID string,
|
|
acsURL string,
|
|
settings *fleet.SSOProviderSettings,
|
|
) (*saml.ServiceProvider, error) {
|
|
entityDescriptor, err := GetMetadata(settings)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
|
Message: "failed to get and parse IdP metadata",
|
|
InternalErr: err,
|
|
})
|
|
}
|
|
parsedACSURL, err := url.Parse(acsURL)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "failed to parse ACS URL")
|
|
}
|
|
return &saml.ServiceProvider{
|
|
EntityID: entityID,
|
|
AcsURL: *parsedACSURL,
|
|
IDPMetadata: entityDescriptor,
|
|
AuthnNameIDFormat: saml.EmailAddressNameIDFormat,
|
|
}, nil
|
|
}
|
|
|
|
// SAMLProviderFromSession creates a SAML provider that can validate SAML responses
|
|
// from a valid SSO session (stored in sessionStore).
|
|
func SAMLProviderFromSession(
|
|
ctx context.Context,
|
|
sessionID string,
|
|
sessionStore SessionStore,
|
|
acsURL *url.URL,
|
|
entityID string,
|
|
expectedAudiences []string,
|
|
) (samlProvider *saml.ServiceProvider, requestID, originalURL string, err error) {
|
|
session, err := sessionStore.Fullfill(sessionID)
|
|
if err != nil {
|
|
return nil, "", "", ctxerr.Wrap(ctx, err, "validate request in session")
|
|
}
|
|
entityDescriptor, err := ParseMetadata([]byte(session.Metadata))
|
|
if err != nil {
|
|
return nil, "", "", ctxerr.Wrap(ctx, err, "failed to parse metadata")
|
|
}
|
|
|
|
return &saml.ServiceProvider{
|
|
EntityID: entityID,
|
|
AcsURL: *acsURL,
|
|
IDPMetadata: entityDescriptor,
|
|
ValidateAudienceRestriction: func(assertion *saml.Assertion) error {
|
|
return validateAudiences(assertion, expectedAudiences)
|
|
},
|
|
}, session.RequestID, session.OriginalURL, nil
|
|
}
|
|
|
|
// SAMLProviderFromSessionOrConfiguredMetadata creates a SAML provider that can validate SAML responses.
|
|
// It will create the SAML provider from an existing SSO session (using sessionStore),
|
|
// if sessionID was generated by Fleet.
|
|
// or it will create a SAML provider from the configured metadata if IdP-initiated logins are enabled.
|
|
func SAMLProviderFromSessionOrConfiguredMetadata(
|
|
ctx context.Context,
|
|
sessionID string,
|
|
sessionStore SessionStore,
|
|
acsURL *url.URL,
|
|
settings *fleet.SSOSettings,
|
|
expectedAudiences []string,
|
|
) (samlProvider *saml.ServiceProvider, requestID string, redirectURL string, err error) {
|
|
idpInitiated := sessionID == ""
|
|
|
|
var entityDescriptor *saml.EntityDescriptor
|
|
if settings.EnableSSOIdPLogin && idpInitiated {
|
|
// Missing request ID indicates this was IdP-initiated. Only allow if
|
|
// configured to do so.
|
|
var err error
|
|
entityDescriptor, err = GetMetadata(&settings.SSOProviderSettings)
|
|
if err != nil {
|
|
return nil, "", "", ctxerr.Wrap(ctx, err, "failed to parse metadata")
|
|
}
|
|
redirectURL = "/"
|
|
} else {
|
|
session, err := sessionStore.Fullfill(sessionID)
|
|
if err != nil {
|
|
return nil, "", "", ctxerr.Wrap(ctx, err, "validate request in session")
|
|
}
|
|
entityDescriptor, err = ParseMetadata([]byte(session.Metadata))
|
|
if err != nil {
|
|
return nil, "", "", ctxerr.Wrap(ctx, err, "failed to parse metadata")
|
|
}
|
|
redirectURL = session.OriginalURL
|
|
requestID = session.RequestID
|
|
}
|
|
|
|
return &saml.ServiceProvider{
|
|
EntityID: settings.EntityID,
|
|
AcsURL: *acsURL,
|
|
DefaultRedirectURI: redirectURL,
|
|
IDPMetadata: entityDescriptor,
|
|
ValidateAudienceRestriction: func(assertion *saml.Assertion) error {
|
|
return validateAudiences(assertion, expectedAudiences)
|
|
},
|
|
AllowIDPInitiated: settings.EnableSSOIdPLogin,
|
|
}, requestID, redirectURL, nil
|
|
}
|