authelia/internal/authentication/file_user_provider.go

204 lines
5.3 KiB
Go
Raw Normal View History

package authentication
import (
_ "embed" // Embed users_database.template.yml.
"errors"
"fmt"
"os"
"sync"
"time"
"github.com/go-crypt/crypt"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging"
)
// FileUserProvider is a provider reading details from a file.
type FileUserProvider struct {
config *schema.FileAuthenticationBackend
hash crypt.Hash
database *FileUserDatabase
mutex *sync.Mutex
timeoutReload time.Time
}
// NewFileUserProvider creates a new instance of FileUserProvider.
func NewFileUserProvider(config *schema.FileAuthenticationBackend) (provider *FileUserProvider) {
return &FileUserProvider{
config: config,
mutex: &sync.Mutex{},
timeoutReload: time.Now().Add(-1 * time.Second),
}
}
// Reload the database.
func (p *FileUserProvider) Reload() (reloaded bool, err error) {
now := time.Now()
p.mutex.Lock()
defer p.mutex.Unlock()
if now.Before(p.timeoutReload) {
return false, nil
}
switch err = p.database.Load(); {
case err == nil:
p.setTimeoutReload(now)
case errors.Is(err, ErrNoContent):
return false, nil
default:
return false, fmt.Errorf("failed to reload: %w", err)
}
p.setTimeoutReload(now)
return true, nil
}
// CheckUserPassword checks if provided password matches for the given user.
func (p *FileUserProvider) CheckUserPassword(username string, password string) (match bool, err error) {
var details DatabaseUserDetails
if details, err = p.database.GetUserDetails(username); err != nil {
return false, err
}
if details.Disabled {
return false, ErrUserNotFound
2019-12-27 17:09:57 +00:00
}
return details.Digest.MatchAdvanced(password)
2019-12-27 17:09:57 +00:00
}
// GetDetails retrieve the groups a user belongs to.
func (p *FileUserProvider) GetDetails(username string) (details *UserDetails, err error) {
var d DatabaseUserDetails
if d, err = p.database.GetUserDetails(username); err != nil {
return nil, err
}
if d.Disabled {
return nil, ErrUserNotFound
}
return d.ToUserDetails(), nil
}
// UpdatePassword update the password of the given user.
func (p *FileUserProvider) UpdatePassword(username string, newPassword string) (err error) {
var details DatabaseUserDetails
if details, err = p.database.GetUserDetails(username); err != nil {
return err
}
if details.Disabled {
return ErrUserNotFound
}
if details.Digest, err = p.hash.Hash(newPassword); err != nil {
return err
}
p.database.SetUserDetails(details.Username, &details)
p.mutex.Lock()
p.setTimeoutReload(time.Now())
p.mutex.Unlock()
if err = p.database.Save(); err != nil {
return err
}
return nil
}
// StartupCheck implements the startup check provider interface.
func (p *FileUserProvider) StartupCheck() (err error) {
if err = checkDatabase(p.config.Path); err != nil {
logging.Logger().WithError(err).Errorf("Error checking user authentication YAML database")
return fmt.Errorf("one or more errors occurred checking the authentication database")
}
if p.hash, err = NewFileCryptoHashFromConfig(p.config.Password); err != nil {
return err
[FEATURE] Support Argon2id password hasing and improved entropy (#679) * [FEATURE] Support Argon2id Passwords - Updated go module github.com/simia-tech/crypt - Added Argon2id support for file based authentication backend - Made it the default method - Made it so backwards compatibility with SHA512 exists - Force seeding of the random string generator used for salts to ensure they are all different - Added command params to the authelia hash-password command - Automatically remove {CRYPT} from hashes as they are updated - Automatically change hashes when they are updated to the configured algorithm - Made the hashing algorithm parameters completely configurable - Added reasonably comprehensive test suites - Updated docs - Updated config template * Adjust error output * Fix unit test * Add unit tests and argon2 version check * Fix new unit tests * Update docs, added tests * Implement configurable values and more comprehensive testing * Added cmd params to hash_password, updated docs, misc fixes * More detailed error for cmd, fixed a typo * Fixed cmd flag error, minor refactoring * Requested Changes and Minor refactoring * Increase entropy * Update docs for entropy changes * Refactor to reduce nesting and easier code maintenance * Cleanup Errors (uniformity for the function call) * Check salt length, fix docs * Add Base64 string validation for argon2id * Cleanup and Finalization - Moved RandomString function from ./internal/authentication/password_hash.go to ./internal/utils/strings.go - Added SplitStringToArrayOfStrings func that splits strings into an array with a fixed max string len - Fixed an error in validator that would allow a zero salt length - Added a test to verify the upstream crypt module supports our defined random salt chars - Updated docs - Removed unused "HashingAlgorithm" string type * Update crypt go mod, support argon2id key length and major refactor * Config Template Update, Final Tests * Use schema defaults for hash-password cmd * Iterations check * Docs requested changes * Test Coverage, suggested edits * Wording edit * Doc changes * Default sanity changes * Default sanity changes - docs * CI Sanity changes * Memory in MB
2020-03-06 01:38:02 +00:00
}
p.database = NewFileUserDatabase(p.config.Path, p.config.Search.Email, p.config.Search.CaseInsensitive)
if err = p.database.Load(); err != nil {
[FEATURE] Support Argon2id password hasing and improved entropy (#679) * [FEATURE] Support Argon2id Passwords - Updated go module github.com/simia-tech/crypt - Added Argon2id support for file based authentication backend - Made it the default method - Made it so backwards compatibility with SHA512 exists - Force seeding of the random string generator used for salts to ensure they are all different - Added command params to the authelia hash-password command - Automatically remove {CRYPT} from hashes as they are updated - Automatically change hashes when they are updated to the configured algorithm - Made the hashing algorithm parameters completely configurable - Added reasonably comprehensive test suites - Updated docs - Updated config template * Adjust error output * Fix unit test * Add unit tests and argon2 version check * Fix new unit tests * Update docs, added tests * Implement configurable values and more comprehensive testing * Added cmd params to hash_password, updated docs, misc fixes * More detailed error for cmd, fixed a typo * Fixed cmd flag error, minor refactoring * Requested Changes and Minor refactoring * Increase entropy * Update docs for entropy changes * Refactor to reduce nesting and easier code maintenance * Cleanup Errors (uniformity for the function call) * Check salt length, fix docs * Add Base64 string validation for argon2id * Cleanup and Finalization - Moved RandomString function from ./internal/authentication/password_hash.go to ./internal/utils/strings.go - Added SplitStringToArrayOfStrings func that splits strings into an array with a fixed max string len - Fixed an error in validator that would allow a zero salt length - Added a test to verify the upstream crypt module supports our defined random salt chars - Updated docs - Removed unused "HashingAlgorithm" string type * Update crypt go mod, support argon2id key length and major refactor * Config Template Update, Final Tests * Use schema defaults for hash-password cmd * Iterations check * Docs requested changes * Test Coverage, suggested edits * Wording edit * Doc changes * Default sanity changes * Default sanity changes - docs * CI Sanity changes * Memory in MB
2020-03-06 01:38:02 +00:00
return err
}
return nil
}
func (p *FileUserProvider) setTimeoutReload(now time.Time) {
p.timeoutReload = now.Add(time.Second / 2)
}
// NewFileCryptoHashFromConfig returns a crypt.Hash given a valid configuration.
func NewFileCryptoHashFromConfig(config schema.Password) (hash crypt.Hash, err error) {
switch config.Algorithm {
case hashArgon2, "":
hash = crypt.NewArgon2Hash().
WithVariant(crypt.NewArgon2Variant(config.Argon2.Variant)).
WithT(config.Argon2.Iterations).
WithM(config.Argon2.Memory).
WithP(config.Argon2.Parallelism).
WithK(config.Argon2.KeyLength).
WithS(config.Argon2.SaltLength)
case hashSHA2Crypt:
hash = crypt.NewSHA2CryptHash().
WithVariant(crypt.NewSHA2CryptVariant(config.SHA2Crypt.Variant)).
WithRounds(config.SHA2Crypt.Iterations).
WithSaltLength(config.SHA2Crypt.SaltLength)
case hashPBKDF2:
hash = crypt.NewPBKDF2Hash().
WithVariant(crypt.NewPBKDF2Variant(config.PBKDF2.Variant)).
WithIterations(config.PBKDF2.Iterations).
WithSaltLength(config.PBKDF2.SaltLength)
case hashSCrypt:
hash = crypt.NewScryptHash().
WithLN(config.SCrypt.Iterations).
WithP(config.SCrypt.Parallelism).
WithR(config.SCrypt.BlockSize)
case hashBCrypt:
hash = crypt.NewBcryptHash().
WithVariant(crypt.NewBcryptVariant(config.BCrypt.Variant)).
WithCost(config.BCrypt.Cost)
default:
return nil, fmt.Errorf("algorithm '%s' is unknown", config.Algorithm)
}
if err = hash.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate hash settings: %w", err)
}
return hash, nil
}
func checkDatabase(path string) (err error) {
if _, err = os.Stat(path); os.IsNotExist(err) {
if err = os.WriteFile(path, userYAMLTemplate, 0600); err != nil {
return fmt.Errorf("user authentication database file doesn't exist at path '%s' and could not be generated: %w", path, err)
}
return fmt.Errorf("user authentication database file doesn't exist at path '%s' and has been generated", path)
} else if err != nil {
return fmt.Errorf("error checking user authentication database file: %w", err)
}
return nil
}
//go:embed users_database.template.yml
var userYAMLTemplate []byte