fleet/server/sso/authorization_response.go
Roberto Dip 78cc59e690
lowercase DisplayName attributes when reading SSO response (#12545)
this is to accommodate providers like [Okta][1] that send the user's
full name as an attribute named `displayName`

[1]:
https://developer.okta.com/docs/reference/api/users/#default-profile-properties
2023-06-28 12:19:13 -03:00

189 lines
5.5 KiB
Go

package sso
import (
"bytes"
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
)
const (
// These are response status codes described in the core SAML spec section
// 3.2.2.1 See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
Success int = iota
Requestor
Responder
VersionMismatch
AuthnFailed
InvalidAttrNameOrValue
InvalidNameIDPolicy
NoAuthnContext
NoAvailableIDP
NoPassive
NoSupportedIDP
PartialLogout
ProxyCountExceeded
RequestDenied
RequestUnsupported
RequestVersionDeprecated
RequestVersionTooHigh
RequestVersionTooLow
ResourceNotRecognized
TooManyResponses
UnknownAttrProfile
UnknownPrincipal
UnsupportedBinding
)
var statusMap = map[string]int{
"urn:oasis:names:tc:SAML:2.0:status:Success": Success,
"urn:oasis:names:tc:SAML:2.0:status:Requester": Requestor,
"urn:oasis:names:tc:SAML:2.0:status:Responder": Responder,
"urn:oasis:names:tc:SAML:2.0:status:VersionMismatch": VersionMismatch,
"urn:oasis:names:tc:SAML:2.0:status:AuthnFailed": AuthnFailed,
"urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue": InvalidAttrNameOrValue,
"urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy": InvalidNameIDPolicy,
"urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext": NoAuthnContext,
"urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP": NoAvailableIDP,
"urn:oasis:names:tc:SAML:2.0:status:NoPassive": NoPassive,
"urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP": NoSupportedIDP,
"urn:oasis:names:tc:SAML:2.0:status:PartialLogout": PartialLogout,
"urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded": ProxyCountExceeded,
"urn:oasis:names:tc:SAML:2.0:status:RequestDenied": RequestDenied,
"urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported": RequestUnsupported,
"urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated": RequestVersionDeprecated,
"urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow": RequestVersionTooLow,
"urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized": ResourceNotRecognized,
"urn:oasis:names:tc:SAML:2.0:status:TooManyResponses": TooManyResponses,
"urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile": UnknownAttrProfile,
"urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal": UnknownPrincipal,
"urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding": UnsupportedBinding,
}
// 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:
//
// - https://docs.ldap.com/specs/rfc2798.txt
// - https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/technical-reference/the-role-of-claims
var validDisplayNameAttrs = map[string]struct{}{
"name": {},
"displayname": {},
"cn": {},
"urn:oid:2.5.4.3": {},
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": {},
}
type resp struct {
response *Response
rawResp []byte
}
var _ fleet.Auth = resp{}
func (r *resp) setResponse(val *Response) {
r.response = val
}
func (r resp) statusDescription() string {
if r.response != nil {
return r.response.Status.StatusCode.Value
}
return "missing response"
}
// UserID partially implements the fleet.Auth interface.
func (r resp) UserID() string {
if r.response != nil {
return r.response.Assertion.Subject.NameID.Value
}
return ""
}
// UserDisplayName partially implements the fleet.Auth interface.
func (r resp) UserDisplayName() string {
if r.response != nil {
for _, attr := range r.response.Assertion.AttributeStatement.Attributes {
if _, ok := validDisplayNameAttrs[strings.ToLower(attr.Name)]; ok {
for _, v := range attr.AttributeValues {
if v.Value != "" {
return v.Value
}
}
}
}
}
return ""
}
func (r resp) status() (int, error) {
if r.response != nil {
statusURI := r.response.Status.StatusCode.Value
if code, ok := statusMap[statusURI]; ok {
return code, nil
}
}
return AuthnFailed, errors.New("malformed or missing auth response")
}
// RequestID partially implements the fleet.Auth interface.
func (r resp) RequestID() string {
if r.response != nil {
return r.response.InResponseTo
}
return ""
}
// AssertionAttributes partially implements the fleet.Auth interface.
func (r resp) AssertionAttributes() []fleet.SAMLAttribute {
if r.response == nil {
return nil
}
var attrs []fleet.SAMLAttribute
for _, attr := range r.response.Assertion.AttributeStatement.Attributes {
var values []fleet.SAMLAttributeValue
for _, value := range attr.AttributeValues {
values = append(values, fleet.SAMLAttributeValue{
Type: value.Type,
Value: value.Value,
})
}
attrs = append(attrs, fleet.SAMLAttribute{
Name: attr.Name,
Values: values,
})
}
return attrs
}
func (r resp) rawResponse() []byte {
return r.rawResp
}
// DecodeAuthResponse extracts SAML assertions from the IDP response (base64 encoded).
func DecodeAuthResponse(samlResponse string) (fleet.Auth, error) {
decoded, err := base64.StdEncoding.DecodeString(samlResponse)
if err != nil {
return nil, fmt.Errorf("decoding saml response: %w", err)
}
return decodeSAMLResponse(decoded)
}
func decodeSAMLResponse(rawXML []byte) (fleet.Auth, error) {
var saml Response
if err := xml.NewDecoder(bytes.NewBuffer(rawXML)).Decode(&saml); err != nil {
return nil, fmt.Errorf("decoding response xml: %w", err)
}
return &resp{
response: &saml,
rawResp: rawXML,
}, nil
}