authelia/internal/authentication/file_user_provider_test.go
James Elliott 26369fff3d
[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 12:38:02 +11:00

337 lines
11 KiB
Go

package authentication
import (
"io/ioutil"
"log"
"os"
"strings"
"testing"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/stretchr/testify/assert"
)
func WithDatabase(content []byte, f func(path string)) {
tmpfile, err := ioutil.TempFile("", "users_database.*.yaml")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
tmpfile.Close()
log.Fatal(err)
}
f(tmpfile.Name())
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
}
func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "password")
assert.NoError(t, err)
assert.True(t, ok)
})
}
func TestShouldCheckUserSHA512PasswordIsCorrect(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("harry", "password")
assert.NoError(t, err)
assert.True(t, ok)
})
}
func TestShouldCheckUserPasswordIsWrong(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "wrong_password")
assert.NoError(t, err)
assert.False(t, ok)
})
}
func TestShouldCheckUserPasswordOfUnexistingUser(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
_, err := provider.CheckUserPassword("fake", "password")
assert.Error(t, err)
assert.Equal(t, "User 'fake' does not exist in database", err.Error())
})
}
func TestShouldRetrieveUserDetails(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
details, err := provider.GetDetails("john")
assert.NoError(t, err)
assert.Equal(t, details.Emails, []string{"john.doe@authelia.com"})
assert.Equal(t, details.Groups, []string{"admins", "dev"})
})
}
func TestShouldUpdatePassword(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
err := provider.UpdatePassword("harry", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("harry", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
})
}
// Checks both that the hashing algo changes and that it removes {CRYPT} from the start.
func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].HashedPassword, "{CRYPT}$6$"))
err := provider.UpdatePassword("harry", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("harry", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].HashedPassword, "$argon2id$"))
})
}
func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
config.PasswordHashing.Algorithm = "sha512"
config.PasswordHashing.Iterations = 50000
provider := NewFileUserProvider(&config)
assert.True(t, strings.HasPrefix(provider.database.Users["john"].HashedPassword, "{CRYPT}$argon2id$"))
err := provider.UpdatePassword("john", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
assert.True(t, strings.HasPrefix(provider.database.Users["john"].HashedPassword, "$6$"))
})
}
func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) {
WithDatabase(MalformedUserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Unable to parse database: yaml: line 4: mapping values are not allowed in this context", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) {
WithDatabase(BadSchemaUserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Invalid schema of database: Users: non zero value required", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadSHA512HashesForTheFirstTime(t *testing.T) {
WithDatabase(BadSHA512HashContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Unable to parse hash of user john: Hash key is not the last parameter, the hash is likely malformed ($6$rounds00000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/).", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSettingsForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashSettingsContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Unable to parse hash of user john: Hash key is not the last parameter, the hash is likely malformed ($argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM).", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashKeyForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashKeyContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Unable to parse hash of user john: Hash key contains invalid base64 characters.", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSaltForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashSaltContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Unable to parse hash of user john: Salt contains invalid base64 characters.", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) {
WithDatabase(UserDatabaseWithoutCryptContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "password")
assert.NoError(t, err)
assert.True(t, ok)
})
}
var (
DefaultFileAuthenticationBackendConfiguration = schema.FileAuthenticationBackendConfiguration{
Path: "",
PasswordHashing: &schema.PasswordHashingConfiguration{
Iterations: schema.DefaultCIPasswordOptionsConfiguration.Iterations,
KeyLength: schema.DefaultCIPasswordOptionsConfiguration.KeyLength,
SaltLength: schema.DefaultCIPasswordOptionsConfiguration.SaltLength,
Algorithm: schema.DefaultCIPasswordOptionsConfiguration.Algorithm,
Memory: schema.DefaultCIPasswordOptionsConfiguration.Memory,
Parallelism: schema.DefaultCIPasswordOptionsConfiguration.Parallelism,
},
}
)
var UserDatabaseContent = []byte(`
users:
john:
password: "{CRYPT}$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com
groups:
- admins
- dev
harry:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: harry.potter@authelia.com
groups: []
bob:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: bob.dylan@authelia.com
groups:
- dev
james:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: james.dean@authelia.com
`)
var MalformedUserDatabaseContent = []byte(`
users
john
email: john.doe@authelia.com
groups:
- admins
- dev
`)
// The YAML is valid but the root key is user instead of users
var BadSchemaUserDatabaseContent = []byte(`
user:
john:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: john.doe@authelia.com
groups:
- admins
- dev
`)
var UserDatabaseWithoutCryptContent = []byte(`
users:
john:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: john.doe@authelia.com
groups:
- admins
- dev
james:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: james.dean@authelia.com
`)
var BadSHA512HashContent = []byte(`
users:
john:
password: "$6$rounds00000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: john.doe@authelia.com
groups:
- admins
- dev
james:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: james.dean@authelia.com
`)
var BadArgon2idHashSettingsContent = []byte(`
users:
john:
password: "$argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com
groups:
- admins
- dev
james:
password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: james.dean@authelia.com
`)
var BadArgon2idHashKeyContent = []byte(`
users:
john:
password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$^^vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com
groups:
- admins
- dev
`)
var BadArgon2idHashSaltContent = []byte(`
users:
john:
password: "$argon2id$v=19$m=65536,t=3,p=2$^^LnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com
groups:
- admins
- dev
`)