veryon/services/identity: CSRFCop uses macaroons to send data along with token.
* Updated CSRFCop to take arbritrary input in NewToken.
* In ValidateToken the input is decoded and the HMAC is verified.
* Updated the identity server revocation to use a macaroon instead of maintaining state.
* Updated csrf_test to test for varying cookies instead of varying tokens.
* This also fixes https://code.google.com/p/envyor/issues/detail?id=319.
Change-Id: Ife8f7dd9533c0c0149a182179f9226253d61f3d3
diff --git a/services/identity/googleoauth/handler.go b/services/identity/googleoauth/handler.go
index 0c878ad..0edf142 100644
--- a/services/identity/googleoauth/handler.go
+++ b/services/identity/googleoauth/handler.go
@@ -29,6 +29,11 @@
"veyron.io/veyron/veyron2/vlog"
)
+const (
+ clientIDCookie = "VeyronHTTPIdentityClientID"
+ revocationCookie = "VeyronHTTPIdentityRevocationID"
+)
+
type HandlerArgs struct {
// URL at which the hander is installed.
// e.g. http://host:port/google/
@@ -62,15 +67,18 @@
// NewHandler returns an http.Handler that expects to be rooted at args.Addr
// and can be used to use OAuth 2.0 to authenticate with Google, mint a new
// identity and bless it with the Google email address.
-func NewHandler(args HandlerArgs) http.Handler {
+func NewHandler(args HandlerArgs) (http.Handler, error) {
config := NewOAuthConfig(args.ClientID, args.ClientSecret, args.redirectURL())
+ csrfCop, err := util.NewCSRFCop()
+ if err != nil {
+ return nil, err
+ }
return &handler{
config: config,
- csrfCop: util.NewCSRFCop(),
+ csrfCop: csrfCop,
auditor: args.Auditor,
revocationManager: args.RevocationManager,
- tokenMap: newTokenRevocationCaveatMap(time.Hour),
- }
+ }, nil
}
// NewOAuthConfig returns the oauth.Config required for obtaining just the email address from Google using OAuth 2.0.
@@ -90,7 +98,6 @@
csrfCop *util.CSRFCop
auditor string
revocationManager *revocation.RevocationManager
- tokenMap *tokenRevocationCaveatMap
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -107,7 +114,7 @@
}
func (h *handler) auth(w http.ResponseWriter, r *http.Request) {
- csrf, err := h.csrfCop.NewToken(w, r, "VeyronHTTPIdentityClientID")
+ csrf, err := h.csrfCop.NewToken(w, r, clientIDCookie, nil)
if err != nil {
vlog.Infof("Failed to create CSRF token[%v] for request %#v", err, r)
util.HTTPBadRequest(w, r, fmt.Errorf("Suspected automated request: %v", err))
@@ -117,7 +124,7 @@
}
func (h *handler) callback(w http.ResponseWriter, r *http.Request) {
- if err := h.csrfCop.ValidateToken(r.FormValue("state"), r, "VeyronHTTPIdentityClientID"); err != nil {
+ if err := h.csrfCop.ValidateToken(r.FormValue("state"), r, clientIDCookie, nil); err != nil {
vlog.Infof("Invalid CSRF token: %v in request: %#v", err, r)
util.HTTPBadRequest(w, r, fmt.Errorf("Suspected request forgery: %v", err))
return
@@ -127,27 +134,20 @@
util.HTTPBadRequest(w, r, err)
return
}
- // Create a new token to protect ensure that the revocation calls are protected.
- csrf, err := h.csrfCop.NewToken(w, r, "VeyronHTTPIdentityRevocationID")
- if err != nil {
- vlog.Infof("Failed to create CSRF token[%v] for request %#v", err, r)
- util.HTTPBadRequest(w, r, fmt.Errorf("Suspected automated request: %v", err))
- return
- }
+
type tmplentry struct {
- Blessee security.PublicID
- Start, End time.Time
- Blessed security.PublicID
- RevocationCaveatID string
- RevocationTime time.Time
+ Blessee security.PublicID
+ Start, End time.Time
+ Blessed security.PublicID
+ RevocationTime time.Time
+ Token string
}
tmplargs := struct {
- Log chan tmplentry
- Email, CSRFToken string
+ Log chan tmplentry
+ Email, Token string
}{
- Log: make(chan tmplentry),
- Email: email,
- CSRFToken: csrf,
+ Log: make(chan tmplentry),
+ Email: email,
}
if entrych, err := auditor.ReadAuditLog(h.auditor, email); err != nil {
vlog.Errorf("Unable to read audit log: %v", err)
@@ -172,17 +172,29 @@
continue
}
if blessEntry.RevocationCaveat != nil {
- tmplentry.RevocationCaveatID = base64.URLEncoding.EncodeToString([]byte(blessEntry.RevocationCaveat.ID()))
if revocationTime := h.revocationManager.GetRevocationTime(blessEntry.RevocationCaveat.ID()); revocationTime != nil {
tmplentry.RevocationTime = *revocationTime
+ } else {
+ caveatID := base64.URLEncoding.EncodeToString([]byte(blessEntry.RevocationCaveat.ID()))
+ if tmplentry.Token, err = h.csrfCop.NewToken(w, r, revocationCookie, caveatID); err != nil {
+ vlog.Infof("Failed to create CSRF token[%v] for request %#v", err, r)
+ util.HTTPBadRequest(w, r, fmt.Errorf("Suspected automated request: %v", err))
+ return
+ }
}
- h.tokenMap.Insert(csrf, tmplentry.RevocationCaveatID)
}
ch <- tmplentry
}
}(tmplargs.Log)
}
w.Header().Set("Context-Type", "text/html")
+ // This MaybeSetCookie call is needed to ensure that a cookie is created. Since the
+ // header cannot be changed once the body is written to, this needs to be called first.
+ if _, err = h.csrfCop.MaybeSetCookie(w, r, revocationCookie); err != nil {
+ vlog.Infof("Failed to set CSRF cookie[%v] for request %#v", err, r)
+ util.HTTPBadRequest(w, r, fmt.Errorf("Suspected automated request: %v", err))
+ return
+ }
if err := tmpl.Execute(w, tmplargs); err != nil {
vlog.Errorf("Unable to execute audit page template: %v", err)
util.HTTPServerError(w, err)
@@ -208,7 +220,7 @@
return
}
var requestParams struct {
- CaveatID, CSRFToken string
+ Token string
}
if err := json.Unmarshal(content, &requestParams); err != nil {
vlog.Infof("json.Unmarshal failed : %s", err)
@@ -216,21 +228,12 @@
return
}
- if err := h.validateTokenCaveatID(requestParams.CSRFToken, requestParams.CaveatID, r); err != nil {
+ var caveatID string
+ if caveatID, err = h.validateRevocationToken(requestParams.Token, r); err != nil {
vlog.Infof("failed to validate token for caveat: %s", err)
w.Write([]byte(failure))
return
}
-
- decodedCaveatID, err := base64.URLEncoding.DecodeString(requestParams.CaveatID)
- if err != nil {
- vlog.Infof("base64 decoding failed: %s", err)
- w.Write([]byte(failure))
- return
- }
-
- caveatID := string(decodedCaveatID)
-
if err := h.revocationManager.Revoke(caveatID); err != nil {
vlog.Infof("Revocation failed: %s", err)
w.Write([]byte(failure))
@@ -241,14 +244,16 @@
return
}
-func (h *handler) validateTokenCaveatID(CSRFToken, revocationCaveatID string, r *http.Request) error {
- if err := h.csrfCop.ValidateToken(CSRFToken, r, "VeyronHTTPIdentityRevocationID"); err != nil {
- return fmt.Errorf("invalid CSRF token: %v in request: %#v", err, r)
+func (h *handler) validateRevocationToken(Token string, r *http.Request) (string, error) {
+ var encCaveatID string
+ if err := h.csrfCop.ValidateToken(Token, r, revocationCookie, &encCaveatID); err != nil {
+ return "", fmt.Errorf("invalid CSRF token: %v in request: %#v", err, r)
}
- if h.tokenMap.Exists(CSRFToken, revocationCaveatID) {
- return nil
+ caveatID, err := base64.URLEncoding.DecodeString(encCaveatID)
+ if err != nil {
+ return "", fmt.Errorf("decode caveatID failed: %v", err)
}
- return fmt.Errorf("this token has no matching entry for the provided caveat ID")
+ return string(caveatID), nil
}
// ExchangeAuthCodeForEmail exchanges the authorization code (which must
diff --git a/services/identity/googleoauth/template.go b/services/identity/googleoauth/template.go
index 1fdaf7d..a0507e1 100644
--- a/services/identity/googleoauth/template.go
+++ b/services/identity/googleoauth/template.go
@@ -42,11 +42,10 @@
url: "/google/revoke",
type: "POST",
data: JSON.stringify({
- "CaveatID": revokeButton.val(),
- "CSRFToken": "{{.CSRFToken}}"
+ "Token": revokeButton.val()
})
}).done(function(data) {
- if (!data.success) {
+ if (data.success == "false") {
failMessage(revokeButton);
return;
}
@@ -91,13 +90,11 @@
<td><div class="unixtime" data-unixtime={{.End.Unix}}>{{.End.String}}</div></td>
<td>{{.Blessee.PublicKey}}</td>
<td>
-{{if .RevocationCaveatID}}
- {{ if .RevocationTime.IsZero }}
- <button class="revoke" value="{{.RevocationCaveatID}}">Revoke</button>
- {{ else }}
+ {{ if .Token }}
+ <button class="revoke" value="{{.Token}}">Revoke</button>
+ {{ else if not .RevocationTime.IsZero }}
<div class="unixtime" data-unixtime={{.RevocationTime.Unix}}>{{.RevocationTime.String}}</div>
{{ end }}
-{{ end }}
</td>
</tr>
{{else}}
diff --git a/services/identity/googleoauth/utils.go b/services/identity/googleoauth/utils.go
index a123f25..6f5230a 100644
--- a/services/identity/googleoauth/utils.go
+++ b/services/identity/googleoauth/utils.go
@@ -4,8 +4,6 @@
"encoding/json"
"fmt"
"io"
- "sync"
- "time"
)
// ClientIDFromJSON parses JSON-encoded API access information in 'r' and returns
@@ -65,54 +63,3 @@
}
return
}
-
-// Map that maintains storage of tokens and caveatIDs and removal after specified timeout.
-// This is need to ensure the correct identities have revoke access from the identity server.
-type tokenRevocationCaveatMap struct {
- mapIndex int // The index of the current map being. This will be the index of the map that should be inserted into.
- tokenMaps [2]map[tokenCaveatIDKey]bool
- mu sync.RWMutex // guards mapIndex and tokenMaps
-}
-
-type tokenCaveatIDKey struct {
- token, caveatID string
-}
-
-// newTokenRevocationCaveatMap returns a map from tokens to CaveatIDs such that every Insert-ed entry will Exist at least
-// until 'timeout' and at most until 2*'timeout'
-func newTokenRevocationCaveatMap(timeout time.Duration) *tokenRevocationCaveatMap {
- var tokenMaps [2]map[tokenCaveatIDKey]bool
- for i := 0; i < 2; i++ {
- tokenMaps[i] = make(map[tokenCaveatIDKey]bool)
- }
- m := tokenRevocationCaveatMap{mapIndex: 0, tokenMaps: tokenMaps}
- go m.timeoutLoop(timeout)
- return &m
-}
-
-func (m *tokenRevocationCaveatMap) Insert(token, caveatID string) {
- m.mu.Lock()
- defer m.mu.Unlock()
- m.tokenMaps[m.mapIndex][tokenCaveatIDKey{token, caveatID}] = true
-}
-
-func (m *tokenRevocationCaveatMap) Exists(token, caveatID string) bool {
- m.mu.RLock()
- defer m.mu.RUnlock()
- for i := 0; i < 2; i++ {
- if _, exists := m.tokenMaps[i][tokenCaveatIDKey{token, caveatID}]; exists {
- return true
- }
- }
- return false
-}
-
-func (m *tokenRevocationCaveatMap) timeoutLoop(timeout time.Duration) {
- for {
- time.Sleep(timeout)
- m.mu.Lock()
- m.mapIndex = (m.mapIndex + 1) % 2
- m.tokenMaps[m.mapIndex] = make(map[tokenCaveatIDKey]bool)
- m.mu.Unlock()
- }
-}
diff --git a/services/identity/googleoauth/utils_test.go b/services/identity/googleoauth/utils_test.go
index 342b1b2..ab2e84e 100644
--- a/services/identity/googleoauth/utils_test.go
+++ b/services/identity/googleoauth/utils_test.go
@@ -3,7 +3,6 @@
import (
"strings"
"testing"
- "time"
)
func TestClientIDAndSecretFromJSON(t *testing.T) {
@@ -19,33 +18,3 @@
t.Errorf("Got %q want %q", secret, "SECRET")
}
}
-
-func TestTokenRevocationCaveatMap(t *testing.T) {
- d := time.Millisecond * 10
- tokenMap := newTokenRevocationCaveatMap(d)
- // Test non-existent token and non-existent caveatID.
- if tokenMap.Exists("NEToken", "NECaveatID") {
- t.Errorf("found non-existent token with non-existent caveatID")
- }
- tokenMap.Insert("Token", "CaveatID")
- // Test token and caveatID when both exist.
- if !tokenMap.Exists("Token", "CaveatID") {
- t.Errorf("did not find existing token and caveatID")
- }
- // Test that after timeout they don't exist.
- time.Sleep(3 * d)
- if tokenMap.Exists("Token", "CaveatID") {
- t.Errorf("token and caveatID should have been removed")
- }
-
- tokenMap.Insert("Token", "CaveatID")
- // Test non-existent token and existent caveatID.
- if tokenMap.Exists("NEToken", "CaveatID") {
- t.Errorf("found non-existent token with existing caveatID")
- }
- // Test existent token and non-existent caveatID.
- if tokenMap.Exists("Token", "NECaveatID") {
- t.Errorf("found existing token with non-existent caveatID")
- }
-
-}
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index ebbca90..d9a9cba 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -87,13 +87,17 @@
}
if clientID, clientSecret, ok := getOAuthClientIDAndSecret(*googleConfigWeb); ok && len(*auditprefix) > 0 {
n := "/google/"
- http.Handle(n, googleoauth.NewHandler(googleoauth.HandlerArgs{
+ h, err := googleoauth.NewHandler(googleoauth.HandlerArgs{
Addr: fmt.Sprintf("%s%s", httpaddress(), n),
ClientID: clientID,
ClientSecret: clientSecret,
Auditor: *auditprefix,
RevocationManager: revocationManager,
- }))
+ })
+ if err != nil {
+ vlog.Fatalf("Failed to create googleoauth handler: %v", err)
+ }
+ http.Handle(n, h)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var servers []string
diff --git a/services/identity/util/csrf.go b/services/identity/util/csrf.go
index f71253d..e1bc218 100644
--- a/services/identity/util/csrf.go
+++ b/services/identity/util/csrf.go
@@ -11,15 +11,122 @@
"time"
"veyron.io/veyron/veyron2/vlog"
+ "veyron.io/veyron/veyron2/vom"
)
-const cookieLen = 16
+const (
+ cookieLen = 16
+ keyLength = 16
+)
-type CSRFCop struct{}
+type CSRFCop struct {
+ key []byte
+}
-func NewCSRFCop() *CSRFCop { return new(CSRFCop) }
+func NewCSRFCop() (*CSRFCop, error) {
+ key := make([]byte, keyLength)
+ if _, err := rand.Read(key); err != nil {
+ return nil, fmt.Errorf("newCSRFCop failed: %v", err)
+ }
+ return &CSRFCop{key}, nil
+}
-func (*CSRFCop) maybeSetCookie(w http.ResponseWriter, req *http.Request, cookieName string) ([]byte, error) {
+// NewToken creates an anti-cross-site-request-forgery, aka CSRF aka XSRF token
+// with some data bound to it that can be obtained by ValidateToken.
+// It returns an error if the token could not be created.
+func (c *CSRFCop) NewToken(w http.ResponseWriter, r *http.Request, cookieName string, data interface{}) (string, error) {
+ cookieValue, err := c.MaybeSetCookie(w, r, cookieName)
+ if err != nil {
+ return "", fmt.Errorf("bad cookie: %v", err)
+ }
+ buf := &bytes.Buffer{}
+ if data != nil {
+ if err := vom.NewEncoder(buf).Encode(data); err != nil {
+ return "", err
+ }
+ }
+ m, err := c.createMacaroon(buf.Bytes(), cookieValue)
+ if err != nil {
+ return "", err
+ }
+ return b64encode(append(m.Data, m.HMAC...)), nil
+}
+
+// ValidateToken checks the validity of the provided CSRF token for the
+// provided request, and extracts the data encoded in the token into 'decoded'.
+// If the token is invalid, return an error. This error should not be shown to end users,
+// it is meant for the consumption by the server process only.
+func (c *CSRFCop) ValidateToken(token string, req *http.Request, cookieName string, decoded interface{}) error {
+ cookie, err := req.Cookie(cookieName)
+ if err != nil {
+ return err
+ }
+ cookieValue, err := decodeCookieValue(cookie.Value)
+ if err != nil {
+ return fmt.Errorf("invalid cookie")
+ }
+ decToken, err := b64decode(token)
+ if err != nil {
+ return fmt.Errorf("invalid token: %v", err)
+ }
+ m := macaroon{
+ Data: decToken[:len(decToken)-sha256.Size],
+ HMAC: decToken[len(decToken)-sha256.Size:],
+ }
+ if decoded != nil {
+ if err := vom.NewDecoder(bytes.NewBuffer(m.Data)).Decode(decoded); err != nil {
+ return fmt.Errorf("invalid token data: %v", err)
+ }
+ }
+ return c.verifyMacaroon(m, cookieValue)
+}
+
+// macaroon encapsulates an arbitrary slice of data with an HMAC for integrity protection.
+// Term borrowed from http://research.google.com/pubs/pub41892.html.
+type macaroon struct {
+ Data, HMAC []byte
+}
+
+func (c *CSRFCop) createMacaroon(input, hiddenInput []byte) (*macaroon, error) {
+ m := &macaroon{Data: input}
+ var err error
+ if m.HMAC, err = c.hmac(m.Data, hiddenInput); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func (c *CSRFCop) verifyMacaroon(m macaroon, hiddenInput []byte) error {
+ hm, err := c.hmac(m.Data, hiddenInput)
+ if err != nil {
+ return fmt.Errorf("invalid macaroon: %v", err)
+ }
+ if !hmac.Equal(m.HMAC, hm) {
+ return fmt.Errorf("invalid macaroon, HMAC does not match")
+ }
+ return nil
+}
+
+func (c *CSRFCop) hmac(input, hiddenInput []byte) ([]byte, error) {
+ hm := hmac.New(sha256.New, c.key)
+ var err error
+ // We hash inputs and hiddenInputs to make each a fixed length to avoid
+ // ambiguity with simple concatenation of bytes.
+ w := func(data []byte) error {
+ tmp := sha256.Sum256(data)
+ _, err = hm.Write(tmp[:])
+ return err
+ }
+ if err := w(input); err != nil {
+ return nil, err
+ }
+ if err := w(hiddenInput); err != nil {
+ return nil, err
+ }
+ return hm.Sum(nil), nil
+}
+
+func (*CSRFCop) MaybeSetCookie(w http.ResponseWriter, req *http.Request, cookieName string) ([]byte, error) {
cookie, err := req.Cookie(cookieName)
switch err {
case nil:
@@ -37,58 +144,16 @@
return nil, fmt.Errorf("failed to create cookie")
}
http.SetCookie(w, cookie)
+ // We need to add the cookie to the request also to prevent repeatedly resetting cookies on multiple
+ // calls from the same request.
+ req.AddCookie(cookie)
return v, nil
}
-// NewToken creates an anti-cross-site-request-forgery, aka CSRF aka XSRF token.
-// It returns an error if the token could not be created.
-func (c *CSRFCop) NewToken(w http.ResponseWriter, r *http.Request, cookieName string) (string, error) {
- cookie, err := c.maybeSetCookie(w, r, cookieName)
- if err != nil {
- return "", fmt.Errorf("bad cookie: %v", err)
- }
- return c.newToken(cookie), nil
-}
-
-func (c *CSRFCop) newToken(cookie []byte) string {
- return b64encode(hmac.New(sha256.New, cookie).Sum(nil))
-}
-
-// ValidateToken checks the validity of the provided CSRF token for the
-// provided request, returning nil if the token is valid and an error
-// otherwise.
-// The returned error should not be shown to end-users, it is meant for
-// consumption of the server process only.
-func (c *CSRFCop) ValidateToken(token string, req *http.Request, cookieName string) error {
- cookie, err := req.Cookie(cookieName)
- if err != nil {
- return err
- }
- cookieValue, err := decodeCookieValue(cookie.Value)
- if err != nil {
- return fmt.Errorf("invalid cookie")
- }
- if token != c.newToken(cookieValue) {
- return fmt.Errorf("invalid CSRF token")
- }
- return nil
-}
-
-func requestString(r *http.Request) string {
- var buf bytes.Buffer
- r.Write(&buf)
- return buf.String()
-}
-
-type badRequestData struct {
- Request string
- Error error
-}
-
func newCookie(cookieName string) (*http.Cookie, []byte) {
b := make([]byte, cookieLen)
- if n, err := rand.Read(b); n != cookieLen || err != nil {
- vlog.Errorf("newCookie failed: Read %d random bytes, wanted %d. err: %v", n, cookieLen, err)
+ if _, err := rand.Read(b); err != nil {
+ vlog.Errorf("newCookie failed: %v", err)
return nil, nil
}
return &http.Cookie{
diff --git a/services/identity/util/csrf_test.go b/services/identity/util/csrf_test.go
index c9518bf..06e7f17 100644
--- a/services/identity/util/csrf_test.go
+++ b/services/identity/util/csrf_test.go
@@ -7,12 +7,19 @@
"testing"
)
+const (
+ cookieName = "VeyronCSRFTestCookie"
+ failCookieName = "FailCookieName"
+)
+
func TestCSRFTokenWithoutCookie(t *testing.T) {
- cookieName := "VeyronCSRFTestCookie"
r := newRequest()
- c := NewCSRFCop()
+ c, err := NewCSRFCop()
+ if err != nil {
+ t.Fatalf("NewCSRFCop() failed: %v", err)
+ }
w := httptest.NewRecorder()
- tok, err := c.NewToken(w, r, cookieName)
+ tok, err := c.NewToken(w, r, cookieName, nil)
if err != nil {
t.Errorf("NewToken failed: %v", err)
}
@@ -22,31 +29,74 @@
}
// Cookie needs to be present for validation
r.AddCookie(&http.Cookie{Name: cookieName, Value: cookie})
- if err := c.ValidateToken(tok, r, cookieName); err != nil {
+ if err := c.ValidateToken(tok, r, cookieName, nil); err != nil {
t.Error("CSRF token failed validation:", err)
}
- if err := c.ValidateToken(tok+"junk", r, cookieName); err == nil {
+
+ w = httptest.NewRecorder()
+ if _, err = c.MaybeSetCookie(w, r, failCookieName); err != nil {
+ t.Error("failed to create cookie: ", err)
+ }
+ cookie = cookieSet(w, failCookieName)
+ if len(cookie) == 0 {
+ t.Errorf("Cookie should have been set. Request: [%v], Response: [%v]", r, w)
+ }
+
+ if err := c.ValidateToken(tok, r, failCookieName, nil); err == nil {
t.Error("CSRF token should have failed validation")
}
}
func TestCSRFTokenWithCookie(t *testing.T) {
- cookieName := "VeyronCSRFTestCookie"
r := newRequest()
- c := NewCSRFCop()
+ c, err := NewCSRFCop()
+ if err != nil {
+ t.Fatalf("NewCSRFCop() failed: %v", err)
+ }
w := httptest.NewRecorder()
r.AddCookie(&http.Cookie{Name: cookieName, Value: "u776AC7hf794pTtGVlO50w=="})
- tok, err := c.NewToken(w, r, cookieName)
+ tok, err := c.NewToken(w, r, cookieName, nil)
if err != nil {
t.Errorf("NewToken failed: %v", err)
}
if len(cookieSet(w, cookieName)) > 0 {
t.Errorf("Cookie should not be set when it is already present. Request: [%v], Response: [%v]", r, w)
}
- if err := c.ValidateToken(tok, r, cookieName); err != nil {
+ if err := c.ValidateToken(tok, r, cookieName, nil); err != nil {
t.Error("CSRF token failed validation:", err)
}
- if err := c.ValidateToken(tok+"junk", r, cookieName); err == nil {
+
+ r.AddCookie(&http.Cookie{Name: failCookieName, Value: "u864AC7gf794pTtCAlO40w=="})
+ if err := c.ValidateToken(tok, r, failCookieName, nil); err == nil {
+ t.Error("CSRF token should have failed validation")
+ }
+}
+
+func TestCSRFTokenWithData(t *testing.T) {
+ r := newRequest()
+ c, err := NewCSRFCop()
+ if err != nil {
+ t.Fatalf("NewCSRFCop() failed: %v", err)
+ }
+ w := httptest.NewRecorder()
+ r.AddCookie(&http.Cookie{Name: cookieName, Value: "u776AC7hf794pTtGVlO50w=="})
+ tok, err := c.NewToken(w, r, cookieName, 1)
+ if err != nil {
+ t.Errorf("NewToken failed: %v", err)
+ }
+ if len(cookieSet(w, cookieName)) > 0 {
+ t.Errorf("Cookie should not be set when it is already present. Request: [%v], Response: [%v]", r, w)
+ }
+ var got int
+ if err := c.ValidateToken(tok, r, cookieName, &got); err != nil {
+ t.Error("CSRF token failed validation:", err)
+ }
+ if want := 1; got != want {
+ t.Errorf("Got %v, want %v", got, want)
+ }
+
+ r.AddCookie(&http.Cookie{Name: failCookieName, Value: "u864AC7gf794pTtCAlO40w=="})
+ if err := c.ValidateToken(tok, r, failCookieName, &got); err == nil {
t.Error("CSRF token should have failed validation")
}
}
diff --git a/services/identity/util/write.go b/services/identity/util/write.go
index 0572454..3983842 100644
--- a/services/identity/util/write.go
+++ b/services/identity/util/write.go
@@ -1,6 +1,7 @@
package util
import (
+ "bytes"
"html/template"
"net/http"
@@ -71,3 +72,14 @@
</body>
</html>`))
)
+
+func requestString(r *http.Request) string {
+ var buf bytes.Buffer
+ r.Write(&buf)
+ return buf.String()
+}
+
+type badRequestData struct {
+ Request string
+ Error error
+}