github.com/go-kivik/kivik/v4@v4.3.2/couchdb/chttp/cookieauth_test.go (about) 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 }