forked from dstpierre/gosaas
-
Notifications
You must be signed in to change notification settings - Fork 0
/
auth.go
155 lines (131 loc) · 3.63 KB
/
auth.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
package gosaas
import (
"context"
"encoding/base64"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/jlb922/gosaas/cache"
"github.com/jlb922/gosaas/data"
"github.com/jlb922/gosaas/model"
)
// Auth represents an authenticated user.
type Auth struct {
AccountID int64
UserID int64
Email string
Role model.Roles
}
// Authenticator middleware used to authenticate requests.
//
// There are 4 ways to authenticate a request:
// 1. Via an HTTP header named X-API-KEY.
// 2. Via a querystring parameter named "key=token".
// 3. Via a cookie named X-API-KEY.
// 4. Via basic authentication.
//
// For routes with MinimumRole set as model.RolePublic there's no authentication performed.
func Authenticator(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
mr := ctx.Value(ContextMinimumRole).(model.Roles)
key, pat, err := extractKeyFromRequest(r)
// if there's no authentication or an error
if mr > model.RolePublic {
if len(key) == 0 || err != nil {
http.Redirect(w, r, "/users/login", http.StatusSeeOther)
return
}
}
ca := &cache.Auth{}
// do we have this key on cache already?
var a Auth
if err := ca.Exists(key, &a); err != nil {
log.Println("error while trying to get cache auth", err)
}
if len(a.Email) > 0 {
ctx = context.WithValue(ctx, ContextAuth, a)
} else {
// if the route required public access we do not
// perform any authentication.
if mr == model.RolePublic {
next.ServeHTTP(w, r.WithContext(ctx))
return
}
db, ok := ctx.Value(ContextDatabase).(*data.DB)
if !ok {
http.Error(w, "database not available", http.StatusUnauthorized)
return
}
id, t := model.ParseToken(key)
acct, usr, err := db.Users.Auth(id, t, pat)
if err != nil {
er := fmt.Sprintf("invalid token key: %v", err)
http.Error(w, er, http.StatusUnauthorized)
return
}
a.AccountID = acct.ID
a.Email = usr.Email
a.UserID = usr.ID
a.Role = usr.Role
// save it to cache
ca.Set(key, a, 30*time.Minute)
ctx = context.WithValue(ctx, ContextAuth, a)
}
// we authorize the request and redirect to login if insufficient
if a.Role < mr {
// TODO: User messaging about lack of role
fmt.Println("Insufficient User Role for this route")
http.Redirect(w, r, "/users/login", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func extractKeyFromRequest(r *http.Request) (key string, pat bool, err error) {
// first let's look if the X-API-KEY is present in the HTTP header
key = r.Header.Get("X-API-KEY")
if len(key) > 0 {
return
}
// check the query string
key = r.URL.Query().Get("key")
if len(key) > 0 {
return
}
// check for cookie
ck, er := r.Cookie("X-API-KEY")
if er != nil {
// If it's ErrNoCookie we must continue
// otherwise this is a legit error
if er != http.ErrNoCookie {
err = er
return
}
} else {
key = ck.Value
return
}
// check if we are supplying basic auth
authorization := r.Header.Get("Authorization")
s := strings.SplitN(authorization, " ", 2)
if len(s) != 2 {
err = fmt.Errorf("invalid basic authentication format: %s - you must provide Basic base64token", authorization)
return
}
b, err := base64.StdEncoding.DecodeString(s[1])
if err != nil {
err = fmt.Errorf("invalid basic authentication format: %s - you must provide Basic base64token", authorization)
return
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 {
err = fmt.Errorf("invalid basic authentication, your token should be _:access_token - got %s", string(b))
return
}
key = pair[1]
pat = true
return
}