Skip to content

Commit

Permalink
Merge pull request #33 from InVisionApp/baiscauth
Browse files Browse the repository at this point in the history
Auth middlewares
  • Loading branch information
talpert authored May 2, 2018
2 parents bc1eed7 + cadc967 commit 86e3e6d
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 49 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,15 +226,15 @@ the example using Gorilla:
| [Access Token](middleware_accesstoken.go) | Provide Access Token validation |
| [CIDR](middleware_cidr.go) | Provide request IP whitelisting |
| [CORS](middleware_cors.go) | Provide CORS functionality for routes |
| [JWT](middleware_jwt.go) | Provide JWT validation |
| [Auth](middleware_auth.go) | Provide Authorization header validation (basic auth, JWT) |
| [Route Logger](middleware_routelogger.go) | Provide basic logging for a specific route |
| [Static File](middleware_static_file.go) | Provides serving a single file |
| [Static Filesystem](middleware_static_filesystem.go) | Provides serving a single file |


### A Note on the JWT Middleware

The [JWT Middleware](middleware_jwt.go) pushes the JWT token onto the Context for use by other middlewares in the chain. This is a convenience that allows any part of your middleware chain quick access to the JWT. Example usage might include a middleware that needs access to your user id or email address stored in the JWT. To access this `Context` variable, the code is very simple:
The [JWT Middleware](middleware_auth.go) pushes the JWT token onto the Context for use by other middlewares in the chain. This is a convenience that allows any part of your middleware chain quick access to the JWT. Example usage might include a middleware that needs access to your user id or email address stored in the JWT. To access this `Context` variable, the code is very simple:
```go
func getJWTfromContext(rw http.ResponseWriter, r *http.Request) *rye.Response {
// Retrieving the value is easy!
Expand Down
15 changes: 13 additions & 2 deletions example/rye_example.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"net/http"

"github.com/InVisionApp/rye"
log "github.com/sirupsen/logrus"
"github.com/cactus/go-statsd-client/statsd"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)

func main() {
Expand Down Expand Up @@ -41,11 +41,22 @@ func main() {
homeHandler,
})).Methods("GET", "OPTIONS")

// If you perform an `curl -i http://localhost:8181/jwt \
// -H "Authorization: Basic dXNlcjE6cGFzczEK"
// you will see that we are allowed through to the handler, if the header is changed, you will get a 401
routes.Handle("/basic-auth", middlewareHandler.Handle([]rye.Handler{
rye.NewMiddlewareAuth(rye.NewBasicAuthFunc(map[string]string{
"user1": "pass1",
"user2": "pass2",
})),
getJwtFromContextHandler,
})).Methods("GET")

// If you perform an `curl -i http://localhost:8181/jwt \
// -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
// you will see that we are allowed through to the handler, if the sample token is changed, we will get a 401
routes.Handle("/jwt", middlewareHandler.Handle([]rye.Handler{
rye.NewMiddlewareJWT("secret"),
rye.NewMiddlewareAuth(rye.NewJWTAuthFunc("secret")),
getJwtFromContextHandler,
})).Methods("GET")

Expand Down
151 changes: 151 additions & 0 deletions middleware_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package rye

import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strings"

jwt "github.com/dgrijalva/jwt-go"
)

/*
NewMiddlewareAuth creates a new middleware to extract the Authorization header
from a request and validate it. It accepts a func of type AuthFunc which is
used to do the credential validation.
An AuthFuncs for Basic auth and JWT are provided here.
Example usage:
routes.Handle("/some/route", myMWHandler.Handle(
[]rye.Handler{
rye.NewMiddlewareAuth(rye.NewBasicAuthFunc(map[string]string{
"user1": "my_password",
})),
yourHandler,
})).Methods("POST")
*/

type AuthFunc func(context.Context, string) *Response

func NewMiddlewareAuth(authFunc AuthFunc) func(rw http.ResponseWriter, req *http.Request) *Response {
return func(rw http.ResponseWriter, r *http.Request) *Response {
auth := r.Header.Get("Authorization")
if auth == "" {
return &Response{
Err: errors.New("unauthorized: no authentication provided"),
StatusCode: http.StatusUnauthorized,
}
}

return authFunc(r.Context(), auth)
}
}

/***********
Basic Auth
***********/

func NewBasicAuthFunc(userPass map[string]string) AuthFunc {
return basicAuth(userPass).authenticate
}

type basicAuth map[string]string

const AUTH_USERNAME_KEY = "request-username"

// basicAuth.authenticate meets the AuthFunc type
func (b basicAuth) authenticate(ctx context.Context, auth string) *Response {
errResp := &Response{
Err: errors.New("unauthorized: invalid authentication provided"),
StatusCode: http.StatusUnauthorized,
}

// parse the Authorization header
u, p, ok := parseBasicAuth(auth)
if !ok {
return errResp
}

// get the password
pass, ok := b[u]
if !ok {
return errResp
}

// compare the password
if pass != p {
return errResp
}

// add username to the context
return &Response{
Context: context.WithValue(ctx, AUTH_USERNAME_KEY, u),
}
}

const basicPrefix = "Basic "

// parseBasicAuth parses an HTTP Basic Authentication string.
// taken from net/http/request.go
func parseBasicAuth(auth string) (username, password string, ok bool) {
if !strings.HasPrefix(auth, basicPrefix) {
return
}
c, err := base64.StdEncoding.DecodeString(auth[len(basicPrefix):])
if err != nil {
return
}
cs := string(c)
s := strings.IndexByte(cs, ':')
if s < 0 {
return
}
return cs[:s], cs[s+1:], true
}

/****
JWT
****/

type jwtAuth struct {
secret string
}

func NewJWTAuthFunc(secret string) AuthFunc {
j := &jwtAuth{secret: secret}
return j.authenticate
}

const bearerPrefix = "Bearer "

func (j *jwtAuth) authenticate(ctx context.Context, auth string) *Response {
// Remove 'Bearer' prefix
if !strings.HasPrefix(auth, bearerPrefix) && !strings.HasPrefix(auth, strings.ToLower(bearerPrefix)) {
return &Response{
Err: errors.New("unauthorized: invalid authentication provided"),
StatusCode: http.StatusUnauthorized,
}
}

token := auth[len(bearerPrefix):]

_, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method")
}
return []byte(j.secret), nil
})
if err != nil {
return &Response{
Err: err,
StatusCode: http.StatusUnauthorized,
}
}

return &Response{
Context: context.WithValue(ctx, CONTEXT_JWT, token),
}
}
151 changes: 151 additions & 0 deletions middleware_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package rye

import (
"net/http"
"net/http/httptest"

"context"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

const AUTH_HEADER_NAME = "Authorization"

var _ = Describe("Auth Middleware", func() {
var (
request *http.Request
response *httptest.ResponseRecorder

testHandler func(http.ResponseWriter, *http.Request) *Response
)

BeforeEach(func() {
response = httptest.NewRecorder()
})

Context("auth", func() {
var (
fakeAuth *recorder
)

BeforeEach(func() {
fakeAuth = &recorder{}

testHandler = NewMiddlewareAuth(fakeAuth.authFunc)
request = &http.Request{
Header: map[string][]string{},
}
})

It("passes the header to the auth func", func() {
testAuth := "foobar"
request.Header.Add(AUTH_HEADER_NAME, testAuth)
resp := testHandler(response, request)

Expect(resp).To(BeNil())
Expect(fakeAuth.header).To(Equal(testAuth))
})

Context("when no header is found", func() {
It("errors", func() {
resp := testHandler(response, request)

Expect(resp).ToNot(BeNil())
Expect(resp.Err).ToNot(BeNil())
Expect(resp.Err.Error()).To(ContainSubstring("no authentication"))
})
})
})

Context("Basic Auth", func() {
var (
username = "user1"
pass = "mypass"
)

BeforeEach(func() {
testHandler = NewMiddlewareAuth(NewBasicAuthFunc(map[string]string{
username: pass,
}))

request = &http.Request{
Header: map[string][]string{},
}
})

It("validates the password", func() {
request.SetBasicAuth(username, pass)
resp := testHandler(response, request)

Expect(resp.Err).To(BeNil())
})

It("adds the username to context", func() {
request.SetBasicAuth(username, pass)
resp := testHandler(response, request)

Expect(resp.Err).To(BeNil())

ctxUname := resp.Context.Value(AUTH_USERNAME_KEY)
uname, ok := ctxUname.(string)
Expect(ok).To(BeTrue())
Expect(uname).To(Equal(username))
})

It("preserves the request context", func() {

})

It("errors if username unknown", func() {
request.SetBasicAuth("noname", pass)
resp := testHandler(response, request)

Expect(resp.Err).ToNot(BeNil())
Expect(resp.Err.Error()).To(ContainSubstring("invalid auth"))
})

It("errors if password wrong", func() {
request.SetBasicAuth(username, "wrong")
resp := testHandler(response, request)

Expect(resp.Err).ToNot(BeNil())
Expect(resp.Err.Error()).To(ContainSubstring("invalid auth"))
})

Context("parseBasicAuth", func() {
It("errors if header not basic", func() {
request.Header.Add(AUTH_HEADER_NAME, "wrong")
resp := testHandler(response, request)

Expect(resp.Err).ToNot(BeNil())
Expect(resp.Err.Error()).To(ContainSubstring("invalid auth"))
})

It("errors if header not base64", func() {
request.Header.Add(AUTH_HEADER_NAME, "Basic ------")
resp := testHandler(response, request)

Expect(resp.Err).ToNot(BeNil())
Expect(resp.Err.Error()).To(ContainSubstring("invalid auth"))
})

It("errors if header wrong format", func() {
request.Header.Add(AUTH_HEADER_NAME, "Basic YXNkZgo=") // asdf no `:`
resp := testHandler(response, request)

Expect(resp.Err).ToNot(BeNil())
Expect(resp.Err.Error()).To(ContainSubstring("invalid auth"))
})
})
})
})

type recorder struct {
header string
}

func (r *recorder) authFunc(ctx context.Context, s string) *Response {
r.header = s
return nil
}
Loading

0 comments on commit 86e3e6d

Please sign in to comment.