1
2
3
4
5
6
7
8
9
10
11
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
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",
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
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
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
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
165
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
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
190
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
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 }
214
View as plain text