Skip to content

Commit

Permalink
[BREAKING] Support Goji v2.
Browse files Browse the repository at this point in the history
- Protect now returns a func(goji.Handler) goji.Handler
- All web.C.Env usage has been replaced with context.Context
- Renamed envError to setEnvError for clarity
- ErrorHandler now accepts a goji.Handler instead of a web.Handler
- Simplified internal (private) functions that accepted, but did not use, the
  request context.
- Removed support for Go 1.4 as per Goji v2
  • Loading branch information
elithrar committed Dec 11, 2015
1 parent d639a61 commit d3b65ac
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 190 deletions.
13 changes: 8 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
language: go
sudo: false
go:
- 1.4
- 1.5
- tip

matrix:
include:
- go: 1.5
- go: tip

install:
- go get golang.org/x/tools/cmd/vet

script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d -s .)
- diff -u <(echo -n) <(gofmt -d .)
- go tool vet .
- go test -v -race ./...
74 changes: 39 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ forgery](http://blog.codinghorror.com/preventing-csrf-and-xsrf-attacks/) (CSRF)
templates to replace a `{{ .csrfField }}` template tag with a hidden input
field.

This library is designed to work with the [Goji](https://github.com/zenazn/goji)
micro-framework, which is a simple web framework for Go that is broadly
compatible with other parts of the Go ecosystem. It makes use of Goji's `web.C`
request context, which doesn't rely on a global map, and is therefore safe to
attach to your top-level router (if you so wish).
This library is designed to work with not just the the [Goji](https://github.com/goji/goji)
micro-framework, but any framework that accepts the `func(context.Context, w http.ResponseWriter, r *http.Request)`
signature. This makes it compatible with other parts of the Go ecosystem. The
`context.Context` request context doesn't rely on a global map, and is therefore
free from contention in a busy web service.

The library also assumes HTTPS by default: sending cookies over vanilla HTTP
is risky and you're likely to get hurt.
Expand All @@ -27,7 +27,7 @@ is risky and you're likely to get hurt.
goji/csrf is easy to use: add the middleware to your stack with the below:

```go
goji.Use(csrf.Protect([]byte("32-byte-long-auth-key")))
goji.UseC(csrf.Protect([]byte("32-byte-long-auth-key")))
```

... and then collect the token with `csrf.Token(c, r)` before passing it to the
Expand All @@ -47,33 +47,35 @@ import (
"html/template"
"net/http"

"goji.io"
"github.com/goji/csrf"
"github.com/zenazn/goji"
"github.com/zenazn/goji/graceful"
)

func main() {
m := goji.NewMux()
// Add the middleware to your router.
goji.Use(csrf.Protect([]byte("32-byte-long-auth-key")))
goji.Get("/signup", ShowSignupForm)
m.UseC(csrf.Protect([]byte("32-byte-long-auth-key")))
m.HandleFuncC(pat.Get("/signup"), ShowSignupForm)
// POST requests without a valid token will return a HTTP 403 Forbidden.
goji.Post("/signup/post", SubmitSignupForm)
m.HandleFuncC(pat.Post("/signup/post"), SubmitSignupForm)

goji.Serve()
graceful.ListenAndServe(":8000", m)
}

func ShowSignupForm(c web.C, w http.ResponseWriter, r *http.Request) {
func ShowSignupForm(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// signup_form.tmpl just needs a {{ .csrfField }} template tag for
// csrf.TemplateField to inject the CSRF token into. Easy!
t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{
csrf.TemplateTag: csrf.TemplateField(c, r),
csrf.TemplateTag: csrf.TemplateField(ctx, r),
})
// We could also retrieve the token directly from csrf.Token(c, r) and
// set it in the request header - w.Header.Set("X-CSRF-Token", token)
// This is useful if your sending JSON to clients or a front-end JavaScript
// framework.
}

func SubmitSignupForm(c web.C, w http.ResponseWriter, r *http.Request) {
func SubmitSignupForm(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// We can trust that requests making it this far have satisfied
// our CSRF protection requirements.
}
Expand All @@ -91,38 +93,38 @@ as we don't handle any POST/PUT/DELETE requests with our top-level router.
package main

import (
"goji.io"
"github.com/goji/csrf"
"github.com/zenazn/goji/graceful"
"github.com/zenazn/goji/web"
)

func main() {
r := web.New()
m := goji.NewMux()
// Our top-level router doesn't need CSRF protection: it's simple.
r.Get("/", ShowIndex)
m.HandleFuncC(pat.Get("/"), ShowIndex)

api := web.New()
r.Handle("/api/*", s)
api := goji.NewMux()
m.HandleC("/api/*", api)
// ... but our /api/* routes do, so we add it to the sub-router only.
s.Use(csrf.Protect([]byte("32-byte-long-auth-key")))
api.UseC(csrf.Protect([]byte("32-byte-long-auth-key")))

s.Get("/api/user/:id", GetUser)
s.Post("/api/user", PostUser)
api.Get("/api/user/:id", GetUser)
api.Post("/api/user", PostUser)

graceful.ListenAndServe(":8000", r)
graceful.ListenAndServe(":8000", m)
}

func GetUser(c web.C, w http.ResponseWriter, r *http.Request) {
func GetUser(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// Authenticate the request, get the :id from the route params,
// and fetch the user from the DB, etc.

// Get the token and pass it in the CSRF header. Our JSON-speaking client
// or JavaScript framework can now read the header and return the token in
// in its own "X-CSRF-Token" request header on the subsequent POST.
w.Header().Set("X-CSRF-Token", csrf.Token(c, r))
w.Header().Set("X-CSRF-Token", csrf.Token(ctx, r))
b, err := json.Marshal(user)
if err != nil {
http.Error(...)
http.Error(w, http.StatusText(500), 500)
return
}

Expand All @@ -138,23 +140,25 @@ goji/csrf provides options for changing these as you see fit:

```go
func main() {
m := goji.NewMux()
CSRF := csrf.Protect(
[]byte("a-32-byte-long-key-goes-here"),
csrf.RequestHeader("Authenticity-Token"),
csrf.FieldName("authenticity_token"),
// Note that csrf.ErrorHandler takes a Goji web.Handler type, else
// your error handler can't retrieve the error reason from the context.
// The signature `func UnauthHandler(c web.C, w http.ResponseWriter, r *http.Request)`
// is a web.Handler, and the simplest to use if you'd like to serve
// Note that csrf.ErrorHandler takes a Goji goji.Handler type, else
// your error handler can't retrieve the error reason from the
// context.
// The signature `func UnauthHandler(ctx context.Context, w http.ResponseWriter, r *http.Request)`
// is a goji.Handler, and the simplest to use if you'd like to serve
// "pretty" error pages (who doesn't?).
csrf.ErrorHandler(web.HandlerFunc(serverError(403))),
csrf.ErrorHandler(goji.HandlerFunc(serverError(403))),
)

goji.Use(CSRF)
goji.Get("/signup", GetSignupForm)
goji.Post("/signup", PostSignupForm)
m.UseC(CSRF)
m.HandleFuncC(pat.Get("/signup"), GetSignupForm)
m.HandleFuncC(pat.Post("/signup"), PostSignupForm)

goji.Serve()
graceful.ListenAndServe(":8000", m)
}
```

Expand Down
70 changes: 36 additions & 34 deletions csrf.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import (
"net/http"
"net/url"

"golang.org/x/net/context"

"goji.io"

"github.com/gorilla/securecookie"
"github.com/zenazn/goji/web"
)

// CSRF token length in bytes.
Expand Down Expand Up @@ -52,8 +55,7 @@ var (
)

type csrf struct {
c *web.C
h http.Handler
h goji.Handler
sc *securecookie.SecureCookie
st store
opts options
Expand All @@ -70,7 +72,7 @@ type options struct {
Secure bool
RequestHeader string
FieldName string
ErrorHandler web.Handler
ErrorHandler goji.Handler
CookieName string
}

Expand Down Expand Up @@ -115,13 +117,13 @@ type options struct {
// // framework.
// }
//
func Protect(authKey []byte, opts ...Option) func(*web.C, http.Handler) http.Handler {
return func(c *web.C, h http.Handler) http.Handler {
func Protect(authKey []byte, opts ...Option) func(goji.Handler) goji.Handler {
return func(h goji.Handler) goji.Handler {
cs := parseOptions(h, opts...)

// Set the defaults if no options have been specified
if cs.opts.ErrorHandler == nil {
cs.opts.ErrorHandler = web.HandlerFunc(unauthorizedHandler)
cs.opts.ErrorHandler = goji.HandlerFunc(unauthorizedHandler)
}

if cs.opts.MaxAge < 1 {
Expand Down Expand Up @@ -163,49 +165,41 @@ func Protect(authKey []byte, opts ...Option) func(*web.C, http.Handler) http.Han
}
}

// Initialize Goji's request context
cs.c = c

return *cs
}
}

// Implements http.Handler for the csrf type.
func (cs csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Create our request context if it does not already exist.
if cs.c.Env == nil {
cs.c.Env = make(map[interface{}]interface{})
}

func (cs csrf) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// Retrieve the token from the session.
// An error represents either a cookie that failed HMAC validation
// or that doesn't exist.
realToken, err := cs.st.Get(cs.c, r)
realToken, err := cs.st.Get(r)
if err != nil || len(realToken) != tokenLength {
// If there was an error retrieving the token, the token doesn't exist
// yet, or it's the wrong length, generate a new token.
// Note that the new token will (correctly) fail validation downstream
// as it will no longer match the request token.
realToken, err = generateRandomBytes(tokenLength)
if err != nil {
envError(cs.c, err)
cs.opts.ErrorHandler.ServeHTTPC(*cs.c, w, r)
setEnvError(ctx, err)
cs.opts.ErrorHandler.ServeHTTPC(ctx, w, r)
return
}

// Save the new (real) token in the session store.
err = cs.st.Save(realToken, w)
if err != nil {
envError(cs.c, err)
cs.opts.ErrorHandler.ServeHTTPC(*cs.c, w, r)
setEnvError(ctx, err)
cs.opts.ErrorHandler.ServeHTTPC(ctx, w, r)
return
}
}

// Save the masked token to the request context
cs.c.Env[tokenKey] = mask(realToken, cs.c, r)
ctx = context.WithValue(ctx, tokenKey, mask(realToken, r))
// Save the field name to the request context
cs.c.Env[formKey] = cs.opts.FieldName
ctx = context.WithValue(ctx, formKey, cs.opts.FieldName)

// HTTP methods not defined as idempotent ("safe") under RFC7231 require
// inspection.
Expand All @@ -218,23 +212,31 @@ func (cs csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// otherwise fails to parse.
referer, err := url.Parse(r.Referer())
if err != nil || referer.String() == "" {
envError(cs.c, ErrNoReferer)
cs.opts.ErrorHandler.ServeHTTPC(*cs.c, w, r)
setEnvError(ctx, ErrNoReferer)
cs.opts.ErrorHandler.ServeHTTPC(ctx, w, r)
return
}

if sameOrigin(r.URL, referer) == false {
envError(cs.c, ErrBadReferer)
cs.opts.ErrorHandler.ServeHTTPC(*cs.c, w, r)
setEnvError(ctx, ErrBadReferer)
cs.opts.ErrorHandler.ServeHTTPC(ctx, w, r)
return
}
}

// If the token returned from the session store is nil for non-idempotent
// ("unsafe") methods, call the error handler.
if realToken == nil {
envError(cs.c, ErrNoToken)
cs.opts.ErrorHandler.ServeHTTPC(*cs.c, w, r)
setEnvError(ctx, ErrNoToken)
cs.opts.ErrorHandler.ServeHTTPC(ctx, w, r)
return
}

// If the token returned from the session store is nil for non-idempotent
// ("unsafe") methods, call the error handler.
if realToken == nil {
setEnvError(ctx, ErrNoToken)
cs.opts.ErrorHandler.ServeHTTPC(ctx, w, r)
return
}

Expand All @@ -243,8 +245,8 @@ func (cs csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {

// Compare the request token against the real token
if !compareTokens(requestToken, realToken) {
envError(cs.c, ErrBadToken)
cs.opts.ErrorHandler.ServeHTTPC(*cs.c, w, r)
setEnvError(ctx, ErrBadToken)
cs.opts.ErrorHandler.ServeHTTPC(ctx, w, r)
return
}

Expand All @@ -254,14 +256,14 @@ func (cs csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Vary", "Cookie")

// Call the wrapped handler/router on success
cs.h.ServeHTTP(w, r)
cs.h.ServeHTTPC(ctx, w, r)
}

// unauthorizedhandler sets a HTTP 403 Forbidden status and writes the
// CSRF failure reason to the response.
func unauthorizedHandler(c web.C, w http.ResponseWriter, r *http.Request) {
func unauthorizedHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
http.Error(w, fmt.Sprintf("%s - %s",
http.StatusText(http.StatusForbidden), FailureReason(c, r)),
http.StatusText(http.StatusForbidden), FailureReason(ctx, r)),
http.StatusForbidden)
return
}
Loading

0 comments on commit d3b65ac

Please sign in to comment.