Skip to content

Commit

Permalink
verify Origin header instead of Referer
Browse files Browse the repository at this point in the history
  • Loading branch information
cainlevy committed Oct 21, 2017
1 parent 03cf86a commit b23bde8
Show file tree
Hide file tree
Showing 10 changed files with 29 additions and 30 deletions.
6 changes: 3 additions & 3 deletions api/accounts/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import (
)

func Routes(app *api.App) []*route.HandledRoute {
refererSecurity := route.RefererSecurity(app.Config.ApplicationDomains)
originSecurity := route.OriginSecurity(app.Config.ApplicationDomains)
authentication := route.BasicAuthSecurity(app.Config.AuthUsername, app.Config.AuthPassword, "Private AuthN Realm")

routes := []*route.HandledRoute{}

if app.Config.EnableSignup {
routes = append(routes,
route.Post("/accounts").
SecuredWith(refererSecurity).
SecuredWith(originSecurity).
Handle(postAccount(app)),
route.Get("/accounts/available").
SecuredWith(refererSecurity).
SecuredWith(originSecurity).
Handle(getAccountsAvailable(app)),
)
}
Expand Down
6 changes: 3 additions & 3 deletions api/passwords/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import (
)

func Routes(app *api.App) []*route.HandledRoute {
refererSecurity := route.RefererSecurity(app.Config.ApplicationDomains)
originSecurity := route.OriginSecurity(app.Config.ApplicationDomains)

routes := []*route.HandledRoute{
route.Post("/password").
SecuredWith(refererSecurity).
SecuredWith(originSecurity).
Handle(postPassword(app)),
}

if app.Config.AppPasswordResetURL != nil {
routes = append(routes,
route.Get("/password/reset").
SecuredWith(refererSecurity).
SecuredWith(originSecurity).
Handle(getPasswordReset(app)),
)
}
Expand Down
5 changes: 3 additions & 2 deletions api/sessions/get_session_refresh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ func TestGetSessionRefreshSuccess(t *testing.T) {
res, err := client.Get("/session/refresh")
require.NoError(t, err)

assert.Equal(t, http.StatusCreated, res.StatusCode)
test.AssertIDTokenResponse(t, res, app.KeyStore, app.Config)
if assert.Equal(t, http.StatusCreated, res.StatusCode) {
test.AssertIDTokenResponse(t, res, app.KeyStore, app.Config)
}
}

func TestGetSessionRefreshFailure(t *testing.T) {
Expand Down
8 changes: 4 additions & 4 deletions api/sessions/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ import (
)

func Routes(app *api.App) []*route.HandledRoute {
refererSecurity := route.RefererSecurity(app.Config.ApplicationDomains)
originSecurity := route.OriginSecurity(app.Config.ApplicationDomains)

return []*route.HandledRoute{
route.Post("/session").
SecuredWith(refererSecurity).
SecuredWith(originSecurity).
Handle(postSession(app)),

route.Delete("/session").
SecuredWith(refererSecurity).
SecuredWith(originSecurity).
Handle(deleteSession(app)),

route.Get("/session/refresh").
SecuredWith(refererSecurity).
SecuredWith(originSecurity).
Handle(getSessionRefresh(app)),
}
}
4 changes: 1 addition & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ title: Server API

AuthN exposes both **public** and **private** endpoints.

**Public** endpoints are intended to receive traffic directly from a client, although you may certainly route that traffic through a gateway if you prefer. These endpoints rely on trusted HTTP Referer† headers to prevent CSRF attacks. V1.0 will also include support for a custom `AUTHN-AUDIENCE` header, intended for native clients.

† The Referer header was found to exist for [99.9% of users over HTTPS](http://seclab.stanford.edu/websec/csrf/csrf.pdf) (which you should be using anyway).
**Public** endpoints are intended to receive traffic directly from a client, although you may certainly route that traffic through a gateway if you prefer. These endpoints rely on trusted Origin headers to prevent CSRF attacks. V1.0 will also include support for a custom `AUTHN-AUDIENCE` header, intended for native clients.

**Private** endpoints are intended to receive only traffic from your application's backend. They require HTTP Basic Auth username and password, and should only be accessed over HTTPS (which you should be using anyway).

Expand Down
4 changes: 2 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ This specifies the base URL of the AuthN service. It will be embedded in all iss

Any domain listed in this variable will be trusted for three things:

1. Requests sent from these domains (as determined by the REFERER header) will satisfy CSRF requirements.
2. Access tokens generated by requests sent from these domains (as determined by the REFERER header) will specify the domain as their intended `aud` (audience).
1. Requests sent from these domains (as determined by the Origin header) will satisfy CSRF requirements.
2. Access tokens generated by requests sent from these domains (as determined by the Origin header) will specify the domain as their intended `aud` (audience).
3. Any endpoints that accept redirects will only allow the redirect if it uses one of these domains.

### `HTTP_AUTH_USERNAME`
Expand Down
4 changes: 2 additions & 2 deletions lib/route/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func NewClient(baseURL string) *Client {
return &Client{baseURL, []modder{}}
}

// Referred will inject a Referer header into a client's requests.
// Referred will inject an Origin header into a client's requests.
func (c *Client) Referred(domain *Domain) *Client {
scheme := "http"
if domain.Port == "443" {
Expand All @@ -41,7 +41,7 @@ func (c *Client) Referred(domain *Domain) *Client {
return &Client{
c.BaseURL,
append(c.Modifiers, func(req *http.Request) *http.Request {
req.Header.Add("Referer", origin)
req.Header.Add("Origin", origin)
return req
}),
}
Expand Down
12 changes: 6 additions & 6 deletions lib/route/referer_security.go → lib/route/origin_security.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ import (

type matchedDomainKey int

// RefererSecurity is a SecurityHandler that will ensure a request comes from a known referer. This
// OriginSecurity is a SecurityHandler that will ensure a request comes from a known origin. This
// can be an effective way to mitigate CSRF attacks, which are unable to forge headers due to the
// passive nature of the attack vector.
//
// RefererSecurity will store the matching domain in the http.Request's Context. Use MatchedDomain
// OriginSecurity will store the matching domain in the http.Request's Context. Use MatchedDomain
// to retrieve the value in later logic.
func RefererSecurity(domains []Domain) SecurityHandler {
func OriginSecurity(domains []Domain) SecurityHandler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
url, err := url.Parse(r.Header.Get("Referer"))
url, err := url.Parse(r.Header.Get("Origin"))
if err != nil {
panic(err)
}
Expand All @@ -33,13 +33,13 @@ func RefererSecurity(domains []Domain) SecurityHandler {
}

w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Referer is not a trusted host."))
w.Write([]byte("Origin is not a trusted host."))
})
}
}

// MatchedDomain will retrieve from the http.Request's Context the domain that satisfied
// RefererSecurity.
// OriginSecurity.
func MatchedDomain(r *http.Request) *Domain {
d, ok := r.Context().Value(matchedDomainKey(0)).(*Domain)
if ok {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/require"
)

func TestRefererSecurity(t *testing.T) {
func TestOriginSecurity(t *testing.T) {
readBody := func(res *http.Response) []byte {
body, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
Expand All @@ -21,7 +21,7 @@ func TestRefererSecurity(t *testing.T) {

testCases := []struct {
domain string
referer string
origin string
success bool
}{
{"example.com", "http://example.com", true},
Expand All @@ -44,14 +44,14 @@ func TestRefererSecurity(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.domain, func(t *testing.T) {
adapter := route.RefererSecurity([]route.Domain{route.ParseDomain(tc.domain)})
adapter := route.OriginSecurity([]route.Domain{route.ParseDomain(tc.domain)})

server := httptest.NewServer(adapter(nextHandler))
defer server.Close()

req, err := http.NewRequest("GET", server.URL, nil)
require.NoError(t, err)
req.Header.Add("Referer", tc.referer)
req.Header.Add("Origin", tc.origin)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)

Expand Down
2 changes: 1 addition & 1 deletion lib/route/route.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Package route is a fluent API for building secured gorilla/mux routes. It requires every route to
// have a defined SecurityHandler, which is a middleware that encodes some authorization strategy.
// Included SecurityHandlers can authorize a request by HTTP Referer (for CSRF security) or HTTP
// Included SecurityHandlers can authorize a request by HTTP Origin (for CSRF security) or HTTP
// Basic Auth. There is also an Unsecured handler for explicitly acknowledging that an endpoint is
// wide open.
package route
Expand Down

0 comments on commit b23bde8

Please sign in to comment.