Skip to content

Commit

Permalink
Add cacheDuration, validUntil to RoleDescriptors (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgyongyosi authored Oct 11, 2023
1 parent 9900411 commit 94154cb
Show file tree
Hide file tree
Showing 15 changed files with 367 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.46.2
version: v1.54.2
7 changes: 6 additions & 1 deletion example/trivial/trivial.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log"
"net/http"
"net/url"
"time"

"github.com/crewjam/saml/samlsp"
)
Expand Down Expand Up @@ -72,5 +73,9 @@ func main() {
http.Handle("/hello", samlMiddleware.RequireAccount(app))
http.Handle("/saml/", samlMiddleware)
http.Handle("/logout", slo)
log.Fatal(http.ListenAndServe(":8000", nil))
server := &http.Server{
Addr: ":8000",
ReadHeaderTimeout: 3 * time.Second,
}
log.Fatal(server.ListenAndServe())
}
2 changes: 2 additions & 0 deletions identity_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ func (idp *IdentityProvider) Metadata() *EntityDescriptor {
SSODescriptor: SSODescriptor{
RoleDescriptor: RoleDescriptor{
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
CacheDuration: validDuration,
ValidUntil: TimeNow().Add(validDuration),
KeyDescriptors: []KeyDescriptor{
{
Use: "signing",
Expand Down
5 changes: 4 additions & 1 deletion identity_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ func TestIDPCanProduceMetadata(t *testing.T) {
{
SSODescriptor: SSODescriptor{
RoleDescriptor: RoleDescriptor{
ValidUntil: TimeNow().Add(DefaultValidDuration),
CacheDuration: DefaultValidDuration,
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
KeyDescriptors: []KeyDescriptor{
{
Expand Down Expand Up @@ -207,7 +209,8 @@ func TestIDPHTTPCanHandleMetadataRequest(t *testing.T) {
test.IDP.Handler().ServeHTTP(w, r)
assert.Check(t, is.Equal(http.StatusOK, w.Code))
assert.Check(t, is.Equal("application/samlmetadata+xml", w.Header().Get("Content-type")))
assert.Check(t, strings.HasPrefix(string(w.Body.Bytes()), "<EntityDescriptor"),
body := string(w.Body.Bytes())
assert.Check(t, strings.HasPrefix(body, "<EntityDescriptor"),
string(w.Body.Bytes()))
}

Expand Down
171 changes: 167 additions & 4 deletions metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ type ContactPerson struct {
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.1
type RoleDescriptor struct {
ID string `xml:",attr,omitempty"`
ValidUntil *time.Time `xml:"validUntil,attr,omitempty"`
ValidUntil time.Time `xml:"validUntil,attr,omitempty"`
CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"`
ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
ErrorURL string `xml:"errorURL,attr,omitempty"`
Expand Down Expand Up @@ -214,9 +214,9 @@ type SSODescriptor struct {
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.3
type IDPSSODescriptor struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"`
SSODescriptor
WantAuthnRequestsSigned *bool `xml:",attr"`
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"`
WantAuthnRequestsSigned *bool `xml:",attr"`

SingleSignOnServices []Endpoint `xml:"SingleSignOnService"`
ArtifactResolutionServices []Endpoint `xml:"ArtifactResolutionService"`
Expand All @@ -226,18 +226,82 @@ type IDPSSODescriptor struct {
Attributes []Attribute `xml:"Attribute"`
}

func (m IDPSSODescriptor) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type Alias IDPSSODescriptor
aux := &struct {
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
*Alias
}{
ValidUntil: RelaxedTime(m.ValidUntil),
CacheDuration: Duration(m.CacheDuration),
Alias: (*Alias)(&m),
}
return e.Encode(aux)
}

// UnmarshalXML implements xml.Unmarshaler
func (m *IDPSSODescriptor) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias IDPSSODescriptor
aux := &struct {
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
*Alias
}{
Alias: (*Alias)(m),
}
if err := d.DecodeElement(aux, &start); err != nil {
return err
}
m.ValidUntil = time.Time(aux.ValidUntil)
m.CacheDuration = time.Duration(aux.CacheDuration)
return nil
}

// SPSSODescriptor represents the SAML SPSSODescriptorType object.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.2
type SPSSODescriptor struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SPSSODescriptor"`
SSODescriptor
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SPSSODescriptor"`
AuthnRequestsSigned *bool `xml:",attr"`
WantAssertionsSigned *bool `xml:",attr"`
AssertionConsumerServices []IndexedEndpoint `xml:"AssertionConsumerService"`
AttributeConsumingServices []AttributeConsumingService `xml:"AttributeConsumingService"`
}

func (m SPSSODescriptor) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type Alias SPSSODescriptor
aux := &struct {
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
*Alias
}{
ValidUntil: RelaxedTime(m.ValidUntil),
CacheDuration: Duration(m.CacheDuration),
Alias: (*Alias)(&m),
}
return e.Encode(aux)
}

// UnmarshalXML implements xml.Unmarshaler
func (m *SPSSODescriptor) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias SPSSODescriptor
aux := &struct {
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
*Alias
}{
Alias: (*Alias)(m),
}
if err := d.DecodeElement(aux, &start); err != nil {
return err
}
m.ValidUntil = time.Time(aux.ValidUntil)
m.CacheDuration = time.Duration(aux.CacheDuration)
return nil
}

// AttributeConsumingService represents the SAML AttributeConsumingService object.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.4.1
Expand All @@ -262,33 +326,132 @@ type RequestedAttribute struct {
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.5
type AuthnAuthorityDescriptor struct {
RoleDescriptor
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata AuthnAuthorityDescriptor"`
AuthnQueryServices []Endpoint `xml:"AuthnQueryService"`
AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
}

func (m AuthnAuthorityDescriptor) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type Alias AuthnAuthorityDescriptor
aux := &struct {
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
*Alias
}{
ValidUntil: RelaxedTime(m.ValidUntil),
CacheDuration: Duration(m.CacheDuration),
Alias: (*Alias)(&m),
}
return e.Encode(aux)
}

// UnmarshalXML implements xml.Unmarshaler
func (m *AuthnAuthorityDescriptor) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias AuthnAuthorityDescriptor
aux := &struct {
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
*Alias
}{
Alias: (*Alias)(m),
}
if err := d.DecodeElement(aux, &start); err != nil {
return err
}
m.ValidUntil = time.Time(aux.ValidUntil)
m.CacheDuration = time.Duration(aux.CacheDuration)
return nil
}

// PDPDescriptor represents the SAML PDPDescriptor object.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.6
type PDPDescriptor struct {
RoleDescriptor
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata PDPDescriptor"`
AuthzServices []Endpoint `xml:"AuthzService"`
AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
}

func (m PDPDescriptor) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type Alias PDPDescriptor
aux := &struct {
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
*Alias
}{
ValidUntil: RelaxedTime(m.ValidUntil),
CacheDuration: Duration(m.CacheDuration),
Alias: (*Alias)(&m),
}
return e.Encode(aux)
}

// UnmarshalXML implements xml.Unmarshaler
func (m *PDPDescriptor) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias PDPDescriptor
aux := &struct {
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
*Alias
}{
Alias: (*Alias)(m),
}
if err := d.DecodeElement(aux, &start); err != nil {
return err
}
m.ValidUntil = time.Time(aux.ValidUntil)
m.CacheDuration = time.Duration(aux.CacheDuration)
return nil
}

// AttributeAuthorityDescriptor represents the SAML AttributeAuthorityDescriptor object.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.7
type AttributeAuthorityDescriptor struct {
RoleDescriptor
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata AttributeAuthorityDescriptor"`
AttributeServices []Endpoint `xml:"AttributeService"`
AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
AttributeProfiles []string `xml:"AttributeProfile"`
Attributes []Attribute `xml:"Attribute"`
}

func (m AttributeAuthorityDescriptor) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type Alias AttributeAuthorityDescriptor
aux := &struct {
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
*Alias
}{
ValidUntil: RelaxedTime(m.ValidUntil),
CacheDuration: Duration(m.CacheDuration),
Alias: (*Alias)(&m),
}
return e.Encode(aux)
}

// UnmarshalXML implements xml.Unmarshaler
func (m *AttributeAuthorityDescriptor) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias AttributeAuthorityDescriptor
aux := &struct {
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
*Alias
}{
Alias: (*Alias)(m),
}
if err := d.DecodeElement(aux, &start); err != nil {
return err
}
m.ValidUntil = time.Time(aux.ValidUntil)
m.CacheDuration = time.Duration(aux.CacheDuration)
return nil
}

// AffiliationDescriptor represents the SAML AffiliationDescriptor object.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.5
Expand Down
5 changes: 4 additions & 1 deletion metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ func TestCanParseMetadata(t *testing.T) {
CacheDuration: time.Hour,
SPSSODescriptors: []SPSSODescriptor{
{
XMLName: xml.Name{Space: "urn:oasis:names:tc:SAML:2.0:metadata", Local: "SPSSODescriptor"},
SSODescriptor: SSODescriptor{
RoleDescriptor: RoleDescriptor{
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
ValidUntil: time.Date(2001, time.February, 3, 4, 5, 6, 789000000, time.UTC),
CacheDuration: time.Hour,
},
},
AuthnRequestsSigned: &False,
Expand Down Expand Up @@ -101,6 +102,8 @@ func TestCanProduceSPMetadata(t *testing.T) {
WantAssertionsSigned: &WantAssertionsSigned,
SSODescriptor: SSODescriptor{
RoleDescriptor: RoleDescriptor{
ValidUntil: validUntil,
CacheDuration: DefaultCacheDuration,
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
KeyDescriptors: []KeyDescriptor{
{
Expand Down
2 changes: 1 addition & 1 deletion samlidp/testdata/http_metadata_response.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2015-12-03T01:57:09Z" cacheDuration="PT48H" entityID="https://idp.example.com/metadata">
<IDPSSODescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<IDPSSODescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2015-12-03T01:57:09Z" cacheDuration="PT48H" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#">
Expand Down
2 changes: 1 addition & 1 deletion samlsp/testdata/expected_middleware_metadata.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2015-12-03T01:57:09.123Z" entityID="https://15661444.ngrok.io/saml2/metadata">
<SPSSODescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2015-12-03T01:57:09.123456789Z" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" AuthnRequestsSigned="false" WantAssertionsSigned="true">
<SPSSODescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2015-12-03T01:57:09.123Z" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" AuthnRequestsSigned="false" WantAssertionsSigned="true">
<KeyDescriptor use="encryption">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#">
Expand Down
2 changes: 1 addition & 1 deletion samlsp/testdata/idp_metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<mdalg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<mdalg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
</Extensions>
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol" cacheDuration='PT1H'>
<Extensions>
<shibmd:Scope regexp="false">testshib.org</shibmd:Scope>
<mdui:UIInfo>
Expand Down
4 changes: 2 additions & 2 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ const (
StatusNoAvailableIDP = "urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP"

// StatusNoPassive means Indicates the responding provider cannot authenticate the principal passively, as has been requested.
StatusNoPassive = "urn:oasis:names:tc:SAML:2.0:status:NoPassive" //nolint:gosec
StatusNoPassive = "urn:oasis:names:tc:SAML:2.0:status:NoPassive" // #nosec G101

// StatusNoSupportedIDP is used by an intermediary to indicate that none of the identity providers in an <IDPList> are supported by the intermediary.
StatusNoSupportedIDP = "urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP"
Expand All @@ -667,7 +667,7 @@ const (
StatusRequestUnsupported = "urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported"

// StatusRequestVersionDeprecated means the SAML responder cannot process any requests with the protocol version specified in the request.
StatusRequestVersionDeprecated = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated"
StatusRequestVersionDeprecated = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated" // #nosec G101

// StatusRequestVersionTooHigh means the SAML responder cannot process the request because the protocol version specified in the request message is a major upgrade from the highest protocol version supported by the responder.
StatusRequestVersionTooHigh = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooHigh"
Expand Down
4 changes: 2 additions & 2 deletions service_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor {
RoleDescriptor: RoleDescriptor{
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
KeyDescriptors: keyDescriptors,
ValidUntil: &validUntil,
ValidUntil: validUntil,
},
SingleLogoutServices: sloEndpoints,
NameIDFormats: []NameIDFormat{sp.AuthnNameIDFormat},
Expand Down Expand Up @@ -1664,7 +1664,7 @@ func elementToBytes(el *etree.Element) ([]byte, error) {
for _, attr := range currentElement.Attr {
// "xmlns" is either the space or the key of the attribute, depending on whether it is a default namespace declaration or not
if attr.Space == "xmlns" || attr.Key == "xmlns" {
// If the namespace is already preset in the list, it means that a child element has overriden it, so skip it
// If the namespace is already preset in the list, it means that a child element has overridden it, so skip it
if _, prefixExists := namespaces[attr.FullKey()]; !prefixExists {
namespaces[attr.FullKey()] = attr.Value
}
Expand Down
24 changes: 23 additions & 1 deletion testdata/TestCanParseMetadata_metadata.xml
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
<?xml version='1.0' encoding='UTF-8'?><md:EntityDescriptor ID='_af805d1c-c2e3-444e-9cf5-efc664eeace6' entityID='https://dev.aa.kndr.org/users/auth/saml/metadata' validUntil='2001-02-03T04:05:06.789' cacheDuration='PT1H' xmlns:md='urn:oasis:names:tc:SAML:2.0:metadata' xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion'><md:SPSSODescriptor AuthnRequestsSigned='false' WantAssertionsSigned='false' protocolSupportEnumeration='urn:oasis:names:tc:SAML:2.0:protocol'><md:AssertionConsumerService Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' Location='https://dev.aa.kndr.org/users/auth/saml/callback' index='0' isDefault='true'/><md:AttributeConsumingService index='1' isDefault='true'><md:ServiceName xml:lang='en'>Required attributes</md:ServiceName><md:RequestedAttribute FriendlyName='Email address' Name='email' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'/><md:RequestedAttribute FriendlyName='Full name' Name='name' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'/><md:RequestedAttribute FriendlyName='Given name' Name='first_name' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'/><md:RequestedAttribute FriendlyName='Family name' Name='last_name' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'/></md:AttributeConsumingService></md:SPSSODescriptor></md:EntityDescriptor>
<?xml version='1.0' encoding='UTF-8'?>
<md:EntityDescriptor ID='_af805d1c-c2e3-444e-9cf5-efc664eeace6'
entityID='https://dev.aa.kndr.org/users/auth/saml/metadata' validUntil='2001-02-03T04:05:06.789'
cacheDuration='PT1H' xmlns:md='urn:oasis:names:tc:SAML:2.0:metadata'
xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion'>
<md:SPSSODescriptor AuthnRequestsSigned='false' WantAssertionsSigned='false'
protocolSupportEnumeration='urn:oasis:names:tc:SAML:2.0:protocol' validUntil='2001-02-03T04:05:06.789'
cacheDuration='PT1H'>
<md:AssertionConsumerService Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
Location='https://dev.aa.kndr.org/users/auth/saml/callback' index='0' isDefault='true' />
<md:AttributeConsumingService index='1' isDefault='true'>
<md:ServiceName xml:lang='en'>Required attributes</md:ServiceName>
<md:RequestedAttribute FriendlyName='Email address' Name='email'
NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic' />
<md:RequestedAttribute FriendlyName='Full name' Name='name'
NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic' />
<md:RequestedAttribute FriendlyName='Given name' Name='first_name'
NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic' />
<md:RequestedAttribute FriendlyName='Family name' Name='last_name'
NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic' />
</md:AttributeConsumingService>
</md:SPSSODescriptor>
</md:EntityDescriptor>
Loading

0 comments on commit 94154cb

Please sign in to comment.