From f8366c2f09c77a55dc53038cae0b101263488867 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 10 Oct 2019 14:37:27 -0600 Subject: [PATCH] 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. --- caddyconfig/httpcaddyfile/directives.go | 1 + modules/caddyhttp/caddyauth/basicauth.go | 164 +++++++++++++++++++++++ modules/caddyhttp/caddyauth/caddyauth.go | 102 ++++++++++++++ modules/caddyhttp/caddyauth/caddyfile.go | 104 ++++++++++++++ modules/caddyhttp/caddyauth/command.go | 80 +++++++++++ modules/caddyhttp/caddyauth/hashes.go | 111 +++++++++++++++ 6 files changed, 562 insertions(+) create mode 100644 modules/caddyhttp/caddyauth/basicauth.go create mode 100644 modules/caddyhttp/caddyauth/caddyauth.go create mode 100644 modules/caddyhttp/caddyauth/caddyfile.go create mode 100644 modules/caddyhttp/caddyauth/command.go create mode 100644 modules/caddyhttp/caddyauth/hashes.go diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index 0d7e0e4c..f1ce0f11 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -28,6 +28,7 @@ import ( var defaultDirectiveOrder = []string{ "rewrite", "try_files", + "basicauth", "headers", "request_header", "encode", diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go new file mode 100644 index 00000000..b7c002be --- /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: " + // 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) +) diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go new file mode 100644 index 00000000..48d4fba1 --- /dev/null +++ b/modules/caddyhttp/caddyauth/caddyauth.go @@ -0,0 +1,102 @@ +// 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 ( + "encoding/json" + "fmt" + "log" + "net/http" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + caddy.RegisterModule(Authentication{}) +} + +// Authentication is a middleware which provides user authentication. +type Authentication struct { + ProvidersRaw map[string]json.RawMessage `json:"providers,omitempty"` + + Providers map[string]Authenticator `json:"-"` +} + +// CaddyModule returns the Caddy module information. +func (Authentication) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.authentication", + New: func() caddy.Module { return new(Authentication) }, + } +} + +// Provision sets up a. +func (a *Authentication) Provision(ctx caddy.Context) error { + a.Providers = make(map[string]Authenticator) + for modName, rawMsg := range a.ProvidersRaw { + val, err := ctx.LoadModule("http.handlers.authentication.providers."+modName, rawMsg) + if err != nil { + return fmt.Errorf("loading authentication provider module '%s': %v", modName, err) + } + a.Providers[modName] = val.(Authenticator) + } + a.ProvidersRaw = nil // allow GC to deallocate + + return nil +} + +func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + var user User + var authed bool + var err error + for provName, prov := range a.Providers { + user, authed, err = prov.Authenticate(w, r) + if err != nil { + log.Printf("[ERROR] Authenticating with %s: %v", provName, err) + continue + } + if authed { + break + } + } + if !authed { + return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")) + } + + repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) + repl.Set("http.handlers.authentication.user.id", user.ID) + + return next.ServeHTTP(w, r) +} + +// Authenticator is a type which can authenticate a request. +// If a request was not authenticated, it returns false. An +// error is only returned if authenticating the request fails +// for a technical reason (not for bad/missing credentials). +type Authenticator interface { + Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) +} + +// User represents an authenticated user. +type User struct { + ID string +} + +// Interface guards +var ( + _ caddy.Provisioner = (*Authentication)(nil) + _ caddyhttp.MiddlewareHandler = (*Authentication)(nil) +) diff --git a/modules/caddyhttp/caddyauth/caddyfile.go b/modules/caddyhttp/caddyauth/caddyfile.go new file mode 100644 index 00000000..36003246 --- /dev/null +++ b/modules/caddyhttp/caddyauth/caddyfile.go @@ -0,0 +1,104 @@ +// 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 ( + "encoding/base64" + "encoding/json" + + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("basicauth", parseCaddyfile) +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// basicauth [] [] { +// [] +// ... +// } +// +// If no hash algorithm is supplied, bcrypt will be assumed. +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + var ba HTTPBasicAuth + + for h.Next() { + var cmp Comparer + args := h.RemainingArgs() + + var hashName string + switch len(args) { + case 0: + hashName = "bcrypt" + case 1: + hashName = args[0] + default: + return nil, h.ArgErr() + } + + switch hashName { + case "bcrypt": + cmp = BcryptHash{} + case "scrypt": + cmp = ScryptHash{} + default: + return nil, h.Errf("unrecognized hash algorithm: %s", hashName) + } + + ba.HashRaw = caddyconfig.JSONModuleObject(cmp, "algorithm", hashName, nil) + + for h.NextBlock(0) { + username := h.Val() + + var b64Pwd, b64Salt string + h.Args(&b64Pwd, &b64Salt) + if h.NextArg() { + return nil, h.ArgErr() + } + + if username == "" || b64Pwd == "" { + return nil, h.Err("username and password cannot be empty or missing") + } + + pwd, err := base64.StdEncoding.DecodeString(b64Pwd) + if err != nil { + return nil, h.Errf("decoding password: %v", err) + } + var salt []byte + if b64Salt != "" { + salt, err = base64.StdEncoding.DecodeString(b64Salt) + if err != nil { + return nil, h.Errf("decoding salt: %v", err) + } + } + + ba.AccountList = append(ba.AccountList, Account{ + Username: username, + Password: pwd, + Salt: salt, + }) + } + } + + return Authentication{ + ProvidersRaw: map[string]json.RawMessage{ + "http_basic": caddyconfig.JSON(ba, nil), + }, + }, nil +} diff --git a/modules/caddyhttp/caddyauth/command.go b/modules/caddyhttp/caddyauth/command.go new file mode 100644 index 00000000..c1100016 --- /dev/null +++ b/modules/caddyhttp/caddyauth/command.go @@ -0,0 +1,80 @@ +// 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 ( + "encoding/base64" + "flag" + "fmt" + + "github.com/caddyserver/caddy/v2" + caddycmd "github.com/caddyserver/caddy/v2/cmd" + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/scrypt" +) + +func init() { + caddycmd.RegisterCommand(caddycmd.Command{ + Name: "hash-password", + Func: cmdHashPassword, + Usage: "--plaintext [--salt ] [--algorithm ]", + Short: "Hashes a password and writes base64", + Long: ` +Convenient way to hash a plaintext password. The resulting +hash is written to stdout as a base64 string. + +--algorithm may be bcrypt or scrypt. If script, the default +parameters are used. + +Use the --salt flag for algorithms which require a salt to +be provided (scrypt). +`, + Flags: func() *flag.FlagSet { + fs := flag.NewFlagSet("hash-password", flag.ExitOnError) + fs.String("algorithm", "bcrypt", "Name of the hash algorithm") + fs.String("plaintext", "", "The plaintext password") + fs.String("salt", "", "The password salt") + return fs + }(), + }) +} + +func cmdHashPassword(fs caddycmd.Flags) (int, error) { + algorithm := fs.String("algorithm") + plaintext := []byte(fs.String("plaintext")) + salt := []byte(fs.String("salt")) + + var hash []byte + var err error + switch algorithm { + case "bcrypt": + hash, err = bcrypt.GenerateFromPassword(plaintext, bcrypt.DefaultCost) + case "scrypt": + def := ScryptHash{} + def.SetDefaults() + hash, err = scrypt.Key(plaintext, salt, def.N, def.R, def.P, def.KeyLength) + default: + return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm) + } + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + hashBase64 := base64.StdEncoding.EncodeToString([]byte(hash)) + + fmt.Println(hashBase64) + + return 0, nil +} diff --git a/modules/caddyhttp/caddyauth/hashes.go b/modules/caddyhttp/caddyauth/hashes.go new file mode 100644 index 00000000..a515c092 --- /dev/null +++ b/modules/caddyhttp/caddyauth/hashes.go @@ -0,0 +1,111 @@ +// 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 ( + "github.com/caddyserver/caddy/v2" + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/scrypt" +) + +func init() { + caddy.RegisterModule(BcryptHash{}) + caddy.RegisterModule(ScryptHash{}) +} + +// BcryptHash implements the bcrypt hash. +type BcryptHash struct{} + +// CaddyModule returns the Caddy module information. +func (BcryptHash) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.authentication.hashes.bcrypt", + New: func() caddy.Module { return new(BcryptHash) }, + } +} + +// Compare compares passwords. +func (BcryptHash) Compare(hashed, plaintext, _ []byte) (bool, error) { + err := bcrypt.CompareHashAndPassword(hashed, plaintext) + if err == bcrypt.ErrMismatchedHashAndPassword { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + +// ScryptHash implements the scrypt KDF as a hash. +type ScryptHash struct { + N int `json:"N,omitempty"` + R int `json:"r,omitempty"` + P int `json:"p,omitempty"` + KeyLength int `json:"key_length,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (ScryptHash) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.authentication.hashes.scrypt", + New: func() caddy.Module { return new(ScryptHash) }, + } +} + +// Provision sets up s. +func (s *ScryptHash) Provision(_ caddy.Context) error { + s.SetDefaults() + return nil +} + +// SetDefaults sets safe default parameters, but does +// not overwrite existing values. Each default parameter +// is set independently; it does not check to ensure +// that r*p < 2^30. The defaults chosen are those as +// recommended in 2019 by +// https://godoc.org/golang.org/x/crypto/scrypt. +func (s *ScryptHash) SetDefaults() { + if s.N == 0 { + s.N = 32768 + } + if s.R == 0 { + s.R = 8 + } + if s.P == 0 { + s.P = 1 + } + if s.KeyLength == 0 { + s.KeyLength = 32 + } +} + +// Compare compares passwords. +func (s ScryptHash) Compare(hashed, plaintext, salt []byte) (bool, error) { + ourHash, err := scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength) + if err != nil { + return false, err + } + if hashesMatch(hashed, ourHash) { + return true, nil + } + return false, nil +} + +// Interface guards +var ( + _ Comparer = (*BcryptHash)(nil) + _ Comparer = (*ScryptHash)(nil) + _ caddy.Provisioner = (*ScryptHash)(nil) +)