Skip to content

Commit

Permalink
Merge pull request #229 from go-kivik/refreshAuth
Browse files Browse the repository at this point in the history
Re-authenticate when cookie-based auth session is expired
  • Loading branch information
flimzy authored Mar 29, 2020
2 parents c093355 + 12d907c commit 98dc776
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 6 deletions.
6 changes: 3 additions & 3 deletions chttp/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ type Authenticator interface {
Authenticate(*Client) error
}

func (a *CookieAuth) setCookieJar(c *Client) {
func (a *CookieAuth) setCookieJar() {
// If a jar is already set, just use it
if c.Jar != nil {
if a.client.Jar != nil {
return
}
// cookiejar.New never returns an error
jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
c.Jar = jar
a.client.Jar = jar
}
26 changes: 23 additions & 3 deletions chttp/cookieauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package chttp
import (
"context"
"net/http"
"time"

kivik "github.com/go-kivik/kivik/v4"
)
Expand All @@ -25,8 +26,8 @@ var _ Authenticator = &CookieAuth{}

// Authenticate initiates a session with the CouchDB server.
func (a *CookieAuth) Authenticate(c *Client) error {
a.setCookieJar(c)
a.client = c
a.setCookieJar()
a.transport = c.Transport
if a.transport == nil {
a.transport = http.DefaultTransport
Expand All @@ -35,6 +36,26 @@ func (a *CookieAuth) Authenticate(c *Client) error {
return nil
}

// shouldAuth returns true if there is no cookie set, or if it has expired.
func (a *CookieAuth) shouldAuth(req *http.Request) bool {
if _, err := req.Cookie(kivik.SessionCookieName); err == nil {
return false
}
cookie := a.Cookie()
if cookie == nil {
return true
}
if !cookie.Expires.IsZero() {
return cookie.Expires.Before(time.Now())
}
// If we get here, it means the server did not include an expiry time in
// the session cookie. Some CouchDB configurations do this, but rather than
// re-authenticating for every request, we'll let the session expire. A
// future change might be to make a client-configurable option to set the
// re-authentication timeout.
return false
}

// Cookie returns the current session cookie if found, or nil if not.
func (a *CookieAuth) Cookie() *http.Cookie {
if a.client == nil {
Expand Down Expand Up @@ -64,8 +85,7 @@ func (a *CookieAuth) authenticate(req *http.Request) error {
if inProg, _ := ctx.Value(authInProgress).(bool); inProg {
return nil
}
if _, ok := req.Header["Cookie"]; ok {
// Request already authenticated
if !a.shouldAuth(req) {
return nil
}
a.client.authMU.Lock()
Expand Down
85 changes: 85 additions & 0 deletions chttp/cookieauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http/httptest"
"net/url"
"testing"
"time"

"gitlab.com/flimzy/testy"
"golang.org/x/net/publicsuffix"
Expand Down Expand Up @@ -150,3 +151,87 @@ func TestCookie(t *testing.T) {
})
}
}

type dummyJar []*http.Cookie

var _ http.CookieJar = &dummyJar{}

func (j dummyJar) Cookies(_ *url.URL) []*http.Cookie {
return []*http.Cookie(j)
}

func (j *dummyJar) SetCookies(_ *url.URL, cookies []*http.Cookie) {
*j = cookies
}

func Test_shouldAuth(t *testing.T) {
type tt struct {
a *CookieAuth
req *http.Request
want bool
}

tests := testy.NewTable()
tests.Add("no session", tt{
a: &CookieAuth{},
req: httptest.NewRequest("GET", "/", nil),
want: true,
})
tests.Add("authed request", func() interface{} {
req := httptest.NewRequest("GET", "/", nil)
req.AddCookie(&http.Cookie{Name: kivik.SessionCookieName})
return tt{
a: &CookieAuth{},
req: req,
want: false,
}
})
tests.Add("valid session", func() interface{} {
c, _ := New("http://example.com/")
c.Jar = &dummyJar{&http.Cookie{
Name: kivik.SessionCookieName,
Expires: time.Now().Add(20 * time.Second),
}}
a := &CookieAuth{client: c}

return tt{
a: a,
req: httptest.NewRequest("GET", "/", nil),
want: false,
}
})
tests.Add("expired session", func() interface{} {
c, _ := New("http://example.com/")
c.Jar = &dummyJar{&http.Cookie{
Name: kivik.SessionCookieName,
Expires: time.Now().Add(-20 * time.Second),
}}
a := &CookieAuth{client: c}

return tt{
a: a,
req: httptest.NewRequest("GET", "/", nil),
want: true,
}
})
tests.Add("no expiry time", func() interface{} {
c, _ := New("http://example.com/")
c.Jar = &dummyJar{&http.Cookie{
Name: kivik.SessionCookieName,
}}
a := &CookieAuth{client: c}

return tt{
a: a,
req: httptest.NewRequest("GET", "/", nil),
want: false,
}
})

tests.Run(t, func(t *testing.T, tt tt) {
got := tt.a.shouldAuth(tt.req)
if got != tt.want {
t.Errorf("Want %t, got %t", tt.want, got)
}
})
}

0 comments on commit 98dc776

Please sign in to comment.