mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-28 06:33:47 +03:00
f8366c2f09
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.
164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
// 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)
|
|
)
|