2017-05-09 00:43:48 +00:00
|
|
|
package sso
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/base64"
|
2021-11-22 14:13:26 +00:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2025-07-07 18:13:46 +00:00
|
|
|
"net/url"
|
|
|
|
|
"slices"
|
2023-06-28 15:19:13 +00:00
|
|
|
"strings"
|
2017-05-09 00:43:48 +00:00
|
|
|
|
2025-07-07 18:13:46 +00:00
|
|
|
"github.com/crewjam/saml"
|
2021-06-26 04:46:51 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2017-05-09 00:43:48 +00:00
|
|
|
)
|
|
|
|
|
|
2022-08-15 17:42:33 +00:00
|
|
|
// Since there's not a standard for display names, I have collected the most
|
|
|
|
|
// commonly used attribute names for it.
|
|
|
|
|
//
|
|
|
|
|
// Most of the items here come from:
|
|
|
|
|
//
|
2022-09-12 23:32:43 +00:00
|
|
|
// - https://docs.ldap.com/specs/rfc2798.txt
|
|
|
|
|
// - https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/technical-reference/the-role-of-claims
|
2022-08-15 17:42:33 +00:00
|
|
|
var validDisplayNameAttrs = map[string]struct{}{
|
|
|
|
|
"name": {},
|
|
|
|
|
"displayname": {},
|
|
|
|
|
"cn": {},
|
|
|
|
|
"urn:oid:2.5.4.3": {},
|
|
|
|
|
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": {},
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-09 00:43:48 +00:00
|
|
|
type resp struct {
|
2025-07-07 18:13:46 +00:00
|
|
|
assertion *saml.Assertion
|
2017-05-09 00:43:48 +00:00
|
|
|
}
|
|
|
|
|
|
2023-03-01 23:18:40 +00:00
|
|
|
var _ fleet.Auth = resp{}
|
|
|
|
|
|
|
|
|
|
// UserID partially implements the fleet.Auth interface.
|
2017-05-09 00:43:48 +00:00
|
|
|
func (r resp) UserID() string {
|
2025-07-07 18:13:46 +00:00
|
|
|
if r.assertion != nil && r.assertion.Subject != nil && r.assertion.Subject.NameID != nil {
|
|
|
|
|
return r.assertion.Subject.NameID.Value
|
2017-05-09 00:43:48 +00:00
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 23:18:40 +00:00
|
|
|
// UserDisplayName partially implements the fleet.Auth interface.
|
2022-08-15 17:42:33 +00:00
|
|
|
func (r resp) UserDisplayName() string {
|
2025-07-07 18:13:46 +00:00
|
|
|
if r.assertion == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
for _, attrStatement := range r.assertion.AttributeStatements {
|
|
|
|
|
for _, attr := range attrStatement.Attributes {
|
2023-06-28 15:19:13 +00:00
|
|
|
if _, ok := validDisplayNameAttrs[strings.ToLower(attr.Name)]; ok {
|
2025-07-07 18:13:46 +00:00
|
|
|
for _, vv := range attr.Values {
|
|
|
|
|
if vv.Value != "" {
|
|
|
|
|
return vv.Value
|
2022-08-15 17:42:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-05-09 00:43:48 +00:00
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 23:18:40 +00:00
|
|
|
// AssertionAttributes partially implements the fleet.Auth interface.
|
|
|
|
|
func (r resp) AssertionAttributes() []fleet.SAMLAttribute {
|
2025-07-07 18:13:46 +00:00
|
|
|
if r.assertion == nil {
|
2023-03-01 23:18:40 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var attrs []fleet.SAMLAttribute
|
2025-07-07 18:13:46 +00:00
|
|
|
for _, attrStatement := range r.assertion.AttributeStatements {
|
|
|
|
|
for _, attr := range attrStatement.Attributes {
|
|
|
|
|
var values []fleet.SAMLAttributeValue
|
|
|
|
|
for _, value := range attr.Values {
|
|
|
|
|
values = append(values, fleet.SAMLAttributeValue{
|
|
|
|
|
Type: value.Type,
|
|
|
|
|
Value: value.Value,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
attrs = append(attrs, fleet.SAMLAttribute{
|
|
|
|
|
Name: attr.Name,
|
|
|
|
|
Values: values,
|
2023-03-01 23:18:40 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return attrs
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-07 18:13:46 +00:00
|
|
|
// DecodeSAMLResponse base64-decodes the SAMLResponse.
|
|
|
|
|
func DecodeSAMLResponse(samlResponse string) ([]byte, error) {
|
|
|
|
|
decodedSAMLResponse, err := base64.StdEncoding.DecodeString(samlResponse)
|
2017-05-09 00:43:48 +00:00
|
|
|
if err != nil {
|
2025-07-07 18:13:46 +00:00
|
|
|
return nil, fmt.Errorf("decoding SAMLResponse: %w", err)
|
2017-05-09 00:43:48 +00:00
|
|
|
}
|
2025-07-07 18:13:46 +00:00
|
|
|
return decodedSAMLResponse, nil
|
|
|
|
|
}
|
2023-03-01 23:18:40 +00:00
|
|
|
|
2025-07-07 18:13:46 +00:00
|
|
|
// validateAudiences validates that the audience restrictions of the assertion is one of the expected audiences.
|
|
|
|
|
func validateAudiences(assertion *saml.Assertion, expectedAudiences []string) error {
|
|
|
|
|
if assertion.Conditions == nil {
|
|
|
|
|
return errors.New("missing conditions in assertion")
|
|
|
|
|
}
|
|
|
|
|
if len(assertion.Conditions.AudienceRestrictions) == 0 {
|
|
|
|
|
return errors.New("missing audience restrictions")
|
|
|
|
|
}
|
|
|
|
|
for _, audienceRestriction := range assertion.Conditions.AudienceRestrictions {
|
|
|
|
|
if slices.Contains(expectedAudiences, audienceRestriction.Audience.Value) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("wrong audience: %+v", assertion.Conditions.AudienceRestrictions)
|
2023-03-01 23:18:40 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-07 18:13:46 +00:00
|
|
|
// ParseAndVerifySAMLResponse runs the parsing and validation of SAMLResponses.
|
|
|
|
|
func ParseAndVerifySAMLResponse(samlProvider *saml.ServiceProvider, samlResponse []byte, requestID string, acsURL *url.URL) (fleet.Auth, error) {
|
|
|
|
|
verifiedAssertion, err := samlProvider.ParseXMLResponse(samlResponse, []string{requestID}, *acsURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if samlErr, ok := err.(*saml.InvalidResponseError); ok {
|
|
|
|
|
err = samlErr.PrivateErr
|
|
|
|
|
}
|
|
|
|
|
return nil, err
|
2017-05-09 00:43:48 +00:00
|
|
|
}
|
2023-03-01 23:18:40 +00:00
|
|
|
return &resp{
|
2025-07-07 18:13:46 +00:00
|
|
|
assertion: verifiedAssertion,
|
2023-03-01 23:18:40 +00:00
|
|
|
}, nil
|
2017-05-09 00:43:48 +00:00
|
|
|
}
|