@marcel91200 / caddy Mirror · read-only / commits / f8366c2f
Fork 0

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 +)