diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 60f3f8f0..1f70a690 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.17.x', '1.18.x', '1.19.x'] + go: [ '1.18.x', '1.19.x', '1.20.x'] steps: - name: Check out code into the Go module directory uses: actions/checkout@v2 diff --git a/identity_provider_go116_test.go b/identity_provider_go116_test.go deleted file mode 100644 index ead0a780..00000000 --- a/identity_provider_go116_test.go +++ /dev/null @@ -1,57 +0,0 @@ -//go:build !go1.17 -// +build !go1.17 - -package saml - -import ( - "bytes" - "compress/flate" - "encoding/base64" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "gotest.tools/assert" - is "gotest.tools/assert/cmp" -) - -func TestIDPHTTPCanHandleSSORequest(t *testing.T) { - test := NewIdentifyProviderTest(t) - w := httptest.NewRecorder() - - const validRequest = `lJJBayoxFIX%2FypC9JhnU5wszAz7lgWCLaNtFd5fMbQ1MkmnunVb%2FfUfbUqEgdhs%2BTr5zkmLW8S5s8KVD4mzvm0Cl6FIwEciRCeCRDFuznd2sTD5Upk2Ro42NyGZEmNjFMI%2BBOo9pi%2BnVWbzfrEqxY27JSEntEPfg2waHNnpJ4JtcgiWRLfoLXYBjwDfu6p%2B8JIoiWy5K4eqBUipXIzVRUwXKKtRK53qkJ3qqQVuNPUjU4TIQQ%2BBS5EqPBzofKH2ntBn%2FMervo8jWnyX%2BuVC78FwKkT1gopNKX1JUxSklXTMIfM0gsv8xeeDL%2BPGk7%2FF0Qg0GdnwQ1cW5PDLUwFDID6uquO1Dlot1bJw9%2FPLRmia%2BzRMCYyk4dSiq6205QSDXOxfy3KAq5Pkvqt4DAAD%2F%2Fw%3D%3D` - - r, _ := http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ - "SAMLRequest="+validRequest, nil) - test.IDP.Handler().ServeHTTP(w, r) - assert.Check(t, is.Equal(http.StatusOK, w.Code)) - - // rejects requests that are invalid - w = httptest.NewRecorder() - r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ - "SAMLRequest=PEF1dGhuUmVxdWVzdA%3D%3D", nil) - test.IDP.Handler().ServeHTTP(w, r) - assert.Check(t, is.Equal(http.StatusBadRequest, w.Code)) - - // rejects requests that contain malformed XML - { - a, _ := url.QueryUnescape(validRequest) - b, _ := base64.StdEncoding.DecodeString(a) - c, _ := ioutil.ReadAll(flate.NewReader(bytes.NewReader(b))) - d := bytes.Replace(c, []byte("]]"), 1) - f := bytes.Buffer{} - e, _ := flate.NewWriter(&f, flate.DefaultCompression) - e.Write(d) - e.Close() - g := base64.StdEncoding.EncodeToString(f.Bytes()) - invalidRequest := url.QueryEscape(g) - - w = httptest.NewRecorder() - r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ - "SAMLRequest="+invalidRequest, nil) - test.IDP.Handler().ServeHTTP(w, r) - assert.Check(t, is.Equal(http.StatusBadRequest, w.Code)) - } -} diff --git a/identity_provider_test.go b/identity_provider_test.go index 0c602b53..3565c492 100644 --- a/identity_provider_test.go +++ b/identity_provider_test.go @@ -1,6 +1,8 @@ package saml import ( + "bytes" + "compress/flate" "crypto" "crypto/rsa" "crypto/x509" @@ -8,6 +10,7 @@ import ( "encoding/pem" "encoding/xml" "fmt" + "io" "math/rand" "net/http" "net/http/httptest" @@ -1013,3 +1016,42 @@ func TestIDPNoDestination(t *testing.T) { err = req.MakeResponse() assert.Check(t, err) } + +func TestIDPHTTPCanHandleSSORequest(t *testing.T) { + test := NewIdentifyProviderTest(t) + w := httptest.NewRecorder() + + const validRequest = `lJJBayoxFIX%2FypC9JhnU5wszAz7lgWCLaNtFd5fMbQ1MkmnunVb%2FfUfbUqEgdhs%2BTr5zkmLW8S5s8KVD4mzvm0Cl6FIwEciRCeCRDFuznd2sTD5Upk2Ro42NyGZEmNjFMI%2BBOo9pi%2BnVWbzfrEqxY27JSEntEPfg2waHNnpJ4JtcgiWRLfoLXYBjwDfu6p%2B8JIoiWy5K4eqBUipXIzVRUwXKKtRK53qkJ3qqQVuNPUjU4TIQQ%2BBS5EqPBzofKH2ntBn%2FMervo8jWnyX%2BuVC78FwKkT1gopNKX1JUxSklXTMIfM0gsv8xeeDL%2BPGk7%2FF0Qg0GdnwQ1cW5PDLUwFDID6uquO1Dlot1bJw9%2FPLRmia%2BzRMCYyk4dSiq6205QSDXOxfy3KAq5Pkvqt4DAAD%2F%2Fw%3D%3D` + + r, _ := http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ + "SAMLRequest="+validRequest, nil) + test.IDP.Handler().ServeHTTP(w, r) + assert.Check(t, is.Equal(http.StatusOK, w.Code)) + + // rejects requests that are invalid + w = httptest.NewRecorder() + r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ + "SAMLRequest=PEF1dGhuUmVxdWVzdA%3D%3D", nil) + test.IDP.Handler().ServeHTTP(w, r) + assert.Check(t, is.Equal(http.StatusBadRequest, w.Code)) + + // rejects requests that contain malformed XML + { + a, _ := url.QueryUnescape(validRequest) + b, _ := base64.StdEncoding.DecodeString(a) + c, _ := io.ReadAll(flate.NewReader(bytes.NewReader(b))) + d := bytes.Replace(c, []byte("]]"), 1) + f := bytes.Buffer{} + e, _ := flate.NewWriter(&f, flate.DefaultCompression) + e.Write(d) + e.Close() + g := base64.StdEncoding.EncodeToString(f.Bytes()) + invalidRequest := url.QueryEscape(g) + + w = httptest.NewRecorder() + r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ + "SAMLRequest="+invalidRequest, nil) + test.IDP.Handler().ServeHTTP(w, r) + assert.Check(t, is.Equal(http.StatusBadRequest, w.Code)) + } +} diff --git a/service_provider_go116_test.go b/service_provider_go116_test.go deleted file mode 100644 index 77395e01..00000000 --- a/service_provider_go116_test.go +++ /dev/null @@ -1,136 +0,0 @@ -//go:build !go1.17 -// +build !go1.17 - -package saml - -import ( - "encoding/base64" - "encoding/xml" - "net/http" - "net/url" - "strings" - "testing" - "time" - - dsig "github.com/russellhaering/goxmldsig" - "gotest.tools/assert" - is "gotest.tools/assert/cmp" - "gotest.tools/golden" -) - -func TestSPRejectsMalformedResponse(t *testing.T) { - test := NewServiceProviderTest(t) - // An actual response from google - TimeNow = func() time.Time { - rv, _ := time.Parse("Mon Jan 2 15:04:05 UTC 2006", "Tue Jan 5 16:55:39 UTC 2016") - return rv - } - Clock = dsig.NewFakeClockAt(TimeNow()) - SamlResponse := golden.Get(t, "TestSPRejectsMalformedResponse_response") - test.IDPMetadata = golden.Get(t, "TestSPRejectsMalformedResponse_IDPMetadata") - - s := ServiceProvider{ - Key: test.Key, - Certificate: test.Certificate, - MetadataURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/metadata"), - AcsURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/acs"), - IDPMetadata: &EntityDescriptor{}, - } - err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) - assert.Check(t, err) - - // this is a valid response - { - req := http.Request{PostForm: url.Values{}} - req.PostForm.Set("SAMLResponse", string(SamlResponse)) - assertion, err := s.ParseResponse(&req, []string{"id-fd419a5ab0472645427f8e07d87a3a5dd0b2e9a6"}) - assert.Check(t, err) - assert.Check(t, is.Equal("ross@octolabs.io", assertion.Subject.NameID.Value)) - } - - // this is a valid response but with a comment injected - { - x, _ := base64.StdEncoding.DecodeString(string(SamlResponse)) - y := strings.Replace(string(x), "World!"))) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot unmarshal response: expected element type but have ")) - - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"wrongRequestID"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "`InResponseTo` does not match any of the possible request IDs (expected [wrongRequestID])")) - - TimeNow = func() time.Time { - rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Nov 30 20:57:09 UTC 2016") - return rv - } - Clock = dsig.NewFakeClockAt(TimeNow()) - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "response IssueInstant expired at 2015-12-01 01:57:51.375 +0000 UTC")) - TimeNow = func() time.Time { - rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Dec 1 01:57:09 UTC 2015") - return rv - } - Clock = dsig.NewFakeClockAt(TimeNow()) - - s.IDPMetadata.EntityID = "http://snakeoil.com" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "response Issuer does not match the IDP metadata (expected \"http://snakeoil.com\")")) - s.IDPMetadata.EntityID = "https://idp.testshib.org/idp/shibboleth" - - oldSpStatusSuccess := StatusSuccess - StatusSuccess = "not:the:success:value" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "urn:oasis:names:tc:SAML:2.0:status:Success")) - StatusSuccess = oldSpStatusSuccess - - s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.X509Data.X509Certificates[0].Data = "invalid" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Response: cannot parse certificate: illegal base64 data at input byte 4")) - - s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.X509Data.X509Certificates[0].Data = "aW52YWxpZA==" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Response: asn1: structure error: tags don't match (16 vs {class:1 tag:9 length:110 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:0 set:false omitEmpty:false} certificate @2")) -} diff --git a/service_provider_go117_test.go b/service_provider_go117_test.go deleted file mode 100644 index 3d4a5835..00000000 --- a/service_provider_go117_test.go +++ /dev/null @@ -1,136 +0,0 @@ -//go:build go1.17 -// +build go1.17 - -package saml - -import ( - "encoding/base64" - "encoding/xml" - "net/http" - "net/url" - "strings" - "testing" - "time" - - dsig "github.com/russellhaering/goxmldsig" - "gotest.tools/assert" - is "gotest.tools/assert/cmp" - "gotest.tools/golden" -) - -func TestSPRejectsMalformedResponse(t *testing.T) { - test := NewServiceProviderTest(t) - // An actual response from google - TimeNow = func() time.Time { - rv, _ := time.Parse("Mon Jan 2 15:04:05 UTC 2006", "Tue Jan 5 16:55:39 UTC 2016") - return rv - } - Clock = dsig.NewFakeClockAt(TimeNow()) - SamlResponse := golden.Get(t, "TestSPRejectsMalformedResponse_response") - test.IDPMetadata = golden.Get(t, "TestSPRejectsMalformedResponse_IDPMetadata") - - s := ServiceProvider{ - Key: test.Key, - Certificate: test.Certificate, - MetadataURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/metadata"), - AcsURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/acs"), - IDPMetadata: &EntityDescriptor{}, - } - err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) - assert.Check(t, err) - - // this is a valid response - { - req := http.Request{PostForm: url.Values{}} - req.PostForm.Set("SAMLResponse", string(SamlResponse)) - assertion, err := s.ParseResponse(&req, []string{"id-fd419a5ab0472645427f8e07d87a3a5dd0b2e9a6"}) - assert.Check(t, err) - assert.Check(t, is.Equal("ross@octolabs.io", assertion.Subject.NameID.Value)) - } - - // this is a valid response but with a comment injected - { - x, _ := base64.StdEncoding.DecodeString(string(SamlResponse)) - y := strings.Replace(string(x), "World!"))) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot unmarshal response: expected element type but have ")) - - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"wrongRequestID"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "`InResponseTo` does not match any of the possible request IDs (expected [wrongRequestID])")) - - TimeNow = func() time.Time { - rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Nov 30 20:57:09 UTC 2016") - return rv - } - Clock = dsig.NewFakeClockAt(TimeNow()) - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "response IssueInstant expired at 2015-12-01 01:57:51.375 +0000 UTC")) - TimeNow = func() time.Time { - rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Dec 1 01:57:09 UTC 2015") - return rv - } - Clock = dsig.NewFakeClockAt(TimeNow()) - - s.IDPMetadata.EntityID = "http://snakeoil.com" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "response Issuer does not match the IDP metadata (expected \"http://snakeoil.com\")")) - s.IDPMetadata.EntityID = "https://idp.testshib.org/idp/shibboleth" - - oldSpStatusSuccess := StatusSuccess - StatusSuccess = "not:the:success:value" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "urn:oasis:names:tc:SAML:2.0:status:Success")) - StatusSuccess = oldSpStatusSuccess - - s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.X509Data.X509Certificates[0].Data = "invalid" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Assertion: cannot parse certificate: illegal base64 data at input byte 4")) - - s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.X509Data.X509Certificates[0].Data = "aW52YWxpZA==" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Assertion: x509: malformed certificate")) -} diff --git a/service_provider_test.go b/service_provider_test.go index cdf5c723..1c099250 100644 --- a/service_provider_test.go +++ b/service_provider_test.go @@ -1851,3 +1851,119 @@ func TestMultipleAssertions(t *testing.T) { assert.Check(t, err) assert.Check(t, profile.Subject.NameID.Value != "admin@evil.com") } + +func TestSPRejectsMalformedResponse(t *testing.T) { + test := NewServiceProviderTest(t) + // An actual response from google + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05 UTC 2006", "Tue Jan 5 16:55:39 UTC 2016") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + SamlResponse := golden.Get(t, "TestSPRejectsMalformedResponse_response") + test.IDPMetadata = golden.Get(t, "TestSPRejectsMalformedResponse_IDPMetadata") + + s := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/metadata"), + AcsURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/acs"), + IDPMetadata: &EntityDescriptor{}, + } + err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) + assert.Check(t, err) + + // this is a valid response + { + req := http.Request{PostForm: url.Values{}} + req.PostForm.Set("SAMLResponse", string(SamlResponse)) + assertion, err := s.ParseResponse(&req, []string{"id-fd419a5ab0472645427f8e07d87a3a5dd0b2e9a6"}) + assert.Check(t, err) + assert.Check(t, is.Equal("ross@octolabs.io", assertion.Subject.NameID.Value)) + } + + // this is a valid response but with a comment injected + { + x, _ := base64.StdEncoding.DecodeString(string(SamlResponse)) + y := strings.Replace(string(x), "World!"))) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "cannot unmarshal response: expected element type but have ")) + + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"wrongRequestID"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "`InResponseTo` does not match any of the possible request IDs (expected [wrongRequestID])")) + + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Nov 30 20:57:09 UTC 2016") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "response IssueInstant expired at 2015-12-01 01:57:51.375 +0000 UTC")) + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Dec 1 01:57:09 UTC 2015") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + + s.IDPMetadata.EntityID = "http://snakeoil.com" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "response Issuer does not match the IDP metadata (expected \"http://snakeoil.com\")")) + s.IDPMetadata.EntityID = "https://idp.testshib.org/idp/shibboleth" + + oldSpStatusSuccess := StatusSuccess + StatusSuccess = "not:the:success:value" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "urn:oasis:names:tc:SAML:2.0:status:Success")) + StatusSuccess = oldSpStatusSuccess + + s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.X509Data.X509Certificates[0].Data = "invalid" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "cannot validate signature on Assertion: cannot parse certificate: illegal base64 data at input byte 4")) + + s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.X509Data.X509Certificates[0].Data = "aW52YWxpZA==" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "cannot validate signature on Assertion: x509: malformed certificate")) +}