forked from openshift/origin
-
Notifications
You must be signed in to change notification settings - Fork 1
/
htpasswd.go
203 lines (176 loc) · 4.89 KB
/
htpasswd.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
package htpasswd
import (
"bufio"
"crypto/sha1"
"encoding/base64"
"errors"
"os"
"strings"
"github.com/golang/glog"
authapi "github.com/openshift/origin/pkg/auth/api"
"github.com/openshift/origin/pkg/auth/authenticator"
"golang.org/x/crypto/bcrypt"
"k8s.io/apiserver/pkg/authentication/user"
)
// Authenticator watches a file generated by htpasswd to validate usernames and passwords
type Authenticator struct {
providerName string
file string
fileInfo os.FileInfo
mapper authapi.UserIdentityMapper
usernames map[string]string
}
// New returns an authenticator which will validate usernames and passwords against the given htpasswd file
func New(providerName string, file string, mapper authapi.UserIdentityMapper) (authenticator.Password, error) {
auth := &Authenticator{
providerName: providerName,
file: file,
mapper: mapper,
}
if err := auth.loadIfNeeded(); err != nil {
return nil, err
}
return auth, nil
}
func (a *Authenticator) AuthenticatePassword(username, password string) (user.Info, bool, error) {
if err := a.loadIfNeeded(); err != nil {
return nil, false, err
}
if len(username) > 255 {
username = username[:255]
}
if strings.Contains(username, ":") {
return nil, false, errors.New("Usernames may not contain : characters")
}
hash, ok := a.usernames[username]
if !ok {
return nil, false, nil
}
if ok, err := testPassword(password, hash); !ok || err != nil {
return nil, false, err
}
identity := authapi.NewDefaultUserIdentityInfo(a.providerName, username)
user, err := a.mapper.UserFor(identity)
if err != nil {
glog.V(4).Infof("Error creating or updating mapping for: %#v due to %v", identity, err)
return nil, false, err
}
glog.V(4).Infof("Got userIdentityMapping: %#v", user)
return user, true, nil
}
func (a *Authenticator) load() error {
file, err := os.Open(a.file)
if err != nil {
return err
}
defer file.Close()
newusernames := map[string]string{}
warnedusernames := map[string]bool{}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if len(line) == 0 {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
glog.Warningf("Ignoring malformed htpasswd line: %s", line)
continue
}
username := parts[0]
password := parts[1]
if _, duplicate := newusernames[username]; duplicate {
if _, warned := warnedusernames[username]; !warned {
warnedusernames[username] = true
glog.Warningf("%s contains multiple passwords for user '%s'. The last one specified will be used.", a.file, username)
}
}
newusernames[username] = password
}
a.usernames = newusernames
return nil
}
func (a *Authenticator) loadIfNeeded() error {
info, err := os.Stat(a.file)
if err != nil {
return err
}
if a.fileInfo == nil || a.fileInfo.ModTime() != info.ModTime() {
glog.V(4).Infof("Loading htpasswd file %s...", a.file)
loadingErr := a.load()
if loadingErr != nil {
return err
}
a.fileInfo = info
return nil
}
return nil
}
func testPassword(password, hash string) (bool, error) {
switch {
case strings.HasPrefix(hash, "$apr1$"):
// MD5, default
return testMD5Password(password, hash)
case strings.HasPrefix(hash, "$2y$") || strings.HasPrefix(hash, "$2a$"):
// Bcrypt, secure
return testBCryptPassword(password, hash)
case strings.HasPrefix(hash, "{SHA}"):
// SHA-1, insecure
return testSHAPassword(password, hash[5:])
case len(hash) == 13:
// looks like crypt
return testCryptPassword(password, hash)
default:
return false, errors.New("Unrecognized hash type")
}
}
func testSHAPassword(password, hash string) (bool, error) {
if len(hash) == 0 {
return false, errors.New("Invalid SHA hash")
}
// Compute hash of password
shasum := sha1.Sum([]byte(password))
// Base-64 encode
base64shasum := base64.StdEncoding.EncodeToString(shasum[:])
// Compare
match := hash == base64shasum
return match, nil
}
func testBCryptPassword(password, hash string) (bool, error) {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
if err == bcrypt.ErrMismatchedHashAndPassword {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func testMD5Password(password, hash string) (bool, error) {
parts := strings.Split(hash, "$")
if len(parts) != 4 {
return false, errors.New("Malformed MD5 hash")
}
salt := parts[2]
if len(salt) == 0 {
return false, errors.New("Malformed MD5 hash: missing salt")
}
if len(salt) > 8 {
salt = salt[:8]
}
md5hash := parts[3]
if len(md5hash) == 0 {
return false, errors.New("Malformed MD5 hash: missing hash")
}
testhash := string(aprMD5([]byte(password), []byte(salt)))
match := testhash == hash
return match, nil
}
func testCryptPassword(password, hash string) (bool, error) {
// if len(password) > 8 {
// password = password[:8]
// }
// salt := hash[0:2]
// hash = hash[2:]
return false, errors.New("crypt password hashes are not supported")
}