2019-04-24 21:52:08 +00:00
|
|
|
package authentication
|
|
|
|
|
|
|
|
import (
|
2020-03-06 01:38:02 +00:00
|
|
|
"errors"
|
2019-04-24 21:52:08 +00:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2019-12-27 16:55:00 +00:00
|
|
|
"strings"
|
2019-04-24 21:52:08 +00:00
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/asaskevich/govalidator"
|
2020-05-01 22:32:09 +00:00
|
|
|
"github.com/simia-tech/crypt"
|
2019-04-24 21:52:08 +00:00
|
|
|
"gopkg.in/yaml.v2"
|
2020-04-05 12:37:21 +00:00
|
|
|
|
|
|
|
"github.com/authelia/authelia/internal/configuration/schema"
|
2020-05-01 22:32:09 +00:00
|
|
|
"github.com/authelia/authelia/internal/utils"
|
2019-04-24 21:52:08 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// FileUserProvider is a provider reading details from a file.
|
|
|
|
type FileUserProvider struct {
|
2020-03-06 01:38:02 +00:00
|
|
|
configuration *schema.FileAuthenticationBackendConfiguration
|
|
|
|
database *DatabaseModel
|
|
|
|
lock *sync.Mutex
|
2020-05-01 22:32:09 +00:00
|
|
|
|
|
|
|
// TODO: Remove this. This is only here to temporarily fix the username enumeration security flaw in #949.
|
|
|
|
fakeHash string
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// UserDetailsModel is the model of user details in the file database.
|
|
|
|
type UserDetailsModel struct {
|
|
|
|
HashedPassword string `yaml:"password" valid:"required"`
|
|
|
|
Email string `yaml:"email"`
|
|
|
|
Groups []string `yaml:"groups"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// DatabaseModel is the model of users file database.
|
|
|
|
type DatabaseModel struct {
|
|
|
|
Users map[string]UserDetailsModel `yaml:"users" valid:"required"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewFileUserProvider creates a new instance of FileUserProvider.
|
2020-03-06 01:38:02 +00:00
|
|
|
func NewFileUserProvider(configuration *schema.FileAuthenticationBackendConfiguration) *FileUserProvider {
|
|
|
|
database, err := readDatabase(configuration.Path)
|
2019-04-24 21:52:08 +00:00
|
|
|
if err != nil {
|
|
|
|
// Panic since the file does not exist when Authelia is starting.
|
2019-12-27 17:09:57 +00:00
|
|
|
panic(err.Error())
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
2019-12-27 17:09:57 +00:00
|
|
|
|
|
|
|
// Early check whether hashed passwords are correct for all users
|
|
|
|
err = checkPasswordHashes(database)
|
|
|
|
if err != nil {
|
|
|
|
panic(err.Error())
|
|
|
|
}
|
|
|
|
|
2020-05-03 04:06:09 +00:00
|
|
|
var cryptAlgo CryptAlgo = HashingAlgorithmArgon2id
|
2020-05-01 22:32:09 +00:00
|
|
|
// TODO: Remove this. This is only here to temporarily fix the username enumeration security flaw in #949.
|
|
|
|
// This generates a hash that should be usable to do a fake CheckUserPassword
|
2020-05-02 16:20:40 +00:00
|
|
|
if configuration.Password.Algorithm == sha512 {
|
2020-05-03 04:06:09 +00:00
|
|
|
cryptAlgo = HashingAlgorithmSHA512
|
2020-05-01 22:32:09 +00:00
|
|
|
}
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2020-05-01 22:32:09 +00:00
|
|
|
settings := getCryptSettings(utils.RandomString(configuration.Password.SaltLength, HashingPossibleSaltCharacters),
|
2020-05-03 04:06:09 +00:00
|
|
|
cryptAlgo, configuration.Password.Iterations, configuration.Password.Memory*1024, configuration.Password.Parallelism,
|
2020-05-01 22:32:09 +00:00
|
|
|
configuration.Password.KeyLength)
|
|
|
|
data := crypt.Base64Encoding.EncodeToString([]byte(utils.RandomString(configuration.Password.KeyLength, HashingPossibleSaltCharacters)))
|
|
|
|
fakeHash := fmt.Sprintf("%s$%s", settings, data)
|
|
|
|
|
2019-04-24 21:52:08 +00:00
|
|
|
return &FileUserProvider{
|
2020-03-06 01:38:02 +00:00
|
|
|
configuration: configuration,
|
|
|
|
database: database,
|
|
|
|
lock: &sync.Mutex{},
|
2020-05-01 22:32:09 +00:00
|
|
|
fakeHash: fakeHash,
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-27 17:09:57 +00:00
|
|
|
func checkPasswordHashes(database *DatabaseModel) error {
|
|
|
|
for u, v := range database.Users {
|
|
|
|
_, err := ParseHash(v.HashedPassword)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Unable to parse hash of user %s: %s", u, err)
|
|
|
|
}
|
|
|
|
}
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2019-12-27 17:09:57 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-04-24 21:52:08 +00:00
|
|
|
func readDatabase(path string) (*DatabaseModel, error) {
|
|
|
|
content, err := ioutil.ReadFile(path)
|
|
|
|
if err != nil {
|
2019-12-27 17:09:57 +00:00
|
|
|
return nil, fmt.Errorf("Unable to read database from file %s: %s", path, err)
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2019-04-24 21:52:08 +00:00
|
|
|
db := DatabaseModel{}
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2019-04-24 21:52:08 +00:00
|
|
|
err = yaml.Unmarshal(content, &db)
|
|
|
|
if err != nil {
|
2019-12-27 17:09:57 +00:00
|
|
|
return nil, fmt.Errorf("Unable to parse database: %s", err)
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ok, err := govalidator.ValidateStruct(db)
|
|
|
|
if err != nil {
|
2019-12-27 17:09:57 +00:00
|
|
|
return nil, fmt.Errorf("Invalid schema of database: %s", err)
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if !ok {
|
2019-12-27 17:09:57 +00:00
|
|
|
return nil, fmt.Errorf("The database format is invalid: %s", err)
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2019-04-24 21:52:08 +00:00
|
|
|
return &db, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CheckUserPassword checks if provided password matches for the given user.
|
|
|
|
func (p *FileUserProvider) CheckUserPassword(username string, password string) (bool, error) {
|
|
|
|
if details, ok := p.database.Users[username]; ok {
|
2019-12-27 16:55:00 +00:00
|
|
|
hashedPassword := strings.ReplaceAll(details.HashedPassword, "{CRYPT}", "")
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2019-04-24 21:52:08 +00:00
|
|
|
ok, err := CheckPassword(password, hashedPassword)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2019-04-24 21:52:08 +00:00
|
|
|
return ok, nil
|
|
|
|
}
|
2020-05-01 22:32:09 +00:00
|
|
|
|
|
|
|
// TODO: Remove this. This is only here to temporarily fix the username enumeration security flaw in #949.
|
|
|
|
hashedPassword := strings.ReplaceAll(p.fakeHash, "{CRYPT}", "")
|
|
|
|
_, err := CheckPassword(password, hashedPassword)
|
|
|
|
|
|
|
|
return false, err
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetDetails retrieve the groups a user belongs to.
|
|
|
|
func (p *FileUserProvider) GetDetails(username string) (*UserDetails, error) {
|
|
|
|
if details, ok := p.database.Users[username]; ok {
|
|
|
|
return &UserDetails{
|
2020-03-15 07:10:25 +00:00
|
|
|
Username: username,
|
|
|
|
Groups: details.Groups,
|
|
|
|
Emails: []string{details.Email},
|
2019-04-24 21:52:08 +00:00
|
|
|
}, nil
|
|
|
|
}
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2019-04-24 21:52:08 +00:00
|
|
|
return nil, fmt.Errorf("User '%s' does not exist in database", username)
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdatePassword update the password of the given user.
|
|
|
|
func (p *FileUserProvider) UpdatePassword(username string, newPassword string) error {
|
|
|
|
details, ok := p.database.Users[username]
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("User '%s' does not exist in database", username)
|
|
|
|
}
|
|
|
|
|
2020-05-03 04:06:09 +00:00
|
|
|
var algorithm CryptAlgo
|
2020-05-06 00:52:06 +00:00
|
|
|
|
|
|
|
switch p.configuration.Password.Algorithm {
|
|
|
|
case argon2id:
|
2020-03-06 01:38:02 +00:00
|
|
|
algorithm = HashingAlgorithmArgon2id
|
2020-05-06 00:52:06 +00:00
|
|
|
case sha512:
|
2020-03-06 01:38:02 +00:00
|
|
|
algorithm = HashingAlgorithmSHA512
|
2020-05-06 00:52:06 +00:00
|
|
|
default:
|
2020-03-06 01:38:02 +00:00
|
|
|
return errors.New("Invalid algorithm in configuration. It should be `argon2id` or `sha512`")
|
|
|
|
}
|
|
|
|
|
|
|
|
hash, err := HashPassword(
|
2020-04-11 03:54:18 +00:00
|
|
|
newPassword, "", algorithm, p.configuration.Password.Iterations,
|
|
|
|
p.configuration.Password.Memory*1024, p.configuration.Password.Parallelism,
|
|
|
|
p.configuration.Password.KeyLength, p.configuration.Password.SaltLength)
|
2020-03-06 01:38:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2020-03-06 01:38:02 +00:00
|
|
|
details.HashedPassword = hash
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2019-04-24 21:52:08 +00:00
|
|
|
p.lock.Lock()
|
|
|
|
p.database.Users[username] = details
|
|
|
|
|
|
|
|
b, err := yaml.Marshal(p.database)
|
|
|
|
if err != nil {
|
|
|
|
p.lock.Unlock()
|
|
|
|
return err
|
|
|
|
}
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2020-05-05 07:57:30 +00:00
|
|
|
err = ioutil.WriteFile(p.configuration.Path, b, 0644) //nolint:gosec // Fixed in future PR.
|
2019-04-24 21:52:08 +00:00
|
|
|
p.lock.Unlock()
|
2020-05-05 19:35:32 +00:00
|
|
|
|
2019-04-24 21:52:08 +00:00
|
|
|
return err
|
|
|
|
}
|