...

Source file src/github.com/go-kivik/kivik/v4/x/server/auth/cookie.go

Documentation: github.com/go-kivik/kivik/v4/x/server/auth

     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  }
   214  

View as plain text