package api
import (
tst "testing"
"encoding/xml"
"github.com/crewjam/saml"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/models"
)
func TestSAMLAssertionUserID(t *tst.T) {
type spec struct {
xml string
userID string
}
examples := []spec{
{
xml: `
https://example.com/saml
transient-name-id
http://localhost:9999/saml/metadata
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
`,
userID: "",
},
{
xml: `
https://example.com/saml
persistent-name-id
http://localhost:9999/saml/metadata
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
`,
userID: "persistent-name-id",
},
{
xml: `
https://example.com/saml
name-id@example.com
http://localhost:9999/saml/metadata
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
`,
userID: "name-id@example.com",
},
{
xml: `
https://example.com/saml
name-id@example.com
http://localhost:9999/saml/metadata
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
subject-id
`,
userID: "subject-id",
},
}
for i, example := range examples {
rawAssertion := saml.Assertion{}
require.NoError(t, xml.Unmarshal([]byte(example.xml), &rawAssertion))
assertion := SAMLAssertion{
&rawAssertion,
}
userID := assertion.UserID()
require.Equal(t, userID, example.userID, "example %d had different user ID", i)
}
}
func TestSAMLAssertionProcessing(t *tst.T) {
type spec struct {
desc string
xml string
mapping models.SAMLAttributeMapping
expected map[string]interface{}
}
examples := []spec{
{
desc: "valid attribute and mapping",
xml: `
someone@example.com
`,
mapping: models.SAMLAttributeMapping{
Keys: map[string]models.SAMLAttribute{
"email": {
Name: "mail",
},
},
},
expected: map[string]interface{}{
"email": "someone@example.com",
},
},
{
desc: "valid attributes, use first attribute found in Names",
xml: `
old-soap@example.com
soap@example.com
`,
mapping: models.SAMLAttributeMapping{
Keys: map[string]models.SAMLAttribute{
"email": {
Names: []string{
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"http://schemas.xmlsoap.org/claims/EmailAddress",
},
},
},
},
expected: map[string]interface{}{
"email": "soap@example.com",
},
},
{
desc: "valid groups attribute",
xml: `
group1
group2
soap@example.com
`,
mapping: models.SAMLAttributeMapping{
Keys: map[string]models.SAMLAttribute{
"email": {
Names: []string{
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"http://schemas.xmlsoap.org/claims/EmailAddress",
},
},
"groups": {
Name: "groups",
Array: true,
},
},
},
expected: map[string]interface{}{
"email": "soap@example.com",
"groups": []string{
"group1",
"group2",
},
},
},
{
desc: "missing attribute use default value",
xml: `
someone@example.com
`,
mapping: models.SAMLAttributeMapping{
Keys: map[string]models.SAMLAttribute{
"email": {
Name: "email",
},
"role": {
Default: "member",
},
},
},
expected: map[string]interface{}{
"email": "someone@example.com",
"role": "member",
},
},
{
desc: "use default value even if attribute exists but is not specified in mapping",
xml: `
someone@example.com
admin
`,
mapping: models.SAMLAttributeMapping{
Keys: map[string]models.SAMLAttribute{
"email": {
Name: "mail",
},
"role": {
Default: "member",
},
},
},
expected: map[string]interface{}{
"email": "someone@example.com",
"role": "member",
},
},
{
desc: "use value in XML when attribute exists and is specified in mapping",
xml: `
someone@example.com
admin
`,
mapping: models.SAMLAttributeMapping{
Keys: map[string]models.SAMLAttribute{
"email": {
Name: "mail",
},
"role": {
Name: "role",
Default: "member",
},
},
},
expected: map[string]interface{}{
"email": "someone@example.com",
"role": "admin",
},
},
}
for i, example := range examples {
t.Run(example.desc, func(t *tst.T) {
rawAssertion := saml.Assertion{}
require.NoError(t, xml.Unmarshal([]byte(example.xml), &rawAssertion))
assertion := SAMLAssertion{
&rawAssertion,
}
result := assertion.Process(example.mapping)
require.Equal(t, example.expected, result, "example %d had different processing", i)
})
}
}