forked from mystiq/dex
e46f2ebe40
* Improve Order of Namespace Declarations and Attributes in Canonical XML. This is related to an issue in goxmldsig for which I created an [pull request](https://github.com/russellhaering/goxmldsig/pull/17). * Do not compress the AuthnRequest if `HTTP-POST` binding is used. * SAML Response is valid if the Message and/or the Assertion is signed. * Add `AssertionConsumerServiceURL` to `AuthnRequest` * Validate Status on the Response * Validate Conditions on the Assertion * Validation SubjectConfirmation on the Subject
271 lines
8.4 KiB
Go
271 lines
8.4 KiB
Go
package saml
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"errors"
|
|
"io/ioutil"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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) {
|
|
data, err := ioutil.ReadFile(ca)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
block, _ := pem.Decode(data)
|
|
if block == nil {
|
|
return nil, errors.New("ca file didn't contain any PEM data")
|
|
}
|
|
return x509.ParseCertificate(block.Bytes)
|
|
}
|
|
|
|
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 := dsig.NewDefaultValidationContext(s)
|
|
|
|
data, err := ioutil.ReadFile(resp)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := verify(validator, data); err != nil {
|
|
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")
|
|
}
|
|
}
|