diff options
| author | James Elliott <james-d-elliott@users.noreply.github.com> | 2020-03-06 12:38:02 +1100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-03-06 12:38:02 +1100 |
| commit | 26369fff3d8d397018a08ae044f36b723fc7d880 (patch) | |
| tree | b91dace87f3bb683491975cb5eb74e8babd6b7f4 /internal/authentication/file_user_provider_test.go | |
| parent | 72a3f1e0d7524a1d86e87781d264d5db195ea3e6 (diff) | |
[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
Diffstat (limited to 'internal/authentication/file_user_provider_test.go')
| -rw-r--r-- | internal/authentication/file_user_provider_test.go | 184 |
1 files changed, 166 insertions, 18 deletions
diff --git a/internal/authentication/file_user_provider_test.go b/internal/authentication/file_user_provider_test.go index 2e3d03a25..09fef413f 100644 --- a/internal/authentication/file_user_provider_test.go +++ b/internal/authentication/file_user_provider_test.go @@ -4,8 +4,10 @@ import ( "io/ioutil" "log" "os" + "strings" "testing" + "github.com/authelia/authelia/internal/configuration/schema" "github.com/stretchr/testify/assert" ) @@ -29,9 +31,11 @@ func WithDatabase(content []byte, f func(path string)) { } } -func TestShouldCheckUserPasswordIsCorrect(t *testing.T) { +func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) { WithDatabase(UserDatabaseContent, func(path string) { - provider := NewFileUserProvider(path) + config := DefaultFileAuthenticationBackendConfiguration + config.Path = path + provider := NewFileUserProvider(&config) ok, err := provider.CheckUserPassword("john", "password") assert.NoError(t, err) @@ -39,9 +43,23 @@ func TestShouldCheckUserPasswordIsCorrect(t *testing.T) { }) } +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) { - provider := NewFileUserProvider(path) + config := DefaultFileAuthenticationBackendConfiguration + config.Path = path + provider := NewFileUserProvider(&config) ok, err := provider.CheckUserPassword("john", "wrong_password") assert.NoError(t, err) @@ -51,7 +69,9 @@ func TestShouldCheckUserPasswordIsWrong(t *testing.T) { func TestShouldCheckUserPasswordOfUnexistingUser(t *testing.T) { WithDatabase(UserDatabaseContent, func(path string) { - provider := NewFileUserProvider(path) + 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()) @@ -60,7 +80,9 @@ func TestShouldCheckUserPasswordOfUnexistingUser(t *testing.T) { func TestShouldRetrieveUserDetails(t *testing.T) { WithDatabase(UserDatabaseContent, func(path string) { - provider := NewFileUserProvider(path) + 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"}) @@ -70,45 +92,125 @@ func TestShouldRetrieveUserDetails(t *testing.T) { func TestShouldUpdatePassword(t *testing.T) { WithDatabase(UserDatabaseContent, func(path string) { - provider := NewFileUserProvider(path) + 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(path) + 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(path) + 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(path) + 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 TestShouldRaiseWhenLoadingDatabaseWithBadHashesForTheFirstTime(t *testing.T) { - WithDatabase(BadHashContent, func(path string) { - assert.PanicsWithValue(t, "Unable to parse hash of user john: Cannot match pattern 'rounds=<int>' to find the number of rounds", func() { - NewFileUserProvider(path) +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(UserDatabaseWithouCryptContent, func(path string) { - provider := NewFileUserProvider(path) + WithDatabase(UserDatabaseWithoutCryptContent, func(path string) { + config := DefaultFileAuthenticationBackendConfiguration + config.Path = path + provider := NewFileUserProvider(&config) ok, err := provider.CheckUserPassword("john", "password") assert.NoError(t, err) @@ -116,10 +218,24 @@ func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) { }) } +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}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + password: "{CRYPT}$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM" email: john.doe@authelia.com groups: - admins @@ -161,7 +277,7 @@ user: - dev `) -var UserDatabaseWithouCryptContent = []byte(` +var UserDatabaseWithoutCryptContent = []byte(` users: john: password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" @@ -174,7 +290,7 @@ users: email: james.dean@authelia.com `) -var BadHashContent = []byte(` +var BadSHA512HashContent = []byte(` users: john: password: "$6$rounds00000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" @@ -186,3 +302,35 @@ users: 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 +`) |
