-
Notifications
You must be signed in to change notification settings - Fork 3
/
token.go
126 lines (107 loc) · 3.61 KB
/
token.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
package tokenauth
import (
"crypto/md5"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"github.com/ant0ine/go-json-rest/rest"
"log"
"net/http"
"strings"
)
var tokenEntropy = 32
// AuthTokenMiddleware provides a Token Auth implementation. On success, the wrapped middleware
// is called, and the userID is made available as request.Env["REMOTE_USER"].(string)
type AuthTokenMiddleware struct {
// Realm name to display to the user. Required.
Realm string
// Callback function that should perform the authentication of the user based on token.
// Must return userID as string on success, empty string on failure. Required.
// The returned userID is normally the primary key for your user record.
Authenticator func(token string) string
// Callback function that should perform the authorization of the authenticated user.
// Must return true on success, false on failure. Optional, defaults to success.
// Called only after an authentication success.
Authorizer func(request *rest.Request) bool
}
// MiddlewareFunc makes AuthTokenMiddleware implement the Middleware interface.
func (mw *AuthTokenMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc {
if mw.Realm == "" {
log.Fatal("Realm is required")
}
if mw.Authenticator == nil {
log.Fatal("Authenticator is required")
}
if mw.Authorizer == nil {
mw.Authorizer = func(request *rest.Request) bool {
return true
}
}
return func(writer rest.ResponseWriter, request *rest.Request) {
authHeader := request.Header.Get("Authorization")
// Authorization header was not provided
if authHeader == "" {
mw.unauthorized(writer)
return
}
token, err := decodeAuthHeader(authHeader)
// Authorization header was *malformed* such that we couldn't extract a token
if err != nil {
mw.unauthorized(writer)
return
}
userID := mw.Authenticator(token)
// The token didn't map to a user, it's most likely either invalid or expired
if userID == "" {
mw.unauthorized(writer)
return
}
// The user's token was valid, but they're not authorized for the current request
if !mw.Authorizer(request) {
mw.unauthorized(writer)
return
}
request.Env["REMOTE_USER"] = userID
handler(writer, request)
}
}
func (mw *AuthTokenMiddleware) unauthorized(writer rest.ResponseWriter) {
writer.Header().Set("WWW-Authenticate", "Token realm="+mw.Realm)
rest.Error(writer, "Not Authorized", http.StatusUnauthorized)
}
// Extract the token from an Authorization header
func decodeAuthHeader(header string) (string, error) {
parts := strings.SplitN(header, " ", 2)
if !(len(parts) == 2 && parts[0] == "Token") {
return "", errors.New("Invalid Authorization header")
}
_, err := base64.URLEncoding.DecodeString(parts[1])
if err != nil {
return "", errors.New("Token encoding not valid")
}
return string(parts[1]), nil
}
// New generates a new random token
func New() (string, error) {
bytes := make([]byte, tokenEntropy)
_, err := rand.Read(bytes[:cap(bytes)])
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
// Equal does constant-time XOR comparison of two tokens
func Equal(a, b string) bool {
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}
// Hash applies a simple MD5 hash over a token, making it safe to store
func Hash(token string) string {
hashed := md5.Sum([]byte(token))
return base64.URLEncoding.EncodeToString(hashed[:])
}
// Token extracts current request's token, useful for logout and refresh where it's used post-auth
func Token(request *rest.Request) (string, error) {
authHeader := request.Header.Get("Authorization")
return decodeAuthHeader(authHeader)
}