From b456570e888eef4d8bea6095d78c9ae0722aed1f Mon Sep 17 00:00:00 2001 From: amdonov Date: Mon, 3 Sep 2018 14:25:37 -0400 Subject: [PATCH 1/2] artifact binding support --- artifact.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++ artifact_test.go | 41 ++++++++++++++++++++ build_request.go | 11 +++++- saml.go | 12 +++++- types/metadata.go | 13 ++++--- xml_constants.go | 1 + 6 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 artifact.go create mode 100644 artifact_test.go diff --git a/artifact.go b/artifact.go new file mode 100644 index 0000000..dec7592 --- /dev/null +++ b/artifact.go @@ -0,0 +1,98 @@ +package saml2 + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/beevik/etree" + "github.com/russellhaering/gosaml2/uuid" +) + +func (sp *SAMLServiceProvider) ResolveArtifact(artifact string) (*AssertionInfo, error) { + if sp.HTTPClient == nil { + return nil, errors.New("HTTPClient must be set for artifact binding") + } + request, err := sp.buildResolveRequest(artifact) + if err != nil { + return nil, err + } + post, err := http.NewRequest(http.MethodPost, sp.IdentityProviderArtifactResolutionServiceURL, request) + if err != nil { + return nil, err + } + post.Header.Add("Content-Type", "text/xml") + post.Header.Add("SOAPAction", "http://www.oasis-open.org/committees/security") + resp, err := sp.HTTPClient.Do(post) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code from artifact resolve request %d", resp.StatusCode) + } + // Buffer the response and base64 encode it. + // It's not ideal, but existing parsing methods expect it to be encoded. + // Attempting to minimize change for now. + doc := etree.NewDocument() + doc.ReadFrom(resp.Body) + el := doc.FindElement("./Envelope/Body/ArtifactResponse/Response") + doc = etree.NewDocument() + doc.SetRoot(el) + var buffer bytes.Buffer + encoder := base64.NewEncoder(base64.StdEncoding, &buffer) + doc.WriteTo(encoder) + encoder.Close() + return sp.RetrieveAssertionInfo(buffer.String()) +} + +func (sp *SAMLServiceProvider) buildResolveRequest(artifact string) (io.Reader, error) { + envelope := &etree.Element{ + Space: "soap-env", + Tag: "Envelope", + } + envelope.CreateAttr("xmlns:soap-env", "http://schemas.xmlsoap.org/soap/envelope/") + body := envelope.CreateElement("soap-env:Body") + artifactResolve := &etree.Element{ + Space: "samlp", + Tag: "ArtifactResolve", + } + artifactResolve.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol") + artifactResolve.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion") + + arID := uuid.NewV4() + artifactResolve.CreateAttr("ID", "_"+arID.String()) + artifactResolve.CreateAttr("Version", "2.0") + artifactResolve.CreateAttr("IssueInstant", sp.Clock.Now().UTC().Format(issueInstantFormat)) + + // NOTE(russell_h): In earlier versions we mistakenly sent the IdentityProviderIssuer + // in the AuthnRequest. For backwards compatibility we will fall back to that + // behavior when ServiceProviderIssuer isn't set. + if sp.ServiceProviderIssuer != "" { + artifactResolve.CreateElement("saml:Issuer").SetText(sp.ServiceProviderIssuer) + } else { + artifactResolve.CreateElement("saml:Issuer").SetText(sp.IdentityProviderIssuer) + } + + artifactResolve.CreateElement("samlp:Artifact").SetText(artifact) + + // TODO should really change this method name + signed, err := sp.SignAuthnRequest(artifactResolve) + if err != nil { + return nil, err + } + + body.AddChild(signed) + doc := etree.NewDocument() + doc.SetRoot(envelope) + message, err := doc.WriteToString() + if err != nil { + return nil, err + } + + return strings.NewReader(message), nil +} diff --git a/artifact_test.go b/artifact_test.go new file mode 100644 index 0000000..8cb0db5 --- /dev/null +++ b/artifact_test.go @@ -0,0 +1,41 @@ +package saml2 + +import ( + "io" + "os" + "testing" + + "github.com/beevik/etree" + dsig "github.com/russellhaering/goxmldsig" + "github.com/stretchr/testify/require" +) + +func TestArtifact(t *testing.T) { + spURL := "https://sp.test" + randomKeyStore := dsig.RandomKeyStoreForTest() + sp := SAMLServiceProvider{ + AssertionConsumerServiceURL: spURL, + AudienceURI: spURL, + ServiceProviderIssuer: spURL, + IdentityProviderSSOURL: "https://idp.test/saml/sso", + SignAuthnRequests: true, + SPKeyStore: randomKeyStore, + } + req, err := sp.buildResolveRequest("1234567") + if err != nil { + t.Fatal(err) + } + + doc := etree.NewDocument() + _, err = doc.ReadFrom(req) + require.NoError(t, err) + + // Make sure request is signed + el := doc.FindElement("./Envelope/Body/ArtifactResolve/Signature") + require.NotNil(t, el) + // Make sure artifact is set + el = doc.FindElement("./Envelope/Body/ArtifactResolve/Artifact") + require.NotNil(t, el) + require.Equal(t, "1234567", el.Text()) + io.Copy(os.Stdout, req) +} diff --git a/build_request.go b/build_request.go index 8454b82..2f293a5 100644 --- a/build_request.go +++ b/build_request.go @@ -27,7 +27,14 @@ func (sp *SAMLServiceProvider) buildAuthnRequest(includeSig bool) (*etree.Docume authnRequest.CreateAttr("ID", "_"+arId.String()) authnRequest.CreateAttr("Version", "2.0") - authnRequest.CreateAttr("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") + switch sp.RequestedBinding { + case "", BindingHttpPost: + authnRequest.CreateAttr("ProtocolBinding", BindingHttpPost) + case BindingHttpArtifact: + authnRequest.CreateAttr("ProtocolBinding", BindingHttpArtifact) + default: + return nil, fmt.Errorf("invalid RequestedBinding, %s", sp.RequestedBinding) + } authnRequest.CreateAttr("AssertionConsumerServiceURL", sp.AssertionConsumerServiceURL) authnRequest.CreateAttr("IssueInstant", sp.Clock.Now().UTC().Format(issueInstantFormat)) authnRequest.CreateAttr("Destination", sp.IdentityProviderSSOURL) @@ -179,7 +186,7 @@ func (sp *SAMLServiceProvider) BuildAuthURL(relayState string) (string, error) { if err != nil { return "", err } - return sp.BuildAuthURLFromDocument(relayState, doc) + return sp.BuildAuthURLRedirect(relayState, doc) } // AuthRedirect takes a ResponseWriter and Request from an http interaction and diff --git a/saml.go b/saml.go index 410b404..b2f3919 100644 --- a/saml.go +++ b/saml.go @@ -2,6 +2,7 @@ package saml2 import ( "encoding/base64" + "net/http" "sync" "time" @@ -23,8 +24,9 @@ func (serr ErrSaml) Error() string { } type SAMLServiceProvider struct { - IdentityProviderSSOURL string - IdentityProviderIssuer string + IdentityProviderSSOURL string + IdentityProviderIssuer string + IdentityProviderArtifactResolutionServiceURL string AssertionConsumerServiceURL string ServiceProviderIssuer string @@ -32,6 +34,7 @@ type SAMLServiceProvider struct { SignAuthnRequests bool SignAuthnRequestsAlgorithm string SignAuthnRequestsCanonicalizer dsig.Canonicalizer + RequestedBinding string // RequestedAuthnContext allows service providers to require that the identity // provider use specific authentication mechanisms. Leaving this unset will @@ -42,6 +45,7 @@ type SAMLServiceProvider struct { IDPCertificateStore dsig.X509CertificateStore SPKeyStore dsig.X509KeyStore // Required encryption key, default signing key SPSigningKeyStore dsig.X509KeyStore // Optional signing key + HTTPClient *http.Client // Optional client for artifact resolution NameIdFormat string ValidateEncryptionCert bool SkipSignatureValidation bool @@ -114,6 +118,10 @@ func (sp *SAMLServiceProvider) Metadata() (*types.EntityDescriptor, error) { Binding: BindingHttpPost, Location: sp.AssertionConsumerServiceURL, Index: 1, + }, { + Binding: BindingHttpArtifact, + Location: sp.AssertionConsumerServiceURL, + Index: 2, }}, }, }, nil diff --git a/types/metadata.go b/types/metadata.go index a709918..6d84df0 100644 --- a/types/metadata.go +++ b/types/metadata.go @@ -40,12 +40,13 @@ type SPSSODescriptor struct { } type IDPSSODescriptor struct { - XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"` - WantAuthnRequestsSigned bool `xml:"WantAuthnRequestsSigned,attr"` - KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"` - NameIDFormats []NameIDFormat `xml:"NameIDFormat"` - SingleSignOnServices []SingleSignOnService `xml:"SingleSignOnService"` - Attributes []Attribute `xml:"Attribute"` + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"` + WantAuthnRequestsSigned bool `xml:"WantAuthnRequestsSigned,attr"` + KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"` + ArtifactResolutionService IndexedEndpoint + NameIDFormats []NameIDFormat `xml:"NameIDFormat"` + SingleSignOnServices []SingleSignOnService `xml:"SingleSignOnService"` + Attributes []Attribute `xml:"Attribute"` } type KeyDescriptor struct { diff --git a/xml_constants.go b/xml_constants.go index f5062f4..5a83600 100644 --- a/xml_constants.go +++ b/xml_constants.go @@ -50,6 +50,7 @@ const ( BindingHttpPost = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" BindingHttpRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + BindingHttpArtifact = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" ) const ( From a6dc0eebf42593fe9994e6fb25932b351c38226e Mon Sep 17 00:00:00 2001 From: amdonov Date: Tue, 4 Sep 2018 08:22:20 -0400 Subject: [PATCH 2/2] using string constant to work with Go 1.5 --- artifact.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artifact.go b/artifact.go index dec7592..dd4c2c4 100644 --- a/artifact.go +++ b/artifact.go @@ -21,7 +21,7 @@ func (sp *SAMLServiceProvider) ResolveArtifact(artifact string) (*AssertionInfo, if err != nil { return nil, err } - post, err := http.NewRequest(http.MethodPost, sp.IdentityProviderArtifactResolutionServiceURL, request) + post, err := http.NewRequest("POST", sp.IdentityProviderArtifactResolutionServiceURL, request) if err != nil { return nil, err }