1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
37 type Auth struct{}
38
39 var _ auth.Handler = &Auth{}
40
41
42 func (a *Auth) MethodName() string {
43 return "cookie"
44 }
45
46
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
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
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
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 }
168
View as plain text