...

Source file src/github.com/go-kivik/kivik/v4/couchdb/chttp/cookieauth.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  	"fmt"
    18  	"net/http"
    19  	"net/http/cookiejar"
    20  	"strings"
    21  	"time"
    22  
    23  	"golang.org/x/net/publicsuffix"
    24  
    25  	kivik "github.com/go-kivik/kivik/v4"
    26  )
    27  
    28  // cookieAuth provides CouchDB Cookie auth services as described at
    29  // http://docs.couchdb.org/en/2.0.0/api/server/authn.html#cookie-authentication
    30  //
    31  // CookieAuth stores authentication state after use, so should not be re-used.
    32  type cookieAuth struct {
    33  	Username string `json:"name"`
    34  	Password string `json:"password"`
    35  
    36  	client *Client
    37  	// transport stores the original transport that is overridden by this auth
    38  	// mechanism
    39  	transport http.RoundTripper
    40  }
    41  
    42  var (
    43  	_ authenticator = &cookieAuth{}
    44  	_ kivik.Option  = (*cookieAuth)(nil)
    45  )
    46  
    47  func (a *cookieAuth) Apply(target interface{}) {
    48  	if auth, ok := target.(*authenticator); ok {
    49  		// Clone this so that it's safe to re-use the same option to multiple
    50  		// client connections. TODO: This can no doubt be refactored.
    51  		*auth = &cookieAuth{
    52  			Username: a.Username,
    53  			Password: a.Password,
    54  		}
    55  	}
    56  }
    57  
    58  func (a *cookieAuth) String() string {
    59  	return fmt.Sprintf("[CookieAuth{user:%s,pass:%s}]", a.Username, strings.Repeat("*", len(a.Password)))
    60  }
    61  
    62  // Authenticate initiates a session with the CouchDB server.
    63  func (a *cookieAuth) Authenticate(c *Client) error {
    64  	a.client = c
    65  	a.setCookieJar()
    66  	a.transport = c.Transport
    67  	if a.transport == nil {
    68  		a.transport = http.DefaultTransport
    69  	}
    70  	c.Transport = a
    71  	return nil
    72  }
    73  
    74  // shouldAuth returns true if there is no cookie set, or if it has expired.
    75  func (a *cookieAuth) shouldAuth(req *http.Request) bool {
    76  	if _, err := req.Cookie(kivik.SessionCookieName); err == nil {
    77  		return false
    78  	}
    79  	cookie := a.Cookie()
    80  	if cookie == nil {
    81  		return true
    82  	}
    83  	if !cookie.Expires.IsZero() {
    84  		return cookie.Expires.Before(time.Now().Add(time.Minute))
    85  	}
    86  	// If we get here, it means the server did not include an expiry time in
    87  	// the session cookie. Some CouchDB configurations do this, but rather than
    88  	// re-authenticating for every request, we'll let the session expire. A
    89  	// future change might be to make a client-configurable option to set the
    90  	// re-authentication timeout.
    91  	return false
    92  }
    93  
    94  // Cookie returns the current session cookie if found, or nil if not.
    95  func (a *cookieAuth) Cookie() *http.Cookie {
    96  	if a.client == nil {
    97  		return nil
    98  	}
    99  	for _, cookie := range a.client.Jar.Cookies(a.client.dsn) {
   100  		if cookie.Name == kivik.SessionCookieName {
   101  			return cookie
   102  		}
   103  	}
   104  	return nil
   105  }
   106  
   107  var authInProgress = &struct{ name string }{"in progress"}
   108  
   109  // RoundTrip fulfills the http.RoundTripper interface. It sets
   110  // (re-)authenticates when the cookie has expired or is not yet set.
   111  // It also drops the auth cookie if we receive a 401 response to ensure
   112  // that follow up requests can try to authenticate again.
   113  func (a *cookieAuth) RoundTrip(req *http.Request) (*http.Response, error) {
   114  	if err := a.authenticate(req); err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	res, err := a.transport.RoundTrip(req)
   119  	if err != nil {
   120  		return res, err
   121  	}
   122  
   123  	if res != nil && res.StatusCode == http.StatusUnauthorized {
   124  		if cookie := a.Cookie(); cookie != nil {
   125  			// set to expire yesterday to allow us to ditch it
   126  			cookie.Expires = time.Now().AddDate(0, 0, -1)
   127  			a.client.Jar.SetCookies(a.client.dsn, []*http.Cookie{cookie})
   128  		}
   129  	}
   130  	return res, nil
   131  }
   132  
   133  func (a *cookieAuth) authenticate(req *http.Request) error {
   134  	ctx := req.Context()
   135  	if inProg, _ := ctx.Value(authInProgress).(bool); inProg {
   136  		return nil
   137  	}
   138  	if !a.shouldAuth(req) {
   139  		return nil
   140  	}
   141  	a.client.authMU.Lock()
   142  	defer a.client.authMU.Unlock()
   143  	if c := a.Cookie(); c != nil {
   144  		// In case another simultaneous process authenticated successfully first
   145  		req.AddCookie(c)
   146  		return nil
   147  	}
   148  	ctx = context.WithValue(ctx, authInProgress, true)
   149  	opts := &Options{
   150  		GetBody: BodyEncoder(a),
   151  		Header: http.Header{
   152  			HeaderIdempotencyKey: []string{},
   153  		},
   154  	}
   155  	if _, err := a.client.DoError(ctx, http.MethodPost, "/_session", opts); err != nil {
   156  		return err
   157  	}
   158  	if c := a.Cookie(); c != nil {
   159  		req.AddCookie(c)
   160  	}
   161  	return nil
   162  }
   163  
   164  func (a *cookieAuth) setCookieJar() {
   165  	// If a jar is already set, just use it
   166  	if a.client.Jar != nil {
   167  		return
   168  	}
   169  	// cookiejar.New never returns an error
   170  	jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
   171  	a.client.Jar = jar
   172  }
   173  

View as plain text