-
Notifications
You must be signed in to change notification settings - Fork 1
/
middleware.go
258 lines (217 loc) · 7.55 KB
/
middleware.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
// Package jwt is a simple net/http middleware for jwt
package jwt
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"strings"
"github.com/dgrijalva/jwt-go"
)
// A function called whenever an error is encountered
type errorHandler func(w http.ResponseWriter, r *http.Request, err string)
// TokenExtractor is a function that takes a request as input and returns
// either a token or an error. An error should only be returned if an attempt
// to specify a token was found, but the information was somehow incorrectly
// formed. In the case where a token is simply not present, this should not
// be treated as an error. An empty string should be returned in that case.
type TokenExtractor func(r *http.Request) (string, error)
// Options is a struct for specifying configuration options for the middleware.
type Options struct {
// The function that will return the Key to validate the JWT.
// It can be either a shared secret or a public key.
// Default value: nil
ValidationKeyGetter jwt.Keyfunc
// The function that will be called when there's an error validating the token
// Default value:
ErrorHandler errorHandler
// A function that extracts the token from the request
// Default: FromAuthHeader (i.e., from Authorization header as bearer token)
Extractor TokenExtractor
// When set, the middleware verifies that tokens are signed with the specific signing algorithm
// If the signing method is not constant the ValidationKeyGetter callback can be used to implement additional checks
// Important to avoid security issues described here: https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/
// Default: nil
SigningMethod jwt.SigningMethod
// The name of the property in the request where the user information
// from the JWT will be stored.
// Default value: "user"
UserProperty string
// A boolean indicating if the credentials are required or not
// Default value: false
CredentialsOptional bool
// Debug flag turns on debugging output
// Default: false
Debug bool
// When set, all requests with the OPTIONS method will use authentication
// Default: false
EnableAuthOnOptions bool
}
// JWTMiddleware contains jwt config options
type JWTMiddleware struct {
Options Options
}
// OnError the default function called whenever an error is encountered
func OnError(w http.ResponseWriter, r *http.Request, err string) {
http.Error(w, err, http.StatusUnauthorized)
}
// New constructs a new Secure instance with supplied options.
func New(options ...Options) *JWTMiddleware {
var opts Options
if len(options) == 0 {
opts = Options{}
} else {
opts = options[0]
}
if opts.UserProperty == "" {
opts.UserProperty = "user"
}
if opts.ErrorHandler == nil {
opts.ErrorHandler = OnError
}
if opts.Extractor == nil {
opts.Extractor = FromAuthHeader
}
return &JWTMiddleware{
Options: opts,
}
}
func (m *JWTMiddleware) logf(format string, args ...interface{}) {
if m.Options.Debug {
log.Printf(format, args...)
}
}
// HandlerWithNext Special implementation for Negroni, but could be used elsewhere.
func (m *JWTMiddleware) HandlerWithNext(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
req, err := m.CheckJWT(w, r)
// If there was an error, do not call next.
if err == nil && next != nil {
next(w, req)
}
}
// Handler implements the http handler
func (m *JWTMiddleware) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
req, err := m.CheckJWT(w, r)
// If there was an error, do not continue.
if err != nil {
return
}
h.ServeHTTP(w, req)
})
}
// FromAuthHeader is a "TokenExtractor" that takes a give request and extracts
// the JWT token from the Authorization header.
func FromAuthHeader(r *http.Request) (string, error) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return "", nil // No error, just no token
}
authHeaderParts := strings.Fields(authHeader)
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
return "", errors.New("Authorization header format must be Bearer {token}")
}
return authHeaderParts[1], nil
}
// FromParameter returns a function that extracts the token from the specified
// query string parameter
func FromParameter(param string) TokenExtractor {
return func(r *http.Request) (string, error) {
return r.URL.Query().Get(param), nil
}
}
// FromFirst returns a function that runs multiple token extractors and takes the
// first token it finds
func FromFirst(extractors ...TokenExtractor) TokenExtractor {
return func(r *http.Request) (string, error) {
for _, ex := range extractors {
token, err := ex(r)
if err != nil {
return "", err
}
if token != "" {
return token, nil
}
}
return "", nil
}
}
// FromCookie returns a function that extracts the token from the specified
// key in the HTTP cookie, like "access_token"
func FromCookie(accessTokenName string) TokenExtractor {
return func(r *http.Request) (string, error) {
cookie, err := r.Cookie(accessTokenName)
if err != nil {
return "", err
}
if cookie != nil {
return cookie.Value, nil
}
return "", nil
}
}
// CheckJWT check jwt
func (m *JWTMiddleware) CheckJWT(w http.ResponseWriter, r *http.Request) (*http.Request, error) {
if !m.Options.EnableAuthOnOptions {
if r.Method == "OPTIONS" {
return r, nil
}
}
// Use the specified token extractor to extract a token from the request
token, err := m.Options.Extractor(r)
// If debugging is turned on, log the outcome
if err != nil {
m.logf("Error extracting JWT: %v", err)
} else {
m.logf("Token extracted: %s", token)
}
// If an error occurs, call the error handler and return an error
if err != nil {
m.Options.ErrorHandler(w, r, err.Error())
return r, fmt.Errorf("Error extracting token: %v", err)
}
// If the token is empty...
if token == "" {
// Check if it was required
if m.Options.CredentialsOptional {
m.logf(" No credentials found (CredentialsOptional=true)")
// No error, just no token (and that is ok given that CredentialsOptional is true)
return r, nil
}
// If we get here, the required token is missing
errorMsg := "Required authorization token not found"
m.Options.ErrorHandler(w, r, errorMsg)
m.logf(" Error: No credentials found (CredentialsOptional=false)")
return r, fmt.Errorf(errorMsg)
}
// Now parse the token
parsedToken, err := jwt.Parse(token, m.Options.ValidationKeyGetter)
// Check if there was an error in parsing...
if err != nil {
m.logf("Error parsing token: %v", err)
m.Options.ErrorHandler(w, r, err.Error())
return r, fmt.Errorf("Error parsing token: %v", err)
}
if m.Options.SigningMethod != nil && m.Options.SigningMethod.Alg() != parsedToken.Header["alg"] {
message := fmt.Sprintf("Expected %s signing method but token specified %s",
m.Options.SigningMethod.Alg(),
parsedToken.Header["alg"])
m.logf("Error validating token algorithm: %s", message)
m.Options.ErrorHandler(w, r, errors.New(message).Error())
return r, fmt.Errorf("Error validating token algorithm: %s", message)
}
// Check if the parsed token is valid...
if !parsedToken.Valid {
m.logf("Token is invalid")
m.Options.ErrorHandler(w, r, "The token isn't valid")
return r, errors.New("Token is invalid")
}
m.logf("JWT: %+v", parsedToken)
// If we get here, everything worked and we can set the user property in context.
ctx := r.Context()
ctx = context.WithValue(ctx, m.Options.UserProperty, parsedToken)
return r.WithContext(ctx), nil
}