github.com/go-kivik/kivik/v4@v4.3.2/x/server/auth/cookie.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 auth 14 15 import ( 16 "bytes" 17 "crypto/hmac" 18 "crypto/sha1" 19 "encoding/base64" 20 "encoding/json" 21 "fmt" 22 "net/http" 23 "net/url" 24 "strconv" 25 "strings" 26 "time" 27 28 "github.com/go-kivik/kivik/v4" 29 internal "github.com/go-kivik/kivik/v4/int/errors" 30 ) 31 32 type cookieAuth struct { 33 secret string 34 timeout time.Duration 35 s Server 36 } 37 38 // CookieAuth returns a cookie auth handler. 39 func CookieAuth(secret string, sessionTimeout time.Duration) Handler { 40 return &cookieAuth{ 41 secret: secret, 42 timeout: sessionTimeout, 43 } 44 } 45 46 func (a *cookieAuth) Init(s Server) (string, AuthenticateFunc) { 47 a.s = s 48 return "cookie", // For compatibility with the name used by CouchDB 49 a.Authenticate 50 } 51 52 func (a *cookieAuth) Authenticate(w http.ResponseWriter, r *http.Request) (*UserContext, error) { 53 if r.URL.Path == "/_session" { 54 switch r.Method { 55 case http.MethodPost: 56 return nil, a.postSession(w, r) 57 case http.MethodDelete: 58 return nil, deleteSession(w) 59 } 60 } 61 return a.validateCookie(r) 62 } 63 64 func (a *cookieAuth) validateCookie(r *http.Request) (*UserContext, error) { 65 cookie, err := r.Cookie(kivik.SessionCookieName) 66 if err != nil { 67 return nil, nil 68 } 69 name, _, err := DecodeCookie(cookie.Value) 70 if err != nil { 71 return nil, nil 72 } 73 user, err := a.s.UserStore().UserCtx(r.Context(), name) 74 if err != nil { 75 // Failed to look up the user 76 return nil, err 77 } 78 valid, err := a.ValidateCookie(user, cookie.Value) 79 if err != nil { 80 return nil, err 81 } 82 if !valid { 83 return nil, &internal.Error{Status: http.StatusUnauthorized, Message: "invalid cookie"} 84 } 85 return user, nil 86 } 87 88 func (a *cookieAuth) postSession(w http.ResponseWriter, r *http.Request) error { 89 var authData struct { 90 Name *string `form:"name" json:"name"` 91 Password string `form:"password" json:"password"` 92 } 93 if err := a.s.Bind(r, &authData); err != nil { 94 return err 95 } 96 if authData.Name == nil { 97 return &internal.Error{Status: http.StatusBadRequest, Message: "request body must contain a username"} 98 } 99 user, err := a.s.UserStore().Validate(r.Context(), *authData.Name, authData.Password) 100 if err != nil { 101 return err 102 } 103 next, err := redirectURL(r) 104 if err != nil { 105 return err 106 } 107 108 // Success, so create a cookie 109 token := CreateAuthToken(*authData.Name, user.Salt, a.secret, time.Now().Unix()) 110 w.Header().Set("Cache-Control", "must-revalidate") 111 http.SetCookie(w, &http.Cookie{ 112 Name: kivik.SessionCookieName, 113 Value: token, 114 Path: "/", 115 MaxAge: int(a.timeout.Seconds()), 116 HttpOnly: true, 117 }) 118 w.Header().Add("Content-Type", typeJSON) 119 if next != "" { 120 w.Header().Add("Location", next) 121 w.WriteHeader(http.StatusFound) 122 } 123 return json.NewEncoder(w).Encode(map[string]interface{}{ 124 "ok": true, 125 "name": user.Name, 126 "roles": user.Roles, 127 }) 128 } 129 130 func redirectURL(r *http.Request) (string, error) { 131 next, ok := stringQueryParam(r, "next") 132 if !ok { 133 return "", nil 134 } 135 if !strings.HasPrefix(next, "/") { 136 return "", &internal.Error{Status: http.StatusBadRequest, Message: "redirection url must be relative to server root"} 137 } 138 if strings.HasPrefix(next, "//") { 139 // Possible schemaless url 140 return "", &internal.Error{Status: http.StatusBadRequest, Message: "invalid redirection url"} 141 } 142 parsed, err := url.Parse(next) 143 if err != nil { 144 return "", &internal.Error{Status: http.StatusBadRequest, Message: "invalid redirection url"} 145 } 146 return parsed.String(), nil 147 } 148 149 func deleteSession(w http.ResponseWriter) error { 150 http.SetCookie(w, &http.Cookie{ 151 Name: kivik.SessionCookieName, 152 Value: "", 153 Path: "/", 154 MaxAge: -1, 155 HttpOnly: true, 156 }) 157 w.Header().Add("Content-Type", typeJSON) 158 w.Header().Set("Cache-Control", "must-revalidate") 159 return json.NewEncoder(w).Encode(map[string]interface{}{ 160 "ok": true, 161 }) 162 } 163 164 // CreateAuthToken hashes a username, salt, timestamp, and the server secret 165 // into an authentication token. 166 func CreateAuthToken(name, salt, secret string, time int64) string { 167 if secret == "" { 168 panic("secret must be set") 169 } 170 if salt == "" { 171 panic("salt must be set") 172 } 173 sessionData := fmt.Sprintf("%s:%X", name, time) 174 h := hmac.New(sha1.New, []byte(secret+salt)) 175 _, _ = h.Write([]byte(sessionData)) 176 hashData := string(h.Sum(nil)) 177 return base64.RawURLEncoding.EncodeToString([]byte(sessionData + ":" + hashData)) 178 } 179 180 // stringQueryParam extracts a query parameter as string. 181 func stringQueryParam(r *http.Request, key string) (string, bool) { 182 values := r.URL.Query() 183 if _, ok := values[key]; !ok { 184 return "", false 185 } 186 return values.Get(key), true 187 } 188 189 // DecodeCookie decodes a Base64-encoded cookie, and returns its component 190 // parts. 191 func DecodeCookie(cookie string) (name string, created int64, err error) { 192 data, err := base64.RawURLEncoding.DecodeString(cookie) 193 if err != nil { 194 return "", 0, err 195 } 196 const partCount = 3 197 parts := bytes.SplitN(data, []byte(":"), partCount) 198 t, err := strconv.ParseInt(string(parts[1]), 16, 64) 199 if err != nil { 200 return "", 0, fmt.Errorf("invalid timestamp: %w", err) 201 } 202 return string(parts[0]), t, nil 203 } 204 205 // ValidateCookie validates the provided cookie against the configured UserStore. 206 func (a *cookieAuth) ValidateCookie(user *UserContext, cookie string) (bool, error) { 207 name, t, err := DecodeCookie(cookie) 208 if err != nil { 209 return false, err 210 } 211 token := CreateAuthToken(name, user.Salt, a.secret, t) 212 return token == cookie, nil 213 }