diff --git a/server/channels/app/saml_test.go b/server/channels/app/saml_test.go new file mode 100644 index 00000000000..8c3df366ee6 --- /dev/null +++ b/server/channels/app/saml_test.go @@ -0,0 +1,124 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testIdpEntityID = "https://idp.example.com/saml" + testIdpSSOURL = "https://idp.example.com/sso" + testIdpCert = "MIICsDCCAhmgAwIBAgIJAODDg4pFEblaMA0GCSqGSIb3DQEBBQUAME8xCzAJBgNV" +) + +func validIDPMetadata(attrs string) string { + return ` + + + + + + ` + testIdpCert + ` + + + + + +` +} + +func TestBuildSamlMetadataObject(t *testing.T) { + a := &App{} + + t.Run("minimal valid metadata", func(t *testing.T) { + data, appErr := a.BuildSamlMetadataObject([]byte(validIDPMetadata(""))) + require.Nil(t, appErr) + assert.Equal(t, testIdpEntityID, data.IdpDescriptorURL) + assert.Equal(t, testIdpSSOURL, data.IdpURL) + assert.Equal(t, testIdpCert, data.IdpPublicCertificate) + }) + + t.Run("cacheDuration on EntityDescriptor", func(t *testing.T) { + // PingFederate emits cacheDuration="PT1440M" by default (xs:duration). + data, appErr := a.BuildSamlMetadataObject([]byte(validIDPMetadata(` cacheDuration="PT1440M"`))) + require.Nil(t, appErr) + assert.Equal(t, testIdpEntityID, data.IdpDescriptorURL) + assert.Equal(t, testIdpSSOURL, data.IdpURL) + assert.Equal(t, testIdpCert, data.IdpPublicCertificate) + }) + + t.Run("cacheDuration on IDPSSODescriptor", func(t *testing.T) { + xml := ` + + + + + + ` + testIdpCert + ` + + + + + +` + data, appErr := a.BuildSamlMetadataObject([]byte(xml)) + require.Nil(t, appErr) + assert.Equal(t, testIdpEntityID, data.IdpDescriptorURL) + assert.Equal(t, testIdpSSOURL, data.IdpURL) + assert.Equal(t, testIdpCert, data.IdpPublicCertificate) + }) + + t.Run("validUntil without timezone", func(t *testing.T) { + // xs:dateTime allows values without a timezone; RFC 3339 does not. + data, appErr := a.BuildSamlMetadataObject([]byte(validIDPMetadata(` validUntil="2027-01-01T00:00:00"`))) + require.Nil(t, appErr) + assert.Equal(t, testIdpEntityID, data.IdpDescriptorURL) + assert.Equal(t, testIdpSSOURL, data.IdpURL) + assert.Equal(t, testIdpCert, data.IdpPublicCertificate) + }) + + t.Run("missing IDPSSODescriptor", func(t *testing.T) { + xml := ` + + +` + _, appErr := a.BuildSamlMetadataObject([]byte(xml)) + require.NotNil(t, appErr) + assert.Equal(t, "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", appErr.Id) + }) + + t.Run("missing SingleSignOnService", func(t *testing.T) { + xml := ` + + + + + + ` + testIdpCert + ` + + + + +` + _, appErr := a.BuildSamlMetadataObject([]byte(xml)) + require.NotNil(t, appErr) + assert.Equal(t, "api.admin.saml.invalid_xml_missing_ssoservices.app_error", appErr.Id) + }) + + t.Run("missing KeyDescriptor", func(t *testing.T) { + xml := ` + + + + +` + _, appErr := a.BuildSamlMetadataObject([]byte(xml)) + require.NotNil(t, appErr) + assert.Equal(t, "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", appErr.Id) + }) +} diff --git a/server/public/model/saml.go b/server/public/model/saml.go index e9e987d8ac8..d47606bf59b 100644 --- a/server/public/model/saml.go +++ b/server/public/model/saml.go @@ -125,10 +125,12 @@ type KeyDescriptor struct { } type RoleDescriptor struct { - XMLName xml.Name - ID string `xml:",attr,omitempty"` - ValidUntil time.Time `xml:"validUntil,attr,omitempty"` - CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"` + XMLName xml.Name + ID string `xml:",attr,omitempty"` + // Deprecated: no longer populated from metadata. + ValidUntil time.Time `xml:"-"` + // Deprecated: no longer populated from metadata. + CacheDuration time.Duration `xml:"-"` ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` ErrorURL string `xml:"errorURL,attr,omitempty"` KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor,omitempty"` @@ -164,11 +166,13 @@ type Organization struct { } type EntityDescriptor struct { - XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"` - EntityID string `xml:"entityID,attr"` - ID string `xml:",attr,omitempty"` - ValidUntil time.Time `xml:"validUntil,attr,omitempty"` - CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"` + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"` + EntityID string `xml:"entityID,attr"` + ID string `xml:",attr,omitempty"` + // Deprecated: no longer populated from metadata. + ValidUntil time.Time `xml:"-"` + // Deprecated: no longer populated from metadata. + CacheDuration time.Duration `xml:"-"` RoleDescriptors []RoleDescriptor `xml:"RoleDescriptor"` IDPSSODescriptors []IDPSSODescriptor `xml:"IDPSSODescriptor"` Organization Organization `xml:"Organization"`