...

Source file src/github.com/go-kivik/kivik/v4/couchdb/chttp/cookieauth_test.go

Documentation: github.com/go-kivik/kivik/v4/couchdb/chttp

     1  // Licensed under the Apache License, Version 2.0 (the "License"); you may not
     2  // use this file except in compliance with the License. You may obtain a copy of
     3  // the License at
     4  //
     5  //  http://www.apache.org/licenses/LICENSE-2.0
     6  //
     7  // Unless required by applicable law or agreed to in writing, software
     8  // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     9  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    10  // License for the specific language governing permissions and limitations under
    11  // the License.
    12  
    13  package chttp
    14  
    15  import (
    16  	"context"
    17  	"net/http"
    18  	"net/http/cookiejar"
    19  	"net/http/httptest"
    20  	"net/url"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"gitlab.com/flimzy/testy"
    26  	"golang.org/x/net/publicsuffix"
    27  
    28  	kivik "github.com/go-kivik/kivik/v4"
    29  	internal "github.com/go-kivik/kivik/v4/int/errors"
    30  	"github.com/go-kivik/kivik/v4/int/mock"
    31  	"github.com/go-kivik/kivik/v4/internal/nettest"
    32  )
    33  
    34  func TestCookieAuthAuthenticate(t *testing.T) {
    35  	type cookieTest struct {
    36  		dsn            string
    37  		auth           *cookieAuth
    38  		err            string
    39  		status         int
    40  		expectedCookie *http.Cookie
    41  	}
    42  
    43  	tests := testy.NewTable()
    44  	tests.Add("success", func(t *testing.T) interface{} {
    45  		var sessCounter int
    46  		s := nettest.NewHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    47  			h := w.Header()
    48  			h.Set("Content-Type", "application/json")
    49  			h.Set("Date", "Sat, 08 Sep 2018 15:49:29 GMT")
    50  			h.Set("Server", "CouchDB/2.2.0 (Erlang OTP/19)")
    51  			if r.URL.Path == "/_session" {
    52  				sessCounter++
    53  				if sessCounter > 1 {
    54  					t.Fatal("Too many calls to /_session")
    55  				}
    56  				h.Set("Set-Cookie", "AuthSession=YWRtaW46NUI5M0VGODk6eLUGqXf0HRSEV9PPLaZX86sBYes; Version=1; Path=/; HttpOnly")
    57  				w.WriteHeader(200)
    58  				_, _ = w.Write([]byte(`{"ok":true,"name":"admin","roles":["_admin"]}`))
    59  			} else {
    60  				if cookie := r.Header.Get("Cookie"); cookie != "AuthSession=YWRtaW46NUI5M0VGODk6eLUGqXf0HRSEV9PPLaZX86sBYes" {
    61  					t.Errorf("Expected cookie not found: %s", cookie)
    62  				}
    63  				w.WriteHeader(200)
    64  				_, _ = w.Write([]byte(`{"ok":true}`))
    65  			}
    66  		}))
    67  		return cookieTest{
    68  			dsn:  s.URL,
    69  			auth: &cookieAuth{Username: "foo", Password: "bar"},
    70  			expectedCookie: &http.Cookie{
    71  				Name:  kivik.SessionCookieName,
    72  				Value: "YWRtaW46NUI5M0VGODk6eLUGqXf0HRSEV9PPLaZX86sBYes",
    73  			},
    74  		}
    75  	})
    76  	tests.Add("cookie not set", func(t *testing.T) interface{} {
    77  		s := nettest.NewHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
    78  			h := w.Header()
    79  			h.Set("Content-Type", "application/json")
    80  			h.Set("Date", "Sat, 08 Sep 2018 15:49:29 GMT")
    81  			h.Set("Server", "CouchDB/2.2.0 (Erlang OTP/19)")
    82  			w.WriteHeader(200)
    83  		}))
    84  		return cookieTest{
    85  			dsn:  s.URL,
    86  			auth: &cookieAuth{Username: "foo", Password: "bar"},
    87  		}
    88  	})
    89  
    90  	tests.Run(t, func(t *testing.T, test cookieTest) {
    91  		c, err := New(&http.Client{}, test.dsn, mock.NilOption)
    92  		if err != nil {
    93  			t.Fatal(err)
    94  		}
    95  		if e := test.auth.Authenticate(c); e != nil {
    96  			t.Fatal(e)
    97  		}
    98  		_, err = c.DoError(context.Background(), "GET", "/foo", nil)
    99  		if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
   100  			t.Error(d)
   101  		}
   102  		if d := testy.DiffInterface(test.expectedCookie, test.auth.Cookie()); d != nil {
   103  			t.Error(d)
   104  		}
   105  
   106  		// Do it again; should be idempotent
   107  		_, err = c.DoError(context.Background(), "GET", "/foo", nil)
   108  		if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
   109  			t.Error(d)
   110  		}
   111  		if d := testy.DiffInterface(test.expectedCookie, test.auth.Cookie()); d != nil {
   112  			t.Error(d)
   113  		}
   114  	})
   115  }
   116  
   117  func TestCookie(t *testing.T) {
   118  	tests := []struct {
   119  		name     string
   120  		auth     *cookieAuth
   121  		expected *http.Cookie
   122  	}{
   123  		{
   124  			name:     "No cookie jar",
   125  			auth:     &cookieAuth{},
   126  			expected: nil,
   127  		},
   128  		{
   129  			name:     "No dsn",
   130  			auth:     &cookieAuth{},
   131  			expected: nil,
   132  		},
   133  		{
   134  			name:     "no cookies",
   135  			auth:     &cookieAuth{},
   136  			expected: nil,
   137  		},
   138  		{
   139  			name: "cookie found",
   140  			auth: func() *cookieAuth {
   141  				dsn, err := url.Parse("http://example.com/")
   142  				if err != nil {
   143  					t.Fatal(err)
   144  				}
   145  				jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
   146  				if err != nil {
   147  					t.Fatal(err)
   148  				}
   149  				jar.SetCookies(dsn, []*http.Cookie{
   150  					{Name: kivik.SessionCookieName, Value: "foo"},
   151  					{Name: "other", Value: "bar"},
   152  				})
   153  				return &cookieAuth{
   154  					client: &Client{
   155  						dsn: dsn,
   156  						Client: &http.Client{
   157  							Jar: jar,
   158  						},
   159  					},
   160  				}
   161  			}(),
   162  			expected: &http.Cookie{Name: kivik.SessionCookieName, Value: "foo"},
   163  		},
   164  	}
   165  	for _, test := range tests {
   166  		t.Run(test.name, func(t *testing.T) {
   167  			result := test.auth.Cookie()
   168  			if d := testy.DiffInterface(test.expected, result); d != nil {
   169  				t.Error(d)
   170  			}
   171  		})
   172  	}
   173  }
   174  
   175  type dummyJar []*http.Cookie
   176  
   177  var _ http.CookieJar = &dummyJar{}
   178  
   179  func (j dummyJar) Cookies(_ *url.URL) []*http.Cookie {
   180  	return []*http.Cookie(j)
   181  }
   182  
   183  func (j *dummyJar) SetCookies(_ *url.URL, cookies []*http.Cookie) {
   184  	*j = cookies
   185  }
   186  
   187  func Test_shouldAuth(t *testing.T) {
   188  	type tt struct {
   189  		a    *cookieAuth
   190  		req  *http.Request
   191  		want bool
   192  	}
   193  
   194  	tests := testy.NewTable()
   195  	tests.Add("no session", tt{
   196  		a:    &cookieAuth{},
   197  		req:  httptest.NewRequest("GET", "/", nil),
   198  		want: true,
   199  	})
   200  	tests.Add("authed request", func() interface{} {
   201  		req := httptest.NewRequest("GET", "/", nil)
   202  		req.AddCookie(&http.Cookie{Name: kivik.SessionCookieName})
   203  		return tt{
   204  			a:    &cookieAuth{},
   205  			req:  req,
   206  			want: false,
   207  		}
   208  	})
   209  	tests.Add("valid session", func() interface{} {
   210  		c, _ := New(&http.Client{}, "http://example.com/", mock.NilOption)
   211  		c.Jar = &dummyJar{&http.Cookie{
   212  			Name:    kivik.SessionCookieName,
   213  			Expires: time.Now().Add(20 * time.Minute),
   214  		}}
   215  		a := &cookieAuth{client: c}
   216  
   217  		return tt{
   218  			a:    a,
   219  			req:  httptest.NewRequest("GET", "/", nil),
   220  			want: false,
   221  		}
   222  	})
   223  	tests.Add("expired session", func() interface{} {
   224  		c, _ := New(&http.Client{}, "http://example.com/", mock.NilOption)
   225  		c.Jar = &dummyJar{&http.Cookie{
   226  			Name:    kivik.SessionCookieName,
   227  			Expires: time.Now().Add(-20 * time.Second),
   228  		}}
   229  		a := &cookieAuth{client: c}
   230  
   231  		return tt{
   232  			a:    a,
   233  			req:  httptest.NewRequest("GET", "/", nil),
   234  			want: true,
   235  		}
   236  	})
   237  	tests.Add("no expiry time", func() interface{} {
   238  		c, _ := New(&http.Client{}, "http://example.com/", mock.NilOption)
   239  		c.Jar = &dummyJar{&http.Cookie{
   240  			Name: kivik.SessionCookieName,
   241  		}}
   242  		a := &cookieAuth{client: c}
   243  
   244  		return tt{
   245  			a:    a,
   246  			req:  httptest.NewRequest("GET", "/", nil),
   247  			want: false,
   248  		}
   249  	})
   250  	tests.Add("about to expire", func() interface{} {
   251  		c, _ := New(&http.Client{}, "http://example.com/", mock.NilOption)
   252  		c.Jar = &dummyJar{&http.Cookie{
   253  			Name:    kivik.SessionCookieName,
   254  			Expires: time.Now().Add(20 * time.Second),
   255  		}}
   256  		a := &cookieAuth{client: c}
   257  
   258  		return tt{
   259  			a:    a,
   260  			req:  httptest.NewRequest("GET", "/", nil),
   261  			want: true,
   262  		}
   263  	})
   264  
   265  	tests.Run(t, func(t *testing.T, tt tt) {
   266  		got := tt.a.shouldAuth(tt.req)
   267  		if got != tt.want {
   268  			t.Errorf("Want %t, got %t", tt.want, got)
   269  		}
   270  	})
   271  }
   272  
   273  func Test401Response(t *testing.T) {
   274  	var sessCounter, getCounter int
   275  	s := nettest.NewHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   276  		h := w.Header()
   277  		h.Set("Content-Type", "application/json")
   278  		h.Set("Date", "Sat, 08 Sep 2018 15:49:29 GMT")
   279  		h.Set("Server", "CouchDB/2.2.0 (Erlang OTP/19)")
   280  		if r.URL.Path == "/_session" {
   281  			sessCounter++
   282  			if sessCounter > 2 {
   283  				t.Fatal("Too many calls to /_session")
   284  			}
   285  			var cookie string
   286  			if sessCounter == 1 {
   287  				// set another cookie at the start too
   288  				h.Add("Set-Cookie", "Other=foo; Version=1; Path=/; HttpOnly")
   289  				cookie = "First"
   290  			} else {
   291  				cookie = "Second"
   292  			}
   293  			h.Add("Set-Cookie", "AuthSession="+cookie+"; Version=1; Path=/; HttpOnly")
   294  			w.WriteHeader(200)
   295  			_, _ = w.Write([]byte(`{"ok":true,"name":"admin","roles":["_admin"]}`))
   296  		} else {
   297  			getCounter++
   298  			cookie := r.Header.Get("Cookie")
   299  			if !(strings.Contains(cookie, "AuthSession=")) {
   300  				t.Errorf("Expected cookie not found: %s", cookie)
   301  			}
   302  			// because of the way the request is baked before the auth loop
   303  			// cookies other than the auth cookie set when calling _session won't
   304  			// get applied to requests until after that first request.
   305  			if getCounter > 1 && !strings.Contains(cookie, "Other=foo") {
   306  				t.Errorf("Expected cookie not found: %s", cookie)
   307  			}
   308  			if getCounter == 2 {
   309  				w.WriteHeader(401)
   310  				_, _ = w.Write([]byte(`{"error":"unauthorized","reason":"You are not authorized to access this db."}`))
   311  				return
   312  			}
   313  			w.WriteHeader(200)
   314  			_, _ = w.Write([]byte(`{"ok":true}`))
   315  		}
   316  	}))
   317  
   318  	c, err := New(&http.Client{}, s.URL, mock.NilOption)
   319  	if err != nil {
   320  		t.Fatal(err)
   321  	}
   322  	auth := &cookieAuth{Username: "foo", Password: "bar"}
   323  	if e := auth.Authenticate(c); e != nil {
   324  		t.Fatal(e)
   325  	}
   326  
   327  	expectedCookie := &http.Cookie{
   328  		Name:  kivik.SessionCookieName,
   329  		Value: "First",
   330  	}
   331  	newCookie := &http.Cookie{
   332  		Name:  kivik.SessionCookieName,
   333  		Value: "Second",
   334  	}
   335  
   336  	_, err = c.DoError(context.Background(), "GET", "/foo", nil)
   337  	if d := internal.StatusErrorDiff("", 0, err); d != "" {
   338  		t.Error(d)
   339  	}
   340  	if d := testy.DiffInterface(expectedCookie, auth.Cookie()); d != nil {
   341  		t.Error(d)
   342  	}
   343  
   344  	_, err = c.DoError(context.Background(), "GET", "/foo", nil)
   345  
   346  	// this causes a skip so this won't work for us.
   347  	// if d := internal.StatusErrorDiff("Unauthorized: You are not authorized to access this db.", 401, err); d != "" { t.Error(d) }
   348  	if !testy.ErrorMatches("Unauthorized: You are not authorized to access this db.", err) {
   349  		t.Fatalf("Unexpected error: %s", err)
   350  	}
   351  	if status := testy.StatusCode(err); status != http.StatusUnauthorized {
   352  		t.Errorf("Unexpected status code: %d", status)
   353  	}
   354  
   355  	var noCookie *http.Cookie
   356  	if d := testy.DiffInterface(noCookie, auth.Cookie()); d != nil {
   357  		t.Error(d)
   358  	}
   359  
   360  	_, err = c.DoError(context.Background(), "GET", "/foo", nil)
   361  	if d := internal.StatusErrorDiff("", 0, err); d != "" {
   362  		t.Error(d)
   363  	}
   364  	if d := testy.DiffInterface(newCookie, auth.Cookie()); d != nil {
   365  		t.Error(d)
   366  	}
   367  }
   368  

View as plain text