blob: 1efcae26a58c579d439849a7171528b87a48dfcc [file] [log] [blame]
Jiri Simsa5293dcb2014-05-10 09:56:38 -07001package util
2
3import (
4 "bytes"
5 "crypto/hmac"
6 "crypto/rand"
7 "crypto/sha256"
8 "encoding/base64"
9 "fmt"
10 "net/http"
11 "time"
12
Jiri Simsa519c5072014-09-17 21:37:57 -070013 "veyron.io/veyron/veyron2/vlog"
Suharsh Sivakumar44300662014-09-23 11:35:06 -070014 "veyron.io/veyron/veyron2/vom"
Jiri Simsa5293dcb2014-05-10 09:56:38 -070015)
16
Suharsh Sivakumar44300662014-09-23 11:35:06 -070017const (
18 cookieLen = 16
19 keyLength = 16
20)
Suharsh Sivakumar5ee0ee62014-09-04 13:16:34 -070021
Suharsh Sivakumar44300662014-09-23 11:35:06 -070022type CSRFCop struct {
23 key []byte
24}
Jiri Simsa5293dcb2014-05-10 09:56:38 -070025
Suharsh Sivakumard308c7e2014-10-03 12:46:50 -070026func (c *CSRFCop) keyForCookie(cookie []byte) []byte {
27 hm := hmac.New(sha256.New, c.key)
28 hm.Write(cookie)
29 return hm.Sum(nil)
30}
31
Suharsh Sivakumar44300662014-09-23 11:35:06 -070032func NewCSRFCop() (*CSRFCop, error) {
33 key := make([]byte, keyLength)
34 if _, err := rand.Read(key); err != nil {
35 return nil, fmt.Errorf("newCSRFCop failed: %v", err)
36 }
37 return &CSRFCop{key}, nil
38}
Jiri Simsa5293dcb2014-05-10 09:56:38 -070039
Suharsh Sivakumar44300662014-09-23 11:35:06 -070040// NewToken creates an anti-cross-site-request-forgery, aka CSRF aka XSRF token
41// with some data bound to it that can be obtained by ValidateToken.
42// It returns an error if the token could not be created.
43func (c *CSRFCop) NewToken(w http.ResponseWriter, r *http.Request, cookieName string, data interface{}) (string, error) {
44 cookieValue, err := c.MaybeSetCookie(w, r, cookieName)
45 if err != nil {
46 return "", fmt.Errorf("bad cookie: %v", err)
47 }
48 buf := &bytes.Buffer{}
49 if data != nil {
50 if err := vom.NewEncoder(buf).Encode(data); err != nil {
51 return "", err
52 }
53 }
Suharsh Sivakumard308c7e2014-10-03 12:46:50 -070054 return string(NewMacaroon(c.keyForCookie(cookieValue), buf.Bytes())), nil
Suharsh Sivakumar44300662014-09-23 11:35:06 -070055}
56
57// ValidateToken checks the validity of the provided CSRF token for the
58// provided request, and extracts the data encoded in the token into 'decoded'.
59// If the token is invalid, return an error. This error should not be shown to end users,
60// it is meant for the consumption by the server process only.
61func (c *CSRFCop) ValidateToken(token string, req *http.Request, cookieName string, decoded interface{}) error {
62 cookie, err := req.Cookie(cookieName)
63 if err != nil {
64 return err
65 }
66 cookieValue, err := decodeCookieValue(cookie.Value)
67 if err != nil {
68 return fmt.Errorf("invalid cookie")
69 }
Suharsh Sivakumard308c7e2014-10-03 12:46:50 -070070 encodedInput, err := Macaroon(token).Decode(c.keyForCookie(cookieValue))
Suharsh Sivakumar44300662014-09-23 11:35:06 -070071 if err != nil {
Suharsh Sivakumard308c7e2014-10-03 12:46:50 -070072 return err
Suharsh Sivakumar44300662014-09-23 11:35:06 -070073 }
74 if decoded != nil {
Suharsh Sivakumard308c7e2014-10-03 12:46:50 -070075 if err := vom.NewDecoder(bytes.NewBuffer(encodedInput)).Decode(decoded); err != nil {
Suharsh Sivakumar44300662014-09-23 11:35:06 -070076 return fmt.Errorf("invalid token data: %v", err)
77 }
78 }
Suharsh Sivakumar44300662014-09-23 11:35:06 -070079 return nil
80}
81
Suharsh Sivakumar44300662014-09-23 11:35:06 -070082func (*CSRFCop) MaybeSetCookie(w http.ResponseWriter, req *http.Request, cookieName string) ([]byte, error) {
Jiri Simsa5293dcb2014-05-10 09:56:38 -070083 cookie, err := req.Cookie(cookieName)
84 switch err {
85 case nil:
86 if v, err := decodeCookieValue(cookie.Value); err == nil {
87 return v, nil
88 }
89 vlog.Infof("Invalid cookie: %#v, err: %v. Regenerating one.", cookie, err)
90 case http.ErrNoCookie:
91 // Intentionally blank: Cookie will be generated below.
92 default:
93 vlog.Infof("Error decoding cookie %q in request: %v. Regenerating one.", cookieName, err)
94 }
Suharsh Sivakumar5ee0ee62014-09-04 13:16:34 -070095 cookie, v := newCookie(cookieName)
Jiri Simsa5293dcb2014-05-10 09:56:38 -070096 if cookie == nil || v == nil {
97 return nil, fmt.Errorf("failed to create cookie")
98 }
99 http.SetCookie(w, cookie)
Suharsh Sivakumar44300662014-09-23 11:35:06 -0700100 // We need to add the cookie to the request also to prevent repeatedly resetting cookies on multiple
101 // calls from the same request.
102 req.AddCookie(cookie)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700103 return v, nil
104}
105
Suharsh Sivakumar5ee0ee62014-09-04 13:16:34 -0700106func newCookie(cookieName string) (*http.Cookie, []byte) {
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700107 b := make([]byte, cookieLen)
Suharsh Sivakumar44300662014-09-23 11:35:06 -0700108 if _, err := rand.Read(b); err != nil {
109 vlog.Errorf("newCookie failed: %v", err)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700110 return nil, nil
111 }
112 return &http.Cookie{
113 Name: cookieName,
114 Value: b64encode(b),
115 Expires: time.Now().Add(time.Hour * 24),
116 HttpOnly: true,
Suharsh Sivakumara057bff2014-10-07 14:33:34 -0700117 Path: "/",
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700118 }, b
119}
120
121func decodeCookieValue(v string) ([]byte, error) {
122 b, err := b64decode(v)
123 if err != nil {
124 return nil, err
125 }
126 if len(b) != cookieLen {
127 return nil, fmt.Errorf("invalid cookie length[%d]", len(b))
128 }
129 return b, nil
130}
131
132// Shorthands.
133func b64encode(b []byte) string { return base64.URLEncoding.EncodeToString(b) }
134func b64decode(s string) ([]byte, error) { return base64.URLEncoding.DecodeString(s) }