github.com/go-kivik/kivik/v4@v4.3.2/x/kivikd/auth/cookie/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 //go:build !js 14 15 // Package cookie provides standard CouchDB cookie auth as described at 16 // http://docs.couchdb.org/en/2.0.0/api/server/authn.html#cookie-authentication 17 package cookie 18 19 import ( 20 "encoding/json" 21 "net/http" 22 "net/url" 23 "strings" 24 "time" 25 26 "github.com/go-kivik/kivik/v4" 27 internal "github.com/go-kivik/kivik/v4/int/errors" 28 "github.com/go-kivik/kivik/v4/x/kivikd" 29 "github.com/go-kivik/kivik/v4/x/kivikd/auth" 30 "github.com/go-kivik/kivik/v4/x/kivikd/authdb" 31 "github.com/go-kivik/kivik/v4/x/kivikd/cookies" 32 ) 33 34 const typeJSON = "application/json" 35 36 // Auth provides CouchDB Cookie authentication. 37 type Auth struct{} 38 39 var _ auth.Handler = &Auth{} 40 41 // MethodName returns "cookie" 42 func (a *Auth) MethodName() string { 43 return "cookie" // For compatibility with the name used by CouchDB 44 } 45 46 // Authenticate authenticates a request with cookie auth against the user store. 47 func (a *Auth) Authenticate(w http.ResponseWriter, r *http.Request) (*authdb.UserContext, error) { 48 if r.URL.Path == "/_session" { 49 switch r.Method { 50 case http.MethodPost: 51 return nil, postSession(w, r) 52 case http.MethodDelete: 53 return nil, deleteSession(w) 54 } 55 } 56 return a.validateCookie(r) 57 } 58 59 func (a *Auth) validateCookie(r *http.Request) (*authdb.UserContext, error) { 60 store := kivikd.GetService(r).UserStore 61 cookie, err := r.Cookie(kivik.SessionCookieName) 62 if err != nil { 63 return nil, nil 64 } 65 name, _, err := cookies.DecodeCookie(cookie.Value) 66 if err != nil { 67 return nil, nil 68 } 69 user, err := store.UserCtx(r.Context(), name) 70 if err != nil { 71 // Failed to look up the user 72 return nil, nil 73 } 74 s := kivikd.GetService(r) 75 valid, err := s.ValidateCookie(user, cookie.Value) 76 if err != nil || !valid { 77 return nil, nil 78 } 79 return user, nil 80 } 81 82 func postSession(w http.ResponseWriter, r *http.Request) error { 83 authData := struct { 84 Name *string `form:"name" json:"name"` 85 Password string `form:"password" json:"password"` 86 }{} 87 if err := kivikd.BindParams(r, &authData); err != nil { 88 return &internal.Error{Status: http.StatusBadRequest, Message: "unable to parse request data"} 89 } 90 if authData.Name == nil { 91 return &internal.Error{Status: http.StatusBadRequest, Message: "request body must contain a username"} 92 } 93 s := kivikd.GetService(r) 94 user, err := s.UserStore.Validate(r.Context(), *authData.Name, authData.Password) 95 if err != nil { 96 return err 97 } 98 next, err := redirectURL(r) 99 if err != nil { 100 return err 101 } 102 103 // Success, so create a cookie 104 token, err := s.CreateAuthToken(*authData.Name, user.Salt, time.Now().Unix()) 105 if err != nil { 106 return err 107 } 108 w.Header().Set("Cache-Control", "must-revalidate") 109 http.SetCookie(w, &http.Cookie{ 110 Name: kivik.SessionCookieName, 111 Value: token, 112 Path: "/", 113 MaxAge: getSessionTimeout(s), 114 HttpOnly: true, 115 }) 116 w.Header().Add("Content-Type", typeJSON) 117 if next != "" { 118 w.Header().Add("Location", next) 119 w.WriteHeader(http.StatusFound) 120 } 121 return json.NewEncoder(w).Encode(map[string]interface{}{ 122 "ok": true, 123 "name": user.Name, 124 "roles": user.Roles, 125 }) 126 } 127 128 func redirectURL(r *http.Request) (string, error) { 129 next, ok := kivikd.StringQueryParam(r, "next") 130 if !ok { 131 return "", nil 132 } 133 if !strings.HasPrefix(next, "/") { 134 return "", &internal.Error{Status: http.StatusBadRequest, Message: "redirection url must be relative to server root"} 135 } 136 if strings.HasPrefix(next, "//") { 137 // Possible schemaless url 138 return "", &internal.Error{Status: http.StatusBadRequest, Message: "invalid redirection url"} 139 } 140 parsed, err := url.Parse(next) 141 if err != nil { 142 return "", &internal.Error{Status: http.StatusBadRequest, Message: "invalid redirection url"} 143 } 144 return parsed.String(), nil 145 } 146 147 func deleteSession(w http.ResponseWriter) error { 148 http.SetCookie(w, &http.Cookie{ 149 Name: kivik.SessionCookieName, 150 Value: "", 151 Path: "/", 152 MaxAge: -1, 153 HttpOnly: true, 154 }) 155 w.Header().Add("Content-Type", typeJSON) 156 w.Header().Set("Cache-Control", "must-revalidate") 157 return json.NewEncoder(w).Encode(map[string]interface{}{ 158 "ok": true, 159 }) 160 } 161 162 func getSessionTimeout(s *kivikd.Service) int { 163 if s.Conf().IsSet("couch_httpd_auth.timeout") { 164 return s.Conf().GetInt("couch_httpd_auth.timeout") 165 } 166 return kivikd.DefaultSessionTimeout 167 }