189 lines
4.9 KiB
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
|
|
}
|