Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion example/trivial/trivial.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func main() {
}

rootURL, _ := url.Parse("http://localhost:8000")
idpMetadataURL, _ := url.Parse("https://www.testshib.org/metadata/testshib-providers.xml")
idpMetadataURL, _ := url.Parse("https://samltest.id/saml/idp")

idpMetadata, err := samlsp.FetchMetadata(
context.Background(),
Expand All @@ -42,6 +42,7 @@ func main() {
IDPMetadata: idpMetadata,
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
Certificate: keyPair.Leaf,
SignRequest: true,
})
if err != nil {
panic(err) // TODO handle error
Expand Down
7 changes: 7 additions & 0 deletions samlsp/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"crypto/rsa"
"crypto/x509"
dsig "github.com/russellhaering/goxmldsig"
"net/http"
"net/url"
"time"
Expand All @@ -22,6 +23,7 @@ type Options struct {
Intermediates []*x509.Certificate
AllowIDPInitiated bool
IDPMetadata *saml.EntityDescriptor
SignRequest bool
ForceAuthn bool // TODO(ross): this should be *bool

// The following fields exist <= 0.3.0, but are superceded by the new
Expand Down Expand Up @@ -125,6 +127,10 @@ func DefaultServiceProvider(opts Options) saml.ServiceProvider {
if opts.ForceAuthn {
forceAuthn = &opts.ForceAuthn
}
signatureMethod := dsig.RSASHA1SignatureMethod
if !opts.SignRequest {
signatureMethod = ""
}

return saml.ServiceProvider{
EntityID: opts.EntityID,
Expand All @@ -136,6 +142,7 @@ func DefaultServiceProvider(opts Options) saml.ServiceProvider {
SloURL: *sloURL,
IDPMetadata: opts.IDPMetadata,
ForceAuthn: forceAuthn,
SignatureMethod: signatureMethod,
AllowIDPInitiated: opts.AllowIDPInitiated,
}
}
Expand Down
62 changes: 55 additions & 7 deletions service_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"compress/flate"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/xml"
Expand Down Expand Up @@ -101,6 +102,9 @@ type ServiceProvider struct {
// SignatureVerifier, if non-nil, allows you to implement an alternative way
// to verify signatures.
SignatureVerifier SignatureVerifier

// SignatureMethod, if non-empty, authentication requests will be signed
SignatureMethod string
}

// MaxIssueDelay is the longest allowed time between when a SAML assertion is
Expand All @@ -126,7 +130,7 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor {
validDuration = sp.MetadataValidDuration
}

authnRequestsSigned := false
authnRequestsSigned := len(sp.SignatureMethod) > 0
wantAssertionsSigned := true
validUntil := TimeNow().Add(validDuration)

Expand All @@ -137,12 +141,6 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor {
certBytes = append(certBytes, intermediate.Raw...)
}
keyDescriptors = []KeyDescriptor{
{
Use: "signing",
KeyInfo: KeyInfo{
Certificate: base64.StdEncoding.EncodeToString(certBytes),
},
},
{
Use: "encryption",
KeyInfo: KeyInfo{
Expand All @@ -156,6 +154,14 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor {
},
},
}
if len(sp.SignatureMethod) > 0 {
keyDescriptors = append(keyDescriptors, KeyDescriptor{
Use: "signing",
KeyInfo: KeyInfo{
Certificate: base64.StdEncoding.EncodeToString(certBytes),
},
})
}
}

return &EntityDescriptor{
Expand Down Expand Up @@ -330,9 +336,51 @@ func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string) (*AuthnReque
},
ForceAuthn: sp.ForceAuthn,
}
if len(sp.SignatureMethod) > 0 {
if err := sp.SignAuthnRequest(&req); err != nil {
return nil, err
}
}
return &req, nil
}

// SignAuthnRequest adds the `Signature` element to the `AuthnRequest`.
func (sp *ServiceProvider) SignAuthnRequest(req *AuthnRequest) error {
keyPair := tls.Certificate{
Certificate: [][]byte{sp.Certificate.Raw},
PrivateKey: sp.Key,
Leaf: sp.Certificate,
}
// TODO: add intermediates for SP
//for _, cert := range sp.Intermediates {
// keyPair.Certificate = append(keyPair.Certificate, cert.Raw)
//}
keyStore := dsig.TLSCertKeyStore(keyPair)

if sp.SignatureMethod != dsig.RSASHA1SignatureMethod &&
sp.SignatureMethod != dsig.RSASHA256SignatureMethod &&
sp.SignatureMethod != dsig.RSASHA512SignatureMethod {
return fmt.Errorf("invalid signing method %s", sp.SignatureMethod)
}
signatureMethod := sp.SignatureMethod
signingContext := dsig.NewDefaultSigningContext(keyStore)
signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList)
if err := signingContext.SetSignatureMethod(signatureMethod); err != nil {
return err
}

assertionEl := req.Element()

signedRequestEl, err := signingContext.SignEnveloped(assertionEl)
if err != nil {
return err
}

sigEl := signedRequestEl.Child[len(signedRequestEl.Child)-1]
req.Signature = sigEl.(*etree.Element)
return nil
}

// MakePostAuthenticationRequest creates a SAML authentication request using
// the HTTP-POST binding. It returns HTML text representing an HTML form that
// can be sent presented to a browser to initiate the login process.
Expand Down
99 changes: 96 additions & 3 deletions service_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func TestSPCanSetAuthenticationNameIDFormat(t *testing.T) {
assert.Equal(t, string(EmailAddressNameIDFormat), *req.NameIDPolicy.Format)
}

func TestSPCanProduceMetadata(t *testing.T) {
func TestSPCanProduceMetadataWithEncryptionCert(t *testing.T) {
test := NewServiceProviderTest()
s := ServiceProvider{
Key: test.Key,
Expand All @@ -116,13 +116,43 @@ func TestSPCanProduceMetadata(t *testing.T) {
assert.Equal(t, ""+
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2015-12-03T01:57:09Z\" entityID=\"https://example.com/saml2/metadata\">\n"+
" <SPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2015-12-03T01:57:09Z\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\" AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"true\">\n"+
" <KeyDescriptor use=\"signing\">\n"+
" <KeyDescriptor use=\"encryption\">\n"+
" <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n"+
" <X509Data>\n"+
" <X509Certificate>MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==</X509Certificate>\n"+
" </X509Data>\n"+
" </KeyInfo>\n"+
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n"+
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n"+
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n"+
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n"+
" </KeyDescriptor>\n"+
" <SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://example.com/saml2/slo\" ResponseLocation=\"https://example.com/saml2/slo\"></SingleLogoutService>\n"+
" <AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://example.com/saml2/acs\" index=\"1\"></AssertionConsumerService>\n"+
" </SPSSODescriptor>\n"+
"</EntityDescriptor>",
string(spMetadata))
}

func TestSPCanProduceMetadataWithBothCerts(t *testing.T) {
test := NewServiceProviderTest()
s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://example.com/saml2/metadata"),
AcsURL: mustParseURL("https://example.com/saml2/acs"),
SloURL: mustParseURL("https://example.com/saml2/slo"),
IDPMetadata: &EntityDescriptor{},
SignatureMethod: "not-empty",
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
assert.NoError(t, err)

spMetadata, err := xml.MarshalIndent(s.Metadata(), "", " ")
assert.NoError(t, err)
assert.Equal(t, ""+
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2015-12-03T01:57:09Z\" entityID=\"https://example.com/saml2/metadata\">\n"+
" <SPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2015-12-03T01:57:09Z\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\" AuthnRequestsSigned=\"true\" WantAssertionsSigned=\"true\">\n"+
" <KeyDescriptor use=\"encryption\">\n"+
" <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n"+
" <X509Data>\n"+
Expand All @@ -134,14 +164,21 @@ func TestSPCanProduceMetadata(t *testing.T) {
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n"+
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n"+
" </KeyDescriptor>\n"+
" <KeyDescriptor use=\"signing\">\n"+
" <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n"+
" <X509Data>\n"+
" <X509Certificate>MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==</X509Certificate>\n"+
" </X509Data>\n"+
" </KeyInfo>\n"+
" </KeyDescriptor>\n"+
" <SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://example.com/saml2/slo\" ResponseLocation=\"https://example.com/saml2/slo\"></SingleLogoutService>\n"+
" <AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://example.com/saml2/acs\" index=\"1\"></AssertionConsumerService>\n"+
" </SPSSODescriptor>\n"+
"</EntityDescriptor>",
string(spMetadata))
}

func TestCanProduceMetadataNoSigningKey(t *testing.T) {
func TestCanProduceMetadataNoCerts(t *testing.T) {
test := NewServiceProviderTest()
s := ServiceProvider{
MetadataURL: mustParseURL("https://example.com/saml2/metadata"),
Expand Down Expand Up @@ -248,6 +285,62 @@ func TestSPCanProducePostRequest(t *testing.T) {
string(form))
}

func TestSPCanProduceSignedRequest(t *testing.T) {
test := NewServiceProviderTest()
TimeNow = func() time.Time {
rv, _ := time.Parse("Mon Jan 2 15:04:05.999999999 UTC 2006", "Mon Dec 1 01:31:21.123456789 UTC 2015")
return rv
}
Clock = dsig.NewFakeClockAt(TimeNow())
s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
IDPMetadata: &EntityDescriptor{},
SignatureMethod: dsig.RSASHA1SignatureMethod,
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
assert.NoError(t, err)

redirectURL, err := s.MakeRedirectAuthenticationRequest("relayState")
assert.NoError(t, err)

decodedRequest, err := testsaml.ParseRedirectRequest(redirectURL)
assert.NoError(t, err)
assert.Equal(t,
"idp.testshib.org",
redirectURL.Host)
assert.Equal(t,
"/idp/profile/SAML2/Redirect/SSO",
redirectURL.Path)
assert.Equal(t,
"<samlp:AuthnRequest xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"id-00020406080a0c0e10121416181a1c1e20222426\" Version=\"2.0\" IssueInstant=\"2015-12-01T01:31:21.123Z\" Destination=\"https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO\" AssertionConsumerServiceURL=\"https://15661444.ngrok.io/saml2/acs\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"><saml:Issuer Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://15661444.ngrok.io/saml2/metadata</saml:Issuer><ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/><ds:SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><ds:Reference URI=\"#id-00020406080a0c0e10121416181a1c1e20222426\"><ds:Transforms><ds:Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/><ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/></ds:Transforms><ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><ds:DigestValue>XQ5+kdgOf34vpAemZRFalLlzjr0=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>Wtomi/PiWx0bMFlImy5soCrrDbdY4BR2Qb8woGqc8KsVtXAwvl6lfYE2tuoT0YS5ipPLMMsFG8dB1TmLcA+0lnUcqfBiTiiHEwTIo3193RIsoH3STlOmXqBQf9Ax2nRdX+/4HwIYF58lgUzOb+nur+zGL6mYw2xjQBw6YGaX9Cc=</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><samlp:NameIDPolicy Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:transient\" AllowCreate=\"true\"/></samlp:AuthnRequest>",
string(decodedRequest))
}

func TestSPFailToProduceSignedRequestWithBogusSignatureMethod(t *testing.T) {
test := NewServiceProviderTest()
TimeNow = func() time.Time {
rv, _ := time.Parse("Mon Jan 2 15:04:05.999999999 UTC 2006", "Mon Dec 1 01:31:21.123456789 UTC 2015")
return rv
}
Clock = dsig.NewFakeClockAt(TimeNow())
s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
IDPMetadata: &EntityDescriptor{},
SignatureMethod: "bogus",
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
assert.NoError(t, err)

_, err = s.MakeRedirectAuthenticationRequest("relayState")
assert.Errorf(t, err, "invalid signing method bogus")
}

func TestSPCanProducePostLogoutRequest(t *testing.T) {
test := NewServiceProviderTest()
TimeNow = func() time.Time {
Expand Down