Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
http: authentication module; hash-password cmd; http_basic provider
This implements HTTP basicauth into Caddy 2. The basic auth module will not work with passwords that are not securely hashed, so a subcommand hash-password was added to make it convenient to produce those hashes. Also included is Caddyfile support. Closes #2747.
Matthew Holt committed
Oct 10, 2019 at 14:37 UTC
f8366c2f09c77a55dc53038cae0b101263488867
6 files changed
+562
caddyconfig/httpcaddyfile/directives.go
+1
@@ -28,6 +28,7 @@ import (
28
var defaultDirectiveOrder = []string{
29
"rewrite",
30
"try_files",
31
+ "basicauth",
32
"headers",
33
"request_header",
34
"encode",
modules/caddyhttp/caddyauth/basicauth.go
new
+164
@@ -0,0 +1,164 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddyauth
16
+
17
+import (
18
+ "crypto/sha256"
19
+ "crypto/subtle"
20
+ "encoding/json"
21
+ "fmt"
22
+ "net/http"
23
+
24
+ "github.com/caddyserver/caddy/v2"
25
+)
26
+
27
+func init() {
28
+ caddy.RegisterModule(HTTPBasicAuth{})
29
+}
30
+
31
+// HTTPBasicAuth facilitates HTTP basic authentication.
32
+type HTTPBasicAuth struct {
33
+ HashRaw json.RawMessage `json:"hash,omitempty"`
34
+ AccountList []Account `json:"accounts,omitempty"`
35
+ Realm string `json:"realm,omitempty"`
36
+
37
+ Accounts map[string]Account `json:"-"`
38
+ Hash Comparer `json:"-"`
39
+}
40
+
41
+// CaddyModule returns the Caddy module information.
42
+func (HTTPBasicAuth) CaddyModule() caddy.ModuleInfo {
43
+ return caddy.ModuleInfo{
44
+ Name: "http.handlers.authentication.providers.http_basic",
45
+ New: func() caddy.Module { return new(HTTPBasicAuth) },
46
+ }
47
+}
48
+
49
+// Provision provisions the HTTP basic auth provider.
50
+func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
51
+ if hba.HashRaw == nil {
52
+ return fmt.Errorf("passwords must be hashed, so a hash must be defined")
53
+ }
54
+
55
+ // load password hasher
56
+ hashIface, err := ctx.LoadModuleInline("algorithm", "http.handlers.authentication.hashes", hba.HashRaw)
57
+ if err != nil {
58
+ return fmt.Errorf("loading password hasher module: %v", err)
59
+ }
60
+ hba.Hash = hashIface.(Comparer)
61
+ hba.HashRaw = nil // allow GC to deallocate
62
+
63
+ if hba.Hash == nil {
64
+ return fmt.Errorf("hash is required")
65
+ }
66
+
67
+ // load account list
68
+ hba.Accounts = make(map[string]Account)
69
+ for _, acct := range hba.AccountList {
70
+ if _, ok := hba.Accounts[acct.Username]; ok {
71
+ return fmt.Errorf("username is not unique: %s", acct.Username)
72
+ }
73
+ hba.Accounts[acct.Username] = acct
74
+ }
75
+ hba.AccountList = nil // allow GC to deallocate
76
+
77
+ return nil
78
+}
79
+
80
+// Authenticate validates the user credentials in req and returns the user, if valid.
81
+func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request) (User, bool, error) {
82
+ username, plaintextPasswordStr, ok := req.BasicAuth()
83
+
84
+ // if basic auth is missing or invalid, prompt for credentials
85
+ if !ok {
86
+ // browsers show a message that says something like:
87
+ // "The website says: <realm>"
88
+ // which is kinda dumb, but whatever.
89
+ realm := hba.Realm
90
+ if realm == "" {
91
+ realm = "restricted"
92
+ }
93
+
94
+ w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
95
+
96
+ return User{}, false, nil
97
+ }
98
+
99
+ plaintextPassword := []byte(plaintextPasswordStr)
100
+
101
+ account, accountExists := hba.Accounts[username]
102
+ // don't return early if account does not exist; we want
103
+ // to try to avoid side-channels that leak existence
104
+
105
+ same, err := hba.Hash.Compare(account.Password, plaintextPassword, account.Salt)
106
+ if err != nil {
107
+ return User{}, false, err
108
+ }
109
+ if !same || !accountExists {
110
+ return User{}, false, nil
111
+ }
112
+
113
+ return User{ID: username}, true, nil
114
+}
115
+
116
+// Comparer is a type that can securely compare
117
+// a plaintext password with a hashed password
118
+// in constant-time. Comparers should hash the
119
+// plaintext password and then use constant-time
120
+// comparison.
121
+type Comparer interface {
122
+ // Compare returns true if the result of hashing
123
+ // plaintextPassword with salt is hashedPassword,
124
+ // false otherwise. An error is returned only if
125
+ // there is a technical/configuration error.
126
+ Compare(hashedPassword, plaintextPassword, salt []byte) (bool, error)
127
+}
128
+
129
+type quickComparer struct{}
130
+
131
+func (quickComparer) Compare(theirHash, plaintext, _ []byte) (bool, error) {
132
+ ourHash := quickHash(plaintext)
133
+ return hashesMatch(ourHash, theirHash), nil
134
+}
135
+
136
+func hashesMatch(pwdHash1, pwdHash2 []byte) bool {
137
+ return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1
138
+}
139
+
140
+// quickHash returns the SHA-256 of v. It
141
+// is not secure for password storage, but
142
+// it is useful for efficiently normalizing
143
+// the length of plaintext passwords for
144
+// constant-time comparisons.
145
+//
146
+// Errors are discarded.
147
+func quickHash(v []byte) []byte {
148
+ h := sha256.New()
149
+ h.Write([]byte(v))
150
+ return h.Sum(nil)
151
+}
152
+
153
+// Account contains a username, password, and salt (if applicable).
154
+type Account struct {
155
+ Username string `json:"username"`
156
+ Password []byte `json:"password"`
157
+ Salt []byte `json:"salt,omitempty"` // for algorithms where external salt is needed
158
+}
159
+
160
+// Interface guards
161
+var (
162
+ _ caddy.Provisioner = (*HTTPBasicAuth)(nil)
163
+ _ Authenticator = (*HTTPBasicAuth)(nil)
164
+)
modules/caddyhttp/caddyauth/caddyauth.go
new
+102
@@ -0,0 +1,102 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddyauth
16
+
17
+import (
18
+ "encoding/json"
19
+ "fmt"
20
+ "log"
21
+ "net/http"
22
+
23
+ "github.com/caddyserver/caddy/v2"
24
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
25
+)
26
+
27
+func init() {
28
+ caddy.RegisterModule(Authentication{})
29
+}
30
+
31
+// Authentication is a middleware which provides user authentication.
32
+type Authentication struct {
33
+ ProvidersRaw map[string]json.RawMessage `json:"providers,omitempty"`
34
+
35
+ Providers map[string]Authenticator `json:"-"`
36
+}
37
+
38
+// CaddyModule returns the Caddy module information.
39
+func (Authentication) CaddyModule() caddy.ModuleInfo {
40
+ return caddy.ModuleInfo{
41
+ Name: "http.handlers.authentication",
42
+ New: func() caddy.Module { return new(Authentication) },
43
+ }
44
+}
45
+
46
+// Provision sets up a.
47
+func (a *Authentication) Provision(ctx caddy.Context) error {
48
+ a.Providers = make(map[string]Authenticator)
49
+ for modName, rawMsg := range a.ProvidersRaw {
50
+ val, err := ctx.LoadModule("http.handlers.authentication.providers."+modName, rawMsg)
51
+ if err != nil {
52
+ return fmt.Errorf("loading authentication provider module '%s': %v", modName, err)
53
+ }
54
+ a.Providers[modName] = val.(Authenticator)
55
+ }
56
+ a.ProvidersRaw = nil // allow GC to deallocate
57
+
58
+ return nil
59
+}
60
+
61
+func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
62
+ var user User
63
+ var authed bool
64
+ var err error
65
+ for provName, prov := range a.Providers {
66
+ user, authed, err = prov.Authenticate(w, r)
67
+ if err != nil {
68
+ log.Printf("[ERROR] Authenticating with %s: %v", provName, err)
69
+ continue
70
+ }
71
+ if authed {
72
+ break
73
+ }
74
+ }
75
+ if !authed {
76
+ return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated"))
77
+ }
78
+
79
+ repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
80
+ repl.Set("http.handlers.authentication.user.id", user.ID)
81
+
82
+ return next.ServeHTTP(w, r)
83
+}
84
+
85
+// Authenticator is a type which can authenticate a request.
86
+// If a request was not authenticated, it returns false. An
87
+// error is only returned if authenticating the request fails
88
+// for a technical reason (not for bad/missing credentials).
89
+type Authenticator interface {
90
+ Authenticate(http.ResponseWriter, *http.Request) (User, bool, error)
91
+}
92
+
93
+// User represents an authenticated user.
94
+type User struct {
95
+ ID string
96
+}
97
+
98
+// Interface guards
99
+var (
100
+ _ caddy.Provisioner = (*Authentication)(nil)
101
+ _ caddyhttp.MiddlewareHandler = (*Authentication)(nil)
102
+)
modules/caddyhttp/caddyauth/caddyfile.go
new
+104
@@ -0,0 +1,104 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddyauth
16
+
17
+import (
18
+ "encoding/base64"
19
+ "encoding/json"
20
+
21
+ "github.com/caddyserver/caddy/v2/caddyconfig"
22
+ "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
23
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
24
+)
25
+
26
+func init() {
27
+ httpcaddyfile.RegisterHandlerDirective("basicauth", parseCaddyfile)
28
+}
29
+
30
+// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
31
+//
32
+// basicauth [<matcher>] [<hash_algorithm>] {
33
+// <username> <hashed_password_base64> [<salt_base64>]
34
+// ...
35
+// }
36
+//
37
+// If no hash algorithm is supplied, bcrypt will be assumed.
38
+func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
39
+ var ba HTTPBasicAuth
40
+
41
+ for h.Next() {
42
+ var cmp Comparer
43
+ args := h.RemainingArgs()
44
+
45
+ var hashName string
46
+ switch len(args) {
47
+ case 0:
48
+ hashName = "bcrypt"
49
+ case 1:
50
+ hashName = args[0]
51
+ default:
52
+ return nil, h.ArgErr()
53
+ }
54
+
55
+ switch hashName {
56
+ case "bcrypt":
57
+ cmp = BcryptHash{}
58
+ case "scrypt":
59
+ cmp = ScryptHash{}
60
+ default:
61
+ return nil, h.Errf("unrecognized hash algorithm: %s", hashName)
62
+ }
63
+
64
+ ba.HashRaw = caddyconfig.JSONModuleObject(cmp, "algorithm", hashName, nil)
65
+
66
+ for h.NextBlock(0) {
67
+ username := h.Val()
68
+
69
+ var b64Pwd, b64Salt string
70
+ h.Args(&b64Pwd, &b64Salt)
71
+ if h.NextArg() {
72
+ return nil, h.ArgErr()
73
+ }
74
+
75
+ if username == "" || b64Pwd == "" {
76
+ return nil, h.Err("username and password cannot be empty or missing")
77
+ }
78
+
79
+ pwd, err := base64.StdEncoding.DecodeString(b64Pwd)
80
+ if err != nil {
81
+ return nil, h.Errf("decoding password: %v", err)
82
+ }
83
+ var salt []byte
84
+ if b64Salt != "" {
85
+ salt, err = base64.StdEncoding.DecodeString(b64Salt)
86
+ if err != nil {
87
+ return nil, h.Errf("decoding salt: %v", err)
88
+ }
89
+ }
90
+
91
+ ba.AccountList = append(ba.AccountList, Account{
92
+ Username: username,
93
+ Password: pwd,
94
+ Salt: salt,
95
+ })
96
+ }
97
+ }
98
+
99
+ return Authentication{
100
+ ProvidersRaw: map[string]json.RawMessage{
101
+ "http_basic": caddyconfig.JSON(ba, nil),
102
+ },
103
+ }, nil
104
+}
modules/caddyhttp/caddyauth/command.go
new
+80
@@ -0,0 +1,80 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddyauth
16
+
17
+import (
18
+ "encoding/base64"
19
+ "flag"
20
+ "fmt"
21
+
22
+ "github.com/caddyserver/caddy/v2"
23
+ caddycmd "github.com/caddyserver/caddy/v2/cmd"
24
+ "golang.org/x/crypto/bcrypt"
25
+ "golang.org/x/crypto/scrypt"
26
+)
27
+
28
+func init() {
29
+ caddycmd.RegisterCommand(caddycmd.Command{
30
+ Name: "hash-password",
31
+ Func: cmdHashPassword,
32
+ Usage: "--plaintext <password> [--salt <string>] [--algorithm <name>]",
33
+ Short: "Hashes a password and writes base64",
34
+ Long: `
35
+Convenient way to hash a plaintext password. The resulting
36
+hash is written to stdout as a base64 string.
37
+
38
+--algorithm may be bcrypt or scrypt. If script, the default
39
+parameters are used.
40
+
41
+Use the --salt flag for algorithms which require a salt to
42
+be provided (scrypt).
43
+`,
44
+ Flags: func() *flag.FlagSet {
45
+ fs := flag.NewFlagSet("hash-password", flag.ExitOnError)
46
+ fs.String("algorithm", "bcrypt", "Name of the hash algorithm")
47
+ fs.String("plaintext", "", "The plaintext password")
48
+ fs.String("salt", "", "The password salt")
49
+ return fs
50
+ }(),
51
+ })
52
+}
53
+
54
+func cmdHashPassword(fs caddycmd.Flags) (int, error) {
55
+ algorithm := fs.String("algorithm")
56
+ plaintext := []byte(fs.String("plaintext"))
57
+ salt := []byte(fs.String("salt"))
58
+
59
+ var hash []byte
60
+ var err error
61
+ switch algorithm {
62
+ case "bcrypt":
63
+ hash, err = bcrypt.GenerateFromPassword(plaintext, bcrypt.DefaultCost)
64
+ case "scrypt":
65
+ def := ScryptHash{}
66
+ def.SetDefaults()
67
+ hash, err = scrypt.Key(plaintext, salt, def.N, def.R, def.P, def.KeyLength)
68
+ default:
69
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)
70
+ }
71
+ if err != nil {
72
+ return caddy.ExitCodeFailedStartup, err
73
+ }
74
+
75
+ hashBase64 := base64.StdEncoding.EncodeToString([]byte(hash))
76
+
77
+ fmt.Println(hashBase64)
78
+
79
+ return 0, nil
80
+}
modules/caddyhttp/caddyauth/hashes.go
new
+111
@@ -0,0 +1,111 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddyauth
16
+
17
+import (
18
+ "github.com/caddyserver/caddy/v2"
19
+ "golang.org/x/crypto/bcrypt"
20
+ "golang.org/x/crypto/scrypt"
21
+)
22
+
23
+func init() {
24
+ caddy.RegisterModule(BcryptHash{})
25
+ caddy.RegisterModule(ScryptHash{})
26
+}
27
+
28
+// BcryptHash implements the bcrypt hash.
29
+type BcryptHash struct{}
30
+
31
+// CaddyModule returns the Caddy module information.
32
+func (BcryptHash) CaddyModule() caddy.ModuleInfo {
33
+ return caddy.ModuleInfo{
34
+ Name: "http.handlers.authentication.hashes.bcrypt",
35
+ New: func() caddy.Module { return new(BcryptHash) },
36
+ }
37
+}
38
+
39
+// Compare compares passwords.
40
+func (BcryptHash) Compare(hashed, plaintext, _ []byte) (bool, error) {
41
+ err := bcrypt.CompareHashAndPassword(hashed, plaintext)
42
+ if err == bcrypt.ErrMismatchedHashAndPassword {
43
+ return false, nil
44
+ }
45
+ if err != nil {
46
+ return false, err
47
+ }
48
+ return true, nil
49
+}
50
+
51
+// ScryptHash implements the scrypt KDF as a hash.
52
+type ScryptHash struct {
53
+ N int `json:"N,omitempty"`
54
+ R int `json:"r,omitempty"`
55
+ P int `json:"p,omitempty"`
56
+ KeyLength int `json:"key_length,omitempty"`
57
+}
58
+
59
+// CaddyModule returns the Caddy module information.
60
+func (ScryptHash) CaddyModule() caddy.ModuleInfo {
61
+ return caddy.ModuleInfo{
62
+ Name: "http.handlers.authentication.hashes.scrypt",
63
+ New: func() caddy.Module { return new(ScryptHash) },
64
+ }
65
+}
66
+
67
+// Provision sets up s.
68
+func (s *ScryptHash) Provision(_ caddy.Context) error {
69
+ s.SetDefaults()
70
+ return nil
71
+}
72
+
73
+// SetDefaults sets safe default parameters, but does
74
+// not overwrite existing values. Each default parameter
75
+// is set independently; it does not check to ensure
76
+// that r*p < 2^30. The defaults chosen are those as
77
+// recommended in 2019 by
78
+// https://godoc.org/golang.org/x/crypto/scrypt.
79
+func (s *ScryptHash) SetDefaults() {
80
+ if s.N == 0 {
81
+ s.N = 32768
82
+ }
83
+ if s.R == 0 {
84
+ s.R = 8
85
+ }
86
+ if s.P == 0 {
87
+ s.P = 1
88
+ }
89
+ if s.KeyLength == 0 {
90
+ s.KeyLength = 32
91
+ }
92
+}
93
+
94
+// Compare compares passwords.
95
+func (s ScryptHash) Compare(hashed, plaintext, salt []byte) (bool, error) {
96
+ ourHash, err := scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength)
97
+ if err != nil {
98
+ return false, err
99
+ }
100
+ if hashesMatch(hashed, ourHash) {
101
+ return true, nil
102
+ }
103
+ return false, nil
104
+}
105
+
106
+// Interface guards
107
+var (
108
+ _ Comparer = (*BcryptHash)(nil)
109
+ _ Comparer = (*ScryptHash)(nil)
110
+ _ caddy.Provisioner = (*ScryptHash)(nil)
111
+)