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  }