github.com/crewjam/saml@v0.4.14/identity_provider_test.go (about)

     1  package saml
     2  
     3  import (
     4  	"bytes"
     5  	"compress/flate"
     6  	"crypto"
     7  	"crypto/rsa"
     8  	"crypto/x509"
     9  	"encoding/base64"
    10  	"encoding/pem"
    11  	"encoding/xml"
    12  	"fmt"
    13  	"io"
    14  	"math/rand"
    15  	"net/http"
    16  	"net/http/httptest"
    17  	"net/url"
    18  	"os"
    19  	"strings"
    20  	"testing"
    21  	"time"
    22  
    23  	"gotest.tools/assert"
    24  	is "gotest.tools/assert/cmp"
    25  	"gotest.tools/golden"
    26  
    27  	"github.com/beevik/etree"
    28  	"github.com/golang-jwt/jwt/v4"
    29  	dsig "github.com/russellhaering/goxmldsig"
    30  
    31  	"github.com/crewjam/saml/logger"
    32  	"github.com/crewjam/saml/testsaml"
    33  	"github.com/crewjam/saml/xmlenc"
    34  )
    35  
    36  type IdentityProviderTest struct {
    37  	SPKey         *rsa.PrivateKey
    38  	SPCertificate *x509.Certificate
    39  	SP            ServiceProvider
    40  
    41  	Key             crypto.PrivateKey
    42  	Signer          crypto.Signer
    43  	Certificate     *x509.Certificate
    44  	SessionProvider SessionProvider
    45  	IDP             IdentityProvider
    46  }
    47  
    48  func mustParseURL(s string) url.URL {
    49  	rv, err := url.Parse(s)
    50  	if err != nil {
    51  		panic(err)
    52  	}
    53  	return *rv
    54  }
    55  
    56  func mustParsePrivateKey(pemStr []byte) crypto.Signer {
    57  	b, _ := pem.Decode(pemStr)
    58  	if b == nil {
    59  		panic("cannot parse PEM")
    60  	}
    61  	k, err := x509.ParsePKCS1PrivateKey(b.Bytes)
    62  	if err != nil {
    63  		panic(err)
    64  	}
    65  	return k
    66  }
    67  
    68  func mustParseCertificate(pemStr []byte) *x509.Certificate {
    69  	b, _ := pem.Decode(pemStr)
    70  	if b == nil {
    71  		panic("cannot parse PEM")
    72  	}
    73  	cert, err := x509.ParseCertificate(b.Bytes)
    74  	if err != nil {
    75  		panic(err)
    76  	}
    77  	return cert
    78  }
    79  
    80  // idpTestOpts are options that can be applied to the identity provider.
    81  type idpTestOpts struct {
    82  	apply func(*testing.T, *IdentityProviderTest)
    83  }
    84  
    85  // applyKey will set the private key for the identity provider.
    86  var applyKey = idpTestOpts{
    87  	apply: func(t *testing.T, test *IdentityProviderTest) {
    88  		test.Key = mustParsePrivateKey(golden.Get(t, "idp_key.pem"))
    89  		(&test.IDP).Key = test.Key
    90  	},
    91  }
    92  
    93  // applySigner will set the signer for the identity provider.
    94  var applySigner = idpTestOpts{
    95  	apply: func(t *testing.T, test *IdentityProviderTest) {
    96  		test.Signer = mustParsePrivateKey(golden.Get(t, "idp_key.pem"))
    97  		(&test.IDP).Signer = test.Signer
    98  	},
    99  }
   100  
   101  func NewIdentityProviderTest(t *testing.T, opts ...idpTestOpts) *IdentityProviderTest {
   102  	test := IdentityProviderTest{}
   103  	TimeNow = func() time.Time {
   104  		rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Dec 1 01:57:09 UTC 2015")
   105  		return rv
   106  	}
   107  	jwt.TimeFunc = TimeNow
   108  	RandReader = &testRandomReader{}                // TODO(ross): remove this and use the below generator
   109  	xmlenc.RandReader = rand.New(rand.NewSource(0)) //nolint:gosec  // deterministic random numbers for tests
   110  
   111  	test.SPKey = mustParsePrivateKey(golden.Get(t, "sp_key.pem")).(*rsa.PrivateKey)
   112  	test.SPCertificate = mustParseCertificate(golden.Get(t, "sp_cert.pem"))
   113  	test.SP = ServiceProvider{
   114  		Key:         test.SPKey,
   115  		Certificate: test.SPCertificate,
   116  		MetadataURL: mustParseURL("https://sp.example.com/saml2/metadata"),
   117  		AcsURL:      mustParseURL("https://sp.example.com/saml2/acs"),
   118  		IDPMetadata: &EntityDescriptor{},
   119  	}
   120  
   121  	test.Certificate = mustParseCertificate(golden.Get(t, "idp_cert.pem"))
   122  
   123  	test.IDP = IdentityProvider{
   124  		Certificate: test.Certificate,
   125  		Logger:      logger.DefaultLogger,
   126  		MetadataURL: mustParseURL("https://idp.example.com/saml/metadata"),
   127  		SSOURL:      mustParseURL("https://idp.example.com/saml/sso"),
   128  		ServiceProviderProvider: &mockServiceProviderProvider{
   129  			GetServiceProviderFunc: func(r *http.Request, serviceProviderID string) (*EntityDescriptor, error) {
   130  				if serviceProviderID == test.SP.MetadataURL.String() {
   131  					return test.SP.Metadata(), nil
   132  				}
   133  				return nil, os.ErrNotExist
   134  			},
   135  		},
   136  		SessionProvider: &mockSessionProvider{
   137  			GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
   138  				return nil
   139  			},
   140  		},
   141  	}
   142  
   143  	// apply the test options
   144  	for _, opt := range opts {
   145  		opt.apply(t, &test)
   146  	}
   147  
   148  	// bind the service provider and the IDP
   149  	test.SP.IDPMetadata = test.IDP.Metadata()
   150  	return &test
   151  }
   152  
   153  type mockSessionProvider struct {
   154  	GetSessionFunc func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session
   155  }
   156  
   157  func (msp *mockSessionProvider) GetSession(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
   158  	return msp.GetSessionFunc(w, r, req)
   159  }
   160  
   161  type mockServiceProviderProvider struct {
   162  	GetServiceProviderFunc func(r *http.Request, serviceProviderID string) (*EntityDescriptor, error)
   163  }
   164  
   165  func (mspp *mockServiceProviderProvider) GetServiceProvider(r *http.Request, serviceProviderID string) (*EntityDescriptor, error) {
   166  	return mspp.GetServiceProviderFunc(r, serviceProviderID)
   167  }
   168  
   169  func TestIDPCanProduceMetadata(t *testing.T) {
   170  	test := NewIdentityProviderTest(t, applyKey)
   171  	expected := &EntityDescriptor{
   172  		ValidUntil:    TimeNow().Add(DefaultValidDuration),
   173  		CacheDuration: DefaultValidDuration,
   174  		EntityID:      "https://idp.example.com/saml/metadata",
   175  		IDPSSODescriptors: []IDPSSODescriptor{
   176  			{
   177  				SSODescriptor: SSODescriptor{
   178  					RoleDescriptor: RoleDescriptor{
   179  						ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
   180  						KeyDescriptors: []KeyDescriptor{
   181  							{
   182  								Use: "signing",
   183  								KeyInfo: KeyInfo{
   184  									XMLName: xml.Name{},
   185  									X509Data: X509Data{
   186  										X509Certificates: []X509Certificate{
   187  											{Data: "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ=="},
   188  										},
   189  									},
   190  								},
   191  								EncryptionMethods: nil,
   192  							},
   193  							{
   194  								Use: "encryption",
   195  								KeyInfo: KeyInfo{
   196  									XMLName: xml.Name{},
   197  									X509Data: X509Data{
   198  										X509Certificates: []X509Certificate{
   199  											{Data: "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ=="},
   200  										},
   201  									},
   202  								},
   203  								EncryptionMethods: []EncryptionMethod{
   204  									{Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"},
   205  									{Algorithm: "http://www.w3.org/2001/04/xmlenc#aes192-cbc"},
   206  									{Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"},
   207  									{Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"},
   208  								},
   209  							},
   210  						},
   211  					},
   212  					NameIDFormats: []NameIDFormat{NameIDFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:transient")},
   213  				},
   214  				SingleSignOnServices: []Endpoint{
   215  					{
   216  						Binding:  "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
   217  						Location: "https://idp.example.com/saml/sso",
   218  					},
   219  					{
   220  						Binding:  "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
   221  						Location: "https://idp.example.com/saml/sso",
   222  					},
   223  				},
   224  			},
   225  		},
   226  	}
   227  	assert.Check(t, is.DeepEqual(expected, test.IDP.Metadata()))
   228  }
   229  
   230  func TestIDPHTTPCanHandleMetadataRequest(t *testing.T) {
   231  	test := NewIdentityProviderTest(t, applyKey)
   232  	w := httptest.NewRecorder()
   233  	r, _ := http.NewRequest("GET", "https://idp.example.com/saml/metadata", nil)
   234  	test.IDP.Handler().ServeHTTP(w, r)
   235  	assert.Check(t, is.Equal(http.StatusOK, w.Code))
   236  	assert.Check(t, is.Equal("application/samlmetadata+xml", w.Header().Get("Content-type")))
   237  	assert.Check(t, strings.HasPrefix(w.Body.String(), "<EntityDescriptor"),
   238  		w.Body.String())
   239  }
   240  
   241  func TestIDPCanHandleRequestWithNewSession(t *testing.T) {
   242  	test := NewIdentityProviderTest(t, applyKey)
   243  	test.IDP.SessionProvider = &mockSessionProvider{
   244  		GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
   245  			fmt.Fprintf(w, "RelayState: %s\nSAMLRequest: %s",
   246  				req.RelayState, req.RequestBuffer)
   247  			return nil
   248  		},
   249  	}
   250  
   251  	w := httptest.NewRecorder()
   252  
   253  	requestURL, err := test.SP.MakeRedirectAuthenticationRequest("ThisIsTheRelayState")
   254  	assert.Check(t, err)
   255  
   256  	decodedRequest, err := testsaml.ParseRedirectRequest(requestURL)
   257  	assert.Check(t, err)
   258  	golden.Assert(t, string(decodedRequest), "idp_authn_request.xml")
   259  	assert.Check(t, is.Equal("ThisIsTheRelayState", requestURL.Query().Get("RelayState")))
   260  
   261  	r, _ := http.NewRequest("GET", requestURL.String(), nil)
   262  	test.IDP.ServeSSO(w, r)
   263  	assert.Check(t, is.Equal(200, w.Code))
   264  	golden.Assert(t, w.Body.String(), t.Name()+"_http_response_body")
   265  }
   266  
   267  func TestIDPCanHandleRequestWithExistingSession(t *testing.T) {
   268  	test := NewIdentityProviderTest(t, applyKey)
   269  	test.IDP.SessionProvider = &mockSessionProvider{
   270  		GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
   271  			return &Session{
   272  				ID:       "f00df00df00d",
   273  				UserName: "alice",
   274  			}
   275  		},
   276  	}
   277  
   278  	w := httptest.NewRecorder()
   279  	requestURL, err := test.SP.MakeRedirectAuthenticationRequest("ThisIsTheRelayState")
   280  	assert.Check(t, err)
   281  
   282  	decodedRequest, err := testsaml.ParseRedirectRequest(requestURL)
   283  	assert.Check(t, err)
   284  	golden.Assert(t, string(decodedRequest), t.Name()+"_decodedRequest")
   285  
   286  	r, _ := http.NewRequest("GET", requestURL.String(), nil)
   287  	test.IDP.ServeSSO(w, r)
   288  	assert.Check(t, is.Equal(200, w.Code))
   289  	golden.Assert(t, w.Body.String(), t.Name()+"_http_response_body")
   290  }
   291  
   292  func TestIDPCanHandlePostRequestWithExistingSession(t *testing.T) {
   293  	test := NewIdentityProviderTest(t, applyKey)
   294  	test.IDP.SessionProvider = &mockSessionProvider{
   295  		GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
   296  			return &Session{
   297  				ID:       "f00df00df00d",
   298  				UserName: "alice",
   299  			}
   300  		},
   301  	}
   302  
   303  	w := httptest.NewRecorder()
   304  
   305  	authRequest, err := test.SP.MakeAuthenticationRequest(test.SP.GetSSOBindingLocation(HTTPRedirectBinding), HTTPRedirectBinding, HTTPPostBinding)
   306  	assert.Check(t, err)
   307  	authRequestBuf, err := xml.Marshal(authRequest)
   308  	assert.Check(t, err)
   309  	q := url.Values{}
   310  	q.Set("SAMLRequest", base64.StdEncoding.EncodeToString(authRequestBuf))
   311  	q.Set("RelayState", "ThisIsTheRelayState")
   312  
   313  	r, _ := http.NewRequest("POST", "https://idp.example.com/saml/sso", strings.NewReader(q.Encode()))
   314  	r.Header.Set("Content-type", "application/x-www-form-urlencoded")
   315  
   316  	test.IDP.ServeSSO(w, r)
   317  	assert.Check(t, is.Equal(200, w.Code))
   318  	golden.Assert(t, w.Body.String(), t.Name()+"_http_response_body")
   319  }
   320  
   321  func TestIDPRejectsInvalidRequest(t *testing.T) {
   322  	test := NewIdentityProviderTest(t, applyKey)
   323  	test.IDP.SessionProvider = &mockSessionProvider{
   324  		GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
   325  			panic("not reached")
   326  		},
   327  	}
   328  
   329  	w := httptest.NewRecorder()
   330  	r, _ := http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&SAMLRequest=XXX", nil)
   331  	test.IDP.ServeSSO(w, r)
   332  	assert.Check(t, is.Equal(http.StatusBadRequest, w.Code))
   333  
   334  	w = httptest.NewRecorder()
   335  	r, _ = http.NewRequest("POST", "https://idp.example.com/saml/sso",
   336  		strings.NewReader("RelayState=ThisIsTheRelayState&SAMLRequest=XXX"))
   337  	r.Header.Set("Content-type", "application/x-www-form-urlencoded")
   338  	test.IDP.ServeSSO(w, r)
   339  	assert.Check(t, is.Equal(http.StatusBadRequest, w.Code))
   340  }
   341  
   342  func TestIDPCanParse(t *testing.T) {
   343  	test := NewIdentityProviderTest(t, applyKey)
   344  	r, _ := http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&SAMLRequest=lJJBayoxFIX%2FypC9JhnU5wszAz7lgWCLaNtFd5fMbQ1MkmnunVb%2FfUfbUqEgdhs%2BTr5zkmLW8S5s8KVD4mzvm0Cl6FIwEciRCeCRDFuznd2sTD5Upk2Ro42NyGZEmNjFMI%2BBOo9pi%2BnVWbzfrEqxY27JSEntEPfg2waHNnpJ4JtcgiWRLfoLXYBjwDfu6p%2B8JIoiWy5K4eqBUipXIzVRUwXKKtRK53qkJ3qqQVuNPUjU4TIQQ%2BBS5EqPBzofKH2ntBn%2FMervo8jWnyX%2BuVC78FwKkT1gopNKX1JUxSklXTMIfM0gsv8xeeDL%2BPGk7%2FF0Qg0GdnwQ1cW5PDLUwFDID6uquO1Dlot1bJw9%2FPLRmia%2BzRMCYyk4dSiq6205QSDXOxfy3KAq5Pkvqt4DAAD%2F%2Fw%3D%3D", nil)
   345  	req, err := NewIdpAuthnRequest(&test.IDP, r)
   346  	assert.Check(t, err)
   347  	assert.Check(t, req.Validate())
   348  
   349  	r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState", nil)
   350  	_, err = NewIdpAuthnRequest(&test.IDP, r)
   351  	assert.Check(t, is.Error(err, "cannot decompress request: unexpected EOF"))
   352  
   353  	r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&SAMLRequest=NotValidBase64", nil)
   354  	_, err = NewIdpAuthnRequest(&test.IDP, r)
   355  	assert.Check(t, is.Error(err, "cannot decode request: illegal base64 data at input byte 12"))
   356  
   357  	r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&SAMLRequest=bm90IGZsYXRlIGVuY29kZWQ%3D", nil)
   358  	_, err = NewIdpAuthnRequest(&test.IDP, r)
   359  	assert.Check(t, is.Error(err, "cannot decompress request: flate: corrupt input before offset 1"))
   360  
   361  	r, _ = http.NewRequest("FROBNICATE", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&SAMLRequest=lJJBayoxFIX%2FypC9JhnU5wszAz7lgWCLaNtFd5fMbQ1MkmnunVb%2FfUfbUqEgdhs%2BTr5zkmLW8S5s8KVD4mzvm0Cl6FIwEciRCeCRDFuznd2sTD5Upk2Ro42NyGZEmNjFMI%2BBOo9pi%2BnVWbzfrEqxY27JSEntEPfg2waHNnpJ4JtcgiWRLfoLXYBjwDfu6p%2B8JIoiWy5K4eqBUipXIzVRUwXKKtRK53qkJ3qqQVuNPUjU4TIQQ%2BBS5EqPBzofKH2ntBn%2FMervo8jWnyX%2BuVC78FwKkT1gopNKX1JUxSklXTMIfM0gsv8xeeDL%2BPGk7%2FF0Qg0GdnwQ1cW5PDLUwFDID6uquO1Dlot1bJw9%2FPLRmia%2BzRMCYyk4dSiq6205QSDXOxfy3KAq5Pkvqt4DAAD%2F%2Fw%3D%3D", nil)
   362  	_, err = NewIdpAuthnRequest(&test.IDP, r)
   363  	assert.Check(t, is.Error(err, "method not allowed"))
   364  }
   365  
   366  func TestIDPCanValidate(t *testing.T) {
   367  	test := NewIdentityProviderTest(t, applyKey)
   368  	req := IdpAuthnRequest{
   369  		Now: TimeNow(),
   370  		IDP: &test.IDP,
   371  		RequestBuffer: []byte("" +
   372  			"<AuthnRequest xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   373  			"  AssertionConsumerServiceURL=\"https://sp.example.com/saml2/acs\" " +
   374  			"  Destination=\"https://idp.example.com/saml/sso\" " +
   375  			"  ID=\"id-00020406080a0c0e10121416181a1c1e\" " +
   376  			"  IssueInstant=\"2015-12-01T01:57:09Z\" ProtocolBinding=\"\" " +
   377  			"  Version=\"2.0\">" +
   378  			"  <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
   379  			"    Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://sp.example.com/saml2/metadata</Issuer>" +
   380  			"  <NameIDPolicy xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   381  			"    AllowCreate=\"true\">urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDPolicy>" +
   382  			"</AuthnRequest>"),
   383  	}
   384  	assert.Check(t, req.Validate())
   385  	assert.Check(t, req.ServiceProviderMetadata != nil)
   386  	assert.Check(t, is.DeepEqual(&IndexedEndpoint{
   387  		Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", Location: "https://sp.example.com/saml2/acs",
   388  		Index: 1,
   389  	}, req.ACSEndpoint))
   390  
   391  	req = IdpAuthnRequest{
   392  		Now:           TimeNow(),
   393  		IDP:           &test.IDP,
   394  		RequestBuffer: []byte("<AuthnRequest"),
   395  	}
   396  	assert.Check(t, is.Error(req.Validate(), "XML syntax error on line 1: unexpected EOF"))
   397  
   398  	req = IdpAuthnRequest{
   399  		Now: TimeNow(),
   400  		IDP: &test.IDP,
   401  		RequestBuffer: []byte("" +
   402  			"<AuthnRequest xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   403  			"  AssertionConsumerServiceURL=\"https://sp.example.com/saml2/acs\" " +
   404  			"  Destination=\"https://idp.wrongDestination.com/saml/sso\" " +
   405  			"  ID=\"id-00020406080a0c0e10121416181a1c1e\" " +
   406  			"  IssueInstant=\"2015-12-01T01:57:09Z\" ProtocolBinding=\"\" " +
   407  			"  Version=\"2.0\">" +
   408  			"  <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
   409  			"    Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://sp.example.com/saml2/metadata</Issuer>" +
   410  			"  <NameIDPolicy xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   411  			"    AllowCreate=\"true\">urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDPolicy>" +
   412  			"</AuthnRequest>"),
   413  	}
   414  	assert.Check(t, is.Error(req.Validate(), "expected destination to be \"https://idp.example.com/saml/sso\", not \"https://idp.wrongDestination.com/saml/sso\""))
   415  
   416  	req = IdpAuthnRequest{
   417  		Now: TimeNow(),
   418  		IDP: &test.IDP,
   419  		RequestBuffer: []byte("" +
   420  			"<AuthnRequest xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   421  			"  AssertionConsumerServiceURL=\"https://sp.example.com/saml2/acs\" " +
   422  			"  Destination=\"https://idp.example.com/saml/sso\" " +
   423  			"  ID=\"id-00020406080a0c0e10121416181a1c1e\" " +
   424  			"  IssueInstant=\"2014-12-01T01:57:09Z\" ProtocolBinding=\"\" " +
   425  			"  Version=\"2.0\">" +
   426  			"  <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
   427  			"    Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://sp.example.com/saml2/metadata</Issuer>" +
   428  			"  <NameIDPolicy xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   429  			"    AllowCreate=\"true\">urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDPolicy>" +
   430  			"</AuthnRequest>"),
   431  	}
   432  	assert.Check(t, is.Error(req.Validate(), "request expired at 2014-12-01 01:58:39 +0000 UTC"))
   433  
   434  	req = IdpAuthnRequest{
   435  		Now: TimeNow(),
   436  		IDP: &test.IDP,
   437  		RequestBuffer: []byte("" +
   438  			"<AuthnRequest xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   439  			"  AssertionConsumerServiceURL=\"https://sp.example.com/saml2/acs\" " +
   440  			"  Destination=\"https://idp.example.com/saml/sso\" " +
   441  			"  ID=\"id-00020406080a0c0e10121416181a1c1e\" " +
   442  			"  IssueInstant=\"2015-12-01T01:57:09Z\" ProtocolBinding=\"\" " +
   443  			"  Version=\"4.2\">" +
   444  			"  <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
   445  			"    Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://sp.example.com/saml2/metadata</Issuer>" +
   446  			"  <NameIDPolicy xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   447  			"    AllowCreate=\"true\">urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDPolicy>" +
   448  			"</AuthnRequest>"),
   449  	}
   450  	assert.Check(t, is.Error(req.Validate(), "expected SAML request version 2.0 got 4.2"))
   451  
   452  	req = IdpAuthnRequest{
   453  		Now: TimeNow(),
   454  		IDP: &test.IDP,
   455  		RequestBuffer: []byte("" +
   456  			"<AuthnRequest xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   457  			"  AssertionConsumerServiceURL=\"https://sp.example.com/saml2/acs\" " +
   458  			"  Destination=\"https://idp.example.com/saml/sso\" " +
   459  			"  ID=\"id-00020406080a0c0e10121416181a1c1e\" " +
   460  			"  IssueInstant=\"2015-12-01T01:57:09Z\" ProtocolBinding=\"\" " +
   461  			"  Version=\"2.0\">" +
   462  			"  <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
   463  			"    Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://unknownSP.example.com/saml2/metadata</Issuer>" +
   464  			"  <NameIDPolicy xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   465  			"    AllowCreate=\"true\">urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDPolicy>" +
   466  			"</AuthnRequest>"),
   467  	}
   468  	assert.Check(t, is.Error(req.Validate(), "cannot handle request from unknown service provider https://unknownSP.example.com/saml2/metadata"))
   469  
   470  	req = IdpAuthnRequest{
   471  		Now: TimeNow(),
   472  		IDP: &test.IDP,
   473  		RequestBuffer: []byte("" +
   474  			"<AuthnRequest xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   475  			"  AssertionConsumerServiceURL=\"https://unknown.example.com/saml2/acs\" " +
   476  			"  Destination=\"https://idp.example.com/saml/sso\" " +
   477  			"  ID=\"id-00020406080a0c0e10121416181a1c1e\" " +
   478  			"  IssueInstant=\"2015-12-01T01:57:09Z\" ProtocolBinding=\"\" " +
   479  			"  Version=\"2.0\">" +
   480  			"  <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
   481  			"    Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://sp.example.com/saml2/metadata</Issuer>" +
   482  			"  <NameIDPolicy xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   483  			"    AllowCreate=\"true\">urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDPolicy>" +
   484  			"</AuthnRequest>"),
   485  	}
   486  	assert.Check(t, is.Error(req.Validate(), "cannot find assertion consumer service: file does not exist"))
   487  
   488  }
   489  
   490  func TestIDPMakeAssertion(t *testing.T) {
   491  	test := NewIdentityProviderTest(t, applyKey)
   492  	req := IdpAuthnRequest{
   493  		Now: TimeNow(),
   494  		IDP: &test.IDP,
   495  		RequestBuffer: []byte("" +
   496  			"<AuthnRequest xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   497  			"  AssertionConsumerServiceURL=\"https://sp.example.com/saml2/acs\" " +
   498  			"  Destination=\"https://idp.example.com/saml/sso\" " +
   499  			"  ID=\"id-00020406080a0c0e10121416181a1c1e\" " +
   500  			"  IssueInstant=\"2015-12-01T01:57:09Z\" ProtocolBinding=\"\" " +
   501  			"  Version=\"2.0\">" +
   502  			"  <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
   503  			"    Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://sp.example.com/saml2/metadata</Issuer>" +
   504  			"  <NameIDPolicy xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   505  			"    AllowCreate=\"true\">urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDPolicy>" +
   506  			"</AuthnRequest>"),
   507  	}
   508  	req.HTTPRequest, _ = http.NewRequest("POST", "http://idp.example.com/saml/sso", nil)
   509  	assert.Check(t, req.Validate())
   510  
   511  	err := DefaultAssertionMaker{}.MakeAssertion(&req, &Session{
   512  		ID:       "f00df00df00d",
   513  		UserName: "alice",
   514  	})
   515  	assert.Check(t, err)
   516  
   517  	expected := &Assertion{
   518  		ID:           "id-00020406080a0c0e10121416181a1c1e20222426",
   519  		IssueInstant: TimeNow(),
   520  		Version:      "2.0",
   521  		Issuer: Issuer{
   522  			Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
   523  			Value:  "https://idp.example.com/saml/metadata",
   524  		},
   525  		Signature: nil,
   526  		Subject: &Subject{
   527  			NameID: &NameID{Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", NameQualifier: "https://idp.example.com/saml/metadata", SPNameQualifier: "https://sp.example.com/saml2/metadata", Value: ""},
   528  			SubjectConfirmations: []SubjectConfirmation{
   529  				{
   530  					Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
   531  					SubjectConfirmationData: &SubjectConfirmationData{
   532  						Address:      "",
   533  						InResponseTo: "id-00020406080a0c0e10121416181a1c1e",
   534  						NotOnOrAfter: TimeNow().Add(MaxIssueDelay),
   535  						Recipient:    "https://sp.example.com/saml2/acs",
   536  					},
   537  				},
   538  			},
   539  		},
   540  		Conditions: &Conditions{
   541  			NotBefore:    TimeNow(),
   542  			NotOnOrAfter: TimeNow().Add(MaxIssueDelay),
   543  			AudienceRestrictions: []AudienceRestriction{
   544  				{
   545  					Audience: Audience{Value: "https://sp.example.com/saml2/metadata"},
   546  				},
   547  			},
   548  		},
   549  		AuthnStatements: []AuthnStatement{
   550  			{
   551  				AuthnInstant:    time.Time{},
   552  				SessionIndex:    "",
   553  				SubjectLocality: &SubjectLocality{},
   554  				AuthnContext: AuthnContext{
   555  					AuthnContextClassRef: &AuthnContextClassRef{Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"},
   556  				},
   557  			},
   558  		},
   559  		AttributeStatements: []AttributeStatement{
   560  			{
   561  				Attributes: []Attribute{
   562  					{
   563  						FriendlyName: "uid",
   564  						Name:         "urn:oid:0.9.2342.19200300.100.1.1",
   565  						NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
   566  						Values: []AttributeValue{
   567  							{
   568  								Type:  "xs:string",
   569  								Value: "alice",
   570  							},
   571  						},
   572  					},
   573  				},
   574  			},
   575  		},
   576  	}
   577  	assert.Check(t, is.DeepEqual(expected, req.Assertion))
   578  
   579  	err = DefaultAssertionMaker{}.MakeAssertion(&req, &Session{
   580  		ID:             "f00df00df00d",
   581  		CreateTime:     TimeNow(),
   582  		ExpireTime:     TimeNow().Add(time.Hour),
   583  		Index:          "9999",
   584  		NameID:         "ba5eba11",
   585  		Groups:         []string{"Users", "Administrators", "♀"},
   586  		UserName:       "alice",
   587  		UserEmail:      "alice@example.com",
   588  		UserCommonName: "Alice Smith",
   589  		UserSurname:    "Smith",
   590  		UserGivenName:  "Alice",
   591  	})
   592  	assert.Check(t, err)
   593  
   594  	expectedAttributes :=
   595  		[]Attribute{
   596  			{
   597  				FriendlyName: "uid",
   598  				Name:         "urn:oid:0.9.2342.19200300.100.1.1",
   599  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
   600  				Values: []AttributeValue{
   601  					{
   602  						Type:  "xs:string",
   603  						Value: "alice",
   604  					},
   605  				},
   606  			},
   607  			{
   608  				FriendlyName: "eduPersonPrincipalName",
   609  				Name:         "urn:oid:1.3.6.1.4.1.5923.1.1.1.6",
   610  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
   611  				Values: []AttributeValue{
   612  					{
   613  						Type:  "xs:string",
   614  						Value: "alice@example.com",
   615  					},
   616  				},
   617  			},
   618  			{
   619  				FriendlyName: "sn",
   620  				Name:         "urn:oid:2.5.4.4",
   621  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
   622  				Values: []AttributeValue{
   623  					{
   624  						Type:  "xs:string",
   625  						Value: "Smith",
   626  					},
   627  				},
   628  			},
   629  			{
   630  				FriendlyName: "givenName",
   631  				Name:         "urn:oid:2.5.4.42",
   632  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
   633  				Values: []AttributeValue{
   634  					{
   635  						Type:  "xs:string",
   636  						Value: "Alice",
   637  					},
   638  				},
   639  			},
   640  			{
   641  				FriendlyName: "cn",
   642  				Name:         "urn:oid:2.5.4.3",
   643  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
   644  				Values: []AttributeValue{
   645  					{
   646  						Type:  "xs:string",
   647  						Value: "Alice Smith",
   648  					},
   649  				},
   650  			},
   651  			{
   652  				FriendlyName: "eduPersonAffiliation",
   653  				Name:         "urn:oid:1.3.6.1.4.1.5923.1.1.1.1",
   654  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
   655  				Values: []AttributeValue{
   656  					{
   657  						Type:  "xs:string",
   658  						Value: "Users",
   659  					},
   660  					{
   661  						Type:  "xs:string",
   662  						Value: "Administrators",
   663  					},
   664  					{
   665  						Type:  "xs:string",
   666  						Value: "♀",
   667  					},
   668  				},
   669  			},
   670  		}
   671  	assert.Check(t, is.DeepEqual(expectedAttributes, req.Assertion.AttributeStatements[0].Attributes))
   672  }
   673  
   674  func TestIDPMarshalAssertion(t *testing.T) {
   675  	test := NewIdentityProviderTest(t, applyKey)
   676  	req := IdpAuthnRequest{
   677  		Now: TimeNow(),
   678  		IDP: &test.IDP,
   679  		RequestBuffer: []byte("" +
   680  			"<AuthnRequest xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   681  			"  AssertionConsumerServiceURL=\"https://sp.example.com/saml2/acs\" " +
   682  			"  Destination=\"https://idp.example.com/saml/sso\" " +
   683  			"  ID=\"id-00020406080a0c0e10121416181a1c1e\" " +
   684  			"  IssueInstant=\"2015-12-01T01:57:09Z\" ProtocolBinding=\"\" " +
   685  			"  Version=\"2.0\">" +
   686  			"  <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
   687  			"    Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://sp.example.com/saml2/metadata</Issuer>" +
   688  			"  <NameIDPolicy xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
   689  			"    AllowCreate=\"true\">urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDPolicy>" +
   690  			"</AuthnRequest>"),
   691  	}
   692  	req.HTTPRequest, _ = http.NewRequest("POST", "http://idp.example.com/saml/sso", nil)
   693  	err := req.Validate()
   694  	assert.Check(t, err)
   695  	err = DefaultAssertionMaker{}.MakeAssertion(&req, &Session{
   696  		ID:       "f00df00df00d",
   697  		UserName: "alice",
   698  	})
   699  	assert.Check(t, err)
   700  	err = req.MakeAssertionEl()
   701  	assert.Check(t, err)
   702  
   703  	// Compare the plaintext first
   704  	expectedPlaintext := "<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id-00020406080a0c0e10121416181a1c1e20222426\" IssueInstant=\"2015-12-01T01:57:09Z\" Version=\"2.0\"><saml:Issuer Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://idp.example.com/saml/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>gjE0eLUMVt+kK0rIGYvnzHV/2Ok=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>Jm1rrxo2x7SYTnaS97bCdnVLQGeQuCMTjiSUvwzBkWFR+xcPr+n38dXmv0q0R68tO7L2ELhLtBdLm/dWsxruN23TMGVQyHIPMgJExdnYb7fwqx6es/NAdbDUBTbSdMX0vhIlTsHu5F0bJ0Tg0iAo9uRk9VeBdkaxtPa7+4yl1PQ=</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><saml:Subject><saml:NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:transient\" NameQualifier=\"https://idp.example.com/saml/metadata\" SPNameQualifier=\"https://sp.example.com/saml2/metadata\"/><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"id-00020406080a0c0e10121416181a1c1e\" NotOnOrAfter=\"2015-12-01T01:58:39Z\" Recipient=\"https://sp.example.com/saml2/acs\"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-12-01T01:57:09Z\" NotOnOrAfter=\"2015-12-01T01:58:39Z\"><saml:AudienceRestriction><saml:Audience>https://sp.example.com/saml2/metadata</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"0001-01-01T00:00:00Z\"><saml:SubjectLocality/><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute FriendlyName=\"uid\" Name=\"urn:oid:0.9.2342.19200300.100.1.1\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xs:string\">alice</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>"
   705  	actualPlaintext := ""
   706  	{
   707  		doc := etree.NewDocument()
   708  		doc.SetRoot(req.AssertionEl)
   709  		el := doc.FindElement("//EncryptedAssertion/EncryptedData")
   710  		actualPlaintextBuf, err := xmlenc.Decrypt(test.SPKey, el)
   711  		assert.Check(t, err)
   712  		actualPlaintext = string(actualPlaintextBuf)
   713  	}
   714  	assert.Check(t, is.Equal(expectedPlaintext, actualPlaintext))
   715  
   716  	doc := etree.NewDocument()
   717  	doc.SetRoot(req.AssertionEl)
   718  	assertionBuffer, err := doc.WriteToBytes()
   719  	assert.Check(t, err)
   720  	golden.Assert(t, string(assertionBuffer), t.Name()+"_encrypted_assertion")
   721  }
   722  
   723  func TestIDPMakeResponsePrivateKey(t *testing.T) {
   724  	test := NewIdentityProviderTest(t, applyKey)
   725  
   726  	testMakeResponse(t, test)
   727  }
   728  
   729  func TestIDPMakeResponseSigner(t *testing.T) {
   730  	test := NewIdentityProviderTest(t, applySigner)
   731  
   732  	testMakeResponse(t, test)
   733  }
   734  
   735  func testMakeResponse(t *testing.T, test *IdentityProviderTest) {
   736  	req := IdpAuthnRequest{
   737  		Now:           TimeNow(),
   738  		IDP:           &test.IDP,
   739  		RequestBuffer: golden.Get(t, "TestIDPMakeResponse_request_buffer"),
   740  	}
   741  	req.HTTPRequest, _ = http.NewRequest("POST", "http://idp.example.com/saml/sso", nil)
   742  	err := req.Validate()
   743  	assert.Check(t, err)
   744  	err = DefaultAssertionMaker{}.MakeAssertion(&req, &Session{
   745  		ID:       "f00df00df00d",
   746  		UserName: "alice",
   747  	})
   748  	assert.Check(t, err)
   749  	err = req.MakeAssertionEl()
   750  	assert.Check(t, err)
   751  
   752  	req.AssertionEl = etree.NewElement("this-is-an-encrypted-assertion")
   753  	err = req.MakeResponse()
   754  	assert.Check(t, err)
   755  
   756  	certificateStore := &dsig.MemoryX509CertificateStore{
   757  		Roots: []*x509.Certificate{
   758  			req.IDP.Certificate,
   759  		},
   760  	}
   761  	validationCtx := dsig.NewDefaultValidationContext(certificateStore)
   762  	validationCtx.Clock = dsig.NewFakeClockAt(req.IDP.Certificate.NotBefore)
   763  	_, err = validationCtx.Validate(req.ResponseEl)
   764  	assert.Check(t, err)
   765  
   766  	response := Response{}
   767  	err = unmarshalEtreeHack(req.ResponseEl, &response)
   768  	assert.Check(t, err)
   769  
   770  	doc := etree.NewDocument()
   771  	doc.SetRoot(req.ResponseEl)
   772  	doc.Indent(2)
   773  	responseStr, err := doc.WriteToString()
   774  	assert.Check(t, err)
   775  	golden.Assert(t, responseStr, "TestIDPMakeResponse_response.xml")
   776  }
   777  
   778  func TestIDPWriteResponse(t *testing.T) {
   779  	test := NewIdentityProviderTest(t, applyKey)
   780  	req := IdpAuthnRequest{
   781  		Now:           TimeNow(),
   782  		IDP:           &test.IDP,
   783  		RelayState:    "THIS_IS_THE_RELAY_STATE",
   784  		RequestBuffer: golden.Get(t, "TestIDPWriteResponse_RequestBuffer.xml"),
   785  		ResponseEl:    etree.NewElement("THIS_IS_THE_SAML_RESPONSE"),
   786  	}
   787  	req.HTTPRequest, _ = http.NewRequest("POST", "http://idp.example.com/saml/sso", nil)
   788  	err := req.Validate()
   789  	assert.Check(t, err)
   790  
   791  	w := httptest.NewRecorder()
   792  	err = req.WriteResponse(w)
   793  	assert.Check(t, err)
   794  	assert.Check(t, is.Equal(200, w.Code))
   795  	golden.Assert(t, w.Body.String(), t.Name()+"response.html")
   796  }
   797  
   798  func TestIDPIDPInitiatedNewSession(t *testing.T) {
   799  	test := NewIdentityProviderTest(t, applyKey)
   800  	test.IDP.SessionProvider = &mockSessionProvider{
   801  		GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
   802  			fmt.Fprintf(w, "RelayState: %s", req.RelayState)
   803  			return nil
   804  		},
   805  	}
   806  
   807  	w := httptest.NewRecorder()
   808  	r, _ := http.NewRequest("GET", "https://idp.example.com/services/sp/whoami", nil)
   809  	test.IDP.ServeIDPInitiated(w, r, test.SP.MetadataURL.String(), "ThisIsTheRelayState")
   810  	assert.Check(t, is.Equal(200, w.Code))
   811  	assert.Check(t, is.Equal("RelayState: ThisIsTheRelayState", w.Body.String()))
   812  }
   813  
   814  func TestIDPIDPInitiatedExistingSession(t *testing.T) {
   815  	test := NewIdentityProviderTest(t, applyKey)
   816  	test.IDP.SessionProvider = &mockSessionProvider{
   817  		GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
   818  			return &Session{
   819  				ID:       "f00df00df00d",
   820  				UserName: "alice",
   821  			}
   822  		},
   823  	}
   824  
   825  	w := httptest.NewRecorder()
   826  	r, _ := http.NewRequest("GET", "https://idp.example.com/services/sp/whoami", nil)
   827  	test.IDP.ServeIDPInitiated(w, r, test.SP.MetadataURL.String(), "ThisIsTheRelayState")
   828  	assert.Check(t, is.Equal(200, w.Code))
   829  	golden.Assert(t, w.Body.String(), t.Name()+"_response")
   830  }
   831  
   832  func TestIDPIDPInitiatedBadServiceProvider(t *testing.T) {
   833  	test := NewIdentityProviderTest(t, applyKey)
   834  	test.IDP.SessionProvider = &mockSessionProvider{
   835  		GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
   836  			return &Session{
   837  				ID:       "f00df00df00d",
   838  				UserName: "alice",
   839  			}
   840  		},
   841  	}
   842  
   843  	w := httptest.NewRecorder()
   844  	r, _ := http.NewRequest("GET", "https://idp.example.com/services/sp/whoami", nil)
   845  	test.IDP.ServeIDPInitiated(w, r, "https://wrong.url/metadata", "ThisIsTheRelayState")
   846  	assert.Check(t, is.Equal(http.StatusNotFound, w.Code))
   847  }
   848  
   849  func TestIDPCanHandleUnencryptedResponse(t *testing.T) {
   850  	test := NewIdentityProviderTest(t, applyKey)
   851  	test.IDP.SessionProvider = &mockSessionProvider{
   852  		GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
   853  			return &Session{ID: "f00df00df00d", UserName: "alice"}
   854  		},
   855  	}
   856  
   857  	metadata := EntityDescriptor{}
   858  	err := xml.Unmarshal(
   859  		golden.Get(t, "TestIDPCanHandleUnencryptedResponse_idp_metadata.xml"),
   860  		&metadata)
   861  	assert.Check(t, err)
   862  	test.IDP.ServiceProviderProvider = &mockServiceProviderProvider{
   863  		GetServiceProviderFunc: func(r *http.Request, serviceProviderID string) (*EntityDescriptor, error) {
   864  			if serviceProviderID == "https://gitlab.example.com/users/saml/metadata" {
   865  				return &metadata, nil
   866  			}
   867  			return nil, os.ErrNotExist
   868  		},
   869  	}
   870  
   871  	req := IdpAuthnRequest{
   872  		Now:           TimeNow(),
   873  		IDP:           &test.IDP,
   874  		RequestBuffer: golden.Get(t, "TestIDPCanHandleUnencryptedResponse_request"),
   875  	}
   876  	req.HTTPRequest, _ = http.NewRequest("POST", "http://idp.example.com/saml/sso", nil)
   877  	err = req.Validate()
   878  	assert.Check(t, err)
   879  	err = DefaultAssertionMaker{}.MakeAssertion(&req, &Session{
   880  		ID:       "f00df00df00d",
   881  		UserName: "alice",
   882  	})
   883  	assert.Check(t, err)
   884  	err = req.MakeAssertionEl()
   885  	assert.Check(t, err)
   886  
   887  	err = req.MakeResponse()
   888  	assert.Check(t, err)
   889  
   890  	doc := etree.NewDocument()
   891  	doc.SetRoot(req.ResponseEl)
   892  	doc.Indent(2)
   893  	responseStr, _ := doc.WriteToString()
   894  	golden.Assert(t, responseStr, t.Name()+"_response")
   895  }
   896  
   897  func TestIDPRequestedAttributes(t *testing.T) {
   898  	test := NewIdentityProviderTest(t, applyKey)
   899  	metadata := EntityDescriptor{}
   900  	err := xml.Unmarshal(golden.Get(t, "TestIDPRequestedAttributes_idp_metadata.xml"), &metadata)
   901  	assert.Check(t, err)
   902  
   903  	requestURL, err := test.SP.MakeRedirectAuthenticationRequest("ThisIsTheRelayState")
   904  	assert.Check(t, err)
   905  
   906  	r, _ := http.NewRequest("GET", requestURL.String(), nil)
   907  	req, err := NewIdpAuthnRequest(&test.IDP, r)
   908  	req.ServiceProviderMetadata = &metadata
   909  	req.ACSEndpoint = &metadata.SPSSODescriptors[0].AssertionConsumerServices[0]
   910  	req.SPSSODescriptor = &metadata.SPSSODescriptors[0]
   911  	assert.Check(t, err)
   912  	err = DefaultAssertionMaker{}.MakeAssertion(req, &Session{
   913  		ID:             "f00df00df00d",
   914  		UserName:       "alice",
   915  		UserEmail:      "alice@example.com",
   916  		UserGivenName:  "Alice",
   917  		UserSurname:    "Smith",
   918  		UserCommonName: "Alice Smith",
   919  	})
   920  	assert.Check(t, err)
   921  
   922  	expectedAttributes := []AttributeStatement{{
   923  		Attributes: []Attribute{
   924  			{
   925  				FriendlyName: "Email address",
   926  				Name:         "email",
   927  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
   928  				Values: []AttributeValue{
   929  					{
   930  						Type:  "xs:string",
   931  						Value: "alice@example.com",
   932  					},
   933  				},
   934  			},
   935  			{
   936  				FriendlyName: "Full name",
   937  				Name:         "name",
   938  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
   939  				Values: []AttributeValue{
   940  					{
   941  						Type:  "xs:string",
   942  						Value: "Alice Smith",
   943  					},
   944  				},
   945  			},
   946  			{
   947  				FriendlyName: "Given name",
   948  				Name:         "first_name",
   949  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
   950  				Values: []AttributeValue{
   951  					{
   952  						Type:  "xs:string",
   953  						Value: "Alice",
   954  					},
   955  				},
   956  			},
   957  			{
   958  				FriendlyName: "Family name",
   959  				Name:         "last_name",
   960  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
   961  				Values: []AttributeValue{
   962  					{
   963  						Type:  "xs:string",
   964  						Value: "Smith",
   965  					},
   966  				},
   967  			},
   968  			{
   969  				FriendlyName: "uid",
   970  				Name:         "urn:oid:0.9.2342.19200300.100.1.1",
   971  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
   972  				Values: []AttributeValue{
   973  					{
   974  						Type:  "xs:string",
   975  						Value: "alice",
   976  					},
   977  				},
   978  			},
   979  			{
   980  				FriendlyName: "eduPersonPrincipalName",
   981  				Name:         "urn:oid:1.3.6.1.4.1.5923.1.1.1.6",
   982  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
   983  				Values: []AttributeValue{
   984  					{
   985  						Type:  "xs:string",
   986  						Value: "alice@example.com",
   987  					},
   988  				},
   989  			},
   990  			{
   991  				FriendlyName: "sn",
   992  				Name:         "urn:oid:2.5.4.4",
   993  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
   994  				Values: []AttributeValue{
   995  					{
   996  						Type:  "xs:string",
   997  						Value: "Smith",
   998  					},
   999  				},
  1000  			},
  1001  			{
  1002  				FriendlyName: "givenName",
  1003  				Name:         "urn:oid:2.5.4.42",
  1004  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
  1005  				Values: []AttributeValue{
  1006  					{
  1007  						Type:  "xs:string",
  1008  						Value: "Alice",
  1009  					},
  1010  				},
  1011  			},
  1012  			{
  1013  				FriendlyName: "cn",
  1014  				Name:         "urn:oid:2.5.4.3",
  1015  				NameFormat:   "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
  1016  				Values: []AttributeValue{
  1017  					{
  1018  						Type:  "xs:string",
  1019  						Value: "Alice Smith",
  1020  					},
  1021  				},
  1022  			},
  1023  		}}}
  1024  	assert.Check(t, is.DeepEqual(expectedAttributes, req.Assertion.AttributeStatements))
  1025  }
  1026  
  1027  func TestIDPNoDestination(t *testing.T) {
  1028  	test := NewIdentityProviderTest(t, applyKey)
  1029  	test.IDP.SessionProvider = &mockSessionProvider{
  1030  		GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
  1031  			return &Session{ID: "f00df00df00d", UserName: "alice"}
  1032  		},
  1033  	}
  1034  
  1035  	metadata := EntityDescriptor{}
  1036  	err := xml.Unmarshal(golden.Get(t, "TestIDPNoDestination_idp_metadata.xml"), &metadata)
  1037  	assert.Check(t, err)
  1038  	test.IDP.ServiceProviderProvider = &mockServiceProviderProvider{
  1039  		GetServiceProviderFunc: func(r *http.Request, serviceProviderID string) (*EntityDescriptor, error) {
  1040  			if serviceProviderID == "https://gitlab.example.com/users/saml/metadata" {
  1041  				return &metadata, nil
  1042  			}
  1043  			return nil, os.ErrNotExist
  1044  		},
  1045  	}
  1046  
  1047  	req := IdpAuthnRequest{
  1048  		Now:           TimeNow(),
  1049  		IDP:           &test.IDP,
  1050  		RequestBuffer: golden.Get(t, "TestIDPNoDestination_request"),
  1051  	}
  1052  	req.HTTPRequest, _ = http.NewRequest("POST", "http://idp.example.com/saml/sso", nil)
  1053  	err = req.Validate()
  1054  	assert.Check(t, err)
  1055  	err = DefaultAssertionMaker{}.MakeAssertion(&req, &Session{
  1056  		ID:       "f00df00df00d",
  1057  		UserName: "alice",
  1058  	})
  1059  	assert.Check(t, err)
  1060  	err = req.MakeAssertionEl()
  1061  	assert.Check(t, err)
  1062  
  1063  	err = req.MakeResponse()
  1064  	assert.Check(t, err)
  1065  }
  1066  
  1067  func TestIDPRejectDecompressionBomb(t *testing.T) {
  1068  	test := NewIdentityProviderTest(t)
  1069  	test.IDP.SessionProvider = &mockSessionProvider{
  1070  		GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
  1071  			fmt.Fprintf(w, "RelayState: %s\nSAMLRequest: %s",
  1072  				req.RelayState, req.RequestBuffer)
  1073  			return nil
  1074  		},
  1075  	}
  1076  
  1077  	data := bytes.Repeat([]byte("a"), 768*1024*1024)
  1078  	var compressed bytes.Buffer
  1079  	w, _ := flate.NewWriter(&compressed, flate.BestCompression)
  1080  	_, err := w.Write(data)
  1081  	assert.Check(t, err)
  1082  	err = w.Close()
  1083  	assert.Check(t, err)
  1084  	encoded := base64.StdEncoding.EncodeToString(compressed.Bytes())
  1085  
  1086  	r, _ := http.NewRequest("GET", "/dontcare?"+url.Values{
  1087  		"SAMLRequest": {encoded},
  1088  	}.Encode(), nil)
  1089  	_, err = NewIdpAuthnRequest(&test.IDP, r)
  1090  	assert.Error(t, err, "cannot decompress request: flate: uncompress limit exceeded (10485760 bytes)")
  1091  }
  1092  
  1093  func TestIDPHTTPCanHandleSSORequest(t *testing.T) {
  1094  	test := NewIdentityProviderTest(t, applyKey)
  1095  	w := httptest.NewRecorder()
  1096  
  1097  	const validRequest = `lJJBayoxFIX%2FypC9JhnU5wszAz7lgWCLaNtFd5fMbQ1MkmnunVb%2FfUfbUqEgdhs%2BTr5zkmLW8S5s8KVD4mzvm0Cl6FIwEciRCeCRDFuznd2sTD5Upk2Ro42NyGZEmNjFMI%2BBOo9pi%2BnVWbzfrEqxY27JSEntEPfg2waHNnpJ4JtcgiWRLfoLXYBjwDfu6p%2B8JIoiWy5K4eqBUipXIzVRUwXKKtRK53qkJ3qqQVuNPUjU4TIQQ%2BBS5EqPBzofKH2ntBn%2FMervo8jWnyX%2BuVC78FwKkT1gopNKX1JUxSklXTMIfM0gsv8xeeDL%2BPGk7%2FF0Qg0GdnwQ1cW5PDLUwFDID6uquO1Dlot1bJw9%2FPLRmia%2BzRMCYyk4dSiq6205QSDXOxfy3KAq5Pkvqt4DAAD%2F%2Fw%3D%3D`
  1098  
  1099  	r, _ := http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+
  1100  		"SAMLRequest="+validRequest, nil)
  1101  	test.IDP.Handler().ServeHTTP(w, r)
  1102  	assert.Check(t, is.Equal(http.StatusOK, w.Code))
  1103  
  1104  	// rejects requests that are invalid
  1105  	w = httptest.NewRecorder()
  1106  	r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+
  1107  		"SAMLRequest=PEF1dGhuUmVxdWVzdA%3D%3D", nil)
  1108  	test.IDP.Handler().ServeHTTP(w, r)
  1109  	assert.Check(t, is.Equal(http.StatusBadRequest, w.Code))
  1110  
  1111  	// rejects requests that contain malformed XML
  1112  	{
  1113  		a, _ := url.QueryUnescape(validRequest)
  1114  		b, _ := base64.StdEncoding.DecodeString(a)
  1115  		c, _ := io.ReadAll(flate.NewReader(bytes.NewReader(b)))
  1116  		d := bytes.Replace(c, []byte("<AuthnRequest"), []byte("<AuthnRequest ::foo=\"bar\">]]"), 1)
  1117  		f := bytes.Buffer{}
  1118  		e, _ := flate.NewWriter(&f, flate.DefaultCompression)
  1119  		_, err := e.Write(d)
  1120  		assert.Check(t, err)
  1121  		err = e.Close()
  1122  		assert.Check(t, err)
  1123  		g := base64.StdEncoding.EncodeToString(f.Bytes())
  1124  		invalidRequest := url.QueryEscape(g)
  1125  
  1126  		w = httptest.NewRecorder()
  1127  		r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+
  1128  			"SAMLRequest="+invalidRequest, nil)
  1129  		test.IDP.Handler().ServeHTTP(w, r)
  1130  		assert.Check(t, is.Equal(http.StatusBadRequest, w.Code))
  1131  	}
  1132  }