forked from apexskier/httpauth
-
Notifications
You must be signed in to change notification settings - Fork 0
/
auth.go
373 lines (344 loc) · 11.7 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
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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
// Package httpauth implements cookie/session based authentication and
// authorization. Intended for use with the net/http or github.com/gorilla/mux
// packages, but may work with github.com/codegangsta/martini as well.
// Credentials are stored as a username + password hash, computed with bcrypt.
//
// Three user storage systems are currently implemented: file based
// (encoding/gob), sql databases (database/sql), and MongoDB databases.
//
// Access can be restricted by a users' role. A higher role will give more
// access.
//
// Users can be redirected to the page that triggered an authentication error.
//
// Messages describing the reason a user could not authenticate are saved in a
// cookie, and can be accessed with the Messages function.
//
// Example source can be found at
// https://github.com/apexskier/httpauth/blob/master/examples/server.go
package httpauth
import (
"errors"
"net/http"
"github.com/gorilla/sessions"
"golang.org/x/crypto/bcrypt"
)
// ErrDeleteNull is returned by DeleteUser when that user didn't exist at the
// time of call.
// ErrMissingUser is returned by Users when a user is not found.
var (
ErrDeleteNull = mkerror("deleting non-existant user")
ErrMissingUser = mkerror("can't find user")
)
// Role represents an interal role. Roles are essentially a string mapped to an
// integer. Roles must be greater than zero.
type Role int
// UserData represents a single user. It contains the users username, email,
// and role as well as a hash of their password. When creating
// users, you should not specify a hash; it will be generated in the Register
// and Update functions.
type UserData struct {
Username string `bson:"Username"`
Email string `bson:"Email"`
Hash []byte `bson:"Hash"`
Role string `bson:"Role"`
}
// Authorizer structures contain the store of user session cookies a reference
// to a backend storage system.
type Authorizer struct {
cookiejar *sessions.CookieStore
backend AuthBackend
defaultRole string
roles map[string]Role
}
// The AuthBackend interface defines a set of methods an AuthBackend must
// implement.
type AuthBackend interface {
SaveUser(u UserData) error
User(username string) (user UserData, e error)
Users() (users []UserData, e error)
DeleteUser(username string) error
Close()
}
// Helper function to add a user directed message to a message queue.
func (a Authorizer) addMessage(rw http.ResponseWriter, req *http.Request, message string) {
messageSession, _ := a.cookiejar.Get(req, "messages")
defer messageSession.Save(req, rw)
messageSession.AddFlash(message)
}
// Helper function to save a redirect to the page a user tried to visit before
// logging in.
func (a Authorizer) goBack(rw http.ResponseWriter, req *http.Request) {
redirectSession, _ := a.cookiejar.Get(req, "redirects")
defer redirectSession.Save(req, rw)
redirectSession.Flashes()
redirectSession.AddFlash(req.URL.Path)
}
func mkerror(msg string) error {
return errors.New("httpauth: " + msg)
}
// NewAuthorizer returns a new Authorizer given an AuthBackend, a cookie store
// key, a default user role, and a map of roles. If the key changes, logged in
// users will need to reauthenticate.
//
// Roles are a map of string to httpauth.Role values (integers). Higher Role values
// have more access.
//
// Example roles:
//
// var roles map[string]httpauth.Role
// roles["user"] = 2
// roles["admin"] = 4
// roles["moderator"] = 3
func NewAuthorizer(backend AuthBackend, key []byte, defaultRole string, roles map[string]Role) (Authorizer, error) {
var a Authorizer
a.cookiejar = sessions.NewCookieStore([]byte(key))
a.backend = backend
a.roles = roles
a.defaultRole = defaultRole
if _, ok := roles[defaultRole]; !ok {
return a, mkerror("httpauth: defaultRole missing")
}
return a, nil
}
// Login logs a user in. They will be redirected to dest or to the last
// location an authorization redirect was triggered (if found) on success. A
// message will be added to the session on failure with the reason.
func (a Authorizer) Login(rw http.ResponseWriter, req *http.Request, u string, p string, dest string) error {
session, _ := a.cookiejar.Get(req, "auth")
if session.Values["username"] == u {
return mkerror("already authenticated")
}
if user, err := a.backend.User(u); err == nil {
verify := bcrypt.CompareHashAndPassword(user.Hash, []byte(p))
if verify != nil {
a.addMessage(rw, req, "Invalid username or password.")
return mkerror("password doesn't match")
}
} else {
a.addMessage(rw, req, "Invalid username or password.")
return mkerror("user not found")
}
session.Values["username"] = u
session.Save(req, rw)
redirectSession, _ := a.cookiejar.Get(req, "redirects")
if flashes := redirectSession.Flashes(); len(flashes) > 0 {
dest = flashes[0].(string)
}
http.Redirect(rw, req, dest, http.StatusSeeOther)
return nil
}
// Register and save a new user. Returns an error and adds a message if the
// username is in use.
//
// Pass in a instance of UserData with at least a username and email specified. If no role
// is given, the default one is used.
func (a Authorizer) Register(rw http.ResponseWriter, req *http.Request, user UserData, password string) error {
if user.Username == "" {
return mkerror("no username given")
}
if user.Email == "" {
return mkerror("no email given")
}
if user.Hash != nil {
return mkerror("hash will be overwritten")
}
if password == "" {
return mkerror("no password given")
}
// Validate username
_, err := a.backend.User(user.Username)
if err == nil {
a.addMessage(rw, req, "Username has been taken.")
return mkerror("user already exists")
} else if err != ErrMissingUser {
if err != nil {
return mkerror(err.Error())
}
return nil
}
// Generate and save hash
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return mkerror("couldn't save password: " + err.Error())
}
user.Hash = hash
// Validate role
if user.Role == "" {
user.Role = a.defaultRole
} else {
if _, ok := a.roles[user.Role]; !ok {
return mkerror("non-existant role")
}
}
err = a.backend.SaveUser(user)
if err != nil {
a.addMessage(rw, req, err.Error())
return mkerror(err.Error())
}
return nil
}
// Update changes data for an existing user.
// The behavior of the update varies depending on how the arguments are passed:
// If an empty username u is passed then it updates the current user from the session
// (self-edit scenario)
// If the username u is passed explicitly then it updates the passed username
// (admin update scenario)
// If an empty password p is passed then it keeps the original rather than
// regenerating the hash, if a new password is passed then it regenerates the hash.
// If an empty email e is passed then it keeps the orginal rather than updating it,
// if a new email is passedn then it updates it.
func (a Authorizer) Update(rw http.ResponseWriter, req *http.Request, u string, p string, e string) error {
var (
hash []byte
email string
username string
ok bool
)
if u != "" {
username = u
} else {
authSession, err := a.cookiejar.Get(req, "auth")
if err != nil {
return mkerror("couldn't get session needed to update user: " + err.Error())
}
username, ok = authSession.Values["username"].(string)
if !ok {
return mkerror("not logged in")
}
}
user, err := a.backend.User(username)
if err == ErrMissingUser {
a.addMessage(rw, req, "User doesn't exist.")
return mkerror("user doesn't exists")
} else if err != nil {
return mkerror(err.Error())
}
if p != "" {
hash, err = bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
if err != nil {
return mkerror("couldn't save password: " + err.Error())
}
} else {
hash = user.Hash
}
if e != "" {
email = e
} else {
email = user.Email
}
newuser := UserData{username, email, hash, user.Role}
err = a.backend.SaveUser(newuser)
if err != nil {
a.addMessage(rw, req, err.Error())
}
return nil
}
// Authorize checks if a user is logged in and returns an error on failed
// authentication. If redirectWithMessage is set, the page being authorized
// will be saved and a "Login to do that." message will be saved to the
// messages list. The next time the user logs in, they will be redirected back
// to the saved page.
func (a Authorizer) Authorize(rw http.ResponseWriter, req *http.Request, redirectWithMessage bool) error {
authSession, err := a.cookiejar.Get(req, "auth")
if err != nil {
if redirectWithMessage {
a.goBack(rw, req)
}
return mkerror("new authorization session")
}
/*if authSession.IsNew {
if redirectWithMessage {
a.goBack(rw, req)
a.addMessage(rw, req, "Log in to do that.")
}
return mkerror("no session existed")
}*/
username := authSession.Values["username"]
if !authSession.IsNew && username != nil {
_, err := a.backend.User(username.(string))
if err == ErrMissingUser {
authSession.Options.MaxAge = -1 // kill the cookie
authSession.Save(req, rw)
if redirectWithMessage {
a.goBack(rw, req)
a.addMessage(rw, req, "Log in to do that.")
}
return mkerror("user not found")
} else if err != nil {
return mkerror(err.Error())
}
}
if username == nil {
if redirectWithMessage {
a.goBack(rw, req)
a.addMessage(rw, req, "Log in to do that.")
}
return mkerror("user not logged in")
}
return nil
}
// AuthorizeRole runs Authorize on a user, then makes sure their role is at
// least as high as the specified one, failing if not.
func (a Authorizer) AuthorizeRole(rw http.ResponseWriter, req *http.Request, role string, redirectWithMessage bool) error {
r, ok := a.roles[role]
if !ok {
return mkerror("role not found")
}
if err := a.Authorize(rw, req, redirectWithMessage); err != nil {
return mkerror(err.Error())
}
authSession, _ := a.cookiejar.Get(req, "auth") // should I check err? I've already checked in call to Authorize
username := authSession.Values["username"]
if user, err := a.backend.User(username.(string)); err == nil {
if a.roles[user.Role] >= r {
return nil
}
a.addMessage(rw, req, "You don't have sufficient privileges.")
return mkerror("user doesn't have high enough role")
}
return mkerror("user not found")
}
// CurrentUser returns the currently logged in user and a boolean validating
// the information.
func (a Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (user UserData, e error) {
if err := a.Authorize(rw, req, false); err != nil {
return user, mkerror(err.Error())
}
authSession, _ := a.cookiejar.Get(req, "auth")
username, ok := authSession.Values["username"].(string)
if !ok {
return user, mkerror("User not found in authsession")
}
return a.backend.User(username)
}
// Logout clears an authentication session and add a logged out message.
func (a Authorizer) Logout(rw http.ResponseWriter, req *http.Request) error {
session, _ := a.cookiejar.Get(req, "auth")
defer session.Save(req, rw)
session.Options.MaxAge = -1 // kill the cookie
a.addMessage(rw, req, "Logged out.")
return nil
}
// DeleteUser removes a user from the Authorize. ErrMissingUser is returned if
// the user to be deleted isn't found.
func (a Authorizer) DeleteUser(username string) error {
err := a.backend.DeleteUser(username)
if err != nil && err != ErrDeleteNull {
return mkerror(err.Error())
}
return err
}
// Messages fetches a list of saved messages. Use this to get a nice message to print to
// the user on a login page or registration page in case something happened
// (username taken, invalid credentials, successful logout, etc).
func (a Authorizer) Messages(rw http.ResponseWriter, req *http.Request) []string {
session, _ := a.cookiejar.Get(req, "messages")
flashes := session.Flashes()
session.Save(req, rw)
var messages []string
for _, val := range flashes {
messages = append(messages, val.(string))
}
return messages
}