...

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

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

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

View as plain text