diff --git a/connector/saml/saml.go b/connector/saml/saml.go index 0c5b806b..6e984ab6 100644 --- a/connector/saml/saml.go +++ b/connector/saml/saml.go @@ -2,8 +2,6 @@ package saml import ( - "bytes" - "compress/flate" "crypto/rand" "crypto/x509" "encoding/base64" @@ -36,6 +34,15 @@ const ( nameIDFormatKerberos = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos" nameIDFormatPersistent = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" nameIDformatTransient = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + + // top level status codes + statusCodeSuccess = "urn:oasis:names:tc:SAML:2.0:status:Success" + + // subject confirmation methods + subjectConfirmationMethodBearer = "urn:oasis:names:tc:SAML:2.0:cm:bearer" + + // allowed clock drift for timestamp validation + allowedClockDrift = time.Duration(30) * time.Second ) var ( @@ -253,6 +260,7 @@ func (p *provider) POSTData(s connector.Scopes) (action, value string, err error AllowCreate: true, Format: p.nameIDPolicyFormat, }, + AssertionConsumerServiceURL: p.redirectURI, } data, err := xml.MarshalIndent(r, "", " ") @@ -260,19 +268,7 @@ func (p *provider) POSTData(s connector.Scopes) (action, value string, err error return "", "", fmt.Errorf("marshal authn request: %v", err) } - buff := new(bytes.Buffer) - fw, err := flate.NewWriter(buff, flate.DefaultCompression) - if err != nil { - return "", "", fmt.Errorf("new flate writer: %v", err) - } - if _, err := fw.Write(data); err != nil { - return "", "", fmt.Errorf("compress message: %v", err) - } - if err := fw.Close(); err != nil { - return "", "", fmt.Errorf("flush message: %v", err) - } - - return p.ssoURL, base64.StdEncoding.EncodeToString(buff.Bytes()), nil + return p.ssoURL, base64.StdEncoding.EncodeToString(data), nil } func (p *provider) HandlePOST(s connector.Scopes, samlResponse string) (ident connector.Identity, err error) { @@ -296,6 +292,10 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse string) (ident co } + if err = p.validateStatus(&resp); err != nil { + return ident, err + } + assertion := resp.Assertion if assertion == nil { return ident, fmt.Errorf("response did not contain an assertion") @@ -305,6 +305,13 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse string) (ident co return ident, fmt.Errorf("response did not contain a subject") } + if err = p.validateConditions(assertion); err != nil { + return ident, err + } + if err = p.validateSubjectConfirmation(subject); err != nil { + return ident, err + } + switch { case subject.NameID != nil: if ident.UserID = subject.NameID.Value; ident.UserID == "" { @@ -348,19 +355,151 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse string) (ident co return ident, nil } +// Validate that the StatusCode of the Response is success. +// Otherwise return a human readable message to the end user +func (p *provider) validateStatus(resp *response) error { + // Status is mandatory in the Response type + status := resp.Status + if status == nil { + return fmt.Errorf("response did not contain a Status") + } + // StatusCode is mandatory in the Status type + statusCode := status.StatusCode + if statusCode == nil { + return fmt.Errorf("response did not contain a StatusCode") + } + if statusCode.Value != statusCodeSuccess { + parts := strings.Split(statusCode.Value, ":") + lastPart := parts[len(parts)-1] + errorMessage := fmt.Sprintf("status code of the Response was not Success, was %q", lastPart) + statusMessage := status.StatusMessage + if statusMessage != nil && statusMessage.Value != "" { + errorMessage += " -> " + statusMessage.Value + } + return fmt.Errorf(errorMessage) + } + return nil +} + +// Multiple subject SubjectConfirmation can be in the assertion +// and at least one SubjectConfirmation must be valid. +// This is described in the spec "Profiles for the OASIS Security +// Assertion Markup Language" in section 3.3 Bearer. +// see https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf +func (p *provider) validateSubjectConfirmation(subject *subject) error { + validSubjectConfirmation := false + subjectConfirmations := subject.SubjectConfirmations + if subjectConfirmations != nil && len(subjectConfirmations) > 0 { + for _, subjectConfirmation := range subjectConfirmations { + // skip if method is wrong + method := subjectConfirmation.Method + if method != "" && method != subjectConfirmationMethodBearer { + continue + } + subjectConfirmationData := subjectConfirmation.SubjectConfirmationData + if subjectConfirmationData == nil { + continue + } + inResponseTo := subjectConfirmationData.InResponseTo + if inResponseTo != "" { + // TODO also validate InResponseTo if present + } + // only validate that subjectConfirmationData is not expired + now := p.now() + notOnOrAfter := time.Time(subjectConfirmationData.NotOnOrAfter) + if !notOnOrAfter.IsZero() { + if now.After(notOnOrAfter) { + continue + } + } + // validate recipient if present + recipient := subjectConfirmationData.Recipient + if recipient != "" && recipient != p.redirectURI { + continue + } + validSubjectConfirmation = true + } + } + if !validSubjectConfirmation { + return fmt.Errorf("no valid SubjectConfirmation was found on this Response") + } + return nil +} + +// Validates the Conditions element and all of it's content +func (p *provider) validateConditions(assertion *assertion) error { + // Checks if a Conditions element exists + conditions := assertion.Conditions + if conditions == nil { + return nil + } + // Validates Assertion timestamps + now := p.now() + notBefore := time.Time(conditions.NotBefore) + if !notBefore.IsZero() { + if now.Add(allowedClockDrift).Before(notBefore) { + return fmt.Errorf("at %s got response that cannot be processed before %s", now, notBefore) + } + } + notOnOrAfter := time.Time(conditions.NotOnOrAfter) + if !notOnOrAfter.IsZero() { + if now.After(notOnOrAfter.Add(allowedClockDrift)) { + return fmt.Errorf("at %s got response that cannot be processed because it expired at %s", now, notOnOrAfter) + } + } + // Validates audience + audienceRestriction := conditions.AudienceRestriction + if audienceRestriction != nil { + audiences := audienceRestriction.Audiences + if audiences != nil && len(audiences) > 0 { + issuerInAudiences := false + for _, audience := range audiences { + if audience.Value == p.issuer { + issuerInAudiences = true + break + } + } + if !issuerInAudiences { + return fmt.Errorf("required audience %s was not in Response audiences %s", p.issuer, audiences) + } + } + } + return nil +} + // verify checks the signature info of a XML document and returns // the signed elements. +// The Validate function of the goxmldsig library only looks for +// signatures on the root element level. But a saml Response is valid +// if the complete message is signed, or only the Assertion is signed, +// or but elements are signed. Therefore we first check a possible +// signature of the Response than of the Assertion. If one of these +// is successful the Response is considered as valid. func verify(validator *dsig.ValidationContext, data []byte) (signed []byte, err error) { doc := etree.NewDocument() - if err := doc.ReadFromBytes(data); err != nil { + if err = doc.ReadFromBytes(data); err != nil { return nil, fmt.Errorf("parse document: %v", err) } - - result, err := validator.Validate(doc.Root()) - if err != nil { - return nil, err + verified := false + response := doc.Root() + transformedResponse, err := validator.Validate(response) + if err == nil { + verified = true + doc.SetRoot(transformedResponse) + } + assertion := response.SelectElement("Assertion") + if assertion == nil { + return nil, fmt.Errorf("response does not contain an Assertion element") + } + transformedAssertion, err := validator.Validate(assertion) + if err == nil { + verified = true + response.RemoveChild(assertion) + response.AddChild(transformedAssertion) + } + if verified != true { + return nil, fmt.Errorf("response does not contain a valid Signature element") } - doc.SetRoot(result) return doc.WriteToBytes() } diff --git a/connector/saml/saml_test.go b/connector/saml/saml_test.go index 4e455688..ec073450 100644 --- a/connector/saml/saml_test.go +++ b/connector/saml/saml_test.go @@ -2,12 +2,24 @@ package saml import ( "crypto/x509" + "encoding/base64" "encoding/pem" "errors" "io/ioutil" + "strings" "testing" + "time" - sdig "github.com/russellhaering/goxmldsig" + "github.com/Sirupsen/logrus" + + dsig "github.com/russellhaering/goxmldsig" + + "github.com/coreos/dex/connector" +) + +const ( + defaultIssuer = "http://localhost:5556/dex/callback" + defaultRedirectURI = "http://localhost:5556/dex/callback" ) func loadCert(ca string) (*x509.Certificate, error) { @@ -22,21 +34,238 @@ func loadCert(ca string) (*x509.Certificate, error) { return x509.ParseCertificate(block.Bytes) } -func TestVerify(t *testing.T) { - cert, err := loadCert("testdata/okta-ca.pem") +func runVerify(t *testing.T, ca string, resp string, shouldSucceed bool) { + cert, err := loadCert(ca) if err != nil { t.Fatal(err) } s := certStore{[]*x509.Certificate{cert}} - validator := sdig.NewDefaultValidationContext(s) + validator := dsig.NewDefaultValidationContext(s) - data, err := ioutil.ReadFile("testdata/okta-resp.xml") + data, err := ioutil.ReadFile(resp) if err != nil { t.Fatal(err) } if _, err := verify(validator, data); err != nil { - t.Fatal(err) + if shouldSucceed { + t.Fatal(err) + } + } else { + if !shouldSucceed { + t.Fatalf("expected an invalid signatrue but verification has been successful") + } + } +} + +func newProvider(issuer string, redirectURI string) *provider { + if issuer == "" { + issuer = defaultIssuer + } + if redirectURI == "" { + redirectURI = defaultRedirectURI + } + now, _ := time.Parse(time.RFC3339, "2017-01-24T20:48:41Z") + timeFunc := func() time.Time { return now } + return &provider{ + issuer: issuer, + ssoURL: "http://idp.org/saml/sso", + now: timeFunc, + usernameAttr: "user", + emailAttr: "email", + redirectURI: redirectURI, + logger: logrus.New(), + } +} + +func TestVerify(t *testing.T) { + runVerify(t, "testdata/okta-ca.pem", "testdata/okta-resp.xml", true) +} + +func TestVerifySignedMessageAndUnsignedAssertion(t *testing.T) { + runVerify(t, "testdata/idp-cert.pem", "testdata/idp-resp-signed-message.xml", true) +} + +func TestVerifyUnsignedMessageAndSignedAssertion(t *testing.T) { + runVerify(t, "testdata/idp-cert.pem", "testdata/idp-resp-signed-assertion.xml", true) +} + +func TestVerifySignedMessageAndSignedAssertion(t *testing.T) { + runVerify(t, "testdata/idp-cert.pem", "testdata/idp-resp-signed-message-and-assertion.xml", true) +} + +func TestVerifyUnsignedMessageAndUnsignedAssertion(t *testing.T) { + runVerify(t, "testdata/idp-cert.pem", "testdata/idp-resp.xml", false) +} + +func TestHandlePOST(t *testing.T) { + p := newProvider("", "") + scopes := connector.Scopes{ + OfflineAccess: false, + Groups: true, + } + data, err := ioutil.ReadFile("testdata/idp-resp.xml") + if err != nil { + t.Fatal(err) + } + ident, err := p.HandlePOST(scopes, base64.StdEncoding.EncodeToString(data)) + if err != nil { + t.Fatal(err) + } + if ident.UserID != "eric.chiang+okta@coreos.com" { + t.Fatalf("unexpected UserID %q", ident.UserID) + } + if ident.Username != "admin" { + t.Fatalf("unexpected Username: %q", ident.UserID) + } +} + +func TestValidateStatus(t *testing.T) { + p := newProvider("", "") + var err error + resp := response{} + // Test missing Status element + err = p.validateStatus(&resp) + if err == nil || !strings.HasSuffix(err.Error(), `Status`) { + t.Fatalf("validation should fail with missing Status") + } + // Test missing StatusCode element + resp.Status = &status{} + err = p.validateStatus(&resp) + if err == nil || !strings.HasSuffix(err.Error(), `StatusCode`) { + t.Fatalf("validation should fail with missing StatusCode") + } + // Test failed request without StatusMessage + resp.Status.StatusCode = &statusCode{ + Value: ":Requester", + } + err = p.validateStatus(&resp) + if err == nil || !strings.HasSuffix(err.Error(), `"Requester"`) { + t.Fatalf("validation should fail with code %q", "Requester") + } + // Test failed request with StatusMessage + resp.Status.StatusMessage = &statusMessage{ + Value: "Failed", + } + err = p.validateStatus(&resp) + if err == nil || !strings.HasSuffix(err.Error(), `"Requester" -> Failed`) { + t.Fatalf("validation should fail with code %q and message %q", "Requester", "Failed") + } +} + +func TestValidateSubjectConfirmation(t *testing.T) { + p := newProvider("", "") + var err error + var notAfter time.Time + subj := &subject{} + // Subject without any SubjectConfirmation + err = p.validateSubjectConfirmation(subj) + if err == nil { + t.Fatalf("validation of %q should fail", "Subject without any SubjectConfirmation") + } + // SubjectConfirmation without Method and SubjectConfirmationData + subj.SubjectConfirmations = []subjectConfirmation{subjectConfirmation{}} + err = p.validateSubjectConfirmation(subj) + if err == nil { + t.Fatalf("validation of %q should fail", "SubjectConfirmation without Method and SubjectConfirmationData") + } + // SubjectConfirmation with invalid Method and no SubjectConfirmationData + subj.SubjectConfirmations = []subjectConfirmation{subjectConfirmation{ + Method: "invalid", + }} + err = p.validateSubjectConfirmation(subj) + if err == nil { + t.Fatalf("validation of %q should fail", "SubjectConfirmation with invalid Method and no SubjectConfirmationData") + } + // SubjectConfirmation with valid Method and empty SubjectConfirmationData + subjConfirmationData := subjectConfirmationData{} + subj.SubjectConfirmations = []subjectConfirmation{subjectConfirmation{ + Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer", + SubjectConfirmationData: &subjConfirmationData, + }} + err = p.validateSubjectConfirmation(subj) + if err != nil { + t.Fatalf("validation of %q should succeed", "SubjectConfirmation with valid Method and empty SubjectConfirmationData") + } + // SubjectConfirmationData with invalid Recipient + subjConfirmationData.Recipient = "invalid" + err = p.validateSubjectConfirmation(subj) + if err == nil { + t.Fatalf("validation of %q should fail", "SubjectConfirmationData with invalid Recipient") + } + // expired SubjectConfirmationData + notAfter = p.now().Add(-time.Duration(60) * time.Second) + subjConfirmationData.NotOnOrAfter = xmlTime(notAfter) + subjConfirmationData.Recipient = defaultRedirectURI + err = p.validateSubjectConfirmation(subj) + if err == nil { + t.Fatalf("validation of %q should fail", " expired SubjectConfirmationData") + } + // valid SubjectConfirmationData + notAfter = p.now().Add(+time.Duration(60) * time.Second) + subjConfirmationData.NotOnOrAfter = xmlTime(notAfter) + subjConfirmationData.Recipient = defaultRedirectURI + err = p.validateSubjectConfirmation(subj) + if err != nil { + t.Fatalf("validation of %q should succed", "valid SubjectConfirmationData") + } +} + +func TestValidateConditions(t *testing.T) { + p := newProvider("", "") + var err error + var notAfter, notBefore time.Time + cond := conditions{ + AudienceRestriction: &audienceRestriction{}, + } + assert := &assertion{} + // Assertion without Conditions + err = p.validateConditions(assert) + if err != nil { + t.Fatalf("validation of %q should succeed", "Assertion without Conditions") + } + // Assertion with empty Conditions + assert.Conditions = &cond + err = p.validateConditions(assert) + if err != nil { + t.Fatalf("validation of %q should succeed", "Assertion with empty Conditions") + } + // Conditions with valid timestamps + notBefore = p.now().Add(-time.Duration(60) * time.Second) + notAfter = p.now().Add(+time.Duration(60) * time.Second) + cond.NotBefore = xmlTime(notBefore) + cond.NotOnOrAfter = xmlTime(notAfter) + err = p.validateConditions(assert) + if err != nil { + t.Fatalf("validation of %q should succeed", "Conditions with valid timestamps") + } + // Conditions where notBefore is 45 seconds after now + notBefore = p.now().Add(+time.Duration(45) * time.Second) + cond.NotBefore = xmlTime(notBefore) + err = p.validateConditions(assert) + if err == nil { + t.Fatalf("validation of %q should fail", "Conditions where notBefore is 45 seconds after now") + } + // Conditions where notBefore is 15 seconds after now + notBefore = p.now().Add(+time.Duration(15) * time.Second) + cond.NotBefore = xmlTime(notBefore) + err = p.validateConditions(assert) + if err != nil { + t.Fatalf("validation of %q should succeed", "Conditions where notBefore is 15 seconds after now") + } + // Audiences contains the issuer + validAudience := audience{Value: p.issuer} + cond.AudienceRestriction.Audiences = []audience{validAudience} + err = p.validateConditions(assert) + if err != nil { + t.Fatalf("validation of %q should succeed", "Audiences contains the issuer") + } + // Audiences is not empty and not contains the issuer + invalidAudience := audience{Value: "invalid"} + cond.AudienceRestriction.Audiences = []audience{invalidAudience} + err = p.validateConditions(assert) + if err == nil { + t.Fatalf("validation of %q should succeed", "Audiences is not empty and not contains the issuer") } } diff --git a/connector/saml/testdata/idp-cert.pem b/connector/saml/testdata/idp-cert.pem new file mode 100644 index 00000000..0c55213e --- /dev/null +++ b/connector/saml/testdata/idp-cert.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEUTCCAzmgAwIBAgIJAJdmunb39nFKMA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQwwCgYDVQQKEwNJRFAxFDASBgNV +BAsTC1NTT1Byb3ZpZGVyMRMwEQYDVQQDEwpkZXYtOTY5MjQ0MRswGQYJKoZIhvcN +AQkBFgxpbmZvQGlkcC5vcmcwHhcNMTcwMTI0MTczMTI3WhcNMjcwMTIyMTczMTI3 +WjB4MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEChMD +SURQMRQwEgYDVQQLEwtTU09Qcm92aWRlcjETMBEGA1UEAxMKZGV2LTk2OTI0NDEb +MBkGCSqGSIb3DQEJARYMaW5mb0BpZHAub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA0X/AE1tmDmhGRROAWaJ82XSORivRfgNt9Fb4rLrf6nIJsQN3 +vNb1Nk4DSUEDdQuvHNaEemSVkSPgfq5qnhh37bJaghr0728J8dOyYzV5eArPvsby +CRcnhXQzpCK2zvHwjgxNJMsNJLbnYpG/U+dCdCtcOOn9JEhKO8wKn06y2tcrvC1u +uVs7bodukPUNq82KJTyvCQP8jh1hEZXeR2siJFDeJj1n2FNTMeCKIqOb42J/i+sB +TlyK3mV5Ni++hI/ssIYVbPwrMIBd6sKLVAgInshBHOj/7XcXW/rMf468YtBKs4Xn +XsE3hLoU02aWCRDlVHa4hm3jfIAqEADOUumklQIDAQABo4HdMIHaMB0GA1UdDgQW +BBRjN/dQSvhZxIsHTXmDKQJkPrjp0TCBqgYDVR0jBIGiMIGfgBRjN/dQSvhZxIsH +TXmDKQJkPrjp0aF8pHoweDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju +aWExDDAKBgNVBAoTA0lEUDEUMBIGA1UECxMLU1NPUHJvdmlkZXIxEzARBgNVBAMT +CmRldi05NjkyNDQxGzAZBgkqhkiG9w0BCQEWDGluZm9AaWRwLm9yZ4IJAJdmunb3 +9nFKMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIqHUglIUAA+BKMW +6B0Q+cqIgDr9fWlsvDwIVK7/cvUeGIH3icSsje9AVZ4nQOJpxmC/E06HfuDXmbT1 +wG16jNo01mPW9qaOGRJuQqlZdegCSF385o/OHcbaEKBRwyYuvLfu80EREj8wcMUK +FpExoaxK7K8DS7hh3w7exLB80jyhIaDEYc1hdyAl+206XpOXSYBetsg7I622R2+a +jSL7ygUxQjmKQ5DyInPdXzCFCL6Ew/BN0dwzfnBEEK223ruOWBLpj13zMC077dor +/NgYyHZU6iqiDS2eYO5jhVMve/mP9734+6N34seQRmekfmsf2dJcEQhPVYr/j0De +Jc3men4= +-----END CERTIFICATE----- diff --git a/connector/saml/testdata/idp-resp-signed-assertion.xml b/connector/saml/testdata/idp-resp-signed-assertion.xml new file mode 100644 index 00000000..26e7200d --- /dev/null +++ b/connector/saml/testdata/idp-resp-signed-assertion.xml @@ -0,0 +1,29 @@ + + http://www.okta.com/exk91cb99lKkKSYoy0h7 + + + + + http://www.okta.com/exk91cb99lKkKSYoy0h7 + + + HFNooGfpAONF7T96W3bFsXkH51k=dI0QBihhNT5rtRYE9iB0lEKXkE7Yr4+QueOItRH2RcKwAXJ6DA/m3D/S7qwXk00Hn8ZpHu48ZO+HJpyweEEh2UuUWJCCTwwggagKybbSoRx3UTnSuNAFTdoDWTGt89z8j4+gRMC0sepYwppF3u87vJKRVBh8HjFfrHmWsZKwNtfoeXOOFCeatwxcI1sKCoBs2fTn78683ThoAJe3pygipSHY5WPt4dfT/yAY5Ars+OPY/N02M80OfIygZXdJwND0tVPJIF3M9DaehSkvCBHs7QA7DARsRXcuXdsYY7R8wHzqDVJZ4OvcsprONamm5AgUIpql1CjT94rFwWOFyxF2tg== +MIIEUTCCAzmgAwIBAgIJAJdmunb39nFKMA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQwwCgYDVQQKEwNJRFAxFDASBgNVBAsTC1NTT1Byb3ZpZGVyMRMwEQYDVQQDEwpkZXYtOTY5MjQ0MRswGQYJKoZIhvcNAQkBFgxpbmZvQGlkcC5vcmcwHhcNMTcwMTI0MTczMTI3WhcNMjcwMTIyMTczMTI3WjB4MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEChMDSURQMRQwEgYDVQQLEwtTU09Qcm92aWRlcjETMBEGA1UEAxMKZGV2LTk2OTI0NDEbMBkGCSqGSIb3DQEJARYMaW5mb0BpZHAub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0X/AE1tmDmhGRROAWaJ82XSORivRfgNt9Fb4rLrf6nIJsQN3vNb1Nk4DSUEDdQuvHNaEemSVkSPgfq5qnhh37bJaghr0728J8dOyYzV5eArPvsbyCRcnhXQzpCK2zvHwjgxNJMsNJLbnYpG/U+dCdCtcOOn9JEhKO8wKn06y2tcrvC1uuVs7bodukPUNq82KJTyvCQP8jh1hEZXeR2siJFDeJj1n2FNTMeCKIqOb42J/i+sBTlyK3mV5Ni++hI/ssIYVbPwrMIBd6sKLVAgInshBHOj/7XcXW/rMf468YtBKs4XnXsE3hLoU02aWCRDlVHa4hm3jfIAqEADOUumklQIDAQABo4HdMIHaMB0GA1UdDgQWBBRjN/dQSvhZxIsHTXmDKQJkPrjp0TCBqgYDVR0jBIGiMIGfgBRjN/dQSvhZxIsHTXmDKQJkPrjp0aF8pHoweDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAoTA0lEUDEUMBIGA1UECxMLU1NPUHJvdmlkZXIxEzARBgNVBAMTCmRldi05NjkyNDQxGzAZBgkqhkiG9w0BCQEWDGluZm9AaWRwLm9yZ4IJAJdmunb39nFKMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIqHUglIUAA+BKMW6B0Q+cqIgDr9fWlsvDwIVK7/cvUeGIH3icSsje9AVZ4nQOJpxmC/E06HfuDXmbT1wG16jNo01mPW9qaOGRJuQqlZdegCSF385o/OHcbaEKBRwyYuvLfu80EREj8wcMUKFpExoaxK7K8DS7hh3w7exLB80jyhIaDEYc1hdyAl+206XpOXSYBetsg7I622R2+ajSL7ygUxQjmKQ5DyInPdXzCFCL6Ew/BN0dwzfnBEEK223ruOWBLpj13zMC077dor/NgYyHZU6iqiDS2eYO5jhVMve/mP9734+6N34seQRmekfmsf2dJcEQhPVYr/j0DeJc3men4= + + eric.chiang+okta@coreos.com + + + + + + + http://localhost:5556/dex/callback + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + diff --git a/connector/saml/testdata/idp-resp-signed-message-and-assertion.xml b/connector/saml/testdata/idp-resp-signed-message-and-assertion.xml new file mode 100644 index 00000000..87cf41e7 --- /dev/null +++ b/connector/saml/testdata/idp-resp-signed-message-and-assertion.xml @@ -0,0 +1,34 @@ + + + + + P2k0nQ19ZcowlcaOz6do6Tyu8WI=ytdy1qRPdMeIGnlkkaeLdblzTPtIFc0EJNm8WktsNU1Mn6G/6AmNaXEUik2BkTpk8zKabHdSf6+le8hwRiyfNWPTF84lzVdMjQ/+I8pnX/srpG534zoSAsP6ZFQvHp46AHPx31KP75H/ymqx2DNppqxh8JjUeMKQkPUEqduWUZ4kFjcsrz9H3MNVsHfxntnswibiknU/wAthtBuY2I6yOIF55RprUgYb5j2TqDd3IArF6LkxWRvHvhaw66MdhY1iiit7AFOcuHJVyPe8Attra94jwM+O1Ch+HQgoI43nX91d/jkP0vyWzWD8Xkcwb+KuRPsQflxjV22UU0+JbwrBYA== +MIIEUTCCAzmgAwIBAgIJAJdmunb39nFKMA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQwwCgYDVQQKEwNJRFAxFDASBgNVBAsTC1NTT1Byb3ZpZGVyMRMwEQYDVQQDEwpkZXYtOTY5MjQ0MRswGQYJKoZIhvcNAQkBFgxpbmZvQGlkcC5vcmcwHhcNMTcwMTI0MTczMTI3WhcNMjcwMTIyMTczMTI3WjB4MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEChMDSURQMRQwEgYDVQQLEwtTU09Qcm92aWRlcjETMBEGA1UEAxMKZGV2LTk2OTI0NDEbMBkGCSqGSIb3DQEJARYMaW5mb0BpZHAub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0X/AE1tmDmhGRROAWaJ82XSORivRfgNt9Fb4rLrf6nIJsQN3vNb1Nk4DSUEDdQuvHNaEemSVkSPgfq5qnhh37bJaghr0728J8dOyYzV5eArPvsbyCRcnhXQzpCK2zvHwjgxNJMsNJLbnYpG/U+dCdCtcOOn9JEhKO8wKn06y2tcrvC1uuVs7bodukPUNq82KJTyvCQP8jh1hEZXeR2siJFDeJj1n2FNTMeCKIqOb42J/i+sBTlyK3mV5Ni++hI/ssIYVbPwrMIBd6sKLVAgInshBHOj/7XcXW/rMf468YtBKs4XnXsE3hLoU02aWCRDlVHa4hm3jfIAqEADOUumklQIDAQABo4HdMIHaMB0GA1UdDgQWBBRjN/dQSvhZxIsHTXmDKQJkPrjp0TCBqgYDVR0jBIGiMIGfgBRjN/dQSvhZxIsHTXmDKQJkPrjp0aF8pHoweDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAoTA0lEUDEUMBIGA1UECxMLU1NPUHJvdmlkZXIxEzARBgNVBAMTCmRldi05NjkyNDQxGzAZBgkqhkiG9w0BCQEWDGluZm9AaWRwLm9yZ4IJAJdmunb39nFKMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIqHUglIUAA+BKMW6B0Q+cqIgDr9fWlsvDwIVK7/cvUeGIH3icSsje9AVZ4nQOJpxmC/E06HfuDXmbT1wG16jNo01mPW9qaOGRJuQqlZdegCSF385o/OHcbaEKBRwyYuvLfu80EREj8wcMUKFpExoaxK7K8DS7hh3w7exLB80jyhIaDEYc1hdyAl+206XpOXSYBetsg7I622R2+ajSL7ygUxQjmKQ5DyInPdXzCFCL6Ew/BN0dwzfnBEEK223ruOWBLpj13zMC077dor/NgYyHZU6iqiDS2eYO5jhVMve/mP9734+6N34seQRmekfmsf2dJcEQhPVYr/j0DeJc3men4= + http://www.okta.com/exk91cb99lKkKSYoy0h7 + + + + + http://www.okta.com/exk91cb99lKkKSYoy0h7 + + + 4jNCSI3tTnbpozZ8qT4FZe+EWV8=xtTnl92ArZyxskD3b34cIjo5LIpeE+3RjW+jgtXMXhUIZp3uGJ2RC6n1CbJ6IWuo4KmezpnVUnSWNz/fgOTZCN/1VlqsfLDpoTf790GrP+q6rKyw8CW7nd0uVS5FRYe05HTO6C5RqnaE9PmZ/YYbiWtLIDx0+kqvu/jFr+D144G/mukaVG4ydnDQ/tl21N6hWIOpi1tWaNPv50OEEgY//9VPql9Us3YuhfrxNggVugauArwY9RL4nVFVjALP1wpkZn1JzpgNMFgvXfY3MxnI1OnWg6ypJESugIKroKqj5RyqMIaLICsUOBwIKk8R4zAATrB+D+kuFV9Ec837duW/Eg== +MIIEUTCCAzmgAwIBAgIJAJdmunb39nFKMA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQwwCgYDVQQKEwNJRFAxFDASBgNVBAsTC1NTT1Byb3ZpZGVyMRMwEQYDVQQDEwpkZXYtOTY5MjQ0MRswGQYJKoZIhvcNAQkBFgxpbmZvQGlkcC5vcmcwHhcNMTcwMTI0MTczMTI3WhcNMjcwMTIyMTczMTI3WjB4MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEChMDSURQMRQwEgYDVQQLEwtTU09Qcm92aWRlcjETMBEGA1UEAxMKZGV2LTk2OTI0NDEbMBkGCSqGSIb3DQEJARYMaW5mb0BpZHAub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0X/AE1tmDmhGRROAWaJ82XSORivRfgNt9Fb4rLrf6nIJsQN3vNb1Nk4DSUEDdQuvHNaEemSVkSPgfq5qnhh37bJaghr0728J8dOyYzV5eArPvsbyCRcnhXQzpCK2zvHwjgxNJMsNJLbnYpG/U+dCdCtcOOn9JEhKO8wKn06y2tcrvC1uuVs7bodukPUNq82KJTyvCQP8jh1hEZXeR2siJFDeJj1n2FNTMeCKIqOb42J/i+sBTlyK3mV5Ni++hI/ssIYVbPwrMIBd6sKLVAgInshBHOj/7XcXW/rMf468YtBKs4XnXsE3hLoU02aWCRDlVHa4hm3jfIAqEADOUumklQIDAQABo4HdMIHaMB0GA1UdDgQWBBRjN/dQSvhZxIsHTXmDKQJkPrjp0TCBqgYDVR0jBIGiMIGfgBRjN/dQSvhZxIsHTXmDKQJkPrjp0aF8pHoweDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAoTA0lEUDEUMBIGA1UECxMLU1NPUHJvdmlkZXIxEzARBgNVBAMTCmRldi05NjkyNDQxGzAZBgkqhkiG9w0BCQEWDGluZm9AaWRwLm9yZ4IJAJdmunb39nFKMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIqHUglIUAA+BKMW6B0Q+cqIgDr9fWlsvDwIVK7/cvUeGIH3icSsje9AVZ4nQOJpxmC/E06HfuDXmbT1wG16jNo01mPW9qaOGRJuQqlZdegCSF385o/OHcbaEKBRwyYuvLfu80EREj8wcMUKFpExoaxK7K8DS7hh3w7exLB80jyhIaDEYc1hdyAl+206XpOXSYBetsg7I622R2+ajSL7ygUxQjmKQ5DyInPdXzCFCL6Ew/BN0dwzfnBEEK223ruOWBLpj13zMC077dor/NgYyHZU6iqiDS2eYO5jhVMve/mP9734+6N34seQRmekfmsf2dJcEQhPVYr/j0DeJc3men4= + + eric.chiang+okta@coreos.com + + + + + + + http://localhost:5556/dex/callback + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + diff --git a/connector/saml/testdata/idp-resp-signed-message.xml b/connector/saml/testdata/idp-resp-signed-message.xml new file mode 100644 index 00000000..4898259e --- /dev/null +++ b/connector/saml/testdata/idp-resp-signed-message.xml @@ -0,0 +1,30 @@ + + + + + Oy9CTB/hzFWyr6QF99EZ4ymIEfY=COKw1DUpDzNVulVNFlcrPwaalCwr8BfTye92GN5snTmx3IDKLudr1PT+WPt5i2N4xaoFcq/X1p/yEVmWtC1O+YYXNNIouFwps9Gyw/iDEMs1TtVKfikbloKWkDdYgfqgcon+mOq/lHagLVAcgz5QRBHVTIrFWcFYbnemsj1hy8q7ToIeoyHX9f5TAZBfZEEbdZcsD581xKPafNGowgfWxkgEwLBzFsJVYg/QfeoNnORTsKlsQBuswiXrsWatZNOOjWpdF9qqYn/3f9axqx2CJPD2HB38Vl0g4dTFnpmMAe45ndJq0IpXr9YJNCDJjUIvR7srdV1AW7qe2Mp6LxBBNw== +MIIEUTCCAzmgAwIBAgIJAJdmunb39nFKMA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQwwCgYDVQQKEwNJRFAxFDASBgNVBAsTC1NTT1Byb3ZpZGVyMRMwEQYDVQQDEwpkZXYtOTY5MjQ0MRswGQYJKoZIhvcNAQkBFgxpbmZvQGlkcC5vcmcwHhcNMTcwMTI0MTczMTI3WhcNMjcwMTIyMTczMTI3WjB4MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEChMDSURQMRQwEgYDVQQLEwtTU09Qcm92aWRlcjETMBEGA1UEAxMKZGV2LTk2OTI0NDEbMBkGCSqGSIb3DQEJARYMaW5mb0BpZHAub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0X/AE1tmDmhGRROAWaJ82XSORivRfgNt9Fb4rLrf6nIJsQN3vNb1Nk4DSUEDdQuvHNaEemSVkSPgfq5qnhh37bJaghr0728J8dOyYzV5eArPvsbyCRcnhXQzpCK2zvHwjgxNJMsNJLbnYpG/U+dCdCtcOOn9JEhKO8wKn06y2tcrvC1uuVs7bodukPUNq82KJTyvCQP8jh1hEZXeR2siJFDeJj1n2FNTMeCKIqOb42J/i+sBTlyK3mV5Ni++hI/ssIYVbPwrMIBd6sKLVAgInshBHOj/7XcXW/rMf468YtBKs4XnXsE3hLoU02aWCRDlVHa4hm3jfIAqEADOUumklQIDAQABo4HdMIHaMB0GA1UdDgQWBBRjN/dQSvhZxIsHTXmDKQJkPrjp0TCBqgYDVR0jBIGiMIGfgBRjN/dQSvhZxIsHTXmDKQJkPrjp0aF8pHoweDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAoTA0lEUDEUMBIGA1UECxMLU1NPUHJvdmlkZXIxEzARBgNVBAMTCmRldi05NjkyNDQxGzAZBgkqhkiG9w0BCQEWDGluZm9AaWRwLm9yZ4IJAJdmunb39nFKMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIqHUglIUAA+BKMW6B0Q+cqIgDr9fWlsvDwIVK7/cvUeGIH3icSsje9AVZ4nQOJpxmC/E06HfuDXmbT1wG16jNo01mPW9qaOGRJuQqlZdegCSF385o/OHcbaEKBRwyYuvLfu80EREj8wcMUKFpExoaxK7K8DS7hh3w7exLB80jyhIaDEYc1hdyAl+206XpOXSYBetsg7I622R2+ajSL7ygUxQjmKQ5DyInPdXzCFCL6Ew/BN0dwzfnBEEK223ruOWBLpj13zMC077dor/NgYyHZU6iqiDS2eYO5jhVMve/mP9734+6N34seQRmekfmsf2dJcEQhPVYr/j0DeJc3men4= + http://www.okta.com/exk91cb99lKkKSYoy0h7 + + + + + http://www.okta.com/exk91cb99lKkKSYoy0h7 + + eric.chiang+okta@coreos.com + + + + + + + http://localhost:5556/dex/callback + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + diff --git a/connector/saml/testdata/idp-resp.xml b/connector/saml/testdata/idp-resp.xml new file mode 100644 index 00000000..44ba20af --- /dev/null +++ b/connector/saml/testdata/idp-resp.xml @@ -0,0 +1,34 @@ + + + http://www.okta.com/exk91cb99lKkKSYoy0h7 + + + + + http://www.okta.com/exk91cb99lKkKSYoy0h7 + + eric.chiang+okta@coreos.com + + + + + + + http://localhost:5556/dex/callback + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + admin + + + eric.chiang+okta@coreos.com + + + + diff --git a/connector/saml/types.go b/connector/saml/types.go index 7c1d89be..54d1b46a 100644 --- a/connector/saml/types.go +++ b/connector/saml/types.go @@ -57,6 +57,8 @@ type authnRequest struct { IsPassive bool `xml:"IsPassive,attr,omitempty"` ProtocolBinding string `xml:"ProtocolBinding,attr,omitempty"` + AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr,omitempty"` + Subject *subject `xml:"Subject,omitempty"` Issuer *issuer `xml:"Issuer,omitempty"` NameIDPolicy *nameIDPolicy `xml:"NameIDPolicy,omitempty"` @@ -68,7 +70,8 @@ type authnRequest struct { type subject struct { XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Subject"` - NameID *nameID `xml:"NameID,omitempty"` + NameID *nameID `xml:"NameID,omitempty"` + SubjectConfirmations []subjectConfirmation `xml:"SubjectConfirmation"` // TODO(ericchiang): Do we need to deal with baseID? } @@ -80,6 +83,60 @@ type nameID struct { Value string `xml:",chardata"` } +type subjectConfirmationData struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmationData"` + + NotOnOrAfter xmlTime `xml:"NotOnOrAfter,attr,omitempty"` + Recipient string `xml:"Recipient,attr,omitempty"` + InResponseTo string `xml:"InResponseTo,attr,omitempty"` +} + +type subjectConfirmation struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmation"` + + Method string `xml:"Method,attr,omitempty"` + SubjectConfirmationData *subjectConfirmationData `xml:"SubjectConfirmationData,omitempty"` +} + +type audience struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Audience"` + Value string `xml:",chardata"` +} + +type audienceRestriction struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AudienceRestriction"` + + Audiences []audience `xml:"Audience"` +} + +type conditions struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Conditions"` + + NotBefore xmlTime `xml:"NotBefore,attr,omitempty"` + NotOnOrAfter xmlTime `xml:"NotOnOrAfter,attr,omitempty"` + + AudienceRestriction *audienceRestriction `xml:"AudienceRestriction,omitempty"` +} + +type statusCode struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol StatusCode"` + + Value string `xml:"Value,attr,omitempty"` +} + +type statusMessage struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol StatusMessage"` + + Value string `xml:",chardata"` +} + +type status struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"` + + StatusCode *statusCode `xml:"StatusCode"` + StatusMessage *statusMessage `xml:"StatusMessage,omitempty"` +} + type issuer struct { XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"` Issuer string `xml:",chardata"` @@ -112,6 +169,8 @@ type response struct { Issuer *issuer `xml:"Issuer,omitempty"` + Status *status `xml:"Status"` + // TODO(ericchiang): How do deal with multiple assertions? Assertion *assertion `xml:"Assertion,omitempty"` } @@ -127,6 +186,8 @@ type assertion struct { Subject *subject `xml:"Subject,omitempty"` + Conditions *conditions `xml:"Conditions"` + AttributeStatement *attributeStatement `xml:"AttributeStatement,omitempty"` } diff --git a/glide.yaml b/glide.yaml index 3e7d13e6..c6d1991f 100644 --- a/glide.yaml +++ b/glide.yaml @@ -134,7 +134,7 @@ import: # XML signature validation for SAML connector - package: github.com/russellhaering/goxmldsig - version: d9f653eb27ee8b145f7d5a45172e81a93def0860 + version: e2990269f42f6ddfea940870a0800a14acdb8c21 - package: github.com/beevik/etree version: 4cd0dd976db869f817248477718071a28e978df0 - package: github.com/jonboulle/clockwork