summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/caddyauth
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
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')
-rw-r--r--modules/caddyhttp/caddyauth/basicauth.go164
-rw-r--r--modules/caddyhttp/caddyauth/caddyauth.go102
-rw-r--r--modules/caddyhttp/caddyauth/caddyfile.go104
-rw-r--r--modules/caddyhttp/caddyauth/command.go80
-rw-r--r--modules/caddyhttp/caddyauth/hashes.go111
5 files changed, 561 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)
+)
diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go
new file mode 100644
index 0000000..48d4fba
--- /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 0000000..3600324
--- /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 [<matcher>] [<hash_algorithm>] {
+// <username> <hashed_password_base64> [<salt_base64>]
+// ...
+// }
+//
+// 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 0000000..c110001
--- /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 <password> [--salt <string>] [--algorithm <name>]",
+ 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 0000000..a515c09
--- /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)
+)