Skip to content
This repository was archived by the owner on Dec 23, 2024. It is now read-only.

Commit 86e3e6d

Browse files
authored
Merge pull request #33 from InVisionApp/baiscauth
Auth middlewares
2 parents bc1eed7 + cadc967 commit 86e3e6d

File tree

6 files changed

+343
-49
lines changed

6 files changed

+343
-49
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,15 @@ the example using Gorilla:
226226
| [Access Token](middleware_accesstoken.go) | Provide Access Token validation |
227227
| [CIDR](middleware_cidr.go) | Provide request IP whitelisting |
228228
| [CORS](middleware_cors.go) | Provide CORS functionality for routes |
229-
| [JWT](middleware_jwt.go) | Provide JWT validation |
229+
| [Auth](middleware_auth.go) | Provide Authorization header validation (basic auth, JWT) |
230230
| [Route Logger](middleware_routelogger.go) | Provide basic logging for a specific route |
231231
| [Static File](middleware_static_file.go) | Provides serving a single file |
232232
| [Static Filesystem](middleware_static_filesystem.go) | Provides serving a single file |
233233

234234

235235
### A Note on the JWT Middleware
236236

237-
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:
237+
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:
238238
```go
239239
func getJWTfromContext(rw http.ResponseWriter, r *http.Request) *rye.Response {
240240
// Retrieving the value is easy!

example/rye_example.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import (
77
"net/http"
88

99
"github.com/InVisionApp/rye"
10-
log "github.com/sirupsen/logrus"
1110
"github.com/cactus/go-statsd-client/statsd"
1211
"github.com/gorilla/mux"
12+
log "github.com/sirupsen/logrus"
1313
)
1414

1515
func main() {
@@ -41,11 +41,22 @@ func main() {
4141
homeHandler,
4242
})).Methods("GET", "OPTIONS")
4343

44+
// If you perform an `curl -i http://localhost:8181/jwt \
45+
// -H "Authorization: Basic dXNlcjE6cGFzczEK"
46+
// you will see that we are allowed through to the handler, if the header is changed, you will get a 401
47+
routes.Handle("/basic-auth", middlewareHandler.Handle([]rye.Handler{
48+
rye.NewMiddlewareAuth(rye.NewBasicAuthFunc(map[string]string{
49+
"user1": "pass1",
50+
"user2": "pass2",
51+
})),
52+
getJwtFromContextHandler,
53+
})).Methods("GET")
54+
4455
// If you perform an `curl -i http://localhost:8181/jwt \
4556
// -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
4657
// you will see that we are allowed through to the handler, if the sample token is changed, we will get a 401
4758
routes.Handle("/jwt", middlewareHandler.Handle([]rye.Handler{
48-
rye.NewMiddlewareJWT("secret"),
59+
rye.NewMiddlewareAuth(rye.NewJWTAuthFunc("secret")),
4960
getJwtFromContextHandler,
5061
})).Methods("GET")
5162

middleware_auth.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package rye
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"strings"
10+
11+
jwt "github.com/dgrijalva/jwt-go"
12+
)
13+
14+
/*
15+
NewMiddlewareAuth creates a new middleware to extract the Authorization header
16+
from a request and validate it. It accepts a func of type AuthFunc which is
17+
used to do the credential validation.
18+
An AuthFuncs for Basic auth and JWT are provided here.
19+
20+
Example usage:
21+
22+
routes.Handle("/some/route", myMWHandler.Handle(
23+
[]rye.Handler{
24+
rye.NewMiddlewareAuth(rye.NewBasicAuthFunc(map[string]string{
25+
"user1": "my_password",
26+
})),
27+
yourHandler,
28+
})).Methods("POST")
29+
*/
30+
31+
type AuthFunc func(context.Context, string) *Response
32+
33+
func NewMiddlewareAuth(authFunc AuthFunc) func(rw http.ResponseWriter, req *http.Request) *Response {
34+
return func(rw http.ResponseWriter, r *http.Request) *Response {
35+
auth := r.Header.Get("Authorization")
36+
if auth == "" {
37+
return &Response{
38+
Err: errors.New("unauthorized: no authentication provided"),
39+
StatusCode: http.StatusUnauthorized,
40+
}
41+
}
42+
43+
return authFunc(r.Context(), auth)
44+
}
45+
}
46+
47+
/***********
48+
Basic Auth
49+
***********/
50+
51+
func NewBasicAuthFunc(userPass map[string]string) AuthFunc {
52+
return basicAuth(userPass).authenticate
53+
}
54+
55+
type basicAuth map[string]string
56+
57+
const AUTH_USERNAME_KEY = "request-username"
58+
59+
// basicAuth.authenticate meets the AuthFunc type
60+
func (b basicAuth) authenticate(ctx context.Context, auth string) *Response {
61+
errResp := &Response{
62+
Err: errors.New("unauthorized: invalid authentication provided"),
63+
StatusCode: http.StatusUnauthorized,
64+
}
65+
66+
// parse the Authorization header
67+
u, p, ok := parseBasicAuth(auth)
68+
if !ok {
69+
return errResp
70+
}
71+
72+
// get the password
73+
pass, ok := b[u]
74+
if !ok {
75+
return errResp
76+
}
77+
78+
// compare the password
79+
if pass != p {
80+
return errResp
81+
}
82+
83+
// add username to the context
84+
return &Response{
85+
Context: context.WithValue(ctx, AUTH_USERNAME_KEY, u),
86+
}
87+
}
88+
89+
const basicPrefix = "Basic "
90+
91+
// parseBasicAuth parses an HTTP Basic Authentication string.
92+
// taken from net/http/request.go
93+
func parseBasicAuth(auth string) (username, password string, ok bool) {
94+
if !strings.HasPrefix(auth, basicPrefix) {
95+
return
96+
}
97+
c, err := base64.StdEncoding.DecodeString(auth[len(basicPrefix):])
98+
if err != nil {
99+
return
100+
}
101+
cs := string(c)
102+
s := strings.IndexByte(cs, ':')
103+
if s < 0 {
104+
return
105+
}
106+
return cs[:s], cs[s+1:], true
107+
}
108+
109+
/****
110+
JWT
111+
****/
112+
113+
type jwtAuth struct {
114+
secret string
115+
}
116+
117+
func NewJWTAuthFunc(secret string) AuthFunc {
118+
j := &jwtAuth{secret: secret}
119+
return j.authenticate
120+
}
121+
122+
const bearerPrefix = "Bearer "
123+
124+
func (j *jwtAuth) authenticate(ctx context.Context, auth string) *Response {
125+
// Remove 'Bearer' prefix
126+
if !strings.HasPrefix(auth, bearerPrefix) && !strings.HasPrefix(auth, strings.ToLower(bearerPrefix)) {
127+
return &Response{
128+
Err: errors.New("unauthorized: invalid authentication provided"),
129+
StatusCode: http.StatusUnauthorized,
130+
}
131+
}
132+
133+
token := auth[len(bearerPrefix):]
134+
135+
_, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
136+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
137+
return nil, fmt.Errorf("Unexpected signing method")
138+
}
139+
return []byte(j.secret), nil
140+
})
141+
if err != nil {
142+
return &Response{
143+
Err: err,
144+
StatusCode: http.StatusUnauthorized,
145+
}
146+
}
147+
148+
return &Response{
149+
Context: context.WithValue(ctx, CONTEXT_JWT, token),
150+
}
151+
}

middleware_auth_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package rye
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
7+
"context"
8+
9+
. "github.com/onsi/ginkgo"
10+
. "github.com/onsi/gomega"
11+
)
12+
13+
const AUTH_HEADER_NAME = "Authorization"
14+
15+
var _ = Describe("Auth Middleware", func() {
16+
var (
17+
request *http.Request
18+
response *httptest.ResponseRecorder
19+
20+
testHandler func(http.ResponseWriter, *http.Request) *Response
21+
)
22+
23+
BeforeEach(func() {
24+
response = httptest.NewRecorder()
25+
})
26+
27+
Context("auth", func() {
28+
var (
29+
fakeAuth *recorder
30+
)
31+
32+
BeforeEach(func() {
33+
fakeAuth = &recorder{}
34+
35+
testHandler = NewMiddlewareAuth(fakeAuth.authFunc)
36+
request = &http.Request{
37+
Header: map[string][]string{},
38+
}
39+
})
40+
41+
It("passes the header to the auth func", func() {
42+
testAuth := "foobar"
43+
request.Header.Add(AUTH_HEADER_NAME, testAuth)
44+
resp := testHandler(response, request)
45+
46+
Expect(resp).To(BeNil())
47+
Expect(fakeAuth.header).To(Equal(testAuth))
48+
})
49+
50+
Context("when no header is found", func() {
51+
It("errors", func() {
52+
resp := testHandler(response, request)
53+
54+
Expect(resp).ToNot(BeNil())
55+
Expect(resp.Err).ToNot(BeNil())
56+
Expect(resp.Err.Error()).To(ContainSubstring("no authentication"))
57+
})
58+
})
59+
})
60+
61+
Context("Basic Auth", func() {
62+
var (
63+
username = "user1"
64+
pass = "mypass"
65+
)
66+
67+
BeforeEach(func() {
68+
testHandler = NewMiddlewareAuth(NewBasicAuthFunc(map[string]string{
69+
username: pass,
70+
}))
71+
72+
request = &http.Request{
73+
Header: map[string][]string{},
74+
}
75+
})
76+
77+
It("validates the password", func() {
78+
request.SetBasicAuth(username, pass)
79+
resp := testHandler(response, request)
80+
81+
Expect(resp.Err).To(BeNil())
82+
})
83+
84+
It("adds the username to context", func() {
85+
request.SetBasicAuth(username, pass)
86+
resp := testHandler(response, request)
87+
88+
Expect(resp.Err).To(BeNil())
89+
90+
ctxUname := resp.Context.Value(AUTH_USERNAME_KEY)
91+
uname, ok := ctxUname.(string)
92+
Expect(ok).To(BeTrue())
93+
Expect(uname).To(Equal(username))
94+
})
95+
96+
It("preserves the request context", func() {
97+
98+
})
99+
100+
It("errors if username unknown", func() {
101+
request.SetBasicAuth("noname", pass)
102+
resp := testHandler(response, request)
103+
104+
Expect(resp.Err).ToNot(BeNil())
105+
Expect(resp.Err.Error()).To(ContainSubstring("invalid auth"))
106+
})
107+
108+
It("errors if password wrong", func() {
109+
request.SetBasicAuth(username, "wrong")
110+
resp := testHandler(response, request)
111+
112+
Expect(resp.Err).ToNot(BeNil())
113+
Expect(resp.Err.Error()).To(ContainSubstring("invalid auth"))
114+
})
115+
116+
Context("parseBasicAuth", func() {
117+
It("errors if header not basic", func() {
118+
request.Header.Add(AUTH_HEADER_NAME, "wrong")
119+
resp := testHandler(response, request)
120+
121+
Expect(resp.Err).ToNot(BeNil())
122+
Expect(resp.Err.Error()).To(ContainSubstring("invalid auth"))
123+
})
124+
125+
It("errors if header not base64", func() {
126+
request.Header.Add(AUTH_HEADER_NAME, "Basic ------")
127+
resp := testHandler(response, request)
128+
129+
Expect(resp.Err).ToNot(BeNil())
130+
Expect(resp.Err.Error()).To(ContainSubstring("invalid auth"))
131+
})
132+
133+
It("errors if header wrong format", func() {
134+
request.Header.Add(AUTH_HEADER_NAME, "Basic YXNkZgo=") // asdf no `:`
135+
resp := testHandler(response, request)
136+
137+
Expect(resp.Err).ToNot(BeNil())
138+
Expect(resp.Err.Error()).To(ContainSubstring("invalid auth"))
139+
})
140+
})
141+
})
142+
})
143+
144+
type recorder struct {
145+
header string
146+
}
147+
148+
func (r *recorder) authFunc(ctx context.Context, s string) *Response {
149+
r.header = s
150+
return nil
151+
}

0 commit comments

Comments
 (0)