feat(storage): encrypted secret values (#2588)

This adds an AES-GCM 256bit encryption layer for storage for sensitive items. This is only TOTP secrets for the time being but this may be expanded later. This will require a configuration change as per https://www.authelia.com/docs/configuration/migration.html#4330.

Closes #682
This commit is contained in:
James Elliott 2021-11-25 12:56:58 +11:00 committed by GitHub
parent eb94960348
commit 347bd1be77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 1338 additions and 253 deletions

View File

@ -507,6 +507,10 @@ regulation:
##
## The available providers are: `local`, `mysql`, `postgres`. You must use one and only one of these providers.
storage:
## The encryption key that is used to encrypt sensitive information in the database. Must be a string with a minimum
## length of 20. Please see the docs if you configure this with an undesirable key and need to change it.
# encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this
##
## Local (Storage Provider)
##

View File

@ -36,6 +36,7 @@ other configuration using the environment but instead of loading a file the valu
|session.secret |AUTHELIA_SESSION_SECRET_FILE |
|session.redis.password |AUTHELIA_SESSION_REDIS_PASSWORD_FILE |
|session.redis.high_availability.sentinel_password|AUTHELIA_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE |
|storage.encryption_key |AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE |
|storage.mysql.password |AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE |
|storage.postgres.password |AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE |
|notifier.smtp.password |AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE |

View File

@ -6,9 +6,43 @@ nav_order: 14
has_children: true
---
# Storage backends
**Authelia** supports multiple storage backends. The backend is used to store user preferences, 2FA device handles and
secrets, authentication logs, etc...
The available storage backends are listed in the table of contents below.
## Configuration
```yaml
storage:
encryption_key: a_very_important_secret
local: {}
mysql: {}
postgres: {}
```
## Options
### encryption_key
<div markdown="1">
type: string
{: .label .label-config .label-purple }
required: yes
{: .label .label-config .label-red }
</div>
The encryption key used to encrypt data in the database. It has a minimum length of 20 and must be provided. We encrypt
data by creating a sha256 checksum of the provided value, and use that to encrypt the data with the AES-GCM 256bit
algorithm.
The encrypted data in the database is as follows:
- TOTP Secret
### local
See [SQLite](./sqlite.md).
### mysql
See [MySQL](./mysql.md).
### postgres
See [PostgreSQL](./postgres.md).

View File

@ -14,6 +14,7 @@ The MySQL storage provider also serves as a MariaDB provider.
```yaml
storage:
encryption_key: a_very_important_secret
mysql:
host: 127.0.0.1
port: 3306
@ -24,6 +25,9 @@ storage:
## Options
### encryption_key
See the [encryption_key docs](./index.md#encryption_key).
### host
<div markdown="1">
type: string

View File

@ -14,6 +14,7 @@ The MySQL storage provider.
```yaml
storage:
encryption_key: a_very_important_secret
mysql:
host: 127.0.0.1
port: 3306
@ -25,6 +26,9 @@ storage:
## Options
### encryption_key
See the [encryption_key docs](./index.md#encryption_key).
### host
<div markdown="1">
type: string

View File

@ -14,6 +14,7 @@ The PostgreSQL storage provider.
```yaml
storage:
encryption_key: a_very_important_secret
postgres:
host: 127.0.0.1
port: 5432
@ -25,6 +26,9 @@ storage:
## Options
### encryption_key
See the [encryption_key docs](./index.md#encryption_key).
### host
<div markdown="1">
type: string

View File

@ -20,12 +20,16 @@ requires you setup an external database.
```yaml
storage:
encryption_key: a_very_important_secret
local:
path: /config/db.sqlite3
```
## Options
### encryption_key
See the [encryption_key docs](./index.md#encryption_key).
### path
<div markdown="1">
type: string

View File

@ -1,5 +1,9 @@
package commands
import (
"errors"
)
const cmdAutheliaExample = `authelia --config /etc/authelia/config.yml --config /etc/authelia/access-control.yml
authelia --config /etc/authelia/config.yml,/etc/authelia/access-control.yml
authelia --config /etc/authelia/config/
@ -80,3 +84,12 @@ const (
storageMigrateDirectionUp = "up"
storageMigrateDirectionDown = "down"
)
const (
storageExportFormatCSV = "csv"
storageExportFormatURI = "uri"
)
var (
errNoStorageProvider = errors.New("no storage provider configured")
)

View File

@ -1,28 +1,84 @@
package commands
import (
"errors"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/notification"
"github.com/authelia/authelia/v4/internal/ntp"
"github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage"
"github.com/authelia/authelia/v4/internal/utils"
)
func getStorageProvider() (provider storage.Provider, err error) {
func getStorageProvider() (provider storage.Provider) {
switch {
case config.Storage.PostgreSQL != nil:
provider = storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL)
return storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL, config.Storage.EncryptionKey)
case config.Storage.MySQL != nil:
provider = storage.NewMySQLProvider(*config.Storage.MySQL)
return storage.NewMySQLProvider(*config.Storage.MySQL, config.Storage.EncryptionKey)
case config.Storage.Local != nil:
provider = storage.NewSQLiteProvider(config.Storage.Local.Path)
return storage.NewSQLiteProvider(config.Storage.Local.Path, config.Storage.EncryptionKey)
default:
return nil, errors.New("no storage provider configured")
return nil
}
if (config.Storage.MySQL != nil && config.Storage.PostgreSQL != nil) ||
(config.Storage.MySQL != nil && config.Storage.Local != nil) ||
(config.Storage.PostgreSQL != nil && config.Storage.Local != nil) {
return nil, errors.New("multiple storage providers are configured but should only configure one")
}
return provider, err
}
func getProviders() (providers middlewares.Providers, warnings []error, errors []error) {
// TODO: Adjust this so the CertPool can be used like a provider.
autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
if len(warnings) != 0 || len(errors) != 0 {
return providers, warnings, errors
}
storageProvider := getStorageProvider()
var (
userProvider authentication.UserProvider
err error
)
switch {
case config.AuthenticationBackend.File != nil:
userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File)
case config.AuthenticationBackend.LDAP != nil:
userProvider = authentication.NewLDAPUserProvider(config.AuthenticationBackend, autheliaCertPool)
}
var notifier notification.Notifier
switch {
case config.Notifier.SMTP != nil:
notifier = notification.NewSMTPNotifier(config.Notifier.SMTP, autheliaCertPool)
case config.Notifier.FileSystem != nil:
notifier = notification.NewFileNotifier(*config.Notifier.FileSystem)
}
var ntpProvider *ntp.Provider
if config.NTP != nil {
ntpProvider = ntp.NewProvider(config.NTP)
}
clock := utils.RealClock{}
authorizer := authorization.NewAuthorizer(config)
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
if err != nil {
errors = append(errors, err)
}
return middlewares.Providers{
Authorizer: authorizer,
UserProvider: userProvider,
Regulator: regulator,
OpenIDConnect: oidcProvider,
StorageProvider: storageProvider,
NTP: ntpProvider,
Notifier: notifier,
SessionProvider: sessionProvider,
}, warnings, errors
}

View File

@ -0,0 +1,15 @@
package commands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
func TestGetStorageProvider(t *testing.T) {
config = &schema.Configuration{}
assert.Nil(t, getStorageProvider())
}

View File

@ -8,19 +8,11 @@ import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/notification"
"github.com/authelia/authelia/v4/internal/ntp"
"github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/server"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage"
"github.com/authelia/authelia/v4/internal/utils"
)
@ -67,7 +59,7 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
logger.Fatalf("Cannot initialize logger: %v", err)
}
providers, warnings, errors := getProviders(config)
providers, warnings, errors := getProviders()
if len(warnings) != 0 {
for _, err := range warnings {
logger.Warn(err)
@ -87,72 +79,6 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
server.Start(*config, providers)
}
func getProviders(config *schema.Configuration) (providers middlewares.Providers, warnings []error, errors []error) {
// TODO: Adjust this so the CertPool can be used like a provider.
autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
if len(warnings) != 0 || len(errors) != 0 {
return providers, warnings, errors
}
var storageProvider storage.Provider
switch {
case config.Storage.PostgreSQL != nil:
storageProvider = storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL)
case config.Storage.MySQL != nil:
storageProvider = storage.NewMySQLProvider(*config.Storage.MySQL)
case config.Storage.Local != nil:
storageProvider = storage.NewSQLiteProvider(config.Storage.Local.Path)
}
var (
userProvider authentication.UserProvider
err error
)
switch {
case config.AuthenticationBackend.File != nil:
userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File)
case config.AuthenticationBackend.LDAP != nil:
userProvider = authentication.NewLDAPUserProvider(config.AuthenticationBackend, autheliaCertPool)
}
var notifier notification.Notifier
switch {
case config.Notifier.SMTP != nil:
notifier = notification.NewSMTPNotifier(config.Notifier.SMTP, autheliaCertPool)
case config.Notifier.FileSystem != nil:
notifier = notification.NewFileNotifier(*config.Notifier.FileSystem)
}
var ntpProvider *ntp.Provider
if config.NTP != nil {
ntpProvider = ntp.NewProvider(config.NTP)
}
clock := utils.RealClock{}
authorizer := authorization.NewAuthorizer(config)
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
if err != nil {
errors = append(errors, err)
}
return middlewares.Providers{
Authorizer: authorizer,
UserProvider: userProvider,
Regulator: regulator,
OpenIDConnect: oidcProvider,
StorageProvider: storageProvider,
NTP: ntpProvider,
Notifier: notifier,
SessionProvider: sessionProvider,
}, warnings, errors
}
func doStartupChecks(config *schema.Configuration, providers *middlewares.Providers) {
logger := logging.Logger()

View File

@ -15,6 +15,8 @@ func NewStorageCmd() (cmd *cobra.Command) {
cmd.PersistentFlags().StringSliceP("config", "c", []string{"config.yml"}, "configuration file to load for the storage migration")
cmd.PersistentFlags().String("encryption-key", "", "the storage encryption key to use")
cmd.PersistentFlags().String("sqlite.path", "", "the SQLite database path")
cmd.PersistentFlags().String("mysql.host", "", "the MySQL hostname")
@ -32,11 +34,74 @@ func NewStorageCmd() (cmd *cobra.Command) {
cmd.AddCommand(
newStorageMigrateCmd(),
newStorageSchemaInfoCmd(),
newStorageEncryptionCmd(),
newStorageExportCmd(),
)
return cmd
}
func newStorageEncryptionCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "encryption",
Short: "Manages encryption",
}
cmd.AddCommand(
newStorageEncryptionChangeKeyCmd(),
newStorageEncryptionCheckCmd(),
)
return cmd
}
func newStorageEncryptionCheckCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "check",
Short: "Checks the encryption key against the database data",
RunE: storageSchemaEncryptionCheckRunE,
}
cmd.Flags().Bool("verbose", false, "enables verbose checking of every row of encrypted data")
return cmd
}
func newStorageEncryptionChangeKeyCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "change-key",
Short: "Changes the encryption key",
RunE: storageSchemaEncryptionChangeKeyRunE,
}
cmd.Flags().String("new-encryption-key", "", "the new key to encrypt the data with")
return cmd
}
func newStorageExportCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "export",
Short: "Performs exports",
}
cmd.AddCommand(newStorageExportTOTPConfigurationsCmd())
return cmd
}
func newStorageExportTOTPConfigurationsCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "totp-configurations",
Short: "Performs exports of the totp configurations",
RunE: storageExportTOTPConfigurationsRunE,
}
cmd.Flags().String("format", storageExportFormatCSV, "changes the format of the export, options are csv and uri")
return cmd
}
func newStorageSchemaInfoCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "schema-info",

View File

@ -12,6 +12,7 @@ import (
"github.com/authelia/authelia/v4/internal/configuration"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/configuration/validator"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/storage"
)
@ -38,6 +39,7 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
}
mapping := map[string]string{
"encryption-key": "storage.encryption_key",
"sqlite.path": "storage.local.path",
"mysql.host": "storage.mysql.host",
"mysql.port": "storage.mysql.port",
@ -100,17 +102,179 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
return nil
}
func storageSchemaEncryptionCheckRunE(cmd *cobra.Command, args []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
)
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
}()
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return err
}
if err = provider.SchemaEncryptionCheckKey(ctx, verbose); err != nil {
switch {
case errors.Is(err, storage.ErrSchemaEncryptionVersionUnsupported):
fmt.Printf("Could not check encryption key for validity. The schema version doesn't support encryption.\n")
case errors.Is(err, storage.ErrSchemaEncryptionInvalidKey):
fmt.Printf("Encryption key validation: failed.\n\nError: %v.\n", err)
default:
fmt.Printf("Could not check encryption key for validity.\n\nError: %v.\n", err)
}
} else {
fmt.Println("Encryption key validation: success.")
}
return nil
}
func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
)
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
}()
if err = checkStorageSchemaUpToDate(ctx, provider); err != nil {
return err
}
version, err := provider.SchemaVersion(ctx)
if err != nil {
return err
}
if version <= 0 {
return errors.New("schema version must be at least version 1 to change the encryption key")
}
key, err := cmd.Flags().GetString("new-encryption-key")
if err != nil {
return err
}
if key == "" {
return errors.New("you must set the --new-encryption-key flag")
}
if len(key) < 20 {
return errors.New("the encryption key must be at least 20 characters")
}
if err = provider.SchemaEncryptionChangeKey(ctx, key); err != nil {
return err
}
fmt.Println("Completed the encryption key change. Please adjust your configuration to use the new key.")
return nil
}
func storageExportTOTPConfigurationsRunE(cmd *cobra.Command, args []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
)
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
}()
if err = checkStorageSchemaUpToDate(ctx, provider); err != nil {
return err
}
format, err := cmd.Flags().GetString("format")
if err != nil {
return err
}
switch format {
case storageExportFormatCSV, storageExportFormatURI:
break
default:
return errors.New("format must be csv or uri")
}
limit := 10
var configurations []models.TOTPConfiguration
for page := 0; true; page++ {
configurations, err = provider.LoadTOTPConfigurations(ctx, limit, page)
if err != nil {
return err
}
if page == 0 && format == storageExportFormatCSV {
fmt.Printf("issuer,username,algorithm,digits,period,secret\n")
}
for _, c := range configurations {
switch format {
case storageExportFormatCSV:
fmt.Printf("%s,%s,%s,%d,%d,%s\n", "Authelia", c.Username, c.Algorithm, c.Digits, c.Period, string(c.Secret))
case storageExportFormatURI:
fmt.Printf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d\n", "Authelia", c.Username, string(c.Secret), "Authelia", c.Algorithm, c.Digits, c.Period)
}
}
if len(configurations) < limit {
break
}
}
return nil
}
func storageMigrateHistoryRunE(_ *cobra.Command, _ []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
)
provider, err = getStorageProvider()
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
}()
version, err := provider.SchemaVersion(ctx)
if err != nil {
return err
}
if version <= 0 {
fmt.Println("No migration history is available for schemas that not version 1 or above.")
return
}
migrations, err := provider.SchemaMigrationHistory(ctx)
if err != nil {
return err
@ -138,11 +302,15 @@ func newStorageMigrateListRunE(up bool) func(cmd *cobra.Command, args []string)
directionStr string
)
provider, err = getStorageProvider()
if err != nil {
return err
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
}()
if up {
migrations, err = provider.SchemaMigrationsUp(ctx, 0)
directionStr = "Up"
@ -151,13 +319,7 @@ func newStorageMigrateListRunE(up bool) func(cmd *cobra.Command, args []string)
directionStr = "Down"
}
if err != nil {
if err.Error() == "cannot migrate to the same version as prior" {
fmt.Printf("No %s migrations found\n", directionStr)
return nil
}
if err != nil && !errors.Is(err, storage.ErrNoAvailableMigrations) && !errors.Is(err, storage.ErrMigrateCurrentVersionSameAsTarget) {
return err
}
@ -182,11 +344,15 @@ func newStorageMigrationRunE(up bool) func(cmd *cobra.Command, args []string) (e
ctx = context.Background()
)
provider, err = getStorageProvider()
if err != nil {
return err
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
}()
target, err := cmd.Flags().GetInt("target")
if err != nil {
return err
@ -253,11 +419,15 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
tablesStr string
)
provider, err = getStorageProvider()
if err != nil {
return err
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
}()
version, err := provider.SchemaVersion(ctx)
if err != nil && err.Error() != "unknown schema state" {
return err
@ -285,7 +455,37 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
upgradeStr = "no"
}
fmt.Printf("Schema Version: %s\nSchema Upgrade Available: %s\nSchema Tables: %s\n", storage.SchemaVersionToString(version), upgradeStr, tablesStr)
var encryption string
if err = provider.SchemaEncryptionCheckKey(ctx, false); err != nil {
if errors.Is(err, storage.ErrSchemaEncryptionVersionUnsupported) {
encryption = "unsupported (schema version)"
} else {
encryption = "invalid"
}
} else {
encryption = "valid"
}
fmt.Printf("Schema Version: %s\nSchema Upgrade Available: %s\nSchema Tables: %s\nSchema Encryption Key: %s\n", storage.SchemaVersionToString(version), upgradeStr, tablesStr, encryption)
return nil
}
func checkStorageSchemaUpToDate(ctx context.Context, provider storage.Provider) (err error) {
version, err := provider.SchemaVersion(ctx)
if err != nil {
return err
}
latest, err := provider.SchemaLatestVersion()
if err != nil {
return err
}
if version != latest {
return fmt.Errorf("schema is version %d which is outdated please migrate to version %d in order to use this command or use an older binary", version, latest)
}
return nil
}

View File

@ -507,6 +507,10 @@ regulation:
##
## The available providers are: `local`, `mysql`, `postgres`. You must use one and only one of these providers.
storage:
## The encryption key that is used to encrypt sensitive information in the database. Must be a string with a minimum
## length of 20. Please see the docs if you configure this with an undesirable key and need to change it.
# encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this
##
## Local (Storage Provider)
##

View File

@ -138,6 +138,7 @@ func TestShouldValidateAndRaiseErrorsOnNormalConfigurationAndSecret(t *testing.T
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "an env storage mysql password"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_ENCRYPTION_KEY", "a_very_bad_encryption_key"))
val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@ -152,6 +153,7 @@ func TestShouldValidateAndRaiseErrorsOnNormalConfigurationAndSecret(t *testing.T
assert.Equal(t, "example_secret value", config.Session.Secret)
assert.Equal(t, "an env storage mysql password", config.Storage.MySQL.Password)
assert.Equal(t, "an env authentication backend ldap password", config.AuthenticationBackend.LDAP.Password)
assert.Equal(t, "a_very_bad_encryption_key", config.Storage.EncryptionKey)
}
func TestShouldRaiseIOErrOnUnreadableFile(t *testing.T) {
@ -184,6 +186,7 @@ func TestShouldValidateConfigurationWithEnvSecrets(t *testing.T) {
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_ENCRYPTION_KEY_FILE", "./test_resources/example_secret"))
val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@ -196,6 +199,7 @@ func TestShouldValidateConfigurationWithEnvSecrets(t *testing.T) {
assert.Equal(t, "example_secret value", config.Session.Secret)
assert.Equal(t, "example_secret value", config.AuthenticationBackend.LDAP.Password)
assert.Equal(t, "example_secret value", config.Storage.MySQL.Password)
assert.Equal(t, "example_secret value", config.Storage.EncryptionKey)
}
func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) {
@ -275,6 +279,7 @@ func testReset() {
testUnsetEnvName("PORT")
testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY")
testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_HMAC_SECRET")
testUnsetEnvName("STORAGE_ENCRYPTION_KEY")
}
func testUnsetEnvName(name string) {

View File

@ -33,6 +33,8 @@ type StorageConfiguration struct {
Local *LocalStorageConfiguration `koanf:"local"`
MySQL *MySQLStorageConfiguration `koanf:"mysql"`
PostgreSQL *PostgreSQLStorageConfiguration `koanf:"postgres"`
EncryptionKey string `koanf:"encryption_key"`
}
// DefaultPostgreSQLStorageConfiguration represents the default PostgreSQL configuration.

View File

@ -28,6 +28,7 @@ func newDefaultConfig() schema.Configuration {
Name: "authelia_session",
Secret: "secret",
}
config.Storage.EncryptionKey = testEncryptionKey
config.Storage.Local = &schema.LocalStorageConfiguration{
Path: "abc",
}

View File

@ -41,6 +41,7 @@ const (
testModeDisabled = "disable"
testTLSCert = "/tmp/cert.pem"
testTLSKey = "/tmp/key.pem"
testEncryptionKey = "a_not_so_secure_encryption_key"
)
// Notifier Error constants.
@ -206,6 +207,8 @@ var ValidKeys = []string{
"session.redis.timeouts.read",
"session.redis.timeouts.write",
"storage.encryption_key",
// Local Storage Keys.
"storage.local.path",

View File

@ -20,6 +20,12 @@ func ValidateStorage(configuration schema.StorageConfiguration, validator *schem
case configuration.Local != nil:
validateLocalStorageConfiguration(configuration.Local, validator)
}
if configuration.EncryptionKey == "" {
validator.Push(errors.New("the configuration option storage.encryption_key must be provided"))
} else if len(configuration.EncryptionKey) < 20 {
validator.Push(errors.New("the configuration option storage.encryption_key must be 20 characters or longer"))
}
}
func validateMySQLConfiguration(configuration *schema.SQLStorageConfiguration, validator *schema.StructValidator) {

View File

@ -16,6 +16,7 @@ type StorageSuite struct {
func (suite *StorageSuite) SetupTest() {
suite.validator = schema.NewStructValidator()
suite.configuration.EncryptionKey = testEncryptionKey
suite.configuration.Local = &schema.LocalStorageConfiguration{
Path: "/this/is/a/path",
}
@ -106,6 +107,26 @@ func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() {
suite.Assert().EqualError(suite.validator.Errors()[0], "SSL mode must be 'disable', 'require', 'verify-ca', or 'verify-full'")
}
func (suite *StorageSuite) TestShouldRaiseErrorOnNoEncryptionKey() {
suite.configuration.EncryptionKey = ""
ValidateStorage(suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "the configuration option storage.encryption_key must be provided")
}
func (suite *StorageSuite) TestShouldRaiseErrorOnShortEncryptionKey() {
suite.configuration.EncryptionKey = "abc"
ValidateStorage(suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "the configuration option storage.encryption_key must be 20 characters or longer")
}
func TestShouldRunStorageSuite(t *testing.T) {
suite.Run(t, new(StorageSuite))
}

View File

@ -59,7 +59,7 @@ func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username strin
Username: username,
Algorithm: otpAlgoToString(algorithm),
Digits: 6,
Secret: key.Secret(),
Secret: []byte(key.Secret()),
Period: key.Period(),
}

View File

@ -38,7 +38,7 @@ func (s *HandlerSignTOTPSuite) TearDownTest() {
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
@ -65,7 +65,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
@ -88,7 +88,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
@ -116,10 +116,10 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&models.TOTPConfiguration{Secret: "secret"}, nil)
Return(&models.TOTPConfiguration{Secret: []byte("secret")}, nil)
verifier.EXPECT().
Verify(gomock.Eq(&models.TOTPConfiguration{Secret: "secret"}), gomock.Eq("abc")).
Verify(gomock.Eq(&models.TOTPConfiguration{Secret: []byte("secret")}), gomock.Eq("abc")).
Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{
@ -136,7 +136,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFixation() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).

View File

@ -34,7 +34,7 @@ func (tv *TOTPVerifierImpl) Verify(config *models.TOTPConfiguration, token strin
Algorithm: otpStringToAlgo(config.Algorithm),
}
return totp.ValidateCustom(token, config.Secret, time.Now().UTC(), opts)
return totp.ValidateCustom(token, string(config.Secret), time.Now().UTC(), opts)
}
func otpAlgoToString(algorithm otp.Algorithm) (out string) {

View File

@ -7,5 +7,5 @@ type TOTPConfiguration struct {
Algorithm string `db:"algorithm"`
Digits int `db:"digits"`
Period uint64 `db:"totp_period"`
Secret string `db:"secret"`
Secret []byte `db:"secret"`
}

View File

@ -12,10 +12,15 @@ const (
tableDUODevices = "duo_devices"
tableAuthenticationLogs = "authentication_logs"
tableMigrations = "migrations"
tableEncryption = "encryption"
tablePrefixBackup = "_bkp_"
)
const (
encryptionNameCheck = "check"
)
// WARNING: Do not change/remove these consts. They are used for Pre1 migrations.
const (
tablePre1TOTPSecrets = "totp_secrets"

View File

@ -8,17 +8,31 @@ var (
// ErrNoU2FDeviceHandle error thrown when no U2F device handle has been found in DB.
ErrNoU2FDeviceHandle = errors.New("no U2F device handle found")
// ErrNoAuthenticationLogs error thrown when no matching authentication logs hve been found in DB.
ErrNoAuthenticationLogs = errors.New("no matching authentication logs found")
// ErrNoTOTPSecret error thrown when no TOTP secret has been found in DB.
ErrNoTOTPSecret = errors.New("no TOTP secret registered")
// ErrNoAvailableMigrations is returned when no available migrations can be found.
ErrNoAvailableMigrations = errors.New("no available migrations")
// ErrMigrateCurrentVersionSameAsTarget is returned when the target version is the same as the current.
ErrMigrateCurrentVersionSameAsTarget = errors.New("current version is same as migration target, no action being taken")
// ErrSchemaAlreadyUpToDate is returned when the schema is already up to date.
ErrSchemaAlreadyUpToDate = errors.New("schema already up to date")
// ErrNoMigrationsFound is returned when no migrations were found.
ErrNoMigrationsFound = errors.New("no schema migrations found")
// ErrSchemaEncryptionVersionUnsupported is returned when the schema is checked if the encryption key is valid for
// the database but the schema doesn't support encryption.
ErrSchemaEncryptionVersionUnsupported = errors.New("schema version doesn't support encryption")
// ErrSchemaEncryptionInvalidKey is returned when the schema is checked if the encryption key is valid for
// the database but the key doesn't appear to be valid.
ErrSchemaEncryptionInvalidKey = errors.New("the encryption key is not valid against the schema check value")
)
// Error formats for the storage provider.

View File

@ -85,7 +85,7 @@ func loadMigration(providerName string, version int, up bool) (migration *Schema
// this indicates the database zero state.
func loadMigrations(providerName string, prior, target int) (migrations []SchemaMigration, err error) {
if prior == target && (prior != -1 || target != -1) {
return nil, errors.New("cannot migrate to the same version as prior")
return nil, ErrMigrateCurrentVersionSameAsTarget
}
entries, err := migrationsFS.ReadDir("migrations")

View File

@ -4,3 +4,4 @@ DROP TABLE IF EXISTS totp_configurations;
DROP TABLE IF EXISTS u2f_devices;
DROP TABLE IF EXISTS user_preferences;
DROP TABLE IF EXISTS migrations;
DROP TABLE IF EXISTS encryption;

View File

@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS totp_configurations (
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6,
totp_period INTEGER NOT NULL DEFAULT 30,
secret VARCHAR(64) NOT NULL,
secret BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY (username)
);
@ -53,3 +53,11 @@ CREATE TABLE IF NOT EXISTS migrations (
application_version VARCHAR(128) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS encryption (
id INTEGER AUTO_INCREMENT,
name VARCHAR(100),
value BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY (name)
);

View File

@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS totp_configurations (
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6,
totp_period INTEGER NOT NULL DEFAULT 30,
secret VARCHAR(64) NOT NULL,
secret BYTEA NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
);
@ -53,3 +53,11 @@ CREATE TABLE IF NOT EXISTS migrations (
application_version VARCHAR(128) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS encryption (
id SERIAL,
name VARCHAR(100),
value BYTEA NOT NULL,
PRIMARY KEY (id),
UNIQUE (name)
);

View File

@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS totp_configurations (
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER(1) NOT NULL DEFAULT 6,
totp_period INTEGER NOT NULL DEFAULT 30,
secret VARCHAR(64) NOT NULL,
secret BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
);
@ -52,3 +52,11 @@ CREATE TABLE IF NOT EXISTS migrations (
application_version VARCHAR(128) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS encryption (
id INTEGER,
name VARCHAR(100),
value BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE (name)
);

View File

@ -24,6 +24,8 @@ type Provider interface {
SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error)
DeleteTOTPConfiguration(ctx context.Context, username string) (err error)
LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, err error)
LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error)
UpdateTOTPConfigurationSecret(ctx context.Context, config models.TOTPConfiguration) (err error)
SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error)
LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error)
@ -33,9 +35,14 @@ type Provider interface {
SchemaMigrate(ctx context.Context, up bool, version int) (err error)
SchemaMigrationHistory(ctx context.Context) (migrations []models.Migration, err error)
SchemaEncryptionChangeKey(ctx context.Context, encryptionKey string) (err error)
SchemaEncryptionCheckKey(ctx context.Context, verbose bool) (err error)
SchemaLatestVersion() (version int, err error)
SchemaMigrationsUp(ctx context.Context, version int) (migrations []SchemaMigration, err error)
SchemaMigrationsDown(ctx context.Context, version int) (migrations []SchemaMigration, err error)
Close() (err error)
}
// RegulatorProvider is an interface providing storage capabilities for persisting any kind of data related to the regulator.

View File

@ -4,13 +4,13 @@
package storage
import (
context "context"
reflect "reflect"
time "time"
"context"
"reflect"
"time"
gomock "github.com/golang/mock/gomock"
"github.com/golang/mock/gomock"
models "github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/models"
)
// MockProvider is a mock of Provider interface.
@ -50,6 +50,20 @@ func (mr *MockProviderMockRecorder) AppendAuthenticationLog(arg0, arg1 interface
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendAuthenticationLog", reflect.TypeOf((*MockProvider)(nil).AppendAuthenticationLog), arg0, arg1)
}
// Close mocks base method.
func (m *MockProvider) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockProviderMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockProvider)(nil).Close))
}
// DeleteTOTPConfiguration mocks base method.
func (m *MockProvider) DeleteTOTPConfiguration(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
@ -124,6 +138,21 @@ func (mr *MockProviderMockRecorder) LoadTOTPConfiguration(arg0, arg1 interface{}
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadTOTPConfiguration", reflect.TypeOf((*MockProvider)(nil).LoadTOTPConfiguration), arg0, arg1)
}
// LoadTOTPConfigurations mocks base method.
func (m *MockProvider) LoadTOTPConfigurations(arg0 context.Context, arg1, arg2 int) ([]models.TOTPConfiguration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadTOTPConfigurations", arg0, arg1, arg2)
ret0, _ := ret[0].([]models.TOTPConfiguration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadTOTPConfigurations indicates an expected call of LoadTOTPConfigurations.
func (mr *MockProviderMockRecorder) LoadTOTPConfigurations(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadTOTPConfigurations", reflect.TypeOf((*MockProvider)(nil).LoadTOTPConfigurations), arg0, arg1, arg2)
}
// LoadU2FDevice mocks base method.
func (m *MockProvider) LoadU2FDevice(arg0 context.Context, arg1 string) (*models.U2FDevice, error) {
m.ctrl.T.Helper()
@ -224,6 +253,34 @@ func (mr *MockProviderMockRecorder) SaveU2FDevice(arg0, arg1 interface{}) *gomoc
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveU2FDevice", reflect.TypeOf((*MockProvider)(nil).SaveU2FDevice), arg0, arg1)
}
// SchemaEncryptionChangeKey mocks base method.
func (m *MockProvider) SchemaEncryptionChangeKey(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaEncryptionChangeKey", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// SchemaEncryptionChangeKey indicates an expected call of SchemaEncryptionChangeKey.
func (mr *MockProviderMockRecorder) SchemaEncryptionChangeKey(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaEncryptionChangeKey", reflect.TypeOf((*MockProvider)(nil).SchemaEncryptionChangeKey), arg0, arg1)
}
// SchemaEncryptionCheckKey mocks base method.
func (m *MockProvider) SchemaEncryptionCheckKey(arg0 context.Context, arg1 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaEncryptionCheckKey", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// SchemaEncryptionCheckKey indicates an expected call of SchemaEncryptionCheckKey.
func (mr *MockProviderMockRecorder) SchemaEncryptionCheckKey(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaEncryptionCheckKey", reflect.TypeOf((*MockProvider)(nil).SchemaEncryptionCheckKey), arg0, arg1)
}
// SchemaLatestVersion mocks base method.
func (m *MockProvider) SchemaLatestVersion() (int, error) {
m.ctrl.T.Helper()
@ -341,3 +398,17 @@ func (mr *MockProviderMockRecorder) StartupCheck() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartupCheck", reflect.TypeOf((*MockProvider)(nil).StartupCheck))
}
// UpdateTOTPConfigurationSecret mocks base method.
func (m *MockProvider) UpdateTOTPConfigurationSecret(arg0 context.Context, arg1 models.TOTPConfiguration) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateTOTPConfigurationSecret", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateTOTPConfigurationSecret indicates an expected call of UpdateTOTPConfigurationSecret.
func (mr *MockProviderMockRecorder) UpdateTOTPConfigurationSecret(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTOTPConfigurationSecret", reflect.TypeOf((*MockProvider)(nil).UpdateTOTPConfigurationSecret), arg0, arg1)
}

View File

@ -2,6 +2,7 @@ package storage
import (
"context"
"crypto/sha256"
"database/sql"
"errors"
"fmt"
@ -16,15 +17,16 @@ import (
)
// NewSQLProvider generates a generic SQLProvider to be used with other SQL provider NewUp's.
func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvider) {
func NewSQLProvider(name, driverName, dataSourceName, encryptionKey string) (provider SQLProvider) {
db, err := sqlx.Open(driverName, dataSourceName)
provider = SQLProvider{
db: db,
key: sha256.Sum256([]byte(encryptionKey)),
name: name,
driverName: driverName,
db: db,
log: logging.Logger(),
errOpen: err,
log: logging.Logger(),
sqlInsertAuthenticationAttempt: fmt.Sprintf(queryFmtInsertAuthenticationLogEntry, tableAuthenticationLogs),
sqlSelectAuthenticationAttemptsByUsername: fmt.Sprintf(queryFmtSelect1FAAuthenticationLogEntryByUsername, tableAuthenticationLogs),
@ -33,9 +35,13 @@ func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvid
sqlDeleteIdentityVerification: fmt.Sprintf(queryFmtDeleteIdentityVerification, tableIdentityVerification),
sqlSelectExistsIdentityVerification: fmt.Sprintf(queryFmtSelectExistsIdentityVerification, tableIdentityVerification),
sqlUpsertTOTPConfig: fmt.Sprintf(queryFmtUpsertTOTPConfiguration, tableTOTPConfigurations),
sqlDeleteTOTPConfig: fmt.Sprintf(queryFmtDeleteTOTPConfiguration, tableTOTPConfigurations),
sqlSelectTOTPConfig: fmt.Sprintf(queryFmtSelectTOTPConfiguration, tableTOTPConfigurations),
sqlUpsertTOTPConfig: fmt.Sprintf(queryFmtUpsertTOTPConfiguration, tableTOTPConfigurations),
sqlDeleteTOTPConfig: fmt.Sprintf(queryFmtDeleteTOTPConfiguration, tableTOTPConfigurations),
sqlSelectTOTPConfig: fmt.Sprintf(queryFmtSelectTOTPConfiguration, tableTOTPConfigurations),
sqlSelectTOTPConfigs: fmt.Sprintf(queryFmtSelectTOTPConfigurations, tableTOTPConfigurations),
sqlUpdateTOTPConfigSecret: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecret, tableTOTPConfigurations),
sqlUpdateTOTPConfigSecretByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecretByUsername, tableTOTPConfigurations),
sqlUpsertU2FDevice: fmt.Sprintf(queryFmtUpsertU2FDevice, tableU2FDevices),
sqlSelectU2FDevice: fmt.Sprintf(queryFmtSelectU2FDevice, tableU2FDevices),
@ -48,20 +54,29 @@ func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvid
sqlSelectMigrations: fmt.Sprintf(queryFmtSelectMigrations, tableMigrations),
sqlSelectLatestMigration: fmt.Sprintf(queryFmtSelectLatestMigration, tableMigrations),
sqlUpsertEncryptionValue: fmt.Sprintf(queryFmtUpsertEncryptionValue, tableEncryption),
sqlSelectEncryptionValue: fmt.Sprintf(queryFmtSelectEncryptionValue, tableEncryption),
sqlFmtRenameTable: queryFmtRenameTable,
}
key := sha256.Sum256([]byte(encryptionKey))
provider.key = key
return provider
}
// SQLProvider is a storage provider persisting data in a SQL database.
type SQLProvider struct {
db *sqlx.DB
log *logrus.Logger
key [32]byte
name string
driverName string
errOpen error
log *logrus.Logger
// Table: authentication_logs.
sqlInsertAuthenticationAttempt string
sqlSelectAuthenticationAttemptsByUsername string
@ -72,9 +87,13 @@ type SQLProvider struct {
sqlSelectExistsIdentityVerification string
// Table: totp_configurations.
sqlUpsertTOTPConfig string
sqlDeleteTOTPConfig string
sqlSelectTOTPConfig string
sqlUpsertTOTPConfig string
sqlDeleteTOTPConfig string
sqlSelectTOTPConfig string
sqlSelectTOTPConfigs string
sqlUpdateTOTPConfigSecret string
sqlUpdateTOTPConfigSecretByUsername string
// Table: u2f_devices.
sqlUpsertU2FDevice string
@ -90,21 +109,29 @@ type SQLProvider struct {
sqlSelectMigrations string
sqlSelectLatestMigration string
// Table: encryption.
sqlUpsertEncryptionValue string
sqlSelectEncryptionValue string
// Utility.
sqlSelectExistingTables string
sqlFmtRenameTable string
}
// Close the underlying database connection.
func (p *SQLProvider) Close() (err error) {
return p.db.Close()
}
// StartupCheck implements the provider startup check interface.
func (p *SQLProvider) StartupCheck() (err error) {
if p.errOpen != nil {
return p.errOpen
return fmt.Errorf("error opening database: %w", p.errOpen)
}
// TODO: Decide if this is needed, or if it should be configurable.
for i := 0; i < 19; i++ {
err = p.db.Ping()
if err == nil {
if err = p.db.Ping(); err == nil {
break
}
@ -112,13 +139,17 @@ func (p *SQLProvider) StartupCheck() (err error) {
}
if err != nil {
return err
return fmt.Errorf("error pinging database: %w", err)
}
p.log.Infof("Storage schema is being checked for updates")
ctx := context.Background()
if err = p.SchemaEncryptionCheckKey(ctx, false); err != nil && !errors.Is(err, ErrSchemaEncryptionVersionUnsupported) {
return err
}
err = p.SchemaMigrate(ctx, true, SchemaLatest)
switch err {
@ -128,7 +159,7 @@ func (p *SQLProvider) StartupCheck() (err error) {
case nil:
return nil
default:
return err
return fmt.Errorf("error during schema migrate: %w", err)
}
}
@ -143,13 +174,13 @@ func (p *SQLProvider) SavePreferred2FAMethod(ctx context.Context, username strin
func (p *SQLProvider) LoadPreferred2FAMethod(ctx context.Context, username string) (method string, err error) {
err = p.db.GetContext(ctx, &method, p.sqlSelectPreferred2FAMethod, username)
switch err {
case sql.ErrNoRows:
switch {
case err == nil:
return method, nil
case errors.Is(err, sql.ErrNoRows):
return "", nil
case nil:
return method, err
default:
return "", err
return "", fmt.Errorf("error selecting preferred two factor method for user '%s': %w", username, err)
}
}
@ -161,120 +192,98 @@ func (p *SQLProvider) LoadUserInfo(ctx context.Context, username string) (info m
case err == nil:
return info, nil
case errors.Is(err, sql.ErrNoRows):
_, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, authentication.PossibleMethods[0])
if err != nil {
return models.UserInfo{}, err
if _, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, authentication.PossibleMethods[0]); err != nil {
return models.UserInfo{}, fmt.Errorf("error upserting preferred two factor method while selecting user info for user '%s': %w", username, err)
}
err = p.db.GetContext(ctx, &info, p.sqlSelectUserInfo, username, username, username)
if err != nil {
return models.UserInfo{}, err
if err = p.db.GetContext(ctx, &info, p.sqlSelectUserInfo, username, username, username); err != nil {
return models.UserInfo{}, fmt.Errorf("error selecting user info for user '%s': %w", username, err)
}
return info, nil
default:
return models.UserInfo{}, err
return models.UserInfo{}, fmt.Errorf("error selecting user info for user '%s': %w", username, err)
}
}
// SaveIdentityVerification save an identity verification record to the database.
func (p *SQLProvider) SaveIdentityVerification(ctx context.Context, verification models.IdentityVerification) (err error) {
_, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification, verification.Token)
if _, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification, verification.Token); err != nil {
return fmt.Errorf("error inserting identity verification: %w", err)
}
return err
return nil
}
// RemoveIdentityVerification remove an identity verification record from the database.
func (p *SQLProvider) RemoveIdentityVerification(ctx context.Context, token string) (err error) {
_, err = p.db.ExecContext(ctx, p.sqlDeleteIdentityVerification, token)
if _, err = p.db.ExecContext(ctx, p.sqlDeleteIdentityVerification, token); err != nil {
return fmt.Errorf("error updating identity verification: %w", err)
}
return err
return nil
}
// FindIdentityVerification checks if an identity verification record is in the database and active.
func (p *SQLProvider) FindIdentityVerification(ctx context.Context, jti string) (found bool, err error) {
err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, jti)
if err != nil {
return false, err
func (p *SQLProvider) FindIdentityVerification(ctx context.Context, token string) (found bool, err error) {
if err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, token); err != nil {
return false, fmt.Errorf("error selecting identity verification exists: %w", err)
}
return found, nil
}
// SaveTOTPConfiguration save a TOTP config of a given user in the database.
// SaveTOTPConfiguration save a TOTP configuration of a given user in the database.
func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error) {
// TODO: Encrypt config.Secret here.
_, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig,
config.Username,
config.Algorithm,
config.Digits,
config.Period,
config.Secret,
)
if config.Secret, err = p.encrypt(config.Secret); err != nil {
return fmt.Errorf("error encrypting the TOTP configuration secret: %v", err)
}
return err
if _, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig,
config.Username, config.Algorithm, config.Digits, config.Period, config.Secret); err != nil {
return fmt.Errorf("error upserting TOTP configuration: %w", err)
}
return nil
}
// DeleteTOTPConfiguration delete a TOTP secret from the database given a username.
// DeleteTOTPConfiguration delete a TOTP configuration from the database given a username.
func (p *SQLProvider) DeleteTOTPConfiguration(ctx context.Context, username string) (err error) {
_, err = p.db.ExecContext(ctx, p.sqlDeleteTOTPConfig, username)
if _, err = p.db.ExecContext(ctx, p.sqlDeleteTOTPConfig, username); err != nil {
return fmt.Errorf("error deleting TOTP configuration: %w", err)
}
return err
return nil
}
// LoadTOTPConfiguration load a TOTP secret given a username from the database.
// LoadTOTPConfiguration load a TOTP configuration given a username from the database.
func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, err error) {
config = &models.TOTPConfiguration{}
err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config)
if err != nil {
if err == sql.ErrNoRows {
if err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoTOTPSecret
}
return nil, err
return nil, fmt.Errorf("error selecting TOTP configuration: %w", err)
}
if config.Secret, err = p.decrypt(config.Secret); err != nil {
return nil, fmt.Errorf("error decrypting the TOTP secret: %v", err)
}
// TODO: Decrypt config.Secret here.
return config, nil
}
// SaveU2FDevice saves a registered U2F device.
func (p *SQLProvider) SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error) {
_, err = p.db.ExecContext(ctx, p.sqlUpsertU2FDevice, device.Username, device.KeyHandle, device.PublicKey)
return err
}
// LoadU2FDevice loads a U2F device registration for a given username.
func (p *SQLProvider) LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error) {
device = &models.U2FDevice{
Username: username,
}
err = p.db.GetContext(ctx, device, p.sqlSelectU2FDevice, username)
// LoadTOTPConfigurations load a set of TOTP configurations.
func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error) {
rows, err := p.db.QueryxContext(ctx, p.sqlSelectTOTPConfigs, limit, limit*page)
if err != nil {
if err == sql.ErrNoRows {
return nil, ErrNoU2FDeviceHandle
if errors.Is(err, sql.ErrNoRows) {
return configs, nil
}
return nil, err
}
return device, nil
}
// AppendAuthenticationLog append a mark to the authentication log.
func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt models.AuthenticationAttempt) (err error) {
_, err = p.db.ExecContext(ctx, p.sqlInsertAuthenticationAttempt, attempt.Time, attempt.Successful, attempt.Username)
return err
}
// LoadAuthenticationLogs retrieve the latest failed authentications from the authentication log.
func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username string, fromDate time.Time, limit, page int) (attempts []models.AuthenticationAttempt, err error) {
rows, err := p.db.QueryxContext(ctx, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page)
if err != nil {
return nil, err
return nil, fmt.Errorf("error selecting TOTP configurations: %w", err)
}
defer func() {
@ -283,13 +292,99 @@ func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username strin
}
}()
configs = make([]models.TOTPConfiguration, 0, limit)
var config models.TOTPConfiguration
for rows.Next() {
if err = rows.StructScan(&config); err != nil {
return nil, fmt.Errorf("error scanning TOTP configuration to struct: %w", err)
}
if config.Secret, err = p.decrypt(config.Secret); err != nil {
return nil, fmt.Errorf("error decrypting the TOTP secret: %v", err)
}
configs = append(configs, config)
}
return configs, nil
}
// UpdateTOTPConfigurationSecret updates a TOTP configuration secret.
func (p *SQLProvider) UpdateTOTPConfigurationSecret(ctx context.Context, config models.TOTPConfiguration) (err error) {
switch config.ID {
case 0:
_, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigSecretByUsername, config.Secret, config.Username)
default:
_, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigSecret, config.Secret, config.ID)
}
if err != nil {
return fmt.Errorf("error updating TOTP configuration secret: %w", err)
}
return nil
}
// SaveU2FDevice saves a registered U2F device.
func (p *SQLProvider) SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlUpsertU2FDevice, device.Username, device.KeyHandle, device.PublicKey); err != nil {
return fmt.Errorf("error upserting U2F device secret: %v", err)
}
return nil
}
// LoadU2FDevice loads a U2F device registration for a given username.
func (p *SQLProvider) LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error) {
device = &models.U2FDevice{
Username: username,
}
if err = p.db.GetContext(ctx, device, p.sqlSelectU2FDevice, username); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoU2FDeviceHandle
}
return nil, fmt.Errorf("error selecting U2F device: %w", err)
}
return device, nil
}
// AppendAuthenticationLog append a mark to the authentication log.
func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt models.AuthenticationAttempt) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlInsertAuthenticationAttempt, attempt.Time, attempt.Successful, attempt.Username); err != nil {
return fmt.Errorf("error inserting authentiation attempt: %w", err)
}
return nil
}
// LoadAuthenticationLogs retrieve the latest failed authentications from the authentication log.
func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username string, fromDate time.Time, limit, page int) (attempts []models.AuthenticationAttempt, err error) {
rows, err := p.db.QueryxContext(ctx, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoAuthenticationLogs
}
return nil, fmt.Errorf("error selecting authentication logs: %w", err)
}
defer func() {
if err := rows.Close(); err != nil {
p.log.Errorf(logFmtErrClosingConn, err)
}
}()
var attempt models.AuthenticationAttempt
attempts = make([]models.AuthenticationAttempt, 0, limit)
for rows.Next() {
var attempt models.AuthenticationAttempt
err = rows.StructScan(&attempt)
if err != nil {
if err = rows.StructScan(&attempt); err != nil {
return nil, err
}

View File

@ -15,9 +15,9 @@ type MySQLProvider struct {
}
// NewMySQLProvider a MySQL provider.
func NewMySQLProvider(config schema.MySQLStorageConfiguration) (provider *MySQLProvider) {
func NewMySQLProvider(config schema.MySQLStorageConfiguration, encryptionKey string) (provider *MySQLProvider) {
provider = &MySQLProvider{
SQLProvider: NewSQLProvider(providerMySQL, providerMySQL, dataSourceNameMySQL(config)),
SQLProvider: NewSQLProvider(providerMySQL, providerMySQL, dataSourceNameMySQL(config), encryptionKey),
}
// All providers have differing SELECT existing table statements.

View File

@ -16,9 +16,9 @@ type PostgreSQLProvider struct {
}
// NewPostgreSQLProvider a PostgreSQL provider.
func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration) (provider *PostgreSQLProvider) {
func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration, encryptionKey string) (provider *PostgreSQLProvider) {
provider = &PostgreSQLProvider{
SQLProvider: NewSQLProvider(providerPostgres, "pgx", dataSourceNamePostgreSQL(config)),
SQLProvider: NewSQLProvider(providerPostgres, "pgx", dataSourceNamePostgreSQL(config), encryptionKey),
}
// All providers have differing SELECT existing table statements.
@ -29,6 +29,7 @@ func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration) (provid
provider.sqlUpsertU2FDevice = fmt.Sprintf(queryFmtPostgresUpsertU2FDevice, tableU2FDevices)
provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtPostgresUpsertTOTPConfiguration, tableTOTPConfigurations)
provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtPostgresUpsertPreferred2FAMethod, tableUserPreferences)
provider.sqlUpsertEncryptionValue = fmt.Sprintf(queryFmtPostgresUpsertEncryptionValue, tableEncryption)
// PostgreSQL requires rebinding of any query that contains a '?' placeholder to use the '$#' notation placeholders.
provider.sqlFmtRenameTable = provider.db.Rebind(provider.sqlFmtRenameTable)
@ -40,10 +41,14 @@ func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration) (provid
provider.sqlSelectTOTPConfig = provider.db.Rebind(provider.sqlSelectTOTPConfig)
provider.sqlUpsertTOTPConfig = provider.db.Rebind(provider.sqlUpsertTOTPConfig)
provider.sqlDeleteTOTPConfig = provider.db.Rebind(provider.sqlDeleteTOTPConfig)
provider.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs)
provider.sqlUpdateTOTPConfigSecret = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecret)
provider.sqlUpdateTOTPConfigSecretByUsername = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecretByUsername)
provider.sqlSelectU2FDevice = provider.db.Rebind(provider.sqlSelectU2FDevice)
provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt)
provider.sqlSelectAuthenticationAttemptsByUsername = provider.db.Rebind(provider.sqlSelectAuthenticationAttemptsByUsername)
provider.sqlInsertMigration = provider.db.Rebind(provider.sqlInsertMigration)
provider.sqlSelectEncryptionValue = provider.db.Rebind(provider.sqlSelectEncryptionValue)
return provider
}

View File

@ -10,9 +10,9 @@ type SQLiteProvider struct {
}
// NewSQLiteProvider constructs a SQLite provider.
func NewSQLiteProvider(path string) (provider *SQLiteProvider) {
func NewSQLiteProvider(path, encryptionKey string) (provider *SQLiteProvider) {
provider = &SQLiteProvider{
SQLProvider: NewSQLProvider(providerSQLite, "sqlite3", path),
SQLProvider: NewSQLProvider(providerSQLite, "sqlite3", path, encryptionKey),
}
// All providers have differing SELECT existing table statements.

View File

@ -0,0 +1,185 @@
package storage
import (
"context"
"crypto/sha256"
"fmt"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/utils"
)
// SchemaEncryptionChangeKey uses the currently configured key to decrypt values in the database and the key provided
// by this command to encrypt the values again and update them using a transaction.
func (p *SQLProvider) SchemaEncryptionChangeKey(ctx context.Context, encryptionKey string) (err error) {
tx, err := p.db.Beginx()
if err != nil {
return fmt.Errorf("error beginning transaction to change encryption key: %w", err)
}
key := sha256.Sum256([]byte(encryptionKey))
var configs []models.TOTPConfiguration
for page := 0; true; page++ {
if configs, err = p.LoadTOTPConfigurations(ctx, 10, page); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
}
return fmt.Errorf("rollback due to error: %w", err)
}
for _, config := range configs {
if config.Secret, err = utils.Encrypt(config.Secret, &key); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
}
return fmt.Errorf("rollback due to error: %w", err)
}
if err = p.UpdateTOTPConfigurationSecret(ctx, config); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
}
return fmt.Errorf("rollback due to error: %w", err)
}
}
if len(configs) != 10 {
break
}
}
if err = p.setNewEncryptionCheckValue(ctx, &key, tx); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
}
return fmt.Errorf("rollback due to error: %w", err)
}
return tx.Commit()
}
// SchemaEncryptionCheckKey checks the encryption key configured is valid for the database.
func (p *SQLProvider) SchemaEncryptionCheckKey(ctx context.Context, verbose bool) (err error) {
version, err := p.SchemaVersion(ctx)
if err != nil {
return err
}
if version < 1 {
return ErrSchemaEncryptionVersionUnsupported
}
var errs []error
if _, err = p.getEncryptionValue(ctx, encryptionNameCheck); err != nil {
errs = append(errs, ErrSchemaEncryptionInvalidKey)
}
if verbose {
var (
config models.TOTPConfiguration
row int
invalid int
total int
)
pageSize := 10
var rows *sqlx.Rows
for page := 0; true; page++ {
if rows, err = p.db.QueryxContext(ctx, p.sqlSelectTOTPConfigs, pageSize, pageSize*page); err != nil {
_ = rows.Close()
return fmt.Errorf("error selecting TOTP configurations: %w", err)
}
row = 0
for rows.Next() {
total++
row++
if err = rows.StructScan(&config); err != nil {
_ = rows.Close()
return fmt.Errorf("error scanning TOTP configuration to struct: %w", err)
}
if _, err = p.decrypt(config.Secret); err != nil {
invalid++
}
}
_ = rows.Close()
if row < pageSize {
break
}
}
if invalid != 0 {
errs = append(errs, fmt.Errorf("%d of %d total TOTP secrets were invalid", invalid, total))
}
}
if len(errs) != 0 {
for i, e := range errs {
if i == 0 {
err = e
continue
}
err = fmt.Errorf("%w, %v", err, e)
}
return err
}
return nil
}
func (p SQLProvider) encrypt(clearText []byte) (cipherText []byte, err error) {
return utils.Encrypt(clearText, &p.key)
}
func (p SQLProvider) decrypt(cipherText []byte) (clearText []byte, err error) {
return utils.Decrypt(cipherText, &p.key)
}
func (p *SQLProvider) getEncryptionValue(ctx context.Context, name string) (value []byte, err error) {
var encryptedValue []byte
err = p.db.GetContext(ctx, &encryptedValue, p.sqlSelectEncryptionValue, name)
if err != nil {
return nil, err
}
return p.decrypt(encryptedValue)
}
func (p *SQLProvider) setNewEncryptionCheckValue(ctx context.Context, key *[32]byte, e sqlx.ExecerContext) (err error) {
valueClearText := uuid.New()
value, err := utils.Encrypt([]byte(valueClearText.String()), key)
if err != nil {
return err
}
if e != nil {
_, err = e.ExecContext(ctx, p.sqlUpsertEncryptionValue, encryptionNameCheck, value)
} else {
_, err = p.db.ExecContext(ctx, p.sqlUpsertEncryptionValue, encryptionNameCheck, value)
}
return err
}

View File

@ -78,6 +78,24 @@ const (
FROM %s
WHERE username = ?;`
queryFmtSelectTOTPConfigurations = `
SELECT id, username, algorithm, digits, totp_period, secret
FROM %s
LIMIT ?
OFFSET ?;`
//nolint:gosec // These are not hardcoded credentials it's a query to obtain credentials.
queryFmtUpdateTOTPConfigurationSecret = `
UPDATE %s
SET secret = ?
WHERE id = ?;`
//nolint:gosec // These are not hardcoded credentials it's a query to obtain credentials.
queryFmtUpdateTOTPConfigurationSecretByUsername = `
UPDATE %s
SET secret = ?
WHERE username = ?;`
queryFmtUpsertTOTPConfiguration = `
REPLACE INTO %s (username, algorithm, digits, totp_period, secret)
VALUES (?, ?, ?, ?, ?);`
@ -123,3 +141,20 @@ const (
LIMIT ?
OFFSET ?;`
)
const (
queryFmtSelectEncryptionValue = `
SELECT (value)
FROM %s
WHERE name = ?`
queryFmtUpsertEncryptionValue = `
REPLACE INTO %s (name, value)
VALUES (?, ?);`
queryFmtPostgresUpsertEncryptionValue = `
INSERT INTO %s (name, value)
VALUES ($1, $2)
ON CONFLICT (name)
DO UPDATE SET value=$2;`
)

View File

@ -63,8 +63,6 @@ func (p *SQLProvider) SchemaVersion(ctx context.Context) (version int, err error
return -1, nil
}
// TODO: Decide if we want to support external tables.
// return -2, ErrUnknownSchemaState
return 0, nil
}
@ -213,7 +211,18 @@ func (p *SQLProvider) schemaMigrateApply(ctx context.Context, migration SchemaMi
return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err)
}
// Skip the migration history insertion in a migration to v0.
if migration.Version == 1 {
// Skip the migration history insertion in a migration to v0.
if !migration.Up {
return nil
}
// Add the schema encryption value if upgrading to v1.
if err = p.setNewEncryptionCheckValue(ctx, &p.key, nil); err != nil {
return err
}
}
if migration.Version == 1 && !migration.Up {
return nil
}

View File

@ -49,6 +49,10 @@ func (p *SQLProvider) schemaMigratePre1To1(ctx context.Context) (err error) {
return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err)
}
if err = p.setNewEncryptionCheckValue(ctx, &p.key, nil); err != nil {
return err
}
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1InsertUserPreferencesFromSelect),
tableUserPreferences, tablePrefixBackup+tableUserPreferences)); err != nil {
return err
@ -213,8 +217,10 @@ func (p *SQLProvider) schemaMigratePre1To1TOTP(ctx context.Context) (err error)
return err
}
// TODO: Add encryption migration here.
encryptedSecret := "encrypted:" + secret
encryptedSecret, err := p.encrypt([]byte(secret))
if err != nil {
return err
}
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: encryptedSecret})
}
@ -288,6 +294,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1(ctx context.Context) (err error) {
tableDUODevices,
tableUserPreferences,
tableAuthenticationLogs,
tableEncryption,
}
if err = p.schemaMigratePre1Rename(ctx, tables, tablesRename); err != nil {
@ -388,18 +395,22 @@ func (p *SQLProvider) schemaMigrate1ToPre1TOTP(ctx context.Context) (err error)
}()
for rows.Next() {
var username, encryptedSecret string
var (
username string
secretCipherText []byte
)
err = rows.Scan(&username, &encryptedSecret)
err = rows.Scan(&username, &secretCipherText)
if err != nil {
return err
}
// TODO: Fix.
// TODO: Add DECRYPTION migration here.
decryptedSecret := strings.Replace(encryptedSecret, "encrypted:", "", 1)
secretClearText, err := p.decrypt(secretCipherText)
if err != nil {
return err
}
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: decryptedSecret})
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: secretClearText})
}
for _, config := range totpConfigs {

View File

@ -37,6 +37,7 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite3

View File

@ -26,6 +26,7 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -26,8 +26,9 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
path: /tmp/db.sqlite
access_control:
default_policy: bypass
@ -43,5 +44,5 @@ access_control:
notifier:
filesystem:
filename: /config/notification.txt
filename: /tmp/notification.txt
...

View File

@ -4,6 +4,9 @@ services:
authelia-backend:
volumes:
- './CLI/configuration.yml:/config/configuration.yml:ro'
- './CLI/storage.yml:/config/configuration.storage.yml:ro'
- './CLI/users.yml:/config/users.yml'
- './common/ssl:/config/ssl:ro'
- '/tmp:/tmp'
user: ${USER_ID}:${GROUP_ID}
...

View File

@ -0,0 +1,6 @@
---
storage:
encryption_key: a_cli_encryption_key_which_isnt_secure
local:
path: /tmp/db.sqlite3
...

View File

@ -27,6 +27,7 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite3

View File

@ -28,6 +28,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -26,6 +26,7 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -110,6 +110,7 @@ regulation:
ban_time: 10
storage:
encryption_key: a_not_so_secure_encryption_key
mysql:
host: mariadb
port: 3306

View File

@ -41,6 +41,7 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite3

View File

@ -28,6 +28,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
encryption_key: a_not_so_secure_encryption_key
mysql:
host: mariadb
port: 3306

View File

@ -29,6 +29,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
encryption_key: a_not_so_secure_encryption_key
mysql:
host: mysql
port: 3306

View File

@ -27,6 +27,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -27,6 +27,7 @@ session:
port: 6379
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -27,6 +27,7 @@ session:
port: 6379
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -27,6 +27,7 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -27,6 +27,7 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -28,6 +28,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
encryption_key: a_not_so_secure_encryption_key
postgres:
host: postgres
port: 5432

View File

@ -27,6 +27,7 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -25,6 +25,7 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /tmp/db.sqlite3

View File

@ -27,6 +27,7 @@ session:
remember_me_duration: 1y
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -32,6 +32,7 @@ session:
password: redis-user-password
storage:
encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite

View File

@ -40,6 +40,8 @@ spec:
value: /app/secrets/session
- name: AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE
value: /app/secrets/sql_password
- name: AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE
value: /app/secrets/encryption_key
- name: ENVIRONMENT
value: dev
volumes:
@ -69,4 +71,6 @@ spec:
path: sql_password
- key: ldap_password
path: ldap_password
- key: encryption_key
path: encryption_key
...

View File

@ -12,4 +12,5 @@ data:
ldap_password: cGFzc3dvcmQ= # password
session: dW5zZWN1cmVfcGFzc3dvcmQ= # unsecure_password
sql_password: cGFzc3dvcmQ= # password
encryption_key: YV9ub3Rfc29fc2VjdXJlX2VuY3J5cHRpb25fa2V5
...

View File

@ -2,6 +2,7 @@ package suites
import (
"fmt"
"os"
"time"
)
@ -35,6 +36,9 @@ func init() {
teardown := func(suitePath string) error {
err := dockerEnvironment.Down()
_ = os.Remove("/tmp/db.sqlite3")
_ = os.Remove("/tmp/db.sqlite")
return err
}

View File

@ -1,11 +1,18 @@
package suites
import (
"context"
"fmt"
"os"
"regexp"
"testing"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/storage"
)
type CLISuite struct {
@ -40,7 +47,7 @@ func (s *CLISuite) SetupTest() {
func (s *CLISuite) TestShouldPrintBuildInformation() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "build-info"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Last Tag: ")
s.Assert().Contains(output, "State: ")
s.Assert().Contains(output, "Branch: ")
@ -55,13 +62,13 @@ func (s *CLISuite) TestShouldPrintBuildInformation() {
func (s *CLISuite) TestShouldPrintVersion() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "--version"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "authelia version")
}
func (s *CLISuite) TestShouldValidateConfig() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "/config/configuration.yml"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Configuration parsed successfully without errors")
}
@ -73,33 +80,33 @@ func (s *CLISuite) TestShouldFailValidateConfig() {
func (s *CLISuite) TestShouldHashPasswordArgon2id() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-m", "32", "-s", "test1234"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Password hash: $argon2id$v=19$m=32768,t=1,p=8")
}
func (s *CLISuite) TestShouldHashPasswordSHA512() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-z"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Password hash: $6$rounds=50000")
}
func (s *CLISuite) TestShouldGenerateCertificateRSA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateRSAWithIPAddress() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "127.0.0.1", "--dir", "/tmp/"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateRSAWithStartDate() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--start-date", "'Jan 1 15:04:05 2011'"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
@ -112,14 +119,14 @@ func (s *CLISuite) TestShouldFailGenerateCertificateRSAWithStartDate() {
func (s *CLISuite) TestShouldGenerateCertificateCA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ca"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateEd25519() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ed25519"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
@ -132,32 +139,238 @@ func (s *CLISuite) TestShouldFailGenerateCertificateECDSA() {
func (s *CLISuite) TestShouldGenerateCertificateECDSAP224() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P224"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP256() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P256"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP384() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P384"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP521() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P521"})
s.Assert().Nil(err)
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestStorageShouldShowErrWithoutConfig() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres', the configuration option storage.encryption_key must be provided\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "history"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres', the configuration option storage.encryption_key must be provided\n")
}
func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
pattern := regexp.MustCompile(`^Schema Version: N/A\nSchema Upgrade Available: yes - version \d+\nSchema Tables: N/A\nSchema Encryption Key: unsupported \(schema version\)`)
s.Assert().Regexp(pattern, output)
patternOutdated := regexp.MustCompile(`Error: schema is version \d+ which is outdated please migrate to version \d+ in order to use this command or use an older binary`)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "export", "totp-configurations", "--config", "/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Regexp(patternOutdated, output)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--config", "/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Regexp(patternOutdated, output)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Could not check encryption key for validity. The schema version doesn't support encryption.")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target", "0", "--destroy-data", "--config", "/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: schema migration target version 0 is the same current version 0")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "up", "--target", "2147483640", "--config", "/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: schema up migration target version 2147483640 is greater then the latest version ")
s.Assert().Contains(output, " which indicates it doesn't exist")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "history"})
s.Assert().NoError(err)
s.Assert().Contains(output, "No migration history is available for schemas that not version 1 or above.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-up"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Storage Schema Migration List (Up)\n\nVersion\t\tDescription\n1\t\tInitial Schema\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-down"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Storage Schema Migration List (Down)\n\nNo Migrations Available\n")
}
func (s *CLISuite) TestStorage01ShouldMigrateUp() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "up"})
s.Require().NoError(err)
pattern0 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is being attempted"`)
pattern1 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is complete"`)
s.Regexp(pattern0, output)
s.Regexp(pattern1, output)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "up"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: schema already up to date\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "history"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Migration History:\n\nID\tDate\t\t\t\tBefore\tAfter\tAuthelia Version\n")
s.Assert().Contains(output, "0\t1")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-up"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Storage Schema Migration List (Up)\n\nNo Migrations Available")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-down"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Storage Schema Migration List (Down)\n\nVersion\t\tDescription\n")
s.Assert().Contains(output, "1\t\tInitial Schema")
}
func (s *CLISuite) TestStorage02ShouldShowSchemaInfo() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
pattern := regexp.MustCompile(`^Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification_tokens, totp_configurations, u2f_devices, user_preferences, migrations, encryption\nSchema Encryption Key: valid`)
s.Assert().Regexp(pattern, output)
}
func (s *CLISuite) TestStorage03ShouldExportTOTP() {
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_cli_encryption_key_which_isnt_secure")
s.Require().NoError(provider.StartupCheck())
ctx := context.Background()
var (
err error
key *otp.Key
config models.TOTPConfiguration
)
var (
expectedLines = make([]string, 0, 3)
expectedLinesCSV = make([]string, 0, 4)
output string
)
expectedLinesCSV = append(expectedLinesCSV, "issuer,username,algorithm,digits,period,secret")
for _, name := range []string{"john", "mary", "fred"} {
key, err = totp.Generate(totp.GenerateOpts{
Issuer: "Authelia",
AccountName: name,
Period: uint(30),
SecretSize: 32,
Digits: otp.Digits(6),
Algorithm: otp.AlgorithmSHA1,
})
s.Require().NoError(err)
config = models.TOTPConfiguration{
Username: name,
Algorithm: "SHA1",
Digits: 6,
Secret: []byte(key.Secret()),
Period: key.Period(),
}
expectedLinesCSV = append(expectedLinesCSV, fmt.Sprintf("%s,%s,%s,%d,%d,%s", "Authelia", config.Username, config.Algorithm, config.Digits, config.Period, string(config.Secret)))
expectedLines = append(expectedLines, fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d", "Authelia", config.Username, string(config.Secret), "Authelia", config.Algorithm, config.Digits, config.Period))
s.Require().NoError(provider.SaveTOTPConfiguration(ctx, config))
}
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "export", "totp-configurations", "--format", "uri", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
for _, expectedLine := range expectedLines {
s.Assert().Contains(output, expectedLine)
}
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "export", "totp-configurations", "--format", "csv", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
for _, expectedLine := range expectedLinesCSV {
s.Assert().Contains(output, expectedLine)
}
}
func (s *CLISuite) TestStorage04ShouldChangeEncryptionKey() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--new-encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Completed the encryption key change. Please adjust your configuration to use the new key.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
pattern := regexp.MustCompile(`Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification_tokens, totp_configurations, u2f_devices, user_preferences, migrations, encryption\nSchema Encryption Key: invalid`)
s.Assert().Regexp(pattern, output)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Encryption key validation: failed.\n\nError: the encryption key is not valid against the schema check value.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Encryption key validation: failed.\n\nError: the encryption key is not valid against the schema check value, 3 of 3 total TOTP secrets were invalid.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Encryption key validation: success.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Encryption key validation: success.\n")
}
func (s *CLISuite) TestStorage05ShouldMigrateDown() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target", "0", "--destroy-data", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
pattern0 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is being attempted"`)
pattern1 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is complete"`)
s.Regexp(pattern0, output)
s.Regexp(pattern1, output)
}
func TestCLISuite(t *testing.T) {
if testing.Short() {
t.Skip("skipping suite test in short mode")

View File

@ -121,7 +121,7 @@ func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice()
password := "password"
// Clean up any TOTP secret already in DB.
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3")
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key")
require.NoError(s.T(), provider.StartupCheck())
require.NoError(s.T(), provider.DeleteTOTPConfiguration(ctx, username))