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 }