github.com/go-kivik/kivik/v4@v4.3.2/couchdb/chttp/cookieauth.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 "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 }