github.com/jcmturner/gokrb5/v8@v8.4.4/spnego/http_test.go (about) 1 package spnego 2 3 import ( 4 "bytes" 5 "crypto/rand" 6 "encoding/base64" 7 "encoding/hex" 8 "errors" 9 "fmt" 10 "io" 11 "log" 12 "mime/multipart" 13 "net/http" 14 "net/http/cookiejar" 15 "net/http/httptest" 16 "os" 17 "sync" 18 "testing" 19 20 "github.com/gorilla/sessions" 21 "github.com/jcmturner/goidentity/v6" 22 "github.com/jcmturner/gokrb5/v8/client" 23 "github.com/jcmturner/gokrb5/v8/config" 24 "github.com/jcmturner/gokrb5/v8/keytab" 25 "github.com/jcmturner/gokrb5/v8/service" 26 "github.com/jcmturner/gokrb5/v8/test" 27 "github.com/jcmturner/gokrb5/v8/test/testdata" 28 "github.com/stretchr/testify/assert" 29 ) 30 31 func TestClient_SetSPNEGOHeader(t *testing.T) { 32 test.Integration(t) 33 b, _ := hex.DecodeString(testdata.KEYTAB_TESTUSER1_TEST_GOKRB5) 34 kt := keytab.New() 35 kt.Unmarshal(b) 36 c, _ := config.NewFromString(testdata.KRB5_CONF) 37 addr := os.Getenv("TEST_KDC_ADDR") 38 if addr == "" { 39 addr = testdata.KDC_IP_TEST_GOKRB5 40 } 41 c.Realms[0].KDC = []string{addr + ":" + testdata.KDC_PORT_TEST_GOKRB5} 42 l := log.New(os.Stderr, "SPNEGO Client:", log.LstdFlags) 43 cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c, client.Logger(l)) 44 45 err := cl.Login() 46 if err != nil { 47 t.Fatalf("error on AS_REQ: %v\n", err) 48 } 49 urls := []string{ 50 "http://cname.test.gokrb5", 51 "http://host.test.gokrb5", 52 } 53 paths := []string{ 54 "/modkerb/index.html", 55 //"/modgssapi/index.html", 56 } 57 for _, url := range urls { 58 for _, p := range paths { 59 r, _ := http.NewRequest("GET", url+p, nil) 60 httpResp, err := http.DefaultClient.Do(r) 61 if err != nil { 62 t.Fatalf("%s request error: %v", url+p, err) 63 } 64 assert.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Status code in response to client with no SPNEGO not as expected") 65 66 err = SetSPNEGOHeader(cl, r, "") 67 if err != nil { 68 t.Fatalf("error setting client SPNEGO header: %v", err) 69 } 70 71 httpResp, err = http.DefaultClient.Do(r) 72 if err != nil { 73 t.Fatalf("%s request error: %v\n", url+p, err) 74 } 75 assert.Equal(t, http.StatusOK, httpResp.StatusCode, "Status code in response to client SPNEGO request not as expected") 76 } 77 } 78 } 79 80 func TestSPNEGOHTTPClient(t *testing.T) { 81 test.Integration(t) 82 b, _ := hex.DecodeString(testdata.KEYTAB_TESTUSER1_TEST_GOKRB5) 83 kt := keytab.New() 84 kt.Unmarshal(b) 85 c, _ := config.NewFromString(testdata.KRB5_CONF) 86 addr := os.Getenv("TEST_KDC_ADDR") 87 if addr == "" { 88 addr = testdata.KDC_IP_TEST_GOKRB5 89 } 90 c.Realms[0].KDC = []string{addr + ":" + testdata.KDC_PORT_TEST_GOKRB5} 91 l := log.New(os.Stderr, "SPNEGO Client:", log.LstdFlags) 92 cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c, client.Logger(l)) 93 94 err := cl.Login() 95 if err != nil { 96 t.Fatalf("error on AS_REQ: %v\n", err) 97 } 98 urls := []string{ 99 "http://cname.test.gokrb5", 100 "http://host.test.gokrb5", 101 } 102 // This path issues a redirect which the http client will automatically follow. 103 // It should cause a replay issue if the negInit token is sent in the first instance. 104 paths := []string{ 105 "/modgssapi", // This issues a redirect which the http client will automatically follow. Could cause a replay issue 106 "/redirect", 107 } 108 for _, url := range urls { 109 for _, p := range paths { 110 r, _ := http.NewRequest("GET", url+p, nil) 111 httpCl := http.DefaultClient 112 httpCl.CheckRedirect = func(req *http.Request, via []*http.Request) error { 113 t.Logf("http client redirect: %+v", *req) 114 return nil 115 } 116 spnegoCl := NewClient(cl, httpCl, "") 117 httpResp, err := spnegoCl.Do(r) 118 if err != nil { 119 t.Fatalf("%s request error: %v", url+p, err) 120 } 121 assert.Equal(t, http.StatusOK, httpResp.StatusCode, "Status code in response to client SPNEGO request not as expected") 122 } 123 } 124 } 125 126 func TestService_SPNEGOKRB_NoAuthHeader(t *testing.T) { 127 s := httpServer() 128 defer s.Close() 129 r, _ := http.NewRequest("GET", s.URL, nil) 130 httpResp, err := http.DefaultClient.Do(r) 131 if err != nil { 132 t.Fatalf("Request error: %v\n", err) 133 } 134 assert.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Status code in response to client with no SPNEGO not as expected") 135 assert.Equal(t, "Negotiate", httpResp.Header.Get("WWW-Authenticate"), "Negotiation header not set by server.") 136 } 137 138 func TestService_SPNEGOKRB_ValidUser(t *testing.T) { 139 test.Integration(t) 140 141 s := httpServer() 142 defer s.Close() 143 r, _ := http.NewRequest("GET", s.URL, nil) 144 145 cl := getClient() 146 err := SetSPNEGOHeader(cl, r, "HTTP/host.test.gokrb5") 147 if err != nil { 148 t.Fatalf("error setting client's SPNEGO header: %v", err) 149 } 150 151 httpResp, err := http.DefaultClient.Do(r) 152 if err != nil { 153 t.Fatalf("Request error: %v\n", err) 154 } 155 assert.Equal(t, http.StatusOK, httpResp.StatusCode, "Status code in response to client SPNEGO request not as expected") 156 } 157 158 func TestService_SPNEGOKRB_ValidUser_RawKRB5Token(t *testing.T) { 159 test.Integration(t) 160 161 s := httpServer() 162 defer s.Close() 163 r, _ := http.NewRequest("GET", s.URL, nil) 164 165 cl := getClient() 166 sc := SPNEGOClient(cl, "HTTP/host.test.gokrb5") 167 err := sc.AcquireCred() 168 if err != nil { 169 t.Fatalf("could not acquire client credential: %v", err) 170 } 171 st, err := sc.InitSecContext() 172 if err != nil { 173 t.Fatalf("could not initialize context: %v", err) 174 } 175 176 // Use the raw KRB5 context token 177 nb := st.(*SPNEGOToken).NegTokenInit.MechTokenBytes 178 hs := "Negotiate " + base64.StdEncoding.EncodeToString(nb) 179 r.Header.Set(HTTPHeaderAuthRequest, hs) 180 181 httpResp, err := http.DefaultClient.Do(r) 182 if err != nil { 183 t.Fatalf("Request error: %v\n", err) 184 } 185 assert.Equal(t, http.StatusOK, httpResp.StatusCode, "Status code in response to client SPNEGO request not as expected") 186 } 187 188 func TestService_SPNEGOKRB_Replay(t *testing.T) { 189 test.Integration(t) 190 191 s := httpServerWithoutSessionManager() 192 defer s.Close() 193 r1, _ := http.NewRequest("GET", s.URL, nil) 194 195 cl := getClient() 196 err := SetSPNEGOHeader(cl, r1, "HTTP/host.test.gokrb5") 197 if err != nil { 198 t.Fatalf("error setting client's SPNEGO header: %v", err) 199 } 200 201 // First request with this ticket should be accepted 202 httpResp, err := http.DefaultClient.Do(r1) 203 if err != nil { 204 t.Fatalf("Request error: %v\n", err) 205 } 206 assert.Equal(t, http.StatusOK, httpResp.StatusCode, "Status code in response to client SPNEGO request not as expected") 207 208 // Use ticket again should be rejected 209 httpResp, err = http.DefaultClient.Do(r1) 210 if err != nil { 211 t.Fatalf("Request error: %v\n", err) 212 } 213 assert.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Status code in response to client with no SPNEGO not as expected. Expected a replay to be detected.") 214 215 // Form a 2nd ticket 216 r2, _ := http.NewRequest("GET", s.URL, nil) 217 218 err = SetSPNEGOHeader(cl, r2, "HTTP/host.test.gokrb5") 219 if err != nil { 220 t.Fatalf("error setting client's SPNEGO header: %v", err) 221 } 222 223 // First use of 2nd ticket should be accepted 224 httpResp, err = http.DefaultClient.Do(r2) 225 if err != nil { 226 t.Fatalf("Request error: %v\n", err) 227 } 228 assert.Equal(t, http.StatusOK, httpResp.StatusCode, "Status code in response to client SPNEGO request not as expected") 229 230 // Using the 1st ticket again should still be rejected 231 httpResp, err = http.DefaultClient.Do(r1) 232 if err != nil { 233 t.Fatalf("Request error: %v\n", err) 234 } 235 assert.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Status code in response to client with no SPNEGO not as expected. Expected a replay to be detected.") 236 237 // Using the 2nd again should be rejected as replay 238 httpResp, err = http.DefaultClient.Do(r2) 239 if err != nil { 240 t.Fatalf("Request error: %v\n", err) 241 } 242 assert.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Status code in response to client with no SPNEGO not as expected. Expected a replay to be detected.") 243 } 244 245 func TestService_SPNEGOKRB_ReplayCache_Concurrency(t *testing.T) { 246 test.Integration(t) 247 248 s := httpServerWithoutSessionManager() 249 defer s.Close() 250 r1, _ := http.NewRequest("GET", s.URL, nil) 251 252 cl := getClient() 253 err := SetSPNEGOHeader(cl, r1, "HTTP/host.test.gokrb5") 254 if err != nil { 255 t.Fatalf("error setting client's SPNEGO header: %v", err) 256 } 257 r1h := r1.Header.Get(HTTPHeaderAuthRequest) 258 259 r2, _ := http.NewRequest("GET", s.URL, nil) 260 261 err = SetSPNEGOHeader(cl, r2, "HTTP/host.test.gokrb5") 262 if err != nil { 263 t.Fatalf("error setting client's SPNEGO header: %v", err) 264 } 265 r2h := r2.Header.Get(HTTPHeaderAuthRequest) 266 267 // Concurrent 1st requests should be OK 268 var wg sync.WaitGroup 269 wg.Add(2) 270 go httpGet(r1, &wg) 271 go httpGet(r2, &wg) 272 wg.Wait() 273 274 // A number of concurrent requests with the same ticket should be rejected due to replay 275 var wg2 sync.WaitGroup 276 noReq := 10 277 wg2.Add(noReq * 2) 278 for i := 0; i < noReq; i++ { 279 rr1, _ := http.NewRequest("GET", s.URL, nil) 280 rr1.Header.Set(HTTPHeaderAuthRequest, r1h) 281 rr2, _ := http.NewRequest("GET", s.URL, nil) 282 rr2.Header.Set(HTTPHeaderAuthRequest, r2h) 283 go httpGet(rr1, &wg2) 284 go httpGet(rr2, &wg2) 285 } 286 wg2.Wait() 287 } 288 289 func TestService_SPNEGOKRB_Upload(t *testing.T) { 290 test.Integration(t) 291 292 s := httpServer() 293 defer s.Close() 294 295 bodyBuf := &bytes.Buffer{} 296 bodyWriter := multipart.NewWriter(bodyBuf) 297 298 fileWriter, err := bodyWriter.CreateFormFile("uploadfile", "testfile.bin") 299 if err != nil { 300 t.Fatalf("error writing to buffer: %v", err) 301 } 302 303 data := make([]byte, 10240) 304 rand.Read(data) 305 br := bytes.NewReader(data) 306 _, err = io.Copy(fileWriter, br) 307 if err != nil { 308 t.Fatalf("error copying bytes: %v", err) 309 } 310 bodyWriter.Close() 311 312 r, _ := http.NewRequest("POST", s.URL, bodyBuf) 313 r.Header.Set("Content-Type", bodyWriter.FormDataContentType()) 314 315 cl := getClient() 316 cookieJar, _ := cookiejar.New(nil) 317 httpCl := http.DefaultClient 318 httpCl.Jar = cookieJar 319 spnegoCl := NewClient(cl, httpCl, "HTTP/host.test.gokrb5") 320 httpResp, err := spnegoCl.Do(r) 321 if err != nil { 322 t.Fatalf("Request error: %v\n", err) 323 } 324 if httpResp.StatusCode != http.StatusOK { 325 bodyBytes, _ := io.ReadAll(httpResp.Body) 326 bodyString := string(bodyBytes) 327 httpResp.Body.Close() 328 t.Errorf("unexpected code from http server (%d): %s", httpResp.StatusCode, bodyString) 329 } 330 } 331 332 func httpGet(r *http.Request, wg *sync.WaitGroup) { 333 defer wg.Done() 334 http.DefaultClient.Do(r) 335 } 336 337 func httpServerWithoutSessionManager() *httptest.Server { 338 l := log.New(os.Stderr, "GOKRB5 Service Tests: ", log.LstdFlags) 339 b, _ := hex.DecodeString(testdata.HTTP_KEYTAB) 340 kt := keytab.New() 341 kt.Unmarshal(b) 342 th := http.HandlerFunc(testAppHandler) 343 s := httptest.NewServer(SPNEGOKRB5Authenticate(th, kt, service.Logger(l))) 344 return s 345 } 346 347 func httpServer() *httptest.Server { 348 l := log.New(os.Stderr, "GOKRB5 Service Tests: ", log.LstdFlags) 349 b, _ := hex.DecodeString(testdata.HTTP_KEYTAB) 350 kt := keytab.New() 351 kt.Unmarshal(b) 352 th := http.HandlerFunc(testAppHandler) 353 s := httptest.NewServer(SPNEGOKRB5Authenticate(th, kt, service.Logger(l), service.SessionManager(NewSessionMgr("gokrb5")))) 354 return s 355 } 356 357 func testAppHandler(w http.ResponseWriter, r *http.Request) { 358 if r.Method == http.MethodPost { 359 maxUploadSize := int64(11240) 360 if err := r.ParseMultipartForm(maxUploadSize); err != nil { 361 http.Error(w, fmt.Sprintf("cannot parse multipart form: %v", err), http.StatusBadRequest) 362 return 363 } 364 r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize) 365 file, _, err := r.FormFile("uploadfile") 366 if err != nil { 367 http.Error(w, "INVALID_FILE", http.StatusBadRequest) 368 return 369 } 370 defer file.Close() 371 372 // write out to /dev/null 373 _, err = io.Copy(io.Discard, file) 374 if err != nil { 375 http.Error(w, "WRITE_ERR", http.StatusInternalServerError) 376 return 377 } 378 } 379 w.WriteHeader(http.StatusOK) 380 id := goidentity.FromHTTPRequestContext(r) 381 fmt.Fprintf(w, "<html>\nTEST.GOKRB5 Handler\nAuthenticed user: %s\nUser's realm: %s\n</html>", 382 id.UserName(), 383 id.Domain()) 384 return 385 } 386 387 func getClient() *client.Client { 388 b, _ := hex.DecodeString(testdata.KEYTAB_TESTUSER1_TEST_GOKRB5) 389 kt := keytab.New() 390 kt.Unmarshal(b) 391 c, _ := config.NewFromString(testdata.KRB5_CONF) 392 c.LibDefaults.NoAddresses = true 393 addr := os.Getenv("TEST_KDC_ADDR") 394 if addr == "" { 395 addr = testdata.KDC_IP_TEST_GOKRB5 396 } 397 c.Realms[0].KDC = []string{addr + ":" + testdata.KDC_PORT_TEST_GOKRB5} 398 c.Realms[0].KPasswdServer = []string{addr + ":464"} 399 cl := client.NewWithKeytab("testuser1", "TEST.GOKRB5", kt, c) 400 return cl 401 } 402 403 type SessionMgr struct { 404 skey []byte 405 store sessions.Store 406 cookieName string 407 } 408 409 func NewSessionMgr(cookieName string) SessionMgr { 410 skey := []byte("thisistestsecret") // Best practice is to load this key from a secure location. 411 return SessionMgr{ 412 skey: skey, 413 store: sessions.NewCookieStore(skey), 414 cookieName: cookieName, 415 } 416 } 417 418 func (smgr SessionMgr) Get(r *http.Request, k string) ([]byte, error) { 419 s, err := smgr.store.Get(r, smgr.cookieName) 420 if err != nil { 421 return nil, err 422 } 423 if s == nil { 424 return nil, errors.New("nil session") 425 } 426 b, ok := s.Values[k].([]byte) 427 if !ok { 428 return nil, fmt.Errorf("could not get bytes held in session at %s", k) 429 } 430 return b, nil 431 } 432 433 func (smgr SessionMgr) New(w http.ResponseWriter, r *http.Request, k string, v []byte) error { 434 s, err := smgr.store.New(r, smgr.cookieName) 435 if err != nil { 436 return fmt.Errorf("could not get new session from session manager: %v", err) 437 } 438 s.Values[k] = v 439 return s.Save(r, w) 440 }