chatdesk-ui/auth_v2.169.0/internal/api/samlassertion.go

189 lines
4.9 KiB
Go

package api
import (
"strings"
"time"
"github.com/crewjam/saml"
"github.com/supabase/auth/internal/models"
)
type SAMLAssertion struct {
*saml.Assertion
}
const (
SAMLSubjectIDAttributeName = "urn:oasis:names:tc:SAML:attribute:subject-id"
)
// Attribute returns the first matching attribute value in the attribute
// statements where name equals the official SAML attribute Name or
// FriendlyName. Returns nil if such an attribute can't be found.
func (a *SAMLAssertion) Attribute(name string) []saml.AttributeValue {
var values []saml.AttributeValue
for _, stmt := range a.AttributeStatements {
for _, attr := range stmt.Attributes {
if strings.EqualFold(attr.Name, name) || strings.EqualFold(attr.FriendlyName, name) {
values = append(values, attr.Values...)
}
}
}
return values
}
// UserID returns the best choice for a persistent user identifier on the
// Identity Provider side. Don't assume the format of the string returned, as
// it's Identity Provider specific.
func (a *SAMLAssertion) UserID() string {
// First we look up the SAMLSubjectIDAttributeName in the attribute
// section of the assertion, as this is the preferred way to
// persistently identify users in SAML 2.0.
// See: https://docs.oasis-open.org/security/saml-subject-id-attr/v1.0/cs01/saml-subject-id-attr-v1.0-cs01.html#_Toc536097226
values := a.Attribute(SAMLSubjectIDAttributeName)
if len(values) > 0 {
return values[0].Value
}
// Otherwise, fall back to the SubjectID value.
subjectID, isPersistent := a.SubjectID()
if !isPersistent {
return ""
}
return subjectID
}
// SubjectID returns the user identifier in present in the Subject section of
// the SAML assertion. Note that this way of identifying the Subject is
// generally superseded by the SAMLSubjectIDAttributeName assertion attribute;
// tho must be present in all assertions. It can have a few formats, of which
// the most important are: saml.EmailAddressNameIDFormat (meaning the user ID
// is an email address), saml.PersistentNameIDFormat (the user ID is an opaque
// string that does not change with each assertion, e.g. UUID),
// saml.TransientNameIDFormat (the user ID changes with each assertion -- can't
// be used to identify a user). The boolean returned identifies if the user ID
// is persistent. If it's an email address, it's lowercased just in case.
func (a *SAMLAssertion) SubjectID() (string, bool) {
if a.Subject == nil {
return "", false
}
if a.Subject.NameID == nil {
return "", false
}
if a.Subject.NameID.Value == "" {
return "", false
}
if a.Subject.NameID.Format == string(saml.EmailAddressNameIDFormat) {
return strings.ToLower(strings.TrimSpace(a.Subject.NameID.Value)), true
}
// all other NameID formats are regarded as persistent
isPersistent := a.Subject.NameID.Format != string(saml.TransientNameIDFormat)
return a.Subject.NameID.Value, isPersistent
}
// Email returns the best guess for an email address.
func (a *SAMLAssertion) Email() string {
attributeNames := []string{
"urn:oid:0.9.2342.19200300.100.1.3",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"http://schemas.xmlsoap.org/claims/EmailAddress",
"mail",
"Mail",
"email",
}
for _, name := range attributeNames {
for _, attr := range a.Attribute(name) {
if attr.Value != "" {
return attr.Value
}
}
}
if a.Subject.NameID.Format == string(saml.EmailAddressNameIDFormat) {
return a.Subject.NameID.Value
}
return ""
}
// Process processes this assertion according to the SAMLAttributeMapping. Never returns nil.
func (a *SAMLAssertion) Process(mapping models.SAMLAttributeMapping) map[string]interface{} {
ret := make(map[string]interface{})
for key, mapper := range mapping.Keys {
names := []string{}
if mapper.Name != "" {
names = append(names, mapper.Name)
}
names = append(names, mapper.Names...)
setKey := false
for _, name := range names {
for _, attr := range a.Attribute(name) {
if attr.Value != "" {
setKey = true
if mapper.Array {
if ret[key] == nil {
ret[key] = []string{}
}
ret[key] = append(ret[key].([]string), attr.Value)
} else {
ret[key] = attr.Value
break
}
}
}
if setKey {
break
}
}
if !setKey && mapper.Default != nil {
ret[key] = mapper.Default
}
}
return ret
}
// NotBefore extracts the time before which this assertion should not be
// considered.
func (a *SAMLAssertion) NotBefore() time.Time {
if a.Conditions != nil && !a.Conditions.NotBefore.IsZero() {
return a.Conditions.NotBefore.UTC()
}
return time.Time{}
}
// NotAfter extracts the time at which or after this assertion should not be
// considered.
func (a *SAMLAssertion) NotAfter() time.Time {
var notOnOrAfter time.Time
for _, statement := range a.AuthnStatements {
if statement.SessionNotOnOrAfter == nil {
continue
}
notOnOrAfter = *statement.SessionNotOnOrAfter
if !notOnOrAfter.IsZero() {
break
}
}
return notOnOrAfter
}