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"`