diff --git a/identity_provider.go b/identity_provider.go
index 921c7746..f8b90a6e 100644
--- a/identity_provider.go
+++ b/identity_provider.go
@@ -15,6 +15,7 @@ import (
"net/url"
"os"
"regexp"
+ "strconv"
"text/template"
"time"
@@ -60,7 +61,7 @@ type ServiceProviderProvider interface {
// service provider ID, which is typically the service provider's
// metadata URL. If an appropriate service provider cannot be found then
// the returned error must be os.ErrNotExist.
- GetServiceProvider(r *http.Request, serviceProviderID string) (*Metadata, error)
+ GetServiceProvider(r *http.Request, serviceProviderID string) (*EntityDescriptor, error)
}
// IdentityProvider implements the SAML Identity Provider role (IDP).
@@ -88,48 +89,52 @@ type IdentityProvider struct {
}
// Metadata returns the metadata structure for this identity provider.
-func (idp *IdentityProvider) Metadata() *Metadata {
+func (idp *IdentityProvider) Metadata() *EntityDescriptor {
certStr := base64.StdEncoding.EncodeToString(idp.Certificate.Raw)
- return &Metadata{
+ return &EntityDescriptor{
EntityID: idp.MetadataURL.String(),
ValidUntil: TimeNow().Add(DefaultValidDuration),
CacheDuration: DefaultValidDuration,
- IDPSSODescriptor: &IDPSSODescriptor{
- ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
- KeyDescriptor: []KeyDescriptor{
- {
- Use: "signing",
- KeyInfo: KeyInfo{
- Certificate: certStr,
+ IDPSSODescriptors: []IDPSSODescriptor{
+ IDPSSODescriptor{
+ SSODescriptor: SSODescriptor{
+ RoleDescriptor: RoleDescriptor{
+ ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
+ KeyDescriptors: []KeyDescriptor{
+ {
+ Use: "signing",
+ KeyInfo: KeyInfo{
+ Certificate: certStr,
+ },
+ },
+ {
+ Use: "encryption",
+ KeyInfo: KeyInfo{
+ Certificate: certStr,
+ },
+ EncryptionMethods: []EncryptionMethod{
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"},
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes192-cbc"},
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"},
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"},
+ },
+ },
+ },
},
+ NameIDFormats: []NameIDFormat{NameIDFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:transient")},
},
- {
- Use: "encryption",
- KeyInfo: KeyInfo{
- Certificate: certStr,
+ SingleSignOnServices: []Endpoint{
+ {
+ Binding: HTTPRedirectBinding,
+ Location: idp.SSOURL.String(),
},
- EncryptionMethods: []EncryptionMethod{
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"},
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes192-cbc"},
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"},
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"},
+ {
+ Binding: HTTPPostBinding,
+ Location: idp.SSOURL.String(),
},
},
},
- NameIDFormat: []string{
- "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
- },
- SingleSignOnService: []Endpoint{
- {
- Binding: HTTPRedirectBinding,
- Location: idp.SSOURL.String(),
- },
- {
- Binding: HTTPPostBinding,
- Location: idp.SSOURL.String(),
- },
- },
},
}
}
@@ -214,6 +219,8 @@ func (idp *IdentityProvider) ServeIDPInitiated(w http.ResponseWriter, r *http.Re
session := idp.SessionProvider.GetSession(w, r, req)
if session == nil {
+ // If GetSession returns nil, it must have written an HTTP response, per the interface
+ // (this is probably because it drew a login form or something)
return
}
@@ -229,9 +236,23 @@ func (idp *IdentityProvider) ServeIDPInitiated(w http.ResponseWriter, r *http.Re
return
}
- for _, endpoint := range req.ServiceProviderMetadata.SPSSODescriptor.AssertionConsumerService {
- req.ACSEndpoint = &endpoint
- break
+ // find an ACS endpoint that we can use
+ for _, spssoDescriptor := range req.ServiceProviderMetadata.SPSSODescriptors {
+ for _, endpoint := range spssoDescriptor.AssertionConsumerServices {
+ if endpoint.Binding == HTTPPostBinding {
+ req.ACSEndpoint = &endpoint
+ req.SPSSODescriptor = &spssoDescriptor
+ break
+ }
+ }
+ if req.ACSEndpoint != nil {
+ break
+ }
+ }
+ if req.ACSEndpoint == nil {
+ idp.Logger.Printf("saml metadata does not contain an Assertion Customer Service url")
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
}
if err := req.MakeAssertion(session); err != nil {
@@ -239,6 +260,7 @@ func (idp *IdentityProvider) ServeIDPInitiated(w http.ResponseWriter, r *http.Re
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
+
if err := req.WriteResponse(w); err != nil {
idp.Logger.Printf("failed to write response: %s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -253,11 +275,12 @@ type IdpAuthnRequest struct {
RelayState string
RequestBuffer []byte
Request AuthnRequest
- ServiceProviderMetadata *Metadata
+ ServiceProviderMetadata *EntityDescriptor
+ SPSSODescriptor *SPSSODescriptor
ACSEndpoint *IndexedEndpoint
Assertion *Assertion
- AssertionBuffer []byte
- Response *Response
+ AssertionEl *etree.Element
+ ResponseEl *etree.Element
}
// NewIdpAuthnRequest returns a new IdpAuthnRequest for the given HTTP request to the authorization
@@ -306,15 +329,14 @@ func (req *IdpAuthnRequest) Validate() error {
// TODO(ross): is this supposed to be the metdata URL? or the target URL?
// i.e. should idp.SSOURL actually be idp.Metadata().EntityID?
if req.Request.Destination != req.IDP.SSOURL.String() {
- return fmt.Errorf("expected destination to be %q, not %q",
- req.IDP.SSOURL.String(), req.Request.Destination)
+ return fmt.Errorf("expected destination to be %q, not %q", req.IDP.SSOURL.String(), req.Request.Destination)
}
if req.Request.IssueInstant.Add(MaxIssueDelay).Before(TimeNow()) {
return fmt.Errorf("request expired at %s",
req.Request.IssueInstant.Add(MaxIssueDelay))
}
if req.Request.Version != "2.0" {
- return fmt.Errorf("expected SAML request version 2, got %q", req.Request.Version)
+ return fmt.Errorf("expected SAML request version 2.0 got %v", req.Request.Version)
}
// find the service provider
@@ -328,26 +350,122 @@ func (req *IdpAuthnRequest) Validate() error {
req.ServiceProviderMetadata = serviceProvider
// Check that the ACS URL matches an ACS endpoint in the SP metadata.
- acsValid := false
- for _, acsEndpoint := range serviceProvider.SPSSODescriptor.AssertionConsumerService {
- if req.Request.AssertionConsumerServiceURL == acsEndpoint.Location {
- req.ACSEndpoint = &acsEndpoint
- acsValid = true
- break
+ if err := req.getACSEndpoint(); err != nil {
+ return fmt.Errorf("cannot find assertion consumer service: %v", err)
+ }
+
+ return nil
+}
+
+func (req *IdpAuthnRequest) getACSEndpoint() error {
+ if req.Request.AssertionConsumerServiceIndex != "" {
+ for _, spssoDescriptor := range req.ServiceProviderMetadata.SPSSODescriptors {
+ for _, spAssertionConsumerService := range spssoDescriptor.AssertionConsumerServices {
+ if strconv.Itoa(spAssertionConsumerService.Index) == req.Request.AssertionConsumerServiceIndex {
+ req.SPSSODescriptor = &spssoDescriptor
+ req.ACSEndpoint = &spAssertionConsumerService
+ return nil
+ }
+ }
}
}
- if !acsValid {
- return fmt.Errorf("invalid ACS url specified in request: %s", req.Request.AssertionConsumerServiceURL)
+
+ if req.Request.AssertionConsumerServiceURL != "" {
+ for _, spssoDescriptor := range req.ServiceProviderMetadata.SPSSODescriptors {
+ for _, spAssertionConsumerService := range spssoDescriptor.AssertionConsumerServices {
+ if spAssertionConsumerService.Location == req.Request.AssertionConsumerServiceURL {
+ req.SPSSODescriptor = &spssoDescriptor
+ req.ACSEndpoint = &spAssertionConsumerService
+ return nil
+ }
+ }
+ }
}
- return nil
+ return os.ErrNotExist // no ACS url found or specified
}
// MakeAssertion produces a SAML assertion for the
// given request and assigns it to req.Assertion.
func (req *IdpAuthnRequest) MakeAssertion(session *Session) error {
-
attributes := []Attribute{}
+
+ var attributeConsumingService *AttributeConsumingService
+ for _, acs := range req.SPSSODescriptor.AttributeConsumingServices {
+ if acs.IsDefault != nil && *acs.IsDefault {
+ attributeConsumingService = &acs
+ break
+ }
+ }
+ if attributeConsumingService == nil {
+ for _, acs := range req.SPSSODescriptor.AttributeConsumingServices {
+ attributeConsumingService = &acs
+ break
+ }
+ }
+ if attributeConsumingService == nil {
+ attributeConsumingService = &AttributeConsumingService{}
+ }
+
+ for _, requestedAttribute := range attributeConsumingService.RequestedAttributes {
+ if requestedAttribute.NameFormat == "urn:oasis:names:tc:SAML:2.0:attrname-format:basic" || requestedAttribute.NameFormat == "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified" {
+ attrName := requestedAttribute.Name
+ attrName = regexp.MustCompile("[^A-Za-z0-9]+").ReplaceAllString(attrName, "")
+ switch attrName {
+ case "email", "emailaddress":
+ attributes = append(attributes, Attribute{
+ FriendlyName: requestedAttribute.FriendlyName,
+ Name: requestedAttribute.Name,
+ NameFormat: requestedAttribute.NameFormat,
+ Values: []AttributeValue{{
+ Type: "xs:string",
+ Value: session.UserEmail,
+ }},
+ })
+ case "name", "fullname", "cn", "commonname":
+ attributes = append(attributes, Attribute{
+ FriendlyName: requestedAttribute.FriendlyName,
+ Name: requestedAttribute.Name,
+ NameFormat: requestedAttribute.NameFormat,
+ Values: []AttributeValue{{
+ Type: "xs:string",
+ Value: session.UserCommonName,
+ }},
+ })
+ case "givenname", "firstname":
+ attributes = append(attributes, Attribute{
+ FriendlyName: requestedAttribute.FriendlyName,
+ Name: requestedAttribute.Name,
+ NameFormat: requestedAttribute.NameFormat,
+ Values: []AttributeValue{{
+ Type: "xs:string",
+ Value: session.UserGivenName,
+ }},
+ })
+ case "surname", "lastname", "familyname":
+ attributes = append(attributes, Attribute{
+ FriendlyName: requestedAttribute.FriendlyName,
+ Name: requestedAttribute.Name,
+ NameFormat: requestedAttribute.NameFormat,
+ Values: []AttributeValue{{
+ Type: "xs:string",
+ Value: session.UserSurname,
+ }},
+ })
+ case "uid", "user", "userid":
+ attributes = append(attributes, Attribute{
+ FriendlyName: requestedAttribute.FriendlyName,
+ Name: requestedAttribute.Name,
+ NameFormat: requestedAttribute.NameFormat,
+ Values: []AttributeValue{{
+ Type: "xs:string",
+ Value: session.UserName,
+ }},
+ })
+ }
+ }
+ }
+
if session.UserName != "" {
attributes = append(attributes, Attribute{
FriendlyName: "uid",
@@ -426,7 +544,7 @@ func (req *IdpAuthnRequest) MakeAssertion(session *Session) error {
ID: fmt.Sprintf("id-%x", randomBytes(20)),
IssueInstant: TimeNow(),
Version: "2.0",
- Issuer: &Issuer{
+ Issuer: Issuer{
Format: "XXX",
Value: req.IDP.Metadata().EntityID,
},
@@ -437,46 +555,57 @@ func (req *IdpAuthnRequest) MakeAssertion(session *Session) error {
SPNameQualifier: req.ServiceProviderMetadata.EntityID,
Value: session.NameID,
},
- SubjectConfirmation: &SubjectConfirmation{
- Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
- SubjectConfirmationData: SubjectConfirmationData{
- Address: req.HTTPRequest.RemoteAddr,
- InResponseTo: req.Request.ID,
- NotOnOrAfter: TimeNow().Add(MaxIssueDelay),
- Recipient: req.ACSEndpoint.Location,
+ SubjectConfirmations: []SubjectConfirmation{
+ SubjectConfirmation{
+ Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
+ SubjectConfirmationData: &SubjectConfirmationData{
+ Address: req.HTTPRequest.RemoteAddr,
+ InResponseTo: req.Request.ID,
+ NotOnOrAfter: TimeNow().Add(MaxIssueDelay),
+ Recipient: req.ACSEndpoint.Location,
+ },
},
},
},
Conditions: &Conditions{
NotBefore: TimeNow(),
NotOnOrAfter: TimeNow().Add(MaxIssueDelay),
- AudienceRestriction: &AudienceRestriction{
- Audience: &Audience{Value: req.ServiceProviderMetadata.EntityID},
+ AudienceRestrictions: []AudienceRestriction{
+ AudienceRestriction{
+ Audience: Audience{Value: req.ServiceProviderMetadata.EntityID},
+ },
},
},
- AuthnStatement: &AuthnStatement{
- AuthnInstant: session.CreateTime,
- SessionIndex: session.Index,
- SubjectLocality: SubjectLocality{
- Address: req.HTTPRequest.RemoteAddr,
- },
- AuthnContext: AuthnContext{
- AuthnContextClassRef: &AuthnContextClassRef{
- Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
+ AuthnStatements: []AuthnStatement{
+ AuthnStatement{
+ AuthnInstant: session.CreateTime,
+ SessionIndex: session.Index,
+ SubjectLocality: &SubjectLocality{
+ Address: req.HTTPRequest.RemoteAddr,
+ },
+ AuthnContext: AuthnContext{
+ AuthnContextClassRef: &AuthnContextClassRef{
+ Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
+ },
},
},
},
- AttributeStatement: &AttributeStatement{
- Attributes: attributes,
+ AttributeStatements: []AttributeStatement{
+ AttributeStatement{
+ Attributes: attributes,
+ },
},
}
return nil
}
-// MarshalAssertion sets `AssertionBuffer` to a signed, encrypted
-// version of `Assertion`.
-func (req *IdpAuthnRequest) MarshalAssertion() error {
+// The Canonicalizer prefix list MUST be empty. Various implementations
+// (maybe ours?) do not appear to support non-empty prefix lists in XML C14N.
+const canonicalizerPrefixList = ""
+
+// MakeAssertionEl sets `AssertionEl` to a signed, possibly encrypted, version of `Assertion`.
+func (req *IdpAuthnRequest) MakeAssertionEl() error {
keyPair := tls.Certificate{
Certificate: [][]byte{req.IDP.Certificate.Raw},
PrivateKey: req.IDP.Key,
@@ -485,17 +614,27 @@ func (req *IdpAuthnRequest) MarshalAssertion() error {
keyStore := dsig.TLSCertKeyStore(keyPair)
signingContext := dsig.NewDefaultSigningContext(keyStore)
+ signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList)
if err := signingContext.SetSignatureMethod(dsig.RSASHA1SignatureMethod); err != nil {
return err
}
- assertionEl, err := marshalEtreeHack(req.Assertion)
+ assertionEl := req.Assertion.Element()
+
+ signedAssertionEl, err := signingContext.SignEnveloped(assertionEl)
if err != nil {
return err
}
- signedAssertionEl, err := signingContext.SignEnveloped(assertionEl)
- if err != nil {
+ sigEl := signedAssertionEl.Child[len(signedAssertionEl.Child)-1]
+ req.Assertion.Signature = sigEl.(*etree.Element)
+ signedAssertionEl = req.Assertion.Element()
+
+ certBuf, err := req.getSPEncryptionCert()
+ if err == os.ErrNotExist {
+ req.AssertionEl = signedAssertionEl
+ return nil
+ } else if err != nil {
return err
}
@@ -512,56 +651,32 @@ func (req *IdpAuthnRequest) MarshalAssertion() error {
encryptor := xmlenc.OAEP()
encryptor.BlockCipher = xmlenc.AES128CBC
encryptor.DigestMethod = &xmlenc.SHA1
- certBuf, err := getSPEncryptionCert(req.ServiceProviderMetadata)
- if err != nil {
- return err
- }
encryptedDataEl, err := encryptor.Encrypt(certBuf, signedAssertionBuf)
if err != nil {
return err
}
encryptedDataEl.CreateAttr("Type", "http://www.w3.org/2001/04/xmlenc#Element")
- {
- encryptedAssertionEl := etree.NewElement("saml2:EncryptedAssertion")
- encryptedAssertionEl.CreateAttr("xmlns:saml2", "urn:oasis:names:tc:SAML:2.0:protocol")
- encryptedAssertionEl.AddChild(encryptedDataEl)
- doc := etree.NewDocument()
- doc.SetRoot(encryptedAssertionEl)
- req.AssertionBuffer, err = doc.WriteToBytes()
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-// marshalEtreeHack returns an etree.Element for the value v.
-//
-// This is a hack -- it first users xml.Marshal and then loads the
-// resulting buffer into an etree.
-func marshalEtreeHack(v interface{}) (*etree.Element, error) {
- buf, err := xml.Marshal(v)
- if err != nil {
- return nil, err
- }
+ encryptedAssertionEl := etree.NewElement("saml2:EncryptedAssertion")
+ encryptedAssertionEl.CreateAttr("xmlns:saml2", "urn:oasis:names:tc:SAML:2.0:protocol")
+ encryptedAssertionEl.AddChild(encryptedDataEl)
+ req.AssertionEl = encryptedAssertionEl
- doc := etree.NewDocument()
- if err := doc.ReadFromBytes(buf); err != nil {
- return nil, err
- }
- return doc.Root(), nil
+ return nil
}
// WriteResponse writes the `Response` to the http.ResponseWriter. If
// `Response` is not already set, it calls MakeResponse to produce it.
func (req *IdpAuthnRequest) WriteResponse(w http.ResponseWriter) error {
- if req.Response == nil {
+ if req.ResponseEl == nil {
if err := req.MakeResponse(); err != nil {
return err
}
}
- responseBuf, err := xml.Marshal(req.Response)
+
+ doc := etree.NewDocument()
+ doc.SetRoot(req.ResponseEl)
+ responseBuf, err := doc.WriteToBytes()
if err != nil {
return err
}
@@ -605,37 +720,41 @@ func (req *IdpAuthnRequest) WriteResponse(w http.ResponseWriter) error {
// getSPEncryptionCert returns the certificate which we can use to encrypt things
// to the SP in PEM format, or nil if no such certificate is found.
-func getSPEncryptionCert(sp *Metadata) ([]byte, error) {
- cert := ""
- for _, keyDescriptor := range sp.SPSSODescriptor.KeyDescriptor {
+func (req *IdpAuthnRequest) getSPEncryptionCert() (*x509.Certificate, error) {
+ certStr := ""
+ for _, keyDescriptor := range req.SPSSODescriptor.KeyDescriptors {
if keyDescriptor.Use == "encryption" {
- cert = keyDescriptor.KeyInfo.Certificate
+ certStr = keyDescriptor.KeyInfo.Certificate
break
}
}
- // If there are no explicitly signing certs, just return the first
+ // If there are no certs explicitly labeled for encryption, return the first
// non-empty cert we find.
- if cert == "" {
- for _, keyDescriptor := range sp.SPSSODescriptor.KeyDescriptor {
+ if certStr == "" {
+ for _, keyDescriptor := range req.SPSSODescriptor.KeyDescriptors {
if keyDescriptor.Use == "" && keyDescriptor.KeyInfo.Certificate != "" {
- cert = keyDescriptor.KeyInfo.Certificate
+ certStr = keyDescriptor.KeyInfo.Certificate
break
}
}
}
- if cert == "" {
- return nil, fmt.Errorf("cannot find a certificate for encryption in the service provider SSO descriptor")
+ if certStr == "" {
+ return nil, os.ErrNotExist
}
// cleanup whitespace and re-encode a PEM
- cert = regexp.MustCompile(`\s+`).ReplaceAllString(cert, "")
- certBytes, err := base64.StdEncoding.DecodeString(cert)
+ certStr = regexp.MustCompile(`\s+`).ReplaceAllString(certStr, "")
+ certBytes, err := base64.StdEncoding.DecodeString(certStr)
+ if err != nil {
+ return nil, fmt.Errorf("cannot decode certificate base64: %v", err)
+ }
+ cert, err := x509.ParseCertificate(certBytes)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("cannot parse certificate: %v", err)
}
- return certBytes, nil
+ return cert, nil
}
// unmarshalEtreeHack parses `el` and sets values in the structure `v`.
@@ -651,16 +770,17 @@ func unmarshalEtreeHack(el *etree.Element, v interface{}) error {
return xml.Unmarshal(buf, v)
}
-// MakeResponse creates and assigns a new SAML response in Response. `Assertion` must
-// be non-nill. If MarshalAssertion() has not been called, this function calls it for
+// MakeResponse creates and assigns a new SAML response in ResponseEl. `Assertion` must
+// be non-nil. If MakeAssertionEl() has not been called, this function calls it for
// you.
func (req *IdpAuthnRequest) MakeResponse() error {
- if req.AssertionBuffer == nil {
- if err := req.MarshalAssertion(); err != nil {
+ if req.AssertionEl == nil {
+ if err := req.MakeAssertionEl(); err != nil {
return err
}
}
- req.Response = &Response{
+
+ response := &Response{
Destination: req.ACSEndpoint.Location,
ID: fmt.Sprintf("id-%x", randomBytes(20)),
InResponseTo: req.Request.ID,
@@ -670,14 +790,16 @@ func (req *IdpAuthnRequest) MakeResponse() error {
Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
Value: req.IDP.MetadataURL.String(),
},
- Status: &Status{
+ Status: Status{
StatusCode: StatusCode{
Value: StatusSuccess,
},
},
- EncryptedAssertion: &EncryptedAssertion{
- EncryptedData: req.AssertionBuffer,
- },
}
+
+ responseEl := response.Element()
+ responseEl.AddChild(req.AssertionEl) // AssertionEl either an EncryptedAssertion or Assertion element
+
+ req.ResponseEl = responseEl
return nil
}
diff --git a/identity_provider_test.go b/identity_provider_test.go
index cc95e79c..6d9f2b8d 100644
--- a/identity_provider_test.go
+++ b/identity_provider_test.go
@@ -117,7 +117,7 @@ OwJlNCASPZRH/JmF8tX0hoHuAQ==
Certificate: test.SPCertificate,
MetadataURL: mustParseURL("https://sp.example.com/saml2/metadata"),
AcsURL: mustParseURL("https://sp.example.com/saml2/acs"),
- IDPMetadata: &Metadata{},
+ IDPMetadata: &EntityDescriptor{},
}
test.Key = mustParsePrivateKey("-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDU8wdiaFmPfTyRYuFlVPi866WrH/2JubkHzp89bBQopDaLXYxi\n3PTu3O6Q/KaKxMOFBqrInwqpv/omOGZ4ycQ51O9I+Yc7ybVlW94lTo2gpGf+Y/8E\nPsVbnZaFutRctJ4dVIp9aQ2TpLiGT0xX1OzBO/JEgq9GzDRf+B+eqSuglwIDAQAB\nAoGBAMuy1eN6cgFiCOgBsB3gVDdTKpww87Qk5ivjqEt28SmXO13A1KNVPS6oQ8SJ\nCT5Azc6X/BIAoJCURVL+LHdqebogKljhH/3yIel1kH19vr4E2kTM/tYH+qj8afUS\nJEmArUzsmmK8ccuNqBcllqdwCZjxL4CHDUmyRudFcHVX9oyhAkEA/OV1OkjM3CLU\nN3sqELdMmHq5QZCUihBmk3/N5OvGdqAFGBlEeewlepEVxkh7JnaNXAXrKHRVu/f/\nfbCQxH+qrwJBANeQERF97b9Sibp9xgolb749UWNlAdqmEpmlvmS202TdcaaT1msU\n4rRLiQN3X9O9mq4LZMSVethrQAdX1whawpkCQQDk1yGf7xZpMJ8F4U5sN+F4rLyM\nRq8Sy8p2OBTwzCUXXK+fYeXjybsUUMr6VMYTRP2fQr/LKJIX+E5ZxvcIyFmDAkEA\nyfjNVUNVaIbQTzEbRlRvT6MqR+PTCefC072NF9aJWR93JimspGZMR7viY6IM4lrr\nvBkm0F5yXKaYtoiiDMzlOQJADqmEwXl0D72ZG/2KDg8b4QZEmC9i5gidpQwJXUc6\nhU+IVQoLxRq0fBib/36K9tcrrO5Ba4iEvDcNY+D8yGbUtA==\n-----END RSA PRIVATE KEY-----\n")
@@ -130,7 +130,7 @@ OwJlNCASPZRH/JmF8tX0hoHuAQ==
MetadataURL: mustParseURL("https://idp.example.com/saml/metadata"),
SSOURL: mustParseURL("https://idp.example.com/saml/sso"),
ServiceProviderProvider: &mockServiceProviderProvider{
- GetServiceProviderFunc: func(r *http.Request, serviceProviderID string) (*Metadata, error) {
+ GetServiceProviderFunc: func(r *http.Request, serviceProviderID string) (*EntityDescriptor, error) {
if serviceProviderID == test.SP.MetadataURL.String() {
return test.SP.Metadata(), nil
}
@@ -157,49 +157,60 @@ func (msp *mockSessionProvider) GetSession(w http.ResponseWriter, r *http.Reques
}
type mockServiceProviderProvider struct {
- GetServiceProviderFunc func(r *http.Request, serviceProviderID string) (*Metadata, error)
+ GetServiceProviderFunc func(r *http.Request, serviceProviderID string) (*EntityDescriptor, error)
}
-func (mspp *mockServiceProviderProvider) GetServiceProvider(r *http.Request, serviceProviderID string) (*Metadata, error) {
+func (mspp *mockServiceProviderProvider) GetServiceProvider(r *http.Request, serviceProviderID string) (*EntityDescriptor, error) {
return mspp.GetServiceProviderFunc(r, serviceProviderID)
}
func (test *IdentityProviderTest) TestCanProduceMetadata(c *C) {
- c.Assert(test.IDP.Metadata(), DeepEquals, &Metadata{
+ c.Assert(test.IDP.Metadata(), DeepEquals, &EntityDescriptor{
ValidUntil: TimeNow().Add(DefaultValidDuration),
CacheDuration: DefaultValidDuration,
EntityID: "https://idp.example.com/saml/metadata",
- IDPSSODescriptor: &IDPSSODescriptor{
- XMLName: xml.Name{},
- ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
- KeyDescriptor: []KeyDescriptor{
- {
- Use: "signing",
- KeyInfo: KeyInfo{
- XMLName: xml.Name{},
- Certificate: "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==",
+ IDPSSODescriptors: []IDPSSODescriptor{
+ IDPSSODescriptor{
+ SSODescriptor: SSODescriptor{
+ RoleDescriptor: RoleDescriptor{
+ ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
+ KeyDescriptors: []KeyDescriptor{
+ {
+ Use: "signing",
+ KeyInfo: KeyInfo{
+ XMLName: xml.Name{},
+ Certificate: "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==",
+ },
+ EncryptionMethods: nil,
+ },
+ {
+ Use: "encryption",
+ KeyInfo: KeyInfo{
+ XMLName: xml.Name{},
+ Certificate: "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==",
+ },
+ EncryptionMethods: []EncryptionMethod{
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"},
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes192-cbc"},
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"},
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"},
+ },
+ },
+ },
},
- EncryptionMethods: nil,
+ NameIDFormats: []NameIDFormat{NameIDFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:transient")},
},
- {
- Use: "encryption",
- KeyInfo: KeyInfo{
- XMLName: xml.Name{},
- Certificate: "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==",
+ SingleSignOnServices: []Endpoint{
+ Endpoint{
+ Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
+ Location: "https://idp.example.com/saml/sso",
},
- EncryptionMethods: []EncryptionMethod{
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"},
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes192-cbc"},
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"},
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"},
+ Endpoint{
+ Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
+ Location: "https://idp.example.com/saml/sso",
},
},
},
- NameIDFormat: []string{"urn:oasis:names:tc:SAML:2.0:nameid-format:transient"},
- SingleSignOnService: []Endpoint{
- {Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", Location: "https://idp.example.com/saml/sso", ResponseLocation: ""},
- {Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", Location: "https://idp.example.com/saml/sso", ResponseLocation: ""},
- },
},
})
}
@@ -242,14 +253,15 @@ func (test *IdentityProviderTest) TestCanHandleRequestWithNewSession(c *C) {
decodedRequest, err := testsaml.ParseRedirectRequest(requestURL)
c.Assert(err, IsNil)
- c.Assert(string(decodedRequest), Equals, "https://sp.example.com/saml2/metadataurn:oasis:names:tc:SAML:2.0:nameid-format:transient")
+ c.Assert(string(decodedRequest), Equals, "https://sp.example.com/saml2/metadata")
c.Assert(requestURL.Query().Get("RelayState"), Equals, "ThisIsTheRelayState")
r, _ := http.NewRequest("GET", requestURL.String(), nil)
test.IDP.ServeSSO(w, r)
c.Assert(w.Code, Equals, 200)
c.Assert(string(w.Body.Bytes()), Equals, ""+
- "RelayState: ThisIsTheRelayState\nSAMLRequest: https://sp.example.com/saml2/metadataurn:oasis:names:tc:SAML:2.0:nameid-format:transient")
+ "RelayState: ThisIsTheRelayState\n"+
+ "SAMLRequest: https://sp.example.com/saml2/metadata")
}
func (test *IdentityProviderTest) TestCanHandleRequestWithExistingSession(c *C) {
@@ -268,7 +280,7 @@ func (test *IdentityProviderTest) TestCanHandleRequestWithExistingSession(c *C)
decodedRequest, err := testsaml.ParseRedirectRequest(requestURL)
c.Assert(err, IsNil)
- c.Assert(string(decodedRequest), Equals, "https://sp.example.com/saml2/metadataurn:oasis:names:tc:SAML:2.0:nameid-format:transient")
+ c.Assert(string(decodedRequest), Equals, "https://sp.example.com/saml2/metadata")
r, _ := http.NewRequest("GET", requestURL.String(), nil)
test.IDP.ServeSSO(w, r)
@@ -425,7 +437,7 @@ func (test *IdentityProviderTest) TestCanValidate(c *C) {
" AllowCreate=\"true\">urn:oasis:names:tc:SAML:2.0:nameid-format:transient" +
""),
}
- c.Assert(req.Validate(), ErrorMatches, "expected SAML request version 2, got \"4.2\"")
+ c.Assert(req.Validate(), ErrorMatches, "expected SAML request version 2.0 got 4.2")
req = IdpAuthnRequest{
IDP: &test.IDP,
@@ -459,7 +471,7 @@ func (test *IdentityProviderTest) TestCanValidate(c *C) {
" AllowCreate=\"true\">urn:oasis:names:tc:SAML:2.0:nameid-format:transient" +
""),
}
- c.Assert(req.Validate(), ErrorMatches, "invalid ACS url specified in request: https://unknown.example.com/saml2/acs")
+ c.Assert(req.Validate(), ErrorMatches, "cannot find assertion consumer service: file does not exist")
}
@@ -493,48 +505,56 @@ func (test *IdentityProviderTest) TestMakeAssertion(c *C) {
ID: "id-00020406080a0c0e10121416181a1c1e20222426",
IssueInstant: TimeNow(),
Version: "2.0",
- Issuer: &Issuer{
+ Issuer: Issuer{
Format: "XXX",
Value: "https://idp.example.com/saml/metadata",
},
Signature: nil,
Subject: &Subject{
NameID: &NameID{Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", NameQualifier: "https://idp.example.com/saml/metadata", SPNameQualifier: "https://sp.example.com/saml2/metadata", Value: ""},
- SubjectConfirmation: &SubjectConfirmation{
- Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
- SubjectConfirmationData: SubjectConfirmationData{
- Address: "",
- InResponseTo: "id-00020406080a0c0e10121416181a1c1e",
- NotOnOrAfter: TimeNow().Add(MaxIssueDelay),
- Recipient: "https://sp.example.com/saml2/acs",
+ SubjectConfirmations: []SubjectConfirmation{
+ SubjectConfirmation{
+ Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
+ SubjectConfirmationData: &SubjectConfirmationData{
+ Address: "",
+ InResponseTo: "id-00020406080a0c0e10121416181a1c1e",
+ NotOnOrAfter: TimeNow().Add(MaxIssueDelay),
+ Recipient: "https://sp.example.com/saml2/acs",
+ },
},
},
},
Conditions: &Conditions{
NotBefore: TimeNow(),
NotOnOrAfter: TimeNow().Add(MaxIssueDelay),
- AudienceRestriction: &AudienceRestriction{
- Audience: &Audience{Value: "https://sp.example.com/saml2/metadata"},
+ AudienceRestrictions: []AudienceRestriction{
+ AudienceRestriction{
+ Audience: Audience{Value: "https://sp.example.com/saml2/metadata"},
+ },
},
},
- AuthnStatement: &AuthnStatement{
- AuthnInstant: time.Time{},
- SessionIndex: "",
- SubjectLocality: SubjectLocality{},
- AuthnContext: AuthnContext{
- AuthnContextClassRef: &AuthnContextClassRef{Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"},
+ AuthnStatements: []AuthnStatement{
+ AuthnStatement{
+ AuthnInstant: time.Time{},
+ SessionIndex: "",
+ SubjectLocality: &SubjectLocality{},
+ AuthnContext: AuthnContext{
+ AuthnContextClassRef: &AuthnContextClassRef{Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"},
+ },
},
},
- AttributeStatement: &AttributeStatement{
- Attributes: []Attribute{
- {
- FriendlyName: "uid",
- Name: "urn:oid:0.9.2342.19200300.100.1.1",
- NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
- Values: []AttributeValue{
- {
- Type: "xs:string",
- Value: "alice",
+ AttributeStatements: []AttributeStatement{
+ AttributeStatement{
+ Attributes: []Attribute{
+ {
+ FriendlyName: "uid",
+ Name: "urn:oid:0.9.2342.19200300.100.1.1",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ Values: []AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "alice",
+ },
},
},
},
@@ -556,7 +576,7 @@ func (test *IdentityProviderTest) TestMakeAssertion(c *C) {
})
c.Assert(err, IsNil)
- c.Assert(req.Assertion.AttributeStatement.Attributes, DeepEquals, []Attribute{
+ c.Assert(req.Assertion.AttributeStatements[0].Attributes, DeepEquals, []Attribute{
{
FriendlyName: "uid",
Name: "urn:oid:0.9.2342.19200300.100.1.1",
@@ -658,17 +678,15 @@ func (test *IdentityProviderTest) TestMarshalAssertion(c *C) {
UserName: "alice",
})
c.Assert(err, IsNil)
- err = req.MarshalAssertion()
+ err = req.MakeAssertionEl()
c.Assert(err, IsNil)
// Compare the plaintext first
- expectedPlaintext := "https://idp.example.com/saml/metadatahttps://sp.example.com/saml2/metadataurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportalicehwaBcsq5Pa63BcYuXO3+EFvc3Kc=VBrfGlfrPiN3E1YJn5i7adtgJ6yrsrGVsnyzYrPyWUKnwv7F8ULiSRh978r6d1Ub5ug3ldxwjgChgLTMXdsv+x+r9Z82e0UThpSBPIWw13SxxhN37lO4PBxrqn6YJtoJ8vDAcNA28xDUUwBU2IXLQRyRrjHyfYx5M/ORHpUxeDI=MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ=="
+ expectedPlaintext := "https://idp.example.com/saml/metadata1vtgtflZQphWGGqXh0fZSitc1m4=zhRNk0gd7EHGQ9ZLHsiHYunopXKXF3JLw8Ou11SKy914IXI4hisqTbrRLIJOtz6WbA9jeFeDOxlclp/512TY8/LDfCntZGeTece21XtOqYg5ZtFLaFoR+R5VoFsVG9Wvw2Z6KQQOvE13+uhHjBcnkz511ook6wxRUGjh6RCYHyQ=MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==https://sp.example.com/saml2/metadataurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportalice"
actualPlaintext := ""
{
doc := etree.NewDocument()
- err := doc.ReadFromBytes(req.AssertionBuffer)
- c.Assert(err, IsNil)
-
+ doc.SetRoot(req.AssertionEl)
el := doc.FindElement("//EncryptedAssertion/EncryptedData")
actualPlaintextBuf, err := xmlenc.Decrypt(test.SPKey, el)
c.Assert(err, IsNil)
@@ -676,7 +694,11 @@ func (test *IdentityProviderTest) TestMarshalAssertion(c *C) {
}
c.Assert(actualPlaintext, Equals, expectedPlaintext)
- c.Assert(string(req.AssertionBuffer), Equals, "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")
+ doc := etree.NewDocument()
+ doc.SetRoot(req.AssertionEl)
+ assertionBuffer, err := doc.WriteToBytes()
+ c.Assert(err, IsNil)
+ c.Assert(string(assertionBuffer), Equals, "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")
}
func (test *IdentityProviderTest) TestMakeResponse(c *C) {
@@ -703,32 +725,29 @@ func (test *IdentityProviderTest) TestMakeResponse(c *C) {
UserName: "alice",
})
c.Assert(err, IsNil)
- err = req.MarshalAssertion()
+ err = req.MakeAssertionEl()
c.Assert(err, IsNil)
- req.AssertionBuffer = []byte("THIS_IS_THE_ENCRYPTED_ASSERTION")
+ req.AssertionEl = etree.NewElement("this-is-an-encrypted-assertion")
err = req.MakeResponse()
c.Assert(err, IsNil)
- c.Assert(req.Response, DeepEquals, &Response{
- Destination: "https://sp.example.com/saml2/acs",
- ID: "id-282a2c2e30323436383a3c3e40424446484a4c4e",
- InResponseTo: "id-00020406080a0c0e10121416181a1c1e",
- IssueInstant: TimeNow(),
- Version: "2.0",
- Issuer: &Issuer{
- Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
- Value: "https://idp.example.com/saml/metadata",
- },
- Status: &Status{
- StatusCode: StatusCode{
- Value: "urn:oasis:names:tc:SAML:2.0:status:Success",
- },
- },
- EncryptedAssertion: &EncryptedAssertion{
- EncryptedData: []byte("THIS_IS_THE_ENCRYPTED_ASSERTION"),
- },
- })
+ response := Response{}
+ err = unmarshalEtreeHack(req.ResponseEl, &response)
+ c.Assert(err, IsNil)
+
+ doc := etree.NewDocument()
+ doc.SetRoot(req.ResponseEl)
+ doc.Indent(2)
+ responseStr, _ := doc.WriteToString()
+ c.Assert(responseStr, DeepEquals, ""+
+ "\n"+
+ " https://idp.example.com/saml/metadata\n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ "\n")
}
func (test *IdentityProviderTest) TestWriteResponse(c *C) {
@@ -747,7 +766,7 @@ func (test *IdentityProviderTest) TestWriteResponse(c *C) {
" urn:oasis:names:tc:SAML:2.0:nameid-format:transient" +
""),
- Response: &Response{ID: "THIS_IS_THE_SAML_RESPONSE"},
+ ResponseEl: etree.NewElement("THIS_IS_THE_SAML_RESPONSE"),
}
req.HTTPRequest, _ = http.NewRequest("POST", "http://idp.example.com/saml/sso", nil)
err := req.Validate()
@@ -757,7 +776,7 @@ func (test *IdentityProviderTest) TestWriteResponse(c *C) {
err = req.WriteResponse(w)
c.Assert(err, IsNil)
c.Assert(w.Code, Equals, 200)
- c.Assert(string(w.Body.Bytes()), Equals, "
")
+ c.Assert(string(w.Body.Bytes()), Equals, "")
}
func (test *IdentityProviderTest) TestIDPInitiatedNewSession(c *C) {
@@ -808,3 +827,247 @@ func (test *IdentityProviderTest) TestIDPInitiatedBadServiceProvider(c *C) {
test.IDP.ServeIDPInitiated(w, r, "https://wrong.url/metadata", "ThisIsTheRelayState")
c.Assert(w.Code, Equals, http.StatusNotFound)
}
+
+func (test *IdentityProviderTest) TestCanHandleUnencryptedResponse(c *C) {
+ test.IDP.SessionProvider = &mockSessionProvider{
+ GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
+ return &Session{ID: "f00df00df00d", UserName: "alice"}
+ },
+ }
+
+ metadata := EntityDescriptor{}
+ err := xml.Unmarshal([]byte(`Required attributes`), &metadata)
+ c.Assert(err, IsNil)
+ test.IDP.ServiceProviderProvider = &mockServiceProviderProvider{
+ GetServiceProviderFunc: func(r *http.Request, serviceProviderID string) (*EntityDescriptor, error) {
+ if serviceProviderID == "https://gitlab.example.com/users/saml/metadata" {
+ return &metadata, nil
+ }
+ return nil, os.ErrNotExist
+ },
+ }
+
+ req := IdpAuthnRequest{
+ IDP: &test.IDP,
+ RequestBuffer: []byte("" +
+ "" +
+ " https://gitlab.example.com/users/saml/metadata" +
+ ""),
+ }
+ req.HTTPRequest, _ = http.NewRequest("POST", "http://idp.example.com/saml/sso", nil)
+ err = req.Validate()
+ c.Assert(err, IsNil)
+ err = req.MakeAssertion(&Session{
+ ID: "f00df00df00d",
+ UserName: "alice",
+ })
+ c.Assert(err, IsNil)
+ err = req.MakeAssertionEl()
+ c.Assert(err, IsNil)
+
+ err = req.MakeResponse()
+ c.Assert(err, IsNil)
+
+ doc := etree.NewDocument()
+ doc.SetRoot(req.ResponseEl)
+ doc.Indent(2)
+ responseStr, _ := doc.WriteToString()
+ c.Assert(responseStr, DeepEquals, ""+
+ "\n"+
+ " https://idp.example.com/saml/metadata\n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " https://idp.example.com/saml/metadata\n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " YGDRz05wKloMNfd8VOcNFym65MA=\n"+
+ " \n"+
+ " \n"+
+ " NT0pM2uc8sexm+GVLt48xQrpz1DhYQBwxibYsuuuNCjOOzJTowRMMn6ouM79w74tj425g6cMPJhXo24TT2X6N3/oRr88Tk43F1Mu4PveJ1jdWqZsMdsufMEFvJdtVONgExncPVB0zhRxaHqkQ97IMz7tigmCipq2KfsrvJ/MaiQ=\n"+
+ " \n"+
+ " \n"+
+ " MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==\n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " https://gitlab.example.com/users/auth/saml/metadata\n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport\n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " alice\n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ "\n")
+}
+
+func (test *IdentityProviderTest) TestRequestedAttributes(c *C) {
+ metadata := EntityDescriptor{}
+ err := xml.Unmarshal([]byte(`Required attributes`), &metadata)
+ c.Assert(err, IsNil)
+
+ requestURL, err := test.SP.MakeRedirectAuthenticationRequest("ThisIsTheRelayState")
+ c.Assert(err, IsNil)
+
+ r, _ := http.NewRequest("GET", requestURL.String(), nil)
+ req, err := NewIdpAuthnRequest(&test.IDP, r)
+ req.ServiceProviderMetadata = &metadata
+ req.ACSEndpoint = &metadata.SPSSODescriptors[0].AssertionConsumerServices[0]
+ req.SPSSODescriptor = &metadata.SPSSODescriptors[0]
+ c.Assert(err, IsNil)
+ err = req.MakeAssertion(&Session{
+ ID: "f00df00df00d",
+ UserName: "alice",
+ UserEmail: "alice@example.com",
+ UserGivenName: "Alice",
+ UserSurname: "Smith",
+ UserCommonName: "Alice Smith",
+ })
+ c.Assert(err, IsNil)
+
+ c.Assert(req.Assertion.AttributeStatements, DeepEquals, []AttributeStatement{AttributeStatement{
+ Attributes: []Attribute{
+ Attribute{
+ FriendlyName: "Email address",
+ Name: "email",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ Values: []AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "alice@example.com",
+ },
+ },
+ },
+ Attribute{
+ FriendlyName: "Full name",
+ Name: "name",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ Values: []AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "Alice Smith",
+ },
+ },
+ },
+ Attribute{
+ FriendlyName: "Given name",
+ Name: "first_name",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ Values: []AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "Alice",
+ },
+ },
+ },
+ Attribute{
+ FriendlyName: "Family name",
+ Name: "last_name",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ Values: []AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "Smith",
+ },
+ },
+ },
+ Attribute{
+ FriendlyName: "uid",
+ Name: "urn:oid:0.9.2342.19200300.100.1.1",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ Values: []AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "alice",
+ },
+ },
+ },
+ Attribute{
+ FriendlyName: "eduPersonPrincipalName",
+ Name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.6",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ Values: []AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "alice@example.com",
+ },
+ },
+ },
+ Attribute{
+ FriendlyName: "sn",
+ Name: "urn:oid:2.5.4.4",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ Values: []AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "Smith",
+ },
+ },
+ },
+ Attribute{
+ FriendlyName: "givenName",
+ Name: "urn:oid:2.5.4.42",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ Values: []AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "Alice",
+ },
+ },
+ },
+ Attribute{
+ FriendlyName: "cn",
+ Name: "urn:oid:2.5.4.3",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ Values: []AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "Alice Smith",
+ },
+ },
+ },
+ }}})
+}
diff --git a/metadata.go b/metadata.go
index 7e3d89a2..2352278d 100644
--- a/metadata.go
+++ b/metadata.go
@@ -3,38 +3,66 @@ package saml
import (
"encoding/xml"
"time"
+
+ "github.com/beevik/etree"
)
// HTTPPostBinding is the official URN for the HTTP-POST binding (transport)
-const HTTPPostBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+var HTTPPostBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
// HTTPRedirectBinding is the official URN for the HTTP-Redirect binding (transport)
-const HTTPRedirectBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+var HTTPRedirectBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
// EntitiesDescriptor represents the SAML object of the same name.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf section 2.3.1
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.3.1
type EntitiesDescriptor struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntitiesDescriptor"`
- EntityDescriptor []*Metadata `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"`
+ XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntitiesDescriptor"`
+ ID *string `xml:",attr,omitempty"`
+ ValidUntil *time.Time `xml:"validUntil,attr,omitempty"`
+ CacheDuration *time.Duration `xml:"cacheDuration,attr,omitempty"`
+ Name *string `xml:",attr,omitempty"`
+ Signature *etree.Element
+ EntitiesDescriptors []EntitiesDescriptor `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntitiesDescriptor"`
+ EntityDescriptors []EntityDescriptor `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"`
}
-// Metadata represents the SAML EntityDescriptor object.
+// Metadata as been renamed to EntityDescriptor
+//
+// This change was made to be consistent with the rest of the API which uses names
+// from the SAML specification for types.
+//
+// This is a tombstone to help you discover this fact. You should update references
+// to saml.Metadata to be saml.EntityDescriptor.
+var Metadata = struct{}{}
+
+// EntityDescriptor represents the SAML EntityDescriptor object.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf section 2.3.2
-type Metadata struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"`
- ValidUntil time.Time `xml:"validUntil,attr"`
- CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"`
- EntityID string `xml:"entityID,attr"`
- SPSSODescriptor *SPSSODescriptor `xml:"SPSSODescriptor"`
- IDPSSODescriptor *IDPSSODescriptor `xml:"IDPSSODescriptor"`
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.3.2
+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"`
+ Signature *etree.Element
+ RoleDescriptors []RoleDescriptor `xml:"RoleDescriptor"`
+ IDPSSODescriptors []IDPSSODescriptor `xml:"IDPSSODescriptor"`
+ SPSSODescriptors []SPSSODescriptor `xml:"SPSSODescriptor"`
+ AuthnAuthorityDescriptors []AuthnAuthorityDescriptor `xml:"AuthnAuthorityDescriptor"`
+ AttributeAuthorityDescriptors []AttributeAuthorityDescriptor `xml:"AttributeAuthorityDescriptor"`
+ PDPDescriptors []PDPDescriptor `xml:"PDPDescriptor"`
+ AffiliationDescriptor *AffiliationDescriptor
+ Organization *Organization
+ ContactPerson *ContactPerson
+ AdditionalMetadataLocations []string `xml:"AdditionalMetadataLocation"`
}
-func (m *Metadata) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
- type Alias Metadata
+// MarshalXML implements xml.Marshaler
+func (m *EntityDescriptor) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+ type Alias EntityDescriptor
aux := &struct {
- ValidUntil RelaxedTime `xml:"validUntil,attr"`
+ ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
*Alias
}{
ValidUntil: RelaxedTime(m.ValidUntil),
@@ -43,10 +71,11 @@ func (m *Metadata) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.Encode(aux)
}
-func (m *Metadata) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
- type Alias Metadata
+// UnmarshalXML implements xml.Unmarshaler
+func (m *EntityDescriptor) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ type Alias EntityDescriptor
aux := &struct {
- ValidUntil RelaxedTime `xml:"validUntil,attr"`
+ ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
*Alias
}{
Alias: (*Alias)(m),
@@ -58,6 +87,58 @@ func (m *Metadata) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil
}
+// Organization represents the SAML Organization object.
+//
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.3.2.1
+type Organization struct {
+ OrganizationNames []LocalizedName `xml:"OrganizationName"`
+ OrganizationDisplayNames []LocalizedName `xml:"OrganizationDisplayName"`
+ OrganizationURLs []LocalizedURI `xml:"OrganizationURL"`
+}
+
+// LocalizedName represents the SAML type localizedNameType.
+//
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.2.4
+type LocalizedName struct {
+ Lang string `xml:"xml lang,attr"`
+ Value string `xml:",chardata"`
+}
+
+// LocalizedURI represents the SAML type localizedURIType.
+//
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.2.5
+type LocalizedURI struct {
+ Lang string `xml:"xml lang,attr"`
+ Value string `xml:",chardata"`
+}
+
+// ContactPerson represents the SAML element ContactPerson.
+//
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.3.2.2
+type ContactPerson struct {
+ ContactType string `xml:"contactType,attr"`
+ Company string
+ GivenName string
+ SurName string
+ EmailAddresses []string `xml:"EmailAddress"`
+ TelephoneNumbers []string `xml:"TelephoneNumber"`
+}
+
+// RoleDescriptor represents the SAML element RoleDescriptor.
+//
+// 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"`
+ CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"`
+ ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
+ ErrorURL string `xml:"errorURL,attr,omitempty"`
+ Signature *etree.Element
+ KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor,omitempty"`
+ Organization *Organization `xml:"Organization,omitempty"`
+ ContactPeople []ContactPerson `xml:"ContactPerson,omitempty"`
+}
+
// KeyDescriptor represents the XMLSEC object of the same name
type KeyDescriptor struct {
Use string `xml:"use,attr"`
@@ -71,6 +152,8 @@ type EncryptionMethod struct {
}
// KeyInfo represents the XMLSEC object of the same name
+//
+// TODO(ross): revisit xmldsig and make this type more complete
type KeyInfo struct {
XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# KeyInfo"`
Certificate string `xml:"X509Data>X509Certificate"`
@@ -78,7 +161,7 @@ type KeyInfo struct {
// Endpoint represents the SAML EndpointType object.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf section 2.2.2
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.2.2
type Endpoint struct {
Binding string `xml:"Binding,attr"`
Location string `xml:"Location,attr"`
@@ -87,38 +170,113 @@ type Endpoint struct {
// IndexedEndpoint represents the SAML IndexedEndpointType object.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf section 2.2.3
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.2.3
type IndexedEndpoint struct {
- Binding string `xml:"Binding,attr"`
- Location string `xml:"Location,attr"`
- Index int `xml:"index,attr"`
+ Binding string `xml:"Binding,attr"`
+ Location string `xml:"Location,attr"`
+ ResponseLocation *string `xml:"ResponseLocation,attr,omitempty"`
+ Index int `xml:"index,attr"`
+ IsDefault *bool `xml:"isDefault,attr"`
}
-// SPSSODescriptor represents the SAML SPSSODescriptorType object.
+// SSODescriptor represents the SAML complex type SSODescriptor
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf section 2.4.2
-type SPSSODescriptor struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SPSSODescriptor"`
- AuthnRequestsSigned bool `xml:",attr"`
- WantAssertionsSigned bool `xml:",attr"`
- ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
- KeyDescriptor []KeyDescriptor `xml:"KeyDescriptor"`
- ArtifactResolutionService []IndexedEndpoint `xml:"ArtifactResolutionService"`
- SingleLogoutService []Endpoint `xml:"SingleLogoutService"`
- ManageNameIDService []Endpoint
- NameIDFormat []string `xml:"NameIDFormat"`
- AssertionConsumerService []IndexedEndpoint `xml:"AssertionConsumerService"`
- AttributeConsumingService []interface{}
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.2
+type SSODescriptor struct {
+ RoleDescriptor
+ ArtifactResolutionServices []IndexedEndpoint `xml:"ArtifactResolutionService"`
+ SingleLogoutServices []Endpoint `xml:"SingleLogoutService"`
+ ManageNameIDServices []Endpoint `xml:"ManageNameIDService"`
+ NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
}
// IDPSSODescriptor represents the SAML IDPSSODescriptorType object.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf section 2.4.3
+// 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"`
- WantAuthnRequestsSigned bool `xml:",attr"`
- ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
- KeyDescriptor []KeyDescriptor `xml:"KeyDescriptor"`
- NameIDFormat []string `xml:"NameIDFormat"`
- SingleSignOnService []Endpoint `xml:"SingleSignOnService"`
+ XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"`
+ SSODescriptor
+ WantAuthnRequestsSigned *bool `xml:",attr"`
+
+ SingleSignOnServices []Endpoint `xml:"SingleSignOnService"`
+ NameIDMappingServices []Endpoint `xml:"NameIDMappingService"`
+ AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
+ AttributeProfiles []string `xml:"AttributeProfile"`
+ Attributes []Attribute `xml:"Attribute"`
+}
+
+// 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
+ AuthnRequestsSigned *bool `xml:",attr"`
+ WantAssertionsSigned *bool `xml:",attr"`
+ AssertionConsumerServices []IndexedEndpoint `xml:"AssertionConsumerService"`
+ AttributeConsumingServices []AttributeConsumingService `xml:"AttributeConsumingService"`
+}
+
+// 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
+type AttributeConsumingService struct {
+ Index int `xml:"index,attr"`
+ IsDefault *bool `xml:"isDefault,attr"`
+ ServiceNames []LocalizedName `xml:"ServiceName"`
+ ServiceDescriptions []LocalizedName `xml:"ServiceDescription"`
+ RequestedAttributes []RequestedAttribute `xml:"RequestedAttribute"`
+}
+
+// RequestedAttribute represents the SAML RequestedAttribute object.
+//
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.4.2
+type RequestedAttribute struct {
+ Attribute
+ IsRequired *bool `xml:"isRequired,attr"`
+}
+
+// AuthnAuthorityDescriptor represents the SAML AuthnAuthorityDescriptor object.
+//
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.5
+type AuthnAuthorityDescriptor struct {
+ RoleDescriptor
+ AuthnQueryServices []Endpoint `xml:"AuthnQueryService"`
+ AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
+ NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
+}
+
+// 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
+ AuthzServices []Endpoint `xml:"AuthzService"`
+ AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
+ NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
+}
+
+// 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
+ AttributeServices []Endpoint `xml:"AttributeService"`
+ AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
+ NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
+ AttributeProfiles []string `xml:"AttributeProfile"`
+ Attributes []Attribute `xml:"Attribute"`
+}
+
+// AffiliationDescriptor represents the SAML AffiliationDescriptor object.
+//
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.5
+type AffiliationDescriptor struct {
+ AffiliationOwnerID string `xml:"affiliationOwnerID,attr"`
+ ID string `xml:",attr"`
+ ValidUntil time.Time `xml:"validUntil,attr,omitempty"`
+ CacheDuration time.Duration `xml:"cacheDuration,attr"`
+ Signature *etree.Element
+ AffiliateMembers []string `xml:"AffiliateMember"`
+ KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"`
}
diff --git a/metadata_test.go b/metadata_test.go
index 6962a8a7..a710fb22 100644
--- a/metadata_test.go
+++ b/metadata_test.go
@@ -1,9 +1,11 @@
package saml
import (
- "encoding/xml"
"time"
+ "encoding/xml"
+
+ "github.com/kr/pretty"
. "gopkg.in/check.v1"
)
@@ -11,20 +13,97 @@ type MetadataTest struct{}
var _ = Suite(&MetadataTest{})
+func (s *MetadataTest) TestCanParseMetadata(c *C) {
+ buf := []byte(`Required attributes`)
+
+ metadata := EntityDescriptor{}
+ err := xml.Unmarshal(buf, &metadata)
+ c.Assert(err, IsNil)
+ pretty.Print(metadata)
+ var False = false
+ var True = true
+ c.Assert(metadata, DeepEquals, EntityDescriptor{
+ EntityID: "https://dev.aa.kndr.org/users/auth/saml/metadata",
+ ID: "_af805d1c-c2e3-444e-9cf5-efc664eeace6",
+ SPSSODescriptors: []SPSSODescriptor{
+ 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",
+ },
+ },
+ AuthnRequestsSigned: &False,
+ WantAssertionsSigned: &False,
+ AssertionConsumerServices: []IndexedEndpoint{
+ IndexedEndpoint{
+ 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,
+ },
+ },
+ AttributeConsumingServices: []AttributeConsumingService{
+ AttributeConsumingService{
+ Index: 1,
+ IsDefault: &True,
+ ServiceNames: []LocalizedName{{Value: "Required attributes"}},
+ RequestedAttributes: []RequestedAttribute{
+ {
+ Attribute: Attribute{
+ FriendlyName: "Email address",
+ Name: "email",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ },
+ },
+ {
+ Attribute: Attribute{
+ FriendlyName: "Full name",
+ Name: "name",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ },
+ },
+ {
+ Attribute: Attribute{
+ FriendlyName: "Given name",
+ Name: "first_name",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ },
+ },
+ {
+ Attribute: Attribute{
+ FriendlyName: "Family name",
+ Name: "last_name",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+}
+
func (s *MetadataTest) TestCanProduceSPMetadata(c *C) {
validUntil, _ := time.Parse("2006-02-01T15:04:05.000000", "2013-10-03T00:32:19.104000")
- metadata := Metadata{
+ AuthnRequestsSigned := true
+ WantAssertionsSigned := true
+ metadata := EntityDescriptor{
EntityID: "http://localhost:5000/e087a985171710fb9fb30f30f41384f9/saml2/metadata/",
ValidUntil: validUntil,
- SPSSODescriptor: &SPSSODescriptor{
- AuthnRequestsSigned: true,
- WantAssertionsSigned: true,
- ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
- KeyDescriptor: []KeyDescriptor{
- {
- Use: "encryption",
- KeyInfo: KeyInfo{
- Certificate: `MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE
+ SPSSODescriptors: []SPSSODescriptor{
+ SPSSODescriptor{
+ AuthnRequestsSigned: &AuthnRequestsSigned,
+ WantAssertionsSigned: &WantAssertionsSigned,
+ SSODescriptor: SSODescriptor{
+ RoleDescriptor: RoleDescriptor{
+ ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
+ KeyDescriptors: []KeyDescriptor{
+ {
+ Use: "encryption",
+ KeyInfo: KeyInfo{
+ Certificate: `MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE
CAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoX
DTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28x
EjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308
@@ -33,12 +112,12 @@ SPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gf
nqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90Dv
TLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+
cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==`,
- },
- },
- {
- Use: "signing",
- KeyInfo: KeyInfo{
- Certificate: `MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE
+ },
+ },
+ {
+ Use: "signing",
+ KeyInfo: KeyInfo{
+ Certificate: `MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE
CAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoX
DTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28x
EjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308
@@ -47,22 +126,47 @@ SPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gf
nqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90Dv
TLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+
cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==`,
+ },
+ },
+ },
},
+
+ SingleLogoutServices: []Endpoint{{
+ Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
+ Location: "http://localhost:5000/e087a985171710fb9fb30f30f41384f9/saml2/ls/",
+ }},
},
+
+ AssertionConsumerServices: []IndexedEndpoint{{
+ Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
+ Location: "http://localhost:5000/e087a985171710fb9fb30f30f41384f9/saml2/ls/",
+ Index: 1,
+ }},
},
- SingleLogoutService: []Endpoint{{
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
- Location: "http://localhost:5000/e087a985171710fb9fb30f30f41384f9/saml2/ls/",
- }},
- AssertionConsumerService: []IndexedEndpoint{{
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
- Location: "http://localhost:5000/e087a985171710fb9fb30f30f41384f9/saml2/ls/",
- Index: 1,
- }},
},
}
- buf, err := xml.Marshal(metadata)
- c.Assert(err, IsNil)
- c.Assert(string(buf), Equals, "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE
CAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoX
DTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28x
EjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308
kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTv
SPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gf
nqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90Dv
TLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+
cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE
CAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoX
DTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28x
EjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308
kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTv
SPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gf
nqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90Dv
TLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+
cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==")
+ buf, err := xml.MarshalIndent(metadata, "", " ")
+ c.Assert(err, IsNil)
+ c.Assert(string(buf), Equals, ""+
+ "\n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE
CAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoX
DTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28x
EjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308
kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTv
SPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gf
nqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90Dv
TLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+
cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==\n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE
CAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoX
DTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28x
EjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308
kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTv
SPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gf
nqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90Dv
TLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+
cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==\n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ " \n"+
+ "")
}
diff --git a/samlidp/samlidp.go b/samlidp/samlidp.go
index 237eb69b..57a87c4d 100644
--- a/samlidp/samlidp.go
+++ b/samlidp/samlidp.go
@@ -37,7 +37,7 @@ type Server struct {
http.Handler
idpConfigMu sync.RWMutex // protects calls into the IDP
logger logger.Interface
- serviceProviders map[string]*saml.Metadata
+ serviceProviders map[string]*saml.EntityDescriptor
IDP saml.IdentityProvider // the underlying IDP
Store Store // the data store
}
@@ -54,7 +54,7 @@ func New(opts Options) (*Server, error) {
}
s := &Server{
- serviceProviders: map[string]*saml.Metadata{},
+ serviceProviders: map[string]*saml.EntityDescriptor{},
IDP: saml.IdentityProvider{
Key: opts.Key,
Logger: logr,
diff --git a/samlidp/samlidp_test.go b/samlidp/samlidp_test.go
index f98259c0..2a5bf289 100644
--- a/samlidp/samlidp_test.go
+++ b/samlidp/samlidp_test.go
@@ -123,7 +123,7 @@ OwJlNCASPZRH/JmF8tX0hoHuAQ==
Certificate: test.SPCertificate,
MetadataURL: mustParseURL("https://sp.example.com/saml2/metadata"),
AcsURL: mustParseURL("https://sp.example.com/saml2/acs"),
- IDPMetadata: &saml.Metadata{},
+ IDPMetadata: &saml.EntityDescriptor{},
Logger: logger.DefaultLogger,
}
test.Key = mustParsePrivateKey("-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDU8wdiaFmPfTyRYuFlVPi866WrH/2JubkHzp89bBQopDaLXYxi\n3PTu3O6Q/KaKxMOFBqrInwqpv/omOGZ4ycQ51O9I+Yc7ybVlW94lTo2gpGf+Y/8E\nPsVbnZaFutRctJ4dVIp9aQ2TpLiGT0xX1OzBO/JEgq9GzDRf+B+eqSuglwIDAQAB\nAoGBAMuy1eN6cgFiCOgBsB3gVDdTKpww87Qk5ivjqEt28SmXO13A1KNVPS6oQ8SJ\nCT5Azc6X/BIAoJCURVL+LHdqebogKljhH/3yIel1kH19vr4E2kTM/tYH+qj8afUS\nJEmArUzsmmK8ccuNqBcllqdwCZjxL4CHDUmyRudFcHVX9oyhAkEA/OV1OkjM3CLU\nN3sqELdMmHq5QZCUihBmk3/N5OvGdqAFGBlEeewlepEVxkh7JnaNXAXrKHRVu/f/\nfbCQxH+qrwJBANeQERF97b9Sibp9xgolb749UWNlAdqmEpmlvmS202TdcaaT1msU\n4rRLiQN3X9O9mq4LZMSVethrQAdX1whawpkCQQDk1yGf7xZpMJ8F4U5sN+F4rLyM\nRq8Sy8p2OBTwzCUXXK+fYeXjybsUUMr6VMYTRP2fQr/LKJIX+E5ZxvcIyFmDAkEA\nyfjNVUNVaIbQTzEbRlRvT6MqR+PTCefC072NF9aJWR93JimspGZMR7viY6IM4lrr\nvBkm0F5yXKaYtoiiDMzlOQJADqmEwXl0D72ZG/2KDg8b4QZEmC9i5gidpQwJXUc6\nhU+IVQoLxRq0fBib/36K9tcrrO5Ba4iEvDcNY+D8yGbUtA==\n-----END RSA PRIVATE KEY-----\n")
diff --git a/samlidp/service.go b/samlidp/service.go
index befdab95..4be925e1 100644
--- a/samlidp/service.go
+++ b/samlidp/service.go
@@ -17,14 +17,14 @@ type Service struct {
Name string
// Metdata is the XML metadata of the service provider.
- Metadata saml.Metadata
+ Metadata saml.EntityDescriptor
}
// GetServiceProvider returns the Service Provider metadata for the
// service provider ID, which is typically the service provider's
// metadata URL. If an appropriate service provider cannot be found then
// the returned error must be os.ErrNotExist.
-func (s *Server) GetServiceProvider(r *http.Request, serviceProviderID string) (*saml.Metadata, error) {
+func (s *Server) GetServiceProvider(r *http.Request, serviceProviderID string) (*saml.EntityDescriptor, error) {
s.idpConfigMu.RLock()
defer s.idpConfigMu.RUnlock()
rv, ok := s.serviceProviders[serviceProviderID]
diff --git a/samlidp/service_test.go b/samlidp/service_test.go
index 85e73dc7..c2c53b00 100644
--- a/samlidp/service_test.go
+++ b/samlidp/service_test.go
@@ -8,7 +8,7 @@ import (
. "gopkg.in/check.v1"
)
-const spMetadata = "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ=="
+const spMetadata = "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ=="
func (test *ServerTest) TestServicesCrud(c *C) {
w := httptest.NewRecorder()
diff --git a/samlsp/middleware.go b/samlsp/middleware.go
index 69e6ef4f..24c7584c 100644
--- a/samlsp/middleware.go
+++ b/samlsp/middleware.go
@@ -258,9 +258,9 @@ func (m *Middleware) Authorize(w http.ResponseWriter, r *http.Request, assertion
claims.StandardClaims.Subject = nameID.Value
}
}
- if assertion.AttributeStatement != nil {
+ for _, attributeStatement := range assertion.AttributeStatements {
claims.Attributes = map[string][]string{}
- for _, attr := range assertion.AttributeStatement.Attributes {
+ for _, attr := range attributeStatement.Attributes {
claimName := attr.FriendlyName
if claimName == "" {
claimName = attr.Name
diff --git a/samlsp/middleware_test.go b/samlsp/middleware_test.go
index bc6a2581..5e41a177 100644
--- a/samlsp/middleware_test.go
+++ b/samlsp/middleware_test.go
@@ -73,7 +73,7 @@ func (test *MiddlewareTest) SetUpTest(c *C) {
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
- IDPMetadata: &saml.Metadata{},
+ IDPMetadata: &saml.EntityDescriptor{},
Logger: logger.DefaultLogger,
},
CookieName: "ttt",
@@ -92,7 +92,7 @@ func (test *MiddlewareTest) TestCanProduceMetadata(c *C) {
c.Assert(resp.Header().Get("Content-type"), Equals, "application/samlmetadata+xml")
c.Assert(string(resp.Body.Bytes()), DeepEquals, ""+
"\n"+
- " \n"+
+ " \n"+
" \n"+
" \n"+
" \n"+
@@ -146,11 +146,11 @@ func (test *MiddlewareTest) TestRequireAccountNoCreds(c *C) {
c.Assert(err, IsNil)
decodedRequest, err := testsaml.ParseRedirectRequest(redirectURL)
c.Assert(err, IsNil)
- c.Assert(string(decodedRequest), Equals, "https://15661444.ngrok.io/saml2/metadataurn:oasis:names:tc:SAML:2.0:nameid-format:transient")
+ c.Assert(string(decodedRequest), Equals, "https://15661444.ngrok.io/saml2/metadata")
}
func (test *MiddlewareTest) TestRequireAccountNoCredsPostBinding(c *C) {
- test.Middleware.ServiceProvider.IDPMetadata.IDPSSODescriptor.SingleSignOnService = test.Middleware.ServiceProvider.IDPMetadata.IDPSSODescriptor.SingleSignOnService[1:2]
+ test.Middleware.ServiceProvider.IDPMetadata.IDPSSODescriptors[0].SingleSignOnServices = test.Middleware.ServiceProvider.IDPMetadata.IDPSSODescriptors[0].SingleSignOnServices[1:2]
c.Assert("", Equals, test.Middleware.ServiceProvider.GetSSOBindingLocation(saml.HTTPRedirectBinding))
handler := test.Middleware.RequireAccount(
@@ -172,7 +172,7 @@ func (test *MiddlewareTest) TestRequireAccountNoCredsPostBinding(c *C) {
""+
""+
""+
@@ -240,7 +240,7 @@ func (test *MiddlewareTest) TestRequireAccountBadCreds(c *C) {
c.Assert(err, IsNil)
decodedRequest, err := testsaml.ParseRedirectRequest(redirectURL)
c.Assert(err, IsNil)
- c.Assert(string(decodedRequest), Equals, "https://15661444.ngrok.io/saml2/metadataurn:oasis:names:tc:SAML:2.0:nameid-format:transient")
+ c.Assert(string(decodedRequest), Equals, "https://15661444.ngrok.io/saml2/metadata")
}
@@ -272,7 +272,7 @@ func (test *MiddlewareTest) TestRequireAccountExpiredCreds(c *C) {
c.Assert(err, IsNil)
decodedRequest, err := testsaml.ParseRedirectRequest(redirectURL)
c.Assert(err, IsNil)
- c.Assert(string(decodedRequest), Equals, "https://15661444.ngrok.io/saml2/metadataurn:oasis:names:tc:SAML:2.0:nameid-format:transient")
+ c.Assert(string(decodedRequest), Equals, "https://15661444.ngrok.io/saml2/metadata")
}
func (test *MiddlewareTest) TestRequireAccountPanicOnRequestToACS(c *C) {
diff --git a/samlsp/samlsp.go b/samlsp/samlsp.go
index a05850b9..4911ce7c 100644
--- a/samlsp/samlsp.go
+++ b/samlsp/samlsp.go
@@ -23,7 +23,7 @@ type Options struct {
Logger logger.Interface
Certificate *x509.Certificate
AllowIDPInitiated bool
- IDPMetadata *saml.Metadata
+ IDPMetadata *saml.EntityDescriptor
IDPMetadataURL *url.URL
}
@@ -86,8 +86,7 @@ func New(opts Options) (*Middleware, error) {
continue
}
- entity := &saml.Metadata{}
-
+ entity := &saml.EntityDescriptor{}
err = xml.Unmarshal(data, entity)
// this comparison is ugly, but it is how the error is generated in encoding/xml
@@ -98,9 +97,9 @@ func New(opts Options) (*Middleware, error) {
}
err = fmt.Errorf("no entity found with IDPSSODescriptor")
- for _, e := range entities.EntityDescriptor {
- if e.IDPSSODescriptor != nil {
- entity = e
+ for _, e := range entities.EntityDescriptors {
+ if len(e.IDPSSODescriptors) > 0 {
+ entity = &e
err = nil
}
}
diff --git a/schema.go b/schema.go
index 5d92d68e..7ce5471c 100644
--- a/schema.go
+++ b/schema.go
@@ -2,7 +2,11 @@ package saml
import (
"encoding/xml"
+ "strconv"
"time"
+
+ "github.com/beevik/etree"
+ "github.com/russellhaering/goxmldsig/etreeutils"
)
// AuthnRequest represents the SAML object of the same name, a request from a service provider
@@ -10,24 +14,92 @@ import (
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
type AuthnRequest struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol AuthnRequest"`
- AssertionConsumerServiceURL string `xml:",attr"`
- Destination string `xml:",attr"`
- ID string `xml:",attr"`
- IssueInstant time.Time `xml:",attr"`
-
- // Protocol binding is a URI reference that identifies a SAML protocol binding to be used when returning
- // the message. See [SAMLBind] for more information about protocol bindings and URI references
- // defined for them. This attribute is mutually exclusive with the AssertionConsumerServiceIndex attribute
- // and is typically accompanied by the AssertionConsumerServiceURL attribute.
- ProtocolBinding string `xml:",attr"`
-
- Version string `xml:",attr"`
- Issuer Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
- Signature *Signature `xml:"http://www.w3.org/2000/09/xmldsig# Signature"`
- NameIDPolicy NameIDPolicy `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"`
+ XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol AuthnRequest"`
+
+ ID string `xml:",attr"`
+ Version string `xml:",attr"`
+ IssueInstant time.Time `xml:",attr"`
+ Destination string `xml:",attr"`
+ Consent string `xml:",attr"`
+ Issuer *Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
+ Signature *etree.Element
+
+ Subject *Subject
+ NameIDPolicy *NameIDPolicy `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"`
+ Conditions *Conditions
+ //RequestedAuthnContext *RequestedAuthnContext // TODO
+ //Scoping *Scoping // TODO
+
+ ForceAuthn *bool `xml:",attr"`
+ IsPassive *bool `xml:",attr"`
+ AssertionConsumerServiceIndex string `xml:",attr"`
+ AssertionConsumerServiceURL string `xml:",attr"`
+ ProtocolBinding string `xml:",attr"`
+ AttributeConsumingServiceIndex string `xml:",attr"`
+ ProviderName string `xml:",attr"`
+}
+
+// Element returns an etree.Element representing the object
+// Element returns an etree.Element representing the object in XML form.
+func (r *AuthnRequest) Element() *etree.Element {
+ el := etree.NewElement("samlp:AuthnRequest")
+ el.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
+ el.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol")
+ el.CreateAttr("ID", r.ID)
+ el.CreateAttr("Version", r.Version)
+ el.CreateAttr("IssueInstant", r.IssueInstant.Format(timeFormat))
+ if r.Destination != "" {
+ el.CreateAttr("Destination", r.Destination)
+ }
+ if r.Consent != "" {
+ el.CreateAttr("Consent", r.Consent)
+ }
+ if r.Issuer != nil {
+ el.AddChild(r.Issuer.Element())
+ }
+ if r.Signature != nil {
+ el.AddChild(r.Signature)
+ }
+ if r.Subject != nil {
+ el.AddChild(r.Subject.Element())
+ }
+ if r.NameIDPolicy != nil {
+ el.AddChild(r.NameIDPolicy.Element())
+ }
+ if r.Conditions != nil {
+ el.AddChild(r.Conditions.Element())
+ }
+ //if r.RequestedAuthnContext != nil {
+ // el.AddChild(r.RequestedAuthnContext.Element())
+ //}
+ //if r.Scoping != nil {
+ // el.AddChild(r.Scoping.Element())
+ //}
+ if r.ForceAuthn != nil {
+ el.CreateAttr("ForceAuthn", strconv.FormatBool(*r.ForceAuthn))
+ }
+ if r.IsPassive != nil {
+ el.CreateAttr("IsPassive", strconv.FormatBool(*r.IsPassive))
+ }
+ if r.AssertionConsumerServiceIndex != "" {
+ el.CreateAttr("AssertionConsumerServiceIndex", r.AssertionConsumerServiceIndex)
+ }
+ if r.AssertionConsumerServiceURL != "" {
+ el.CreateAttr("AssertionConsumerServiceURL", r.AssertionConsumerServiceURL)
+ }
+ if r.ProtocolBinding != "" {
+ el.CreateAttr("ProtocolBinding", r.ProtocolBinding)
+ }
+ if r.AttributeConsumingServiceIndex != "" {
+ el.CreateAttr("AttributeConsumingServiceIndex", r.AttributeConsumingServiceIndex)
+ }
+ if r.ProviderName != "" {
+ el.CreateAttr("ProviderName", r.ProviderName)
+ }
+ return el
}
+// MarshalXML implements xml.Marshaler
func (a *AuthnRequest) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type Alias AuthnRequest
aux := &struct {
@@ -40,6 +112,7 @@ func (a *AuthnRequest) MarshalXML(e *xml.Encoder, start xml.StartElement) error
return e.Encode(aux)
}
+// UnmarshalXML implements xml.Unmarshaler
func (a *AuthnRequest) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias AuthnRequest
aux := &struct {
@@ -59,36 +132,124 @@ func (a *AuthnRequest) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
type Issuer struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
- Format string `xml:",attr"`
- Value string `xml:",chardata"`
+ XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
+ NameQualifier string `xml:",attr"`
+ SPNameQualifier string `xml:",attr"`
+ Format string `xml:",attr"`
+ SPProvidedID string `xml:",attr"`
+ Value string `xml:",chardata"`
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (a *Issuer) Element() *etree.Element {
+ el := etree.NewElement("saml:Issuer")
+ if a.NameQualifier != "" {
+ el.CreateAttr("NameQualifier", a.NameQualifier)
+ }
+ if a.SPNameQualifier != "" {
+ el.CreateAttr("SPNameQualifier", a.SPNameQualifier)
+ }
+ if a.Format != "" {
+ el.CreateAttr("Format", a.Format)
+ }
+ if a.SPProvidedID != "" {
+ el.CreateAttr("SPProvidedID", a.SPProvidedID)
+ }
+ el.SetText(a.Value)
+ return el
}
// NameIDPolicy represents the SAML object of the same name.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
type NameIDPolicy struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"`
- AllowCreate bool `xml:",attr"`
- Format string `xml:",chardata"`
+ XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"`
+ Format *string `xml:",attr"`
+ SPNameQualifier *string `xml:",attr"`
+ AllowCreate *bool `xml:",attr"`
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (a *NameIDPolicy) Element() *etree.Element {
+ el := etree.NewElement("samlp:NameIDPolicy")
+ if a.Format != nil {
+ el.CreateAttr("Format", *a.Format)
+ }
+ if a.SPNameQualifier != nil {
+ el.CreateAttr("SPNameQualifier", *a.SPNameQualifier)
+ }
+ if a.AllowCreate != nil {
+ el.CreateAttr("AllowCreate", strconv.FormatBool(*a.AllowCreate))
+ }
+ return el
}
// Response represents the SAML object of the same name.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
type Response struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"`
- Destination string `xml:",attr"`
- ID string `xml:",attr"`
- InResponseTo string `xml:",attr"`
- IssueInstant time.Time `xml:",attr"`
- Version string `xml:",attr"`
- Issuer *Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
- Status *Status `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"`
- EncryptedAssertion *EncryptedAssertion
- Assertion *Assertion `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"`
+ XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"`
+ ID string `xml:",attr"`
+ InResponseTo string `xml:",attr"`
+ Version string `xml:",attr"`
+ IssueInstant time.Time `xml:",attr"`
+ Destination string `xml:",attr"`
+ Consent string `xml:",attr"`
+ Issuer *Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
+ Signature *etree.Element
+ Status Status `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"`
+
+ // TODO(ross): more than one EncryptedAssertion is allowed
+ EncryptedAssertion *etree.Element `xml:"urn:oasis:names:tc:SAML:2.0:assertion EncryptedAssertion"`
+
+ // TODO(ross): more than one Assertion is allowed
+ Assertion *Assertion `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"`
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (r *Response) Element() *etree.Element {
+ el := etree.NewElement("samlp:Response")
+ el.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
+ el.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol")
+
+ // Note: This namespace is not used by any element or attribute name, but
+ // is required so that the AttributeValue type element can have a value like
+ // "xs:string". If we don't declare it here, then it will be stripped by the
+ // cannonicalizer. This could be avoided by providing a prefix list to the
+ // cannonicalizer, but prefix lists do not appear to be implemented correctly
+ // in some libraries, so the safest action is to always produce XML that is
+ // (a) in cannonical form and (b) does not require prefix lists.
+ el.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
+
+ el.CreateAttr("ID", r.ID)
+ if r.InResponseTo != "" {
+ el.CreateAttr("InResponseTo", r.InResponseTo)
+ }
+ el.CreateAttr("Version", r.Version)
+ el.CreateAttr("IssueInstant", r.IssueInstant.Format(timeFormat))
+ if r.Destination != "" {
+ el.CreateAttr("Destination", r.Destination)
+ }
+ if r.Consent != "" {
+ el.CreateAttr("Consent", r.Consent)
+ }
+ if r.Issuer != nil {
+ el.AddChild(r.Issuer.Element())
+ }
+ if r.Signature != nil {
+ el.AddChild(r.Signature)
+ }
+ el.AddChild(r.Status.Element())
+ if r.EncryptedAssertion != nil {
+ el.AddChild(r.EncryptedAssertion)
+ }
+ if r.Assertion != nil {
+ el.AddChild(r.Assertion.Element())
+ }
+ return el
}
+// MarshalXML implements xml.Marshaler
func (r *Response) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type Alias Response
aux := &struct {
@@ -101,6 +262,7 @@ func (r *Response) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.Encode(aux)
}
+// UnmarshalXML implements xml.Unmarshaler
func (r *Response) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias Response
aux := &struct {
@@ -120,58 +282,203 @@ func (r *Response) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
type Status struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"`
- StatusCode StatusCode
+ XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"`
+ StatusCode StatusCode
+ StatusMessage *StatusMessage
+ StatusDetail *StatusDetail
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (s *Status) Element() *etree.Element {
+ el := etree.NewElement("samlp:Status")
+ el.AddChild(s.StatusCode.Element())
+ if s.StatusMessage != nil {
+ el.AddChild(s.StatusMessage.Element())
+ }
+ if s.StatusDetail != nil {
+ el.AddChild(s.StatusDetail.Element())
+ }
+ return el
}
// StatusCode represents the SAML object of the same name.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
type StatusCode struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol StatusCode"`
- Value string `xml:",attr"`
+ XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol StatusCode"`
+ Value string `xml:",attr"`
+ StatusCode *StatusCode
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (s *StatusCode) Element() *etree.Element {
+ el := etree.NewElement("samlp:StatusCode")
+ el.CreateAttr("Value", s.Value)
+ if s.StatusCode != nil {
+ el.AddChild(s.StatusCode.Element())
+ }
+ return el
}
-// StatusSuccess is the value of a StatusCode element when the authentication succeeds.
-// (nominally a constant, except for testing)
+// StatusSuccess means the request succeeded. Additional information MAY be returned in the and/or elements.
+//
+// TODO(ross): this value is mostly constant, but is mutated in tests. Fix the hacky test so this can be const.
var StatusSuccess = "urn:oasis:names:tc:SAML:2.0:status:Success"
-// EncryptedAssertion represents the SAML object of the same name.
+const (
+ // The permissible top-level values are as follows:
+
+ // StatusRequester means the request could not be performed due to an error on the part of the requester.
+ StatusRequester = "urn:oasis:names:tc:SAML:2.0:status:Requester"
+
+ // StatusResponder means the request could not be performed due to an error on the part of the SAML responder or SAML authority.
+ StatusResponder = "urn:oasis:names:tc:SAML:2.0:status:Responder"
+
+ // StatusVersionMismatch means the SAML responder could not process the request because the version of the request message was incorrect.
+ StatusVersionMismatch = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"
+
+ // The following second-level status codes are referenced at various places in this specification. Additional
+ // second-level status codes MAY be defined in future versions of the SAML specification. System entities
+ // are free to define more specific status codes by defining appropriate URI references.
+
+ // StatusAuthnFailed means the responding provider was unable to successfully authenticate the principal.
+ StatusAuthnFailed = "urn:oasis:names:tc:SAML:2.0:status:AuthnFailed"
+
+ // StatusInvalidAttrNameOrValue means Unexpected or invalid content was encountered within a or element.
+ StatusInvalidAttrNameOrValue = "urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue"
+
+ // StatusInvalidNameIDPolicy means the responding provider cannot or will not support the requested name identifier policy.
+ StatusInvalidNameIDPolicy = "urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy"
+
+ // StatusNoAuthnContext means the specified authentication context requirements cannot be met by the responder.
+ StatusNoAuthnContext = "urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext"
+
+ // StatusNoAvailableIDP is used by an intermediary to indicate that none of the supported identity provider elements in an can be resolved or that none of the supported identity providers are available.
+ 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"
+
+ // StatusNoSupportedIDP is used by an intermediary to indicate that none of the identity providers in an are supported by the intermediary.
+ StatusNoSupportedIDP = "urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP"
+
+ // StatusPartialLogout is used by a session authority to indicate to a session participant that it was not able to propagate logout to all other session participants.
+ StatusPartialLogout = "urn:oasis:names:tc:SAML:2.0:status:PartialLogout"
+
+ // StatusProxyCountExceeded means Indicates that a responding provider cannot authenticate the principal directly and is not permitted to proxy the request further.
+ StatusProxyCountExceeded = "urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded"
+
+ // StatusRequestDenied means the SAML responder or SAML authority is able to process the request but has chosen not to respond. This status code MAY be used when there is concern about the security context of the request message or the sequence of request messages received from a particular requester.
+ StatusRequestDenied = "urn:oasis:names:tc:SAML:2.0:status:RequestDenied"
+
+ // StatusRequestUnsupported means the SAML responder or SAML authority does not support the request.
+ 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"
+
+ // 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"
+
+ // StatusRequestVersionTooLow means the SAML responder cannot process the request because the protocol version specified in the request message is too low.
+ StatusRequestVersionTooLow = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow"
+
+ // StatusResourceNotRecognized means the resource value provided in the request message is invalid or unrecognized.
+ StatusResourceNotRecognized = "urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized"
+
+ // StatusTooManyResponses means the response message would contain more elements than the SAML responder is able to return.
+ StatusTooManyResponses = "urn:oasis:names:tc:SAML:2.0:status:TooManyResponses"
+
+ // StatusUnknownAttrProfile means an entity that has no knowledge of a particular attribute profile has been presented with an attribute means drawn from that profile.
+ StatusUnknownAttrProfile = "urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile"
+
+ // StatusUnknownPrincipal means the responding provider does not recognize the principal specified or implied by the request.
+ StatusUnknownPrincipal = "urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal"
+
+ // StatusUnsupportedBinding means the SAML responder cannot properly fulfill the request using the protocol binding specified in the request.
+ StatusUnsupportedBinding = "urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding"
+)
+
+// StatusMessage represents the SAML element StatusMessage.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
-type EncryptedAssertion struct {
- Assertion *Assertion
- EncryptedData []byte `xml:",innerxml"`
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §3.2.2.3
+type StatusMessage struct {
+ Value string
}
-// Assertion represents the SAML object of the same name.
+// Element returns an etree.Element representing the object in XML form.
+func (sm StatusMessage) Element() *etree.Element {
+ el := etree.NewElement("samlp:StatusMessage")
+ el.SetText(sm.Value)
+ return el
+}
+
+// StatusDetail represents the SAML element StatusDetail.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §3.2.2.4
+type StatusDetail struct {
+ Children []*etree.Element
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (sm StatusDetail) Element() *etree.Element {
+ el := etree.NewElement("samlp:StatusDetail")
+ for _, child := range sm.Children {
+ el.AddChild(child)
+ }
+ return el
+}
+
+// Assertion represents the SAML element Assertion.
+//
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.3.3
type Assertion struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"`
- ID string `xml:",attr"`
- IssueInstant time.Time `xml:",attr"`
- Version string `xml:",attr"`
- Issuer *Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
- Signature *Signature
- Subject *Subject
- Conditions *Conditions
- AuthnStatement *AuthnStatement
- AttributeStatement *AttributeStatement
-}
-
-func (a *Assertion) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
- type Alias Assertion
- aux := &struct {
- IssueInstant RelaxedTime `xml:",attr"`
- *Alias
- }{
- IssueInstant: RelaxedTime(a.IssueInstant),
- Alias: (*Alias)(a),
+ XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"`
+ ID string `xml:",attr"`
+ IssueInstant time.Time `xml:",attr"`
+ Version string `xml:",attr"`
+ Issuer Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
+ Signature *etree.Element
+ Subject *Subject
+ Conditions *Conditions
+ // Advice *Advice
+ // Statements []Statement
+ AuthnStatements []AuthnStatement `xml:"AuthnStatement"`
+ // AuthzDecisionStatements []AuthzDecisionStatement
+ AttributeStatements []AttributeStatement `xml:"AttributeStatement"`
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (a *Assertion) Element() *etree.Element {
+ el := etree.NewElement("saml:Assertion")
+ el.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
+ el.CreateAttr("Version", "2.0")
+ el.CreateAttr("ID", a.ID)
+ el.CreateAttr("IssueInstant", a.IssueInstant.Format(timeFormat))
+ el.AddChild(a.Issuer.Element())
+ if a.Signature != nil {
+ el.AddChild(a.Signature)
}
- return e.Encode(aux)
+ if a.Subject != nil {
+ el.AddChild(a.Subject.Element())
+ }
+ if a.Conditions != nil {
+ el.AddChild(a.Conditions.Element())
+ }
+ for _, authnStatement := range a.AuthnStatements {
+ el.AddChild(authnStatement.Element())
+ }
+ for _, attributeStatement := range a.AttributeStatements {
+ el.AddChild(attributeStatement.Element())
+ }
+ err := etreeutils.TransformExcC14n(el, canonicalizerPrefixList)
+ if err != nil {
+ panic(err)
+ }
+ return el
}
+// UnmarshalXML implements xml.Unmarshaler
func (a *Assertion) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias Assertion
aux := &struct {
@@ -187,43 +494,118 @@ func (a *Assertion) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil
}
-// Subject represents the SAML object of the same name.
+// Subject represents the SAML element Subject.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.4.1
type Subject struct {
- XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Subject"`
- NameID *NameID
- SubjectConfirmation *SubjectConfirmation
+ XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Subject"`
+ // BaseID *BaseID ... TODO
+ NameID *NameID
+ // EncryptedID *EncryptedID ... TODO
+ SubjectConfirmations []SubjectConfirmation `xml:"SubjectConfirmation"`
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (a *Subject) Element() *etree.Element {
+ el := etree.NewElement("saml:Subject")
+ if a.NameID != nil {
+ el.AddChild(a.NameID.Element())
+ }
+ for _, v := range a.SubjectConfirmations {
+ el.AddChild(v.Element())
+ }
+ return el
}
-// NameID represents the SAML object of the same name.
+// NameID represents the SAML element NameID.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.2.3
type NameID struct {
- Format string `xml:",attr"`
NameQualifier string `xml:",attr"`
SPNameQualifier string `xml:",attr"`
+ Format string `xml:",attr"`
+ SPProvidedID string `xml:",attr"`
Value string `xml:",chardata"`
}
-// SubjectConfirmation represents the SAML object of the same name.
+// Element returns an etree.Element representing the object in XML form.
+func (a *NameID) Element() *etree.Element {
+ el := etree.NewElement("saml:NameID")
+ if a.NameQualifier != "" {
+ el.CreateAttr("NameQualifier", a.NameQualifier)
+ }
+ if a.SPNameQualifier != "" {
+ el.CreateAttr("SPNameQualifier", a.SPNameQualifier)
+ }
+ if a.Format != "" {
+ el.CreateAttr("Format", a.Format)
+ }
+ if a.SPProvidedID != "" {
+ el.CreateAttr("SPProvidedID", a.SPProvidedID)
+ }
+ if a.Value != "" {
+ el.SetText(a.Value)
+ }
+ return el
+}
+
+// SubjectConfirmation represents the SAML element SubjectConfirmation.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.4.1.1
type SubjectConfirmation struct {
- Method string `xml:",attr"`
- SubjectConfirmationData SubjectConfirmationData
+ Method string `xml:",attr"`
+ // BaseID *BaseID ... TODO
+ NameID *NameID
+ // EncryptedID *EncryptedID ... TODO
+ SubjectConfirmationData *SubjectConfirmationData
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (a *SubjectConfirmation) Element() *etree.Element {
+ el := etree.NewElement("saml:SubjectConfirmation")
+ el.CreateAttr("Method", a.Method)
+ if a.NameID != nil {
+ el.AddChild(a.NameID.Element())
+ }
+ if a.SubjectConfirmationData != nil {
+ el.AddChild(a.SubjectConfirmationData.Element())
+ }
+ return el
}
-// SubjectConfirmationData represents the SAML object of the same name.
+// SubjectConfirmationData represents the SAML element SubjectConfirmationData.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.4.1.2
type SubjectConfirmationData struct {
- Address string `xml:",attr"`
- InResponseTo string `xml:",attr"`
+ NotBefore time.Time `xml:",attr"`
NotOnOrAfter time.Time `xml:",attr"`
Recipient string `xml:",attr"`
+ InResponseTo string `xml:",attr"`
+ Address string `xml:",attr"`
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (s *SubjectConfirmationData) Element() *etree.Element {
+ el := etree.NewElement("saml:SubjectConfirmationData")
+ if !s.NotBefore.IsZero() {
+ el.CreateAttr("NotBefore", s.NotBefore.Format(timeFormat))
+ }
+ if !s.NotOnOrAfter.IsZero() {
+ el.CreateAttr("NotOnOrAfter", s.NotOnOrAfter.Format(timeFormat))
+ }
+ if s.Recipient != "" {
+ el.CreateAttr("Recipient", s.Recipient)
+ }
+ if s.InResponseTo != "" {
+ el.CreateAttr("InResponseTo", s.InResponseTo)
+ }
+ if s.Address != "" {
+ el.CreateAttr("Address", s.Address)
+ }
+ return el
}
+// MarshalXML implements xml.Marshaler
func (s *SubjectConfirmationData) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type Alias SubjectConfirmationData
aux := &struct {
@@ -236,6 +618,7 @@ func (s *SubjectConfirmationData) MarshalXML(e *xml.Encoder, start xml.StartElem
return e.EncodeElement(aux, start)
}
+// UnmarshalXML implements xml.Unmarshaler
func (s *SubjectConfirmationData) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias SubjectConfirmationData
aux := &struct {
@@ -251,15 +634,39 @@ func (s *SubjectConfirmationData) UnmarshalXML(d *xml.Decoder, start xml.StartEl
return nil
}
-// Conditions represents the SAML object of the same name.
+// Conditions represents the SAML element Conditions.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.5.1
type Conditions struct {
- NotBefore time.Time `xml:",attr"`
- NotOnOrAfter time.Time `xml:",attr"`
- AudienceRestriction *AudienceRestriction
+ NotBefore time.Time `xml:",attr"`
+ NotOnOrAfter time.Time `xml:",attr"`
+ AudienceRestrictions []AudienceRestriction `xml:"AudienceRestriction"`
+ OneTimeUse *OneTimeUse
+ ProxyRestriction *ProxyRestriction
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (c *Conditions) Element() *etree.Element {
+ el := etree.NewElement("saml:Conditions")
+ if !c.NotBefore.IsZero() {
+ el.CreateAttr("NotBefore", c.NotBefore.Format(timeFormat))
+ }
+ if !c.NotOnOrAfter.IsZero() {
+ el.CreateAttr("NotOnOrAfter", c.NotOnOrAfter.Format(timeFormat))
+ }
+ for _, v := range c.AudienceRestrictions {
+ el.AddChild(v.Element())
+ }
+ if c.OneTimeUse != nil {
+ el.AddChild(c.OneTimeUse.Element())
+ }
+ if c.ProxyRestriction != nil {
+ el.AddChild(c.ProxyRestriction.Element())
+ }
+ return el
}
+// MarshalXML implements xml.Marshaler
func (c *Conditions) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type Alias Conditions
aux := &struct {
@@ -274,6 +681,7 @@ func (c *Conditions) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(aux, start)
}
+// UnmarshalXML implements xml.Unmarshaler
func (c *Conditions) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias Conditions
aux := &struct {
@@ -291,30 +699,90 @@ func (c *Conditions) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
return nil
}
-// AudienceRestriction represents the SAML object of the same name.
+// AudienceRestriction represents the SAML element AudienceRestriction.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.5.1.4
type AudienceRestriction struct {
- Audience *Audience
+ Audience Audience
}
-// Audience represents the SAML object of the same name.
+// Element returns an etree.Element representing the object in XML form.
+func (a *AudienceRestriction) Element() *etree.Element {
+ el := etree.NewElement("saml:AudienceRestriction")
+ el.AddChild(a.Audience.Element())
+ return el
+}
+
+// Audience represents the SAML element Audience.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.5.1.4
type Audience struct {
Value string `xml:",chardata"`
}
-// AuthnStatement represents the SAML object of the same name.
+// Element returns an etree.Element representing the object in XML form.
+func (a *Audience) Element() *etree.Element {
+ el := etree.NewElement("saml:Audience")
+ el.SetText(a.Value)
+ return el
+}
+
+// OneTimeUse represents the SAML element OneTimeUse.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.5.1.5
+type OneTimeUse struct{}
+
+// Element returns an etree.Element representing the object in XML form.
+func (a *OneTimeUse) Element() *etree.Element {
+ return etree.NewElement("saml:OneTimeUse")
+}
+
+// ProxyRestriction represents the SAML element ProxyRestriction.
+//
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.5.1.6
+type ProxyRestriction struct {
+ Count *int
+ Audiences []Audience
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (a *ProxyRestriction) Element() *etree.Element {
+ el := etree.NewElement("saml:ProxyRestriction")
+ if a.Count != nil {
+ el.CreateAttr("Count", strconv.Itoa(*a.Count))
+ }
+ for _, v := range a.Audiences {
+ el.AddChild(v.Element())
+ }
+ return el
+}
+
+// AuthnStatement represents the SAML element AuthnStatement.
+//
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.7.2
type AuthnStatement struct {
- AuthnInstant time.Time `xml:",attr"`
- SessionIndex string `xml:",attr"`
- SubjectLocality SubjectLocality
- AuthnContext AuthnContext
+ AuthnInstant time.Time `xml:",attr"`
+ SessionIndex string `xml:",attr"`
+ SessionNotOnOrAfter *time.Time `xml:",attr"`
+ SubjectLocality *SubjectLocality
+ AuthnContext AuthnContext
+}
+
+// Element returns an etree.Element representing the object in XML form.
+func (a *AuthnStatement) Element() *etree.Element {
+ el := etree.NewElement("saml:AuthnStatement")
+ el.CreateAttr("AuthnInstant", a.AuthnInstant.Format(timeFormat))
+ if a.SessionIndex != "" {
+ el.CreateAttr("SessionIndex", a.SessionIndex)
+ }
+ if a.SubjectLocality != nil {
+ el.AddChild(a.SubjectLocality.Element())
+ }
+ el.AddChild(a.AuthnContext.Element())
+ return el
}
+// MarshalXML implements xml.Marshaler
func (a *AuthnStatement) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type Alias AuthnStatement
aux := &struct {
@@ -327,6 +795,7 @@ func (a *AuthnStatement) MarshalXML(e *xml.Encoder, start xml.StartElement) erro
return e.EncodeElement(aux, start)
}
+// UnmarshalXML implements xml.Unmarshaler
func (a *AuthnStatement) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias AuthnStatement
aux := &struct {
@@ -342,37 +811,78 @@ func (a *AuthnStatement) UnmarshalXML(d *xml.Decoder, start xml.StartElement) er
return nil
}
-// SubjectLocality represents the SAML object of the same name.
+// SubjectLocality represents the SAML element SubjectLocality.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.7.2.1
type SubjectLocality struct {
Address string `xml:",attr"`
+ DNSName string `xml:",attr"`
}
-// AuthnContext represents the SAML object of the same name.
+// Element returns an etree.Element representing the object in XML form.
+func (a *SubjectLocality) Element() *etree.Element {
+ el := etree.NewElement("saml:SubjectLocality")
+ if a.Address != "" {
+ el.CreateAttr("Address", a.Address)
+ }
+ if a.DNSName != "" {
+ el.CreateAttr("DNSName", a.DNSName)
+ }
+ return el
+}
+
+// AuthnContext represents the SAML element AuthnContext.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.7.2.2
type AuthnContext struct {
AuthnContextClassRef *AuthnContextClassRef
+ //AuthnContextDecl *AuthnContextDecl ... TODO
+ //AuthnContextDeclRef *AuthnContextDeclRef ... TODO
+ //AuthenticatingAuthorities []AuthenticatingAuthority... TODO
}
-// AuthnContextClassRef represents the SAML object of the same name.
+// Element returns an etree.Element representing the object in XML form.
+func (a *AuthnContext) Element() *etree.Element {
+ el := etree.NewElement("saml:AuthnContext")
+ if a.AuthnContextClassRef != nil {
+ el.AddChild(a.AuthnContextClassRef.Element())
+ }
+ return el
+}
+
+// AuthnContextClassRef represents the SAML element AuthnContextClassRef.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.7.2.2
type AuthnContextClassRef struct {
Value string `xml:",chardata"`
}
-// AttributeStatement represents the SAML object of the same name.
+// Element returns an etree.Element representing the object in XML form.
+func (a *AuthnContextClassRef) Element() *etree.Element {
+ el := etree.NewElement("saml:AuthnContextClassRef")
+ el.SetText(a.Value)
+ return el
+}
+
+// AttributeStatement represents the SAML element AttributeStatement.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.7.3
type AttributeStatement struct {
Attributes []Attribute `xml:"Attribute"`
}
-// Attribute represents the SAML object of the same name.
+// Element returns an etree.Element representing the object in XML form.
+func (a *AttributeStatement) Element() *etree.Element {
+ el := etree.NewElement("saml:AttributeStatement")
+ for _, v := range a.Attributes {
+ el.AddChild(v.Element())
+ }
+ return el
+}
+
+// Attribute represents the SAML element Attribute.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.7.3.1
type Attribute struct {
FriendlyName string `xml:",attr"`
Name string `xml:",attr"`
@@ -380,11 +890,42 @@ type Attribute struct {
Values []AttributeValue `xml:"AttributeValue"`
}
-// AttributeValue represents the SAML object of the same name.
+// Element returns an etree.Element representing the object in XML form.
+func (a *Attribute) Element() *etree.Element {
+ el := etree.NewElement("saml:Attribute")
+ if a.FriendlyName != "" {
+ el.CreateAttr("FriendlyName", a.FriendlyName)
+ }
+ if a.Name != "" {
+ el.CreateAttr("Name", a.Name)
+ }
+ if a.NameFormat != "" {
+ el.CreateAttr("NameFormat", a.NameFormat)
+ }
+ for _, v := range a.Values {
+ el.AddChild(v.Element())
+ }
+ return el
+}
+
+// AttributeValue represents the SAML element AttributeValue.
//
-// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf §2.7.3.1.1
type AttributeValue struct {
Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"`
Value string `xml:",chardata"`
NameID *NameID
}
+
+// Element returns an etree.Element representing the object in XML form.
+func (a *AttributeValue) Element() *etree.Element {
+ el := etree.NewElement("saml:AttributeValue")
+ el.CreateAttr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
+ el.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
+ el.CreateAttr("xsi:type", a.Type)
+ if a.NameID != nil {
+ el.AddChild(a.NameID.Element())
+ }
+ el.SetText(a.Value)
+ return el
+}
diff --git a/schema_test.go b/schema_test.go
new file mode 100644
index 00000000..2a55d2d7
--- /dev/null
+++ b/schema_test.go
@@ -0,0 +1,36 @@
+package saml
+
+import (
+ "encoding/xml"
+
+ "github.com/beevik/etree"
+ . "gopkg.in/check.v1"
+)
+
+var _ = Suite(&SchemaTest{})
+
+type SchemaTest struct {
+}
+
+func (test *SchemaTest) TestAttributeXMLRoundTrip(c *C) {
+ expected := Attribute{
+ FriendlyName: "TestFriendlyName",
+ Name: "TestName",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ Values: []AttributeValue{AttributeValue{
+ Type: "xs:string",
+ Value: "test",
+ }},
+ }
+
+ doc := etree.NewDocument()
+ doc.SetRoot(expected.Element())
+ x, err := doc.WriteToBytes()
+ c.Assert(err, IsNil)
+ c.Assert(string(x), Equals, "test")
+
+ var actual Attribute
+ err = xml.Unmarshal(x, &actual)
+ c.Assert(err, IsNil)
+ c.Assert(actual, DeepEquals, expected)
+}
diff --git a/service_provider.go b/service_provider.go
index 89d12a77..ca00a4b2 100644
--- a/service_provider.go
+++ b/service_provider.go
@@ -25,6 +25,12 @@ import (
// NameIDFormat is the format of the id
type NameIDFormat string
+func (n NameIDFormat) Element() *etree.Element {
+ el := etree.NewElement("")
+ el.SetText(string(n))
+ return el
+}
+
// Name ID formats
const (
UnspecifiedNameIDFormat NameIDFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
@@ -57,7 +63,7 @@ type ServiceProvider struct {
AcsURL url.URL
// IDPMetadata is the metadata from the identity provider.
- IDPMetadata *Metadata
+ IDPMetadata *EntityDescriptor
// AuthnNameIDFormat is the format used in the NameIDPolicy for
// authentication requests
@@ -88,44 +94,56 @@ const DefaultValidDuration = time.Hour * 24 * 2
const DefaultCacheDuration = time.Hour * 24 * 1
// Metadata returns the service provider metadata
-func (sp *ServiceProvider) Metadata() *Metadata {
+func (sp *ServiceProvider) Metadata() *EntityDescriptor {
validDuration := DefaultValidDuration
if sp.MetadataValidDuration > 0 {
validDuration = sp.MetadataValidDuration
}
- return &Metadata{
+ authnRequestsSigned := false
+ wantAssertionsSigned := true
+ return &EntityDescriptor{
EntityID: sp.MetadataURL.String(),
ValidUntil: TimeNow().Add(validDuration),
- SPSSODescriptor: &SPSSODescriptor{
- AuthnRequestsSigned: false,
- WantAssertionsSigned: true,
- ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
- KeyDescriptor: []KeyDescriptor{
- {
- Use: "signing",
- KeyInfo: KeyInfo{
- Certificate: base64.StdEncoding.EncodeToString(sp.Certificate.Raw),
+
+ SPSSODescriptors: []SPSSODescriptor{
+ SPSSODescriptor{
+ SSODescriptor: SSODescriptor{
+ RoleDescriptor: RoleDescriptor{
+ ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
+ KeyDescriptors: []KeyDescriptor{
+ {
+ Use: "signing",
+ KeyInfo: KeyInfo{
+ Certificate: base64.StdEncoding.EncodeToString(sp.Certificate.Raw),
+ },
+ },
+ {
+ Use: "encryption",
+ KeyInfo: KeyInfo{
+ Certificate: base64.StdEncoding.EncodeToString(sp.Certificate.Raw),
+ },
+ EncryptionMethods: []EncryptionMethod{
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"},
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes192-cbc"},
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"},
+ {Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"},
+ },
+ },
+ },
},
},
- {
- Use: "encryption",
- KeyInfo: KeyInfo{
- Certificate: base64.StdEncoding.EncodeToString(sp.Certificate.Raw),
- },
- EncryptionMethods: []EncryptionMethod{
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"},
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes192-cbc"},
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"},
- {Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"},
+ AuthnRequestsSigned: &authnRequestsSigned,
+ WantAssertionsSigned: &wantAssertionsSigned,
+
+ AssertionConsumerServices: []IndexedEndpoint{
+ IndexedEndpoint{
+ Binding: HTTPPostBinding,
+ Location: sp.AcsURL.String(),
+ Index: 1,
},
},
},
- AssertionConsumerService: []IndexedEndpoint{{
- Binding: HTTPPostBinding,
- Location: sp.AcsURL.String(),
- Index: 1,
- }},
},
}
}
@@ -146,7 +164,9 @@ func (req *AuthnRequest) Redirect(relayState string) *url.URL {
w := &bytes.Buffer{}
w1 := base64.NewEncoder(base64.StdEncoding, w)
w2, _ := flate.NewWriter(w1, 9)
- if err := xml.NewEncoder(w2).Encode(req); err != nil {
+ doc := etree.NewDocument()
+ doc.SetRoot(req.Element())
+ if _, err := doc.WriteTo(w2); err != nil {
panic(err)
}
w2.Close()
@@ -167,9 +187,11 @@ func (req *AuthnRequest) Redirect(relayState string) *url.URL {
// GetSSOBindingLocation returns URL for the IDP's Single Sign On Service binding
// of the specified type (HTTPRedirectBinding or HTTPPostBinding)
func (sp *ServiceProvider) GetSSOBindingLocation(binding string) string {
- for _, singleSignOnService := range sp.IDPMetadata.IDPSSODescriptor.SingleSignOnService {
- if singleSignOnService.Binding == binding {
- return singleSignOnService.Location
+ for _, idpSSODescriptor := range sp.IDPMetadata.IDPSSODescriptors {
+ for _, singleSignOnService := range idpSSODescriptor.SingleSignOnServices {
+ if singleSignOnService.Binding == binding {
+ return singleSignOnService.Location
+ }
}
}
return ""
@@ -179,20 +201,24 @@ func (sp *ServiceProvider) GetSSOBindingLocation(binding string) string {
// signed by the IDP in PEM format, or nil if no such certificate is found.
func (sp *ServiceProvider) getIDPSigningCert() (*x509.Certificate, error) {
certStr := ""
- for _, keyDescriptor := range sp.IDPMetadata.IDPSSODescriptor.KeyDescriptor {
- if keyDescriptor.Use == "signing" {
- certStr = keyDescriptor.KeyInfo.Certificate
- break
+ for _, idpSSODescriptor := range sp.IDPMetadata.IDPSSODescriptors {
+ for _, keyDescriptor := range idpSSODescriptor.KeyDescriptors {
+ if keyDescriptor.Use == "signing" {
+ certStr = keyDescriptor.KeyInfo.Certificate
+ break
+ }
}
}
// If there are no explicitly signing certs, just return the first
// non-empty cert we find.
if certStr == "" {
- for _, keyDescriptor := range sp.IDPMetadata.IDPSSODescriptor.KeyDescriptor {
- if keyDescriptor.Use == "" && keyDescriptor.KeyInfo.Certificate != "" {
- certStr = keyDescriptor.KeyInfo.Certificate
- break
+ for _, idpSSODescriptor := range sp.IDPMetadata.IDPSSODescriptors {
+ for _, keyDescriptor := range idpSSODescriptor.KeyDescriptors {
+ if keyDescriptor.Use == "" && keyDescriptor.KeyInfo.Certificate != "" {
+ certStr = keyDescriptor.KeyInfo.Certificate
+ break
+ }
}
}
}
@@ -217,17 +243,18 @@ func (sp *ServiceProvider) getIDPSigningCert() (*x509.Certificate, error) {
// MakeAuthenticationRequest produces a new AuthnRequest object for idpURL.
func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string) (*AuthnRequest, error) {
- var nameIDFormat NameIDFormat
+ var nameIDFormat string
switch sp.AuthnNameIDFormat {
case "":
// To maintain library back-compat, use "transient" if unset.
- nameIDFormat = TransientNameIDFormat
+ nameIDFormat = string(TransientNameIDFormat)
case UnspecifiedNameIDFormat:
// Spec defines an empty value as "unspecified" so don't set one.
default:
- nameIDFormat = sp.AuthnNameIDFormat
+ nameIDFormat = string(sp.AuthnNameIDFormat)
}
+ allowCreate := true
req := AuthnRequest{
AssertionConsumerServiceURL: sp.AcsURL.String(),
Destination: idpURL,
@@ -235,16 +262,16 @@ func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string) (*AuthnReque
ID: fmt.Sprintf("id-%x", randomBytes(20)),
IssueInstant: TimeNow(),
Version: "2.0",
- Issuer: Issuer{
+ Issuer: &Issuer{
Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
Value: sp.MetadataURL.String(),
},
- NameIDPolicy: NameIDPolicy{
- AllowCreate: true,
+ NameIDPolicy: &NameIDPolicy{
+ AllowCreate: &allowCreate,
// TODO(ross): figure out exactly policy we need
// urn:mace:shibboleth:1.0:nameIdentifier
// urn:oasis:names:tc:SAML:2.0:nameid-format:transient
- Format: string(nameIDFormat),
+ Format: &nameIDFormat,
},
}
return &req, nil
@@ -263,7 +290,9 @@ func (sp *ServiceProvider) MakePostAuthenticationRequest(relayState string) ([]b
// Post returns an HTML form suitable for using the HTTP-POST binding with the request
func (req *AuthnRequest) Post(relayState string) []byte {
- reqBuf, err := xml.Marshal(req)
+ doc := etree.NewDocument()
+ doc.SetRoot(req.Element())
+ reqBuf, err := doc.WriteToBytes()
if err != nil {
panic(err)
}
@@ -470,21 +499,23 @@ func (sp *ServiceProvider) validateAssertion(assertion *Assertion, possibleReque
if assertion.Issuer.Value != sp.IDPMetadata.EntityID {
return fmt.Errorf("issuer is not %q", sp.IDPMetadata.EntityID)
}
- requestIDvalid := false
- for _, possibleRequestID := range possibleRequestIDs {
- if assertion.Subject.SubjectConfirmation.SubjectConfirmationData.InResponseTo == possibleRequestID {
- requestIDvalid = true
- break
+ for _, subjectConfirmation := range assertion.Subject.SubjectConfirmations {
+ requestIDvalid := false
+ for _, possibleRequestID := range possibleRequestIDs {
+ if subjectConfirmation.SubjectConfirmationData.InResponseTo == possibleRequestID {
+ requestIDvalid = true
+ break
+ }
+ }
+ if !requestIDvalid {
+ return fmt.Errorf("SubjectConfirmation one of the possible request IDs (%v)", possibleRequestIDs)
+ }
+ if subjectConfirmation.SubjectConfirmationData.Recipient != sp.AcsURL.String() {
+ return fmt.Errorf("SubjectConfirmation Recipient is not %s", sp.AcsURL.String())
+ }
+ if subjectConfirmation.SubjectConfirmationData.NotOnOrAfter.Add(MaxClockSkew).Before(now) {
+ return fmt.Errorf("SubjectConfirmationData is expired")
}
- }
- if !requestIDvalid {
- return fmt.Errorf("SubjectConfirmation one of the possible request IDs (%v)", possibleRequestIDs)
- }
- if assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient != sp.AcsURL.String() {
- return fmt.Errorf("SubjectConfirmation Recipient is not %s", sp.AcsURL.String())
- }
- if assertion.Subject.SubjectConfirmation.SubjectConfirmationData.NotOnOrAfter.Add(MaxClockSkew).Before(now) {
- return fmt.Errorf("SubjectConfirmationData is expired")
}
if assertion.Conditions.NotBefore.Add(-MaxClockSkew).After(now) {
return fmt.Errorf("Conditions is not yet valid")
@@ -492,8 +523,15 @@ func (sp *ServiceProvider) validateAssertion(assertion *Assertion, possibleReque
if assertion.Conditions.NotOnOrAfter.Add(MaxClockSkew).Before(now) {
return fmt.Errorf("Conditions is expired")
}
- if assertion.Conditions.AudienceRestriction.Audience.Value != sp.MetadataURL.String() {
- return fmt.Errorf("Conditions AudienceRestriction is not %q", sp.MetadataURL.String())
+
+ audienceRestrictionsValid := false
+ for _, audienceRestriction := range assertion.Conditions.AudienceRestrictions {
+ if audienceRestriction.Audience.Value == sp.MetadataURL.String() {
+ audienceRestrictionsValid = true
+ }
+ }
+ if !audienceRestrictionsValid {
+ return fmt.Errorf("Conditions AudienceRestriction does not contain %q", sp.MetadataURL.String())
}
return nil
}
diff --git a/service_provider_test.go b/service_provider_test.go
index e4889264..55ad9751 100644
--- a/service_provider_test.go
+++ b/service_provider_test.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/crewjam/saml/testsaml"
+ "github.com/kr/pretty"
dsig "github.com/russellhaering/goxmldsig"
"crypto/rsa"
@@ -78,25 +79,25 @@ func (test *ServiceProviderTest) TestCanSetAuthenticationNameIDFormat(c *C) {
// defaults to "transient"
req, err := s.MakeAuthenticationRequest("")
c.Assert(err, IsNil)
- c.Assert(req.NameIDPolicy.Format, Equals, string(TransientNameIDFormat))
+ c.Assert(*req.NameIDPolicy.Format, Equals, string(TransientNameIDFormat))
// explicitly set to "transient"
s.AuthnNameIDFormat = TransientNameIDFormat
req, err = s.MakeAuthenticationRequest("")
c.Assert(err, IsNil)
- c.Assert(req.NameIDPolicy.Format, Equals, string(TransientNameIDFormat))
+ c.Assert(*req.NameIDPolicy.Format, Equals, string(TransientNameIDFormat))
// explicitly set to "unspecified"
s.AuthnNameIDFormat = UnspecifiedNameIDFormat
req, err = s.MakeAuthenticationRequest("")
c.Assert(err, IsNil)
- c.Assert(req.NameIDPolicy.Format, Equals, "")
+ c.Assert(*req.NameIDPolicy.Format, Equals, "")
// explicitly set to "emailAddress"
s.AuthnNameIDFormat = EmailAddressNameIDFormat
req, err = s.MakeAuthenticationRequest("")
c.Assert(err, IsNil)
- c.Assert(req.NameIDPolicy.Format, Equals, string(EmailAddressNameIDFormat))
+ c.Assert(*req.NameIDPolicy.Format, Equals, string(EmailAddressNameIDFormat))
}
func (test *ServiceProviderTest) TestCanProduceMetadata(c *C) {
@@ -105,7 +106,7 @@ func (test *ServiceProviderTest) TestCanProduceMetadata(c *C) {
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://example.com/saml2/metadata"),
AcsURL: mustParseURL("https://example.com/saml2/acs"),
- IDPMetadata: &Metadata{},
+ IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
c.Assert(err, IsNil)
@@ -114,7 +115,7 @@ func (test *ServiceProviderTest) TestCanProduceMetadata(c *C) {
c.Assert(err, IsNil)
c.Assert(string(spMetadata), DeepEquals, ""+
"\n"+
- " \n"+
+ " \n"+
" \n"+
" \n"+
" \n"+
@@ -149,7 +150,7 @@ func (test *ServiceProviderTest) TestCanProduceRedirectRequest(c *C) {
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
- IDPMetadata: &Metadata{},
+ IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
c.Assert(err, IsNil)
@@ -161,7 +162,7 @@ func (test *ServiceProviderTest) TestCanProduceRedirectRequest(c *C) {
c.Assert(err, IsNil)
c.Assert(redirectURL.Host, Equals, "idp.testshib.org")
c.Assert(redirectURL.Path, Equals, "/idp/profile/SAML2/Redirect/SSO")
- c.Assert(string(decodedRequest), Equals, "https://15661444.ngrok.io/saml2/metadataurn:oasis:names:tc:SAML:2.0:nameid-format:transient")
+ c.Assert(string(decodedRequest), Equals, "https://15661444.ngrok.io/saml2/metadata")
}
func (test *ServiceProviderTest) TestCanProducePostRequest(c *C) {
@@ -174,7 +175,7 @@ func (test *ServiceProviderTest) TestCanProducePostRequest(c *C) {
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
- IDPMetadata: &Metadata{},
+ IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
c.Assert(err, IsNil)
@@ -184,7 +185,7 @@ func (test *ServiceProviderTest) TestCanProducePostRequest(c *C) {
c.Assert(string(form), Equals, ``+
``+
``+
@@ -246,7 +247,7 @@ uzZ1y9sNHH6kH8GFnvS2MqyHiNz0h0Sq/q6n+w==
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/metadata"),
AcsURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/acs"),
- IDPMetadata: &Metadata{},
+ IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
c.Assert(err, IsNil)
@@ -260,7 +261,7 @@ uzZ1y9sNHH6kH8GFnvS2MqyHiNz0h0Sq/q6n+w==
c.Assert(err, IsNil)
c.Assert(assertion.Subject.NameID.Value, DeepEquals, "ross@kndr.org")
- c.Assert(assertion.AttributeStatement.Attributes, DeepEquals, []Attribute{
+ c.Assert(assertion.AttributeStatements[0].Attributes, DeepEquals, []Attribute{
{
Name: "User.email",
NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
@@ -358,7 +359,7 @@ PUkfbaYHQGP6IS0lzeCeDX0wab3qRoh7/jJt5/BR8Iwf
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/metadata"),
AcsURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/acs"),
- IDPMetadata: &Metadata{},
+ IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
c.Assert(err, IsNil)
@@ -372,7 +373,7 @@ PUkfbaYHQGP6IS0lzeCeDX0wab3qRoh7/jJt5/BR8Iwf
c.Assert(err, IsNil)
c.Assert(assertion.Subject.NameID.Value, DeepEquals, "ross@octolabs.io")
- c.Assert(assertion.AttributeStatement.Attributes, DeepEquals, []Attribute{
+ c.Assert(assertion.AttributeStatements[0].Attributes, DeepEquals, []Attribute{
{
Name: "phone",
Values: nil,
@@ -412,7 +413,7 @@ func (test *ServiceProviderTest) TestCanParseResponse(c *C) {
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
- IDPMetadata: &Metadata{},
+ IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
c.Assert(err, IsNil)
@@ -422,7 +423,7 @@ func (test *ServiceProviderTest) TestCanParseResponse(c *C) {
assertion, err := s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"})
c.Assert(err, IsNil)
- c.Assert(assertion.AttributeStatement.Attributes, DeepEquals, []Attribute{
+ c.Assert(assertion.AttributeStatements[0].Attributes, DeepEquals, []Attribute{
{
FriendlyName: "uid",
Name: "urn:oid:0.9.2342.19200300.100.1.1",
@@ -549,7 +550,7 @@ func (test *ServiceProviderTest) TestInvalidResponses(c *C) {
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
- IDPMetadata: &Metadata{},
+ IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
c.Assert(err, IsNil)
@@ -600,12 +601,12 @@ func (test *ServiceProviderTest) TestInvalidResponses(c *C) {
c.Assert(err.(*InvalidResponseError).PrivateErr.Error(), Equals, "Status code was not not:the:success:value")
StatusSuccess = oldSpStatusSuccess
- s.IDPMetadata.IDPSSODescriptor.KeyDescriptor[0].KeyInfo.Certificate = "invalid"
+ s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.Certificate = "invalid"
req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString([]byte(test.SamlResponse)))
_, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"})
c.Assert(err.(*InvalidResponseError).PrivateErr, ErrorMatches, "cannot validate signature on Response: cannot parse certificate: illegal base64 data at input byte 4")
- s.IDPMetadata.IDPSSODescriptor.KeyDescriptor[0].KeyInfo.Certificate = "aW52YWxpZA=="
+ s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.Certificate = "aW52YWxpZA=="
req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString([]byte(test.SamlResponse)))
_, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"})
c.Assert(err.(*InvalidResponseError).PrivateErr, ErrorMatches, "cannot validate signature on Response: asn1: structure error: tags don't match .*")
@@ -617,14 +618,14 @@ func (test *ServiceProviderTest) TestInvalidAssertions(c *C) {
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
- IDPMetadata: &Metadata{},
+ IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
c.Assert(err, IsNil)
req := http.Request{PostForm: url.Values{}}
req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString([]byte(test.SamlResponse)))
- s.IDPMetadata.IDPSSODescriptor.KeyDescriptor[0].KeyInfo.Certificate = "invalid"
+ s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.Certificate = "invalid"
_, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"})
assertionBuf := []byte(err.(*InvalidResponseError).Response)
@@ -638,45 +639,53 @@ func (test *ServiceProviderTest) TestInvalidAssertions(c *C) {
assertion.Issuer.Value = "bob"
err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow())
c.Assert(err.Error(), Equals, "issuer is not \"https://idp.testshib.org/idp/shibboleth\"")
+ assertion = Assertion{}
xml.Unmarshal(assertionBuf, &assertion)
assertion.Subject.NameID.NameQualifier = "bob"
err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow())
c.Assert(err, IsNil) // not verified
+ assertion = Assertion{}
xml.Unmarshal(assertionBuf, &assertion)
assertion.Subject.NameID.SPNameQualifier = "bob"
err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow())
c.Assert(err, IsNil) // not verified
+ assertion = Assertion{}
xml.Unmarshal(assertionBuf, &assertion)
+ pretty.Print(assertion.Subject.SubjectConfirmations)
err = s.validateAssertion(&assertion, []string{"any request id"}, TimeNow())
- c.Assert(err.Error(), Equals,
- "SubjectConfirmation one of the possible request IDs ([any request id])")
+ c.Assert(err, ErrorMatches, "SubjectConfirmation one of the possible request IDs .*")
- assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient = "wrong/acs/url"
+ assertion.Subject.SubjectConfirmations[0].SubjectConfirmationData.Recipient = "wrong/acs/url"
err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow())
c.Assert(err.Error(), Equals, "SubjectConfirmation Recipient is not https://15661444.ngrok.io/saml2/acs")
+ assertion = Assertion{}
xml.Unmarshal(assertionBuf, &assertion)
- assertion.Subject.SubjectConfirmation.SubjectConfirmationData.NotOnOrAfter = TimeNow().Add(-1 * time.Hour)
+ assertion.Subject.SubjectConfirmations[0].SubjectConfirmationData.NotOnOrAfter = TimeNow().Add(-1 * time.Hour)
err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow())
c.Assert(err.Error(), Equals, "SubjectConfirmationData is expired")
+ assertion = Assertion{}
xml.Unmarshal(assertionBuf, &assertion)
assertion.Conditions.NotBefore = TimeNow().Add(time.Hour)
err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow())
c.Assert(err.Error(), Equals, "Conditions is not yet valid")
+ assertion = Assertion{}
xml.Unmarshal(assertionBuf, &assertion)
assertion.Conditions.NotOnOrAfter = TimeNow().Add(-1 * time.Hour)
err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow())
c.Assert(err.Error(), Equals, "Conditions is expired")
+ assertion = Assertion{}
xml.Unmarshal(assertionBuf, &assertion)
- assertion.Conditions.AudienceRestriction.Audience.Value = "not/our/metadata/url"
+ assertion.Conditions.AudienceRestrictions[0].Audience.Value = "not/our/metadata/url"
err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow())
- c.Assert(err.Error(), Equals, "Conditions AudienceRestriction is not \"https://15661444.ngrok.io/saml2/metadata\"")
+ c.Assert(err.Error(), Equals, "Conditions AudienceRestriction does not contain \"https://15661444.ngrok.io/saml2/metadata\"")
+ assertion = Assertion{}
xml.Unmarshal(assertionBuf, &assertion)
}
@@ -703,7 +712,7 @@ DgefdDXhYNmeuQtwGtcu/FI66atQMNTDoChXJQ==AQABAQAB