forked from kataras/basicauth
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathuser.go
325 lines (281 loc) · 8.03 KB
/
user.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
package basicauth
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3"
)
// ReadFile can be used to customize the way the
// AllowUsersFile function is loading the filename from.
// Example of usage: embedded users.yml file.
// Defaults to the `ioutil.ReadFile` which reads the file from the physical disk.
var ReadFile = ioutil.ReadFile
// User can be implemented by custom struct values
// to provide the username and the password as
// basic authentication credentials for a user list.
//
// Look AllowUsers package-level function and the Options.Allow field.
type User interface {
GetUsername() string
GetPassword() string
}
// SimpleUser implements the User interface
// and it is used internally to store the
// current authenticated user to the HTTP request value
// when the Options.Allow does not return any value.
type SimpleUser struct {
Username string
Password string
}
// GetUsername returns the Username field.
func (u *SimpleUser) GetUsername() string {
return u.Username
}
// GetPassword returns the Username field.
func (u *SimpleUser) GetPassword() string {
return u.Password
}
// UserAuthOptions holds optional user authentication options
// that can be given to the builtin Default and Load (and AllowUsers, AllowUsersFile) functions.
type UserAuthOptions struct {
// Defaults to plain check, can be modified for encrypted passwords,
// see the BCRYPT optional function.
ComparePassword func(stored, userPassword string) bool
}
// UserAuthOption is the option function type
// for the Default and Load (and AllowUsers, AllowUsersFile) functions.
//
// See BCRYPT for an implementation.
type UserAuthOption func(*UserAuthOptions)
// BCRYPT it is a UserAuthOption, it compares a bcrypt hashed password with its user input.
// Reports true on success and false on failure.
//
// Useful when the users passwords are encrypted
// using the Provos and Mazières's bcrypt adaptive hashing algorithm.
// See https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf.
//
// Usage:
//
// Default(..., BCRYPT) OR
// Load(..., BCRYPT) OR
// Options.Allow = AllowUsers(..., BCRYPT) OR
// OPtions.Allow = AllowUsersFile(..., BCRYPT)
func BCRYPT(opts *UserAuthOptions) {
opts.ComparePassword = func(stored, userPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(userPassword))
return err == nil
}
}
func toUserAuthOptions(opts []UserAuthOption) (options UserAuthOptions) {
for _, opt := range opts {
opt(&options)
}
if options.ComparePassword == nil {
options.ComparePassword = func(stored, userPassword string) bool {
return stored == userPassword
}
}
return options
}
// AllowUsers is an AuthFunc which authenticates user input based on a (static) user list.
// The "users" input parameter can be one of the following forms:
//
// map[string]string e.g. {username: password, username: password...}.
// []map[string]interface{} e.g. []{"username": "...", "password": "...", "other_field": ...}, ...}.
// []T which T completes the User interface.
// []T which T contains at least Username and Password fields.
//
// Usage:
// New(Options{Allow: AllowUsers(..., [BCRYPT])})
func AllowUsers(users interface{}, opts ...UserAuthOption) AuthFunc {
// create a local user structure to be used in the map copy,
// takes longer to initialize but faster to serve.
type user struct {
password string
ref interface{}
}
cp := make(map[string]*user)
v := reflect.Indirect(reflect.ValueOf(users))
switch v.Kind() {
case reflect.Slice:
for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface()
// MUST contain a username and password.
username, password, ok := extractUsernameAndPassword(elem)
if !ok {
continue
}
cp[username] = &user{
password: password,
ref: elem,
}
}
case reflect.Map:
elem := v.Interface()
switch m := elem.(type) {
case map[string]string:
return userMap(m, opts...)
case map[string]interface{}:
username, password, ok := mapUsernameAndPassword(m)
if !ok {
break
}
cp[username] = &user{
password: password,
ref: m,
}
default:
panic(fmt.Sprintf("unsupported type of map: %T", users))
}
default:
panic(fmt.Sprintf("unsupported type: %T", users))
}
options := toUserAuthOptions(opts)
return func(_ *http.Request, username, password string) (interface{}, bool) {
if u, ok := cp[username]; ok { // fast map access,
if options.ComparePassword(u.password, password) {
return u.ref, true
}
}
return nil, false
}
}
func userMap(usernamePassword map[string]string, opts ...UserAuthOption) AuthFunc {
options := toUserAuthOptions(opts)
return func(_ *http.Request, username, password string) (interface{}, bool) {
pass, ok := usernamePassword[username]
return nil, ok && options.ComparePassword(pass, password)
}
}
// AllowUsersFile is an AuthFunc which authenticates user input based on a (static) user list
// loaded from a file on initialization.
//
// Example Code:
//
// New(Options{Allow: AllowUsersFile("users.yml", BCRYPT)})
//
// The users.yml file looks like the following:
// - username: kataras
// password: kataras_pass
// age: 27
// role: admin
// - username: makis
// password: makis_password
// ...
func AllowUsersFile(jsonOrYamlFilename string, opts ...UserAuthOption) AuthFunc {
var (
usernamePassword map[string]string
// no need to support too much forms, this would be for:
// "$username": { "password": "$pass", "other_field": ...}
userList []map[string]interface{}
)
if err := decodeFile(jsonOrYamlFilename, &usernamePassword, &userList); err != nil {
panic(err)
}
if len(usernamePassword) > 0 {
// JSON Form: { "$username":"$pass", "$username": "$pass" }
// YAML Form: $username: $pass
// $username: $pass
return userMap(usernamePassword, opts...)
}
if len(userList) > 0 {
// JSON Form: [{"username": "$username", "password": "$pass", "other_field": ...}, {"username": ...}, ... ]
// YAML Form:
// - username: $username
// password: $password
// other_field: ...
return AllowUsers(userList, opts...)
}
panic("malformed document file: " + jsonOrYamlFilename)
}
func decodeFile(src string, dest ...interface{}) error {
data, err := ReadFile(src)
if err != nil {
return err
}
// We use unmarshal instead of file decoder
// as we may need to read it more than once (dests, see below).
var (
unmarshal func(data []byte, v interface{}) error
ext string
)
if idx := strings.LastIndexByte(src, '.'); idx > 0 {
ext = src[idx:]
}
switch ext {
case "", ".json":
unmarshal = json.Unmarshal
case ".yml", ".yaml":
unmarshal = yaml.Unmarshal
default:
return fmt.Errorf("unexpected file extension: %s", ext)
}
var (
ok bool
lastErr error
)
for _, d := range dest {
if err = unmarshal(data, d); err == nil {
ok = true
} else {
lastErr = err
}
}
if !ok {
return lastErr
}
return nil // if at least one is succeed we are ok.
}
func extractUsernameAndPassword(s interface{}) (username, password string, ok bool) {
if s == nil {
return
}
switch u := s.(type) {
case User:
username = u.GetUsername()
password = u.GetPassword()
ok = username != "" && password != ""
return
case map[string]interface{}:
return mapUsernameAndPassword(u)
default:
b, err := json.Marshal(u)
if err != nil {
return
}
var m map[string]interface{}
if err = json.Unmarshal(b, &m); err != nil {
return
}
return mapUsernameAndPassword(m)
}
}
func mapUsernameAndPassword(m map[string]interface{}) (username, password string, ok bool) {
// type of username: password.
if len(m) == 1 {
for username, v := range m {
if password, ok := v.(string); ok {
ok := username != "" && password != ""
return username, password, ok
}
}
}
var usernameFound, passwordFound bool
for k, v := range m {
switch k {
case "username", "Username":
username, usernameFound = v.(string)
case "password", "Password":
password, passwordFound = v.(string)
}
if usernameFound && passwordFound {
ok = true
break
}
}
return
}