954 lines
28 KiB
Go
954 lines
28 KiB
Go
package authentication
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-crypt/crypt/algorithm/bcrypt"
|
|
"github.com/go-crypt/crypt/algorithm/pbkdf2"
|
|
"github.com/go-crypt/crypt/algorithm/scrypt"
|
|
"github.com/golang/mock/gomock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
|
)
|
|
|
|
func TestShouldErrorPermissionsOnLocalFS(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("skipping test due to being on windows")
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
|
|
_ = os.Mkdir(filepath.Join(dir, "noperms"), 0000)
|
|
|
|
f := filepath.Join(dir, "noperms", "users_database.yml")
|
|
require.EqualError(t, checkDatabase(f), fmt.Sprintf("error checking user authentication database file: stat %s: permission denied", f))
|
|
}
|
|
|
|
func TestShouldErrorAndGenerateUserDB(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
f := filepath.Join(dir, "users_database.yml")
|
|
|
|
require.EqualError(t, checkDatabase(f), fmt.Sprintf("user authentication database file doesn't exist at path '%s' and has been generated", f))
|
|
}
|
|
|
|
func TestShouldErrorFailCreateDB(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
assert.NoError(t, os.Mkdir(filepath.Join(dir, "x"), 0000))
|
|
|
|
f := filepath.Join(dir, "x", "users.yml")
|
|
|
|
provider := NewFileUserProvider(&schema.AuthenticationBackendFile{Path: f, Password: schema.DefaultPasswordConfig})
|
|
|
|
require.NotNil(t, provider)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "one or more errors occurred checking the authentication database")
|
|
|
|
assert.NotNil(t, provider.database)
|
|
|
|
reloaded, err := provider.Reload()
|
|
|
|
assert.False(t, reloaded)
|
|
assert.EqualError(t, err, fmt.Sprintf("failed to reload: error reading the authentication database: failed to read the '%s' file: open %s: permission denied", f, f))
|
|
}
|
|
|
|
func TestShouldErrorBadPasswordConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
f := filepath.Join(dir, "users.yml")
|
|
|
|
require.NoError(t, os.WriteFile(f, UserDatabaseContent, 0600))
|
|
|
|
provider := NewFileUserProvider(&schema.AuthenticationBackendFile{Path: f})
|
|
|
|
require.NotNil(t, provider)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "failed to initialize hash settings: argon2 validation error: parameter is invalid: parameter 't' must be between 1 and 2147483647 but is set to '0'")
|
|
}
|
|
|
|
func TestShouldNotPanicOnNilDB(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
f := filepath.Join(dir, "users.yml")
|
|
|
|
assert.NoError(t, os.WriteFile(f, UserDatabaseContent, 0600))
|
|
|
|
provider := &FileUserProvider{
|
|
config: &schema.AuthenticationBackendFile{Path: f, Password: schema.DefaultPasswordConfig},
|
|
mutex: &sync.Mutex{},
|
|
timeoutReload: time.Now().Add(-1 * time.Second),
|
|
}
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
}
|
|
|
|
func TestShouldReloadDatabase(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "users.yml")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
setup func(t *testing.T, provider *FileUserProvider)
|
|
expected bool
|
|
err string
|
|
}{
|
|
{
|
|
"ShouldSkipReloadRecentlyReloaded",
|
|
func(t *testing.T, provider *FileUserProvider) {
|
|
provider.timeoutReload = time.Now().Add(time.Minute)
|
|
},
|
|
false,
|
|
"",
|
|
},
|
|
{
|
|
"ShouldReloadWithoutError",
|
|
func(t *testing.T, provider *FileUserProvider) {
|
|
provider.timeoutReload = time.Now().Add(time.Minute * -1)
|
|
},
|
|
true,
|
|
"",
|
|
},
|
|
{
|
|
"ShouldNotReloadWithNoContent",
|
|
func(t *testing.T, provider *FileUserProvider) {
|
|
p := filepath.Join(dir, "empty.yml")
|
|
|
|
_, _ = os.Create(p)
|
|
|
|
provider.timeoutReload = time.Now().Add(time.Minute * -1)
|
|
|
|
provider.config.Path = p
|
|
|
|
provider.database = NewFileUserDatabase(p, provider.config.Search.Email, provider.config.Search.CaseInsensitive)
|
|
},
|
|
false,
|
|
"",
|
|
},
|
|
}
|
|
|
|
require.NoError(t, os.WriteFile(path, UserDatabaseContent, 0600))
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
provider := NewFileUserProvider(&schema.AuthenticationBackendFile{
|
|
Path: path,
|
|
Password: schema.DefaultPasswordConfig,
|
|
})
|
|
|
|
tc.setup(t, provider)
|
|
|
|
actual, theError := provider.Reload()
|
|
|
|
assert.Equal(t, tc.expected, actual)
|
|
if tc.err == "" {
|
|
assert.NoError(t, theError)
|
|
} else {
|
|
assert.EqualError(t, theError, tc.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err := provider.CheckUserPassword("john", "password")
|
|
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestShouldCheckUserSHA512PasswordIsCorrect(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err := provider.CheckUserPassword("harry", "password")
|
|
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestShouldCheckUserPasswordIsWrong(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err := provider.CheckUserPassword("john", "wrong_password")
|
|
|
|
assert.NoError(t, err)
|
|
assert.False(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestShouldCheckUserPasswordIsWrongForEnumerationCompare(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err := provider.CheckUserPassword("enumeration", "wrong_password")
|
|
assert.NoError(t, err)
|
|
assert.False(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestShouldCheckUserPasswordOfUserThatDoesNotExist(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err := provider.CheckUserPassword("fake", "password")
|
|
assert.Error(t, err)
|
|
assert.Equal(t, false, ok)
|
|
assert.EqualError(t, err, "user not found")
|
|
})
|
|
}
|
|
|
|
func TestShouldRetrieveUserDetails(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
details, err := provider.GetDetails("john")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "john", details.Username)
|
|
assert.Equal(t, []string{"john.doe@authelia.com"}, details.Emails)
|
|
assert.Equal(t, []string{"admins", "dev"}, details.Groups)
|
|
})
|
|
}
|
|
|
|
func TestShouldErrOnUserDetailsNoUser(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
details, err := provider.GetDetails("nouser")
|
|
assert.Nil(t, details)
|
|
assert.Equal(t, err, ErrUserNotFound)
|
|
|
|
details, err = provider.GetDetails("dis")
|
|
assert.Nil(t, details)
|
|
assert.Equal(t, err, ErrUserNotFound)
|
|
})
|
|
}
|
|
|
|
func TestShouldUpdatePassword(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
err := provider.UpdatePassword("harry", "newpassword")
|
|
assert.NoError(t, err)
|
|
|
|
// Reset the provider to force a read from disk.
|
|
provider = NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
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(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
db, ok := provider.database.(*FileUserDatabase)
|
|
require.True(t, ok)
|
|
|
|
assert.True(t, strings.HasPrefix(db.Users["harry"].Password.Encode(), "$6$"))
|
|
err := provider.UpdatePassword("harry", "newpassword")
|
|
assert.NoError(t, err)
|
|
|
|
// Reset the provider to force a read from disk.
|
|
provider = NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err = provider.CheckUserPassword("harry", "newpassword")
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
assert.True(t, strings.HasPrefix(db.Users["harry"].Password.Encode(), "$argon2id$"))
|
|
})
|
|
}
|
|
|
|
func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
config.Password.Algorithm = "sha2crypt"
|
|
config.Password.SHA2Crypt.Iterations = 50000
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
db, ok := provider.database.(*FileUserDatabase)
|
|
require.True(t, ok)
|
|
|
|
assert.True(t, strings.HasPrefix(db.Users["john"].Password.Encode(), "$argon2id$"))
|
|
err := provider.UpdatePassword("john", "newpassword")
|
|
assert.NoError(t, err)
|
|
|
|
// Reset the provider to force a read from disk.
|
|
provider = NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err = provider.CheckUserPassword("john", "newpassword")
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
assert.True(t, strings.HasPrefix(db.Users["john"].Password.Encode(), "$6$"))
|
|
})
|
|
}
|
|
|
|
func TestShouldErrOnUpdatePasswordNoUser(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
assert.Equal(t, provider.UpdatePassword("nousers", "newpassword"), ErrUserNotFound)
|
|
assert.Equal(t, provider.UpdatePassword("dis", "example"), ErrUserNotFound)
|
|
})
|
|
}
|
|
|
|
func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) {
|
|
WithDatabase(t, MalformedUserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "error reading the authentication database: could not parse the YAML database: yaml: line 4: mapping values are not allowed in this context")
|
|
})
|
|
}
|
|
|
|
func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) {
|
|
WithDatabase(t, BadSchemaUserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "error reading the authentication database: could not validate the schema: users: non zero value required")
|
|
})
|
|
}
|
|
|
|
func TestShouldRaiseWhenLoadingDatabaseWithBadSHA512HashesForTheFirstTime(t *testing.T) {
|
|
WithDatabase(t, BadSHA512HashContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "error decoding the authentication database: failed to parse hash for user 'john': shacrypt decode error: parameter pair 'rounds00000' is not properly encoded: does not contain kv separator '='")
|
|
})
|
|
}
|
|
|
|
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSettingsForTheFirstTime(t *testing.T) {
|
|
WithDatabase(t, BadArgon2idHashSettingsContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "error decoding the authentication database: failed to parse hash for user 'john': argon2 decode error: parameter pair 'm65536' is not properly encoded: does not contain kv separator '='")
|
|
})
|
|
}
|
|
|
|
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashKeyForTheFirstTime(t *testing.T) {
|
|
WithDatabase(t, BadArgon2idHashKeyContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "error decoding the authentication database: failed to parse hash for user 'john': argon2 decode error: provided encoded hash has a key value that can't be decoded: illegal base64 data at input byte 0")
|
|
})
|
|
}
|
|
|
|
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSaltForTheFirstTime(t *testing.T) {
|
|
WithDatabase(t, BadArgon2idHashSaltContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "error decoding the authentication database: failed to parse hash for user 'john': argon2 decode error: provided encoded hash has a salt value that can't be decoded: illegal base64 data at input byte 0")
|
|
})
|
|
}
|
|
|
|
func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseWithoutCryptContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err := provider.CheckUserPassword("john", "password")
|
|
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestShouldNotAllowLoginOfDisabledUsers(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err := provider.CheckUserPassword("dis", "password")
|
|
|
|
assert.False(t, ok)
|
|
assert.EqualError(t, err, "user not found")
|
|
})
|
|
}
|
|
|
|
func TestShouldErrorOnInvalidCaseSensitiveFile(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContentInvalidSearchCaseInsenstive, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
config.Search.Email = false
|
|
config.Search.CaseInsensitive = true
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "error loading authentication database: username 'JOHN' is not lowercase but this is required when case-insensitive search is enabled")
|
|
})
|
|
}
|
|
|
|
func TestShouldErrorOnDuplicateEmail(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContentInvalidSearchEmail, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
config.Search.Email = true
|
|
config.Search.CaseInsensitive = false
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
err := provider.StartupCheck()
|
|
assert.Regexp(t, regexp.MustCompile(`^error loading authentication database: email 'john.doe@authelia.com' is configured for for more than one user \(users are '(harry|john)', '(harry|john)'\) which isn't allowed when email search is enabled$`), err.Error())
|
|
})
|
|
}
|
|
|
|
func TestShouldNotErrorOnEmailAsUsername(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContentSearchEmailAsUsername, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
config.Search.Email = true
|
|
config.Search.CaseInsensitive = false
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
})
|
|
}
|
|
|
|
func TestShouldErrorOnEmailAsUsernameWithDuplicateEmail(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContentInvalidSearchEmailAsUsername, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
config.Search.Email = true
|
|
config.Search.CaseInsensitive = false
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "error loading authentication database: email 'john.doe@authelia.com' is also a username which isn't allowed when email search is enabled")
|
|
})
|
|
}
|
|
|
|
func TestShouldErrorOnEmailAsUsernameWithDuplicateEmailCase(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContentInvalidSearchEmailAsUsernameCase, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
config.Search.Email = false
|
|
config.Search.CaseInsensitive = true
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.EqualError(t, provider.StartupCheck(), "error loading authentication database: username 'john.doe@authelia.com' is configured as an email for user with username 'john' which isn't allowed when case-insensitive search is enabled")
|
|
})
|
|
}
|
|
|
|
func TestShouldAllowLookupByEmail(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
config.Search.Email = true
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err := provider.CheckUserPassword("john", "password")
|
|
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
|
|
ok, err = provider.CheckUserPassword("john.doe@authelia.com", "password")
|
|
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
|
|
ok, err = provider.CheckUserPassword("JOHN.doe@authelia.com", "password")
|
|
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestShouldAllowLookupCI(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Path = path
|
|
config.Search.CaseInsensitive = true
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ok, err := provider.CheckUserPassword("john", "password")
|
|
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
|
|
ok, err = provider.CheckUserPassword("John", "password")
|
|
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestNewFileCryptoHashFromConfig(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
have schema.AuthenticationBackendFilePassword
|
|
expected any
|
|
err string
|
|
}{
|
|
{
|
|
"ShouldCreatePBKDF2",
|
|
schema.AuthenticationBackendFilePassword{
|
|
Algorithm: "pbkdf2",
|
|
PBKDF2: schema.AuthenticationBackendFilePasswordPBKDF2{
|
|
Variant: "sha256",
|
|
Iterations: 100000,
|
|
SaltLength: 16,
|
|
},
|
|
},
|
|
&pbkdf2.Hasher{},
|
|
"",
|
|
},
|
|
{
|
|
"ShouldCreateSCrypt",
|
|
schema.AuthenticationBackendFilePassword{
|
|
Algorithm: "scrypt",
|
|
SCrypt: schema.AuthenticationBackendFilePasswordSCrypt{
|
|
Iterations: 12,
|
|
SaltLength: 16,
|
|
Parallelism: 1,
|
|
BlockSize: 1,
|
|
KeyLength: 32,
|
|
},
|
|
},
|
|
&scrypt.Hasher{},
|
|
"",
|
|
},
|
|
{
|
|
"ShouldCreateBCrypt",
|
|
schema.AuthenticationBackendFilePassword{
|
|
Algorithm: "bcrypt",
|
|
BCrypt: schema.AuthenticationBackendFilePasswordBCrypt{
|
|
Variant: "standard",
|
|
Cost: 12,
|
|
},
|
|
},
|
|
&bcrypt.Hasher{},
|
|
"",
|
|
},
|
|
{
|
|
"ShouldFailToCreateSCryptInvalidParameter",
|
|
schema.AuthenticationBackendFilePassword{
|
|
Algorithm: "scrypt",
|
|
},
|
|
nil,
|
|
"failed to initialize hash settings: scrypt validation error: parameter is invalid: parameter 'iterations' must be between 1 and 58 but is set to '0'",
|
|
},
|
|
{
|
|
"ShouldFailUnknown",
|
|
schema.AuthenticationBackendFilePassword{
|
|
Algorithm: "unknown",
|
|
},
|
|
nil,
|
|
"algorithm 'unknown' is unknown",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
actual, theError := NewFileCryptoHashFromConfig(tc.have)
|
|
|
|
if tc.err == "" {
|
|
assert.NoError(t, theError)
|
|
require.NotNil(t, actual)
|
|
assert.IsType(t, tc.expected, actual)
|
|
} else {
|
|
assert.EqualError(t, theError, tc.err)
|
|
assert.Nil(t, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHashError(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Search.CaseInsensitive = true
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
mock := NewMockHash(ctrl)
|
|
provider.hash = mock
|
|
|
|
mock.EXPECT().Hash("apple123").Return(nil, fmt.Errorf("failed to mock hash"))
|
|
|
|
assert.EqualError(t, provider.UpdatePassword("john", "apple123"), "failed to mock hash")
|
|
})
|
|
}
|
|
|
|
func TestDatabaseError(t *testing.T) {
|
|
WithDatabase(t, UserDatabaseContent, func(path string) {
|
|
db := NewFileUserDatabase(path, false, false)
|
|
assert.NoError(t, db.Load())
|
|
|
|
config := DefaultFileAuthenticationBackendConfiguration
|
|
config.Search.CaseInsensitive = true
|
|
config.Path = path
|
|
|
|
provider := NewFileUserProvider(&config)
|
|
|
|
assert.NoError(t, provider.StartupCheck())
|
|
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
mock := NewMockFileUserDatabase(ctrl)
|
|
|
|
provider.database = mock
|
|
|
|
gomock.InOrder(
|
|
mock.EXPECT().GetUserDetails("john").Return(db.GetUserDetails("john")),
|
|
mock.EXPECT().SetUserDetails("john", gomock.Any()),
|
|
mock.EXPECT().Save().Return(fmt.Errorf("failed to mock save")),
|
|
)
|
|
|
|
assert.EqualError(t, provider.UpdatePassword("john", "apple123"), "failed to mock save")
|
|
})
|
|
}
|
|
|
|
var (
|
|
DefaultFileAuthenticationBackendConfiguration = schema.AuthenticationBackendFile{
|
|
Path: "",
|
|
Password: schema.DefaultCIPasswordConfig,
|
|
}
|
|
)
|
|
|
|
var UserDatabaseContent = []byte(`
|
|
users:
|
|
john:
|
|
displayname: "John Doe"
|
|
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:
|
|
displayname: "Harry Potter"
|
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: harry.potter@authelia.com
|
|
groups: []
|
|
|
|
bob:
|
|
displayname: "Bob Dylan"
|
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: bob.dylan@authelia.com
|
|
groups:
|
|
- dev
|
|
|
|
james:
|
|
displayname: "James Dean"
|
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: james.dean@authelia.com
|
|
|
|
|
|
enumeration:
|
|
displayname: "Enumeration"
|
|
password: "$argon2id$v=19$m=131072,p=8$BpLnfgDsc2WD8F2q$O126GHPeZ5fwj7OLSs7PndXsTbje76R+QW9/EGfhkJg"
|
|
email: enumeration@authelia.com
|
|
|
|
|
|
dis:
|
|
displayname: "Enumeration"
|
|
password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
|
|
disabled: true
|
|
email: disabled@authelia.com
|
|
`)
|
|
|
|
var UserDatabaseContentInvalidSearchCaseInsenstive = []byte(`
|
|
users:
|
|
john:
|
|
displayname: "John Doe"
|
|
password: "{CRYPT}$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
|
|
email: john.doe@authelia.com
|
|
groups:
|
|
- admins
|
|
- dev
|
|
|
|
JOHN:
|
|
displayname: "Harry Potter"
|
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: harry.potter@authelia.com
|
|
groups: []
|
|
`)
|
|
|
|
var UserDatabaseContentInvalidSearchEmail = []byte(`
|
|
users:
|
|
john:
|
|
displayname: "John Doe"
|
|
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:
|
|
displayname: "Harry Potter"
|
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: john.doe@authelia.com
|
|
groups: []
|
|
`)
|
|
|
|
var UserDatabaseContentSearchEmailAsUsername = []byte(`
|
|
users:
|
|
john.doe@authelia.com:
|
|
displayname: "John Doe"
|
|
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:
|
|
displayname: "Harry Potter"
|
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: harry.potter@authelia.com
|
|
groups: []
|
|
`)
|
|
|
|
var UserDatabaseContentInvalidSearchEmailAsUsername = []byte(`
|
|
users:
|
|
john.doe@authelia.com:
|
|
displayname: "John Doe"
|
|
password: "{CRYPT}$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
|
|
email: john@authelia.com
|
|
groups:
|
|
- admins
|
|
- dev
|
|
|
|
harry:
|
|
displayname: "Harry Potter"
|
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: john.doe@authelia.com
|
|
groups: []
|
|
`)
|
|
|
|
var UserDatabaseContentInvalidSearchEmailAsUsernameCase = []byte(`
|
|
users:
|
|
john.doe@authelia.com:
|
|
displayname: "John Doe"
|
|
password: "{CRYPT}$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
|
|
email: JOHN@authelia.com
|
|
groups:
|
|
- admins
|
|
- dev
|
|
|
|
john:
|
|
displayname: "John Potter"
|
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: john.doe@authelia.com
|
|
groups: []
|
|
`)
|
|
|
|
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:
|
|
displayname: "John Doe"
|
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: john.doe@authelia.com
|
|
groups:
|
|
- admins
|
|
- dev
|
|
`)
|
|
|
|
var UserDatabaseWithoutCryptContent = []byte(`
|
|
users:
|
|
john:
|
|
displayname: "John Doe"
|
|
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: john.doe@authelia.com
|
|
groups:
|
|
- admins
|
|
- dev
|
|
james:
|
|
displayname: "James Dean"
|
|
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: james.dean@authelia.com
|
|
`)
|
|
|
|
var BadSHA512HashContent = []byte(`
|
|
users:
|
|
john:
|
|
displayname: "John Doe"
|
|
password: "$6$rounds00000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: john.doe@authelia.com
|
|
groups:
|
|
- admins
|
|
- dev
|
|
james:
|
|
displayname: "James Dean"
|
|
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
|
email: james.dean@authelia.com
|
|
`)
|
|
|
|
var BadArgon2idHashSettingsContent = []byte(`
|
|
users:
|
|
john:
|
|
displayname: "John Doe"
|
|
password: "$argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
|
|
email: john.doe@authelia.com
|
|
groups:
|
|
- admins
|
|
- dev
|
|
james:
|
|
displayname: "James Dean"
|
|
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:
|
|
displayname: "John Doe"
|
|
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:
|
|
displayname: "John Doe"
|
|
password: "$argon2id$v=19$m=65536,t=3,p=2$^^LnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
|
|
email: john.doe@authelia.com
|
|
groups:
|
|
- admins
|
|
- dev
|
|
`)
|
|
|
|
func WithDatabase(t *testing.T, content []byte, f func(path string)) {
|
|
dir := t.TempDir()
|
|
|
|
db, err := os.CreateTemp(dir, "users_database.*.yaml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = db.Write(content)
|
|
require.NoError(t, err)
|
|
|
|
f(db.Name())
|
|
|
|
require.NoError(t, db.Close())
|
|
}
|