summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/caddyauth/basicauth.go
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2019-10-10 14:37:27 -0600
committerMatthew Holt <mholt@users.noreply.github.com>2019-10-10 14:37:27 -0600
commitf8366c2f09c77a55dc53038cae0b101263488867 (patch)
tree359105a5265369a6510342291715d6fe556c5250 /modules/caddyhttp/caddyauth/basicauth.go
parentfe36d26b63b6398592e46604d1795f84ce0477d4 (diff)
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.
Diffstat (limited to 'modules/caddyhttp/caddyauth/basicauth.go')
-rw-r--r--modules/caddyhttp/caddyauth/basicauth.go164
1 files changed, 164 insertions, 0 deletions
diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go
new file mode 100644
index 0000000..b7c002b
--- /dev/null
+++ b/modules/caddyhttp/caddyauth/basicauth.go
@@ -0,0 +1,164 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package caddyauth
+
+import (
+ "crypto/sha256"
+ "crypto/subtle"
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+ caddy.RegisterModule(HTTPBasicAuth{})
+}
+
+// HTTPBasicAuth facilitates HTTP basic authentication.
+type HTTPBasicAuth struct {
+ HashRaw json.RawMessage `json:"hash,omitempty"`
+ AccountList []Account `json:"accounts,omitempty"`
+ Realm string `json:"realm,omitempty"`
+
+ Accounts map[string]Account `json:"-"`
+ Hash Comparer `json:"-"`
+}
+
+// CaddyModule returns the Caddy module information.
+func (HTTPBasicAuth) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ Name: "http.handlers.authentication.providers.http_basic",
+ New: func() caddy.Module { return new(HTTPBasicAuth) },
+ }
+}
+
+// Provision provisions the HTTP basic auth provider.
+func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
+ if hba.HashRaw == nil {
+ return fmt.Errorf("passwords must be hashed, so a hash must be defined")
+ }
+
+ // load password hasher
+ hashIface, err := ctx.LoadModuleInline("algorithm", "http.handlers.authentication.hashes", hba.HashRaw)
+ if err != nil {
+ return fmt.Errorf("loading password hasher module: %v", err)
+ }
+ hba.Hash = hashIface.(Comparer)
+ hba.HashRaw = nil // allow GC to deallocate
+
+ if hba.Hash == nil {
+ return fmt.Errorf("hash is required")
+ }
+
+ // load account list
+ hba.Accounts = make(map[string]Account)
+ for _, acct := range hba.AccountList {
+ if _, ok := hba.Accounts[acct.Username]; ok {
+ return fmt.Errorf("username is not unique: %s", acct.Username)
+ }
+ hba.Accounts[acct.Username] = acct
+ }
+ hba.AccountList = nil // allow GC to deallocate
+
+ return nil
+}
+
+// Authenticate validates the user credentials in req and returns the user, if valid.
+func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request) (User, bool, error) {
+ username, plaintextPasswordStr, ok := req.BasicAuth()
+
+ // if basic auth is missing or invalid, prompt for credentials
+ if !ok {
+ // browsers show a message that says something like:
+ // "The website says: <realm>"
+ // which is kinda dumb, but whatever.
+ realm := hba.Realm
+ if realm == "" {
+ realm = "restricted"
+ }
+
+ w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
+
+ return User{}, false, nil
+ }
+
+ plaintextPassword := []byte(plaintextPasswordStr)
+
+ account, accountExists := hba.Accounts[username]
+ // don't return early if account does not exist; we want
+ // to try to avoid side-channels that leak existence
+
+ same, err := hba.Hash.Compare(account.Password, plaintextPassword, account.Salt)
+ if err != nil {
+ return User{}, false, err
+ }
+ if !same || !accountExists {
+ return User{}, false, nil
+ }
+
+ return User{ID: username}, true, nil
+}
+
+// Comparer is a type that can securely compare
+// a plaintext password with a hashed password
+// in constant-time. Comparers should hash the
+// plaintext password and then use constant-time
+// comparison.
+type Comparer interface {
+ // Compare returns true if the result of hashing
+ // plaintextPassword with salt is hashedPassword,
+ // false otherwise. An error is returned only if
+ // there is a technical/configuration error.
+ Compare(hashedPassword, plaintextPassword, salt []byte) (bool, error)
+}
+
+type quickComparer struct{}
+
+func (quickComparer) Compare(theirHash, plaintext, _ []byte) (bool, error) {
+ ourHash := quickHash(plaintext)
+ return hashesMatch(ourHash, theirHash), nil
+}
+
+func hashesMatch(pwdHash1, pwdHash2 []byte) bool {
+ return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1
+}
+
+// quickHash returns the SHA-256 of v. It
+// is not secure for password storage, but
+// it is useful for efficiently normalizing
+// the length of plaintext passwords for
+// constant-time comparisons.
+//
+// Errors are discarded.
+func quickHash(v []byte) []byte {
+ h := sha256.New()
+ h.Write([]byte(v))
+ return h.Sum(nil)
+}
+
+// Account contains a username, password, and salt (if applicable).
+type Account struct {
+ Username string `json:"username"`
+ Password []byte `json:"password"`
+ Salt []byte `json:"salt,omitempty"` // for algorithms where external salt is needed
+}
+
+// Interface guards
+var (
+ _ caddy.Provisioner = (*HTTPBasicAuth)(nil)
+ _ Authenticator = (*HTTPBasicAuth)(nil)
+)