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
pull/2632/head
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. ## The available providers are: `local`, `mysql`, `postgres`. You must use one and only one of these providers.
storage: 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) ## 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.secret |AUTHELIA_SESSION_SECRET_FILE |
|session.redis.password |AUTHELIA_SESSION_REDIS_PASSWORD_FILE | |session.redis.password |AUTHELIA_SESSION_REDIS_PASSWORD_FILE |
|session.redis.high_availability.sentinel_password|AUTHELIA_REDIS_HIGH_AVAILABILITY_SENTINEL_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.mysql.password |AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE |
|storage.postgres.password |AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE | |storage.postgres.password |AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE |
|notifier.smtp.password |AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE | |notifier.smtp.password |AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE |

View File

@ -6,9 +6,43 @@ nav_order: 14
has_children: true has_children: true
--- ---
# Storage backends
**Authelia** supports multiple storage backends. The backend is used to store user preferences, 2FA device handles and **Authelia** supports multiple storage backends. The backend is used to store user preferences, 2FA device handles and
secrets, authentication logs, etc... secrets, authentication logs, etc...
The available storage backends are listed in the table of contents below. 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 ```yaml
storage: storage:
encryption_key: a_very_important_secret
mysql: mysql:
host: 127.0.0.1 host: 127.0.0.1
port: 3306 port: 3306
@ -24,6 +25,9 @@ storage:
## Options ## Options
### encryption_key
See the [encryption_key docs](./index.md#encryption_key).
### host ### host
<div markdown="1"> <div markdown="1">
type: string type: string

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +1,84 @@
package commands package commands
import ( 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/storage"
"github.com/authelia/authelia/v4/internal/utils"
) )
func getStorageProvider() (provider storage.Provider, err error) { func getStorageProvider() (provider storage.Provider) {
switch { switch {
case config.Storage.PostgreSQL != nil: 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: 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: case config.Storage.Local != nil:
provider = storage.NewSQLiteProvider(config.Storage.Local.Path) return storage.NewSQLiteProvider(config.Storage.Local.Path, config.Storage.EncryptionKey)
default: default:
return nil, errors.New("no storage provider configured") return nil
}
} }
if (config.Storage.MySQL != nil && config.Storage.PostgreSQL != nil) || func getProviders() (providers middlewares.Providers, warnings []error, errors []error) {
(config.Storage.MySQL != nil && config.Storage.Local != nil) || // TODO: Adjust this so the CertPool can be used like a provider.
(config.Storage.PostgreSQL != nil && config.Storage.Local != nil) { autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
return nil, errors.New("multiple storage providers are configured but should only configure one") if len(warnings) != 0 || len(errors) != 0 {
return providers, warnings, errors
} }
return provider, err 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/sirupsen/logrus"
"github.com/spf13/cobra" "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/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/models" "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/server"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
@ -67,7 +59,7 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
logger.Fatalf("Cannot initialize logger: %v", err) logger.Fatalf("Cannot initialize logger: %v", err)
} }
providers, warnings, errors := getProviders(config) providers, warnings, errors := getProviders()
if len(warnings) != 0 { if len(warnings) != 0 {
for _, err := range warnings { for _, err := range warnings {
logger.Warn(err) logger.Warn(err)
@ -87,72 +79,6 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
server.Start(*config, providers) 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) { func doStartupChecks(config *schema.Configuration, providers *middlewares.Providers) {
logger := logging.Logger() 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().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("sqlite.path", "", "the SQLite database path")
cmd.PersistentFlags().String("mysql.host", "", "the MySQL hostname") cmd.PersistentFlags().String("mysql.host", "", "the MySQL hostname")
@ -32,11 +34,74 @@ func NewStorageCmd() (cmd *cobra.Command) {
cmd.AddCommand( cmd.AddCommand(
newStorageMigrateCmd(), newStorageMigrateCmd(),
newStorageSchemaInfoCmd(), newStorageSchemaInfoCmd(),
newStorageEncryptionCmd(),
newStorageExportCmd(),
) )
return cmd 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) { func newStorageSchemaInfoCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{ cmd = &cobra.Command{
Use: "schema-info", Use: "schema-info",

View File

@ -12,6 +12,7 @@ import (
"github.com/authelia/authelia/v4/internal/configuration" "github.com/authelia/authelia/v4/internal/configuration"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/configuration/validator" "github.com/authelia/authelia/v4/internal/configuration/validator"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/storage" "github.com/authelia/authelia/v4/internal/storage"
) )
@ -38,6 +39,7 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
} }
mapping := map[string]string{ mapping := map[string]string{
"encryption-key": "storage.encryption_key",
"sqlite.path": "storage.local.path", "sqlite.path": "storage.local.path",
"mysql.host": "storage.mysql.host", "mysql.host": "storage.mysql.host",
"mysql.port": "storage.mysql.port", "mysql.port": "storage.mysql.port",
@ -100,17 +102,179 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
return nil 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) { func storageMigrateHistoryRunE(_ *cobra.Command, _ []string) (err error) {
var ( var (
provider storage.Provider provider storage.Provider
ctx = context.Background() 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 { if err != nil {
return err 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) migrations, err := provider.SchemaMigrationHistory(ctx)
if err != nil { if err != nil {
return err return err
@ -138,11 +302,15 @@ func newStorageMigrateListRunE(up bool) func(cmd *cobra.Command, args []string)
directionStr string directionStr string
) )
provider, err = getStorageProvider() provider = getStorageProvider()
if err != nil { if provider == nil {
return err return errNoStorageProvider
} }
defer func() {
_ = provider.Close()
}()
if up { if up {
migrations, err = provider.SchemaMigrationsUp(ctx, 0) migrations, err = provider.SchemaMigrationsUp(ctx, 0)
directionStr = "Up" directionStr = "Up"
@ -151,13 +319,7 @@ func newStorageMigrateListRunE(up bool) func(cmd *cobra.Command, args []string)
directionStr = "Down" directionStr = "Down"
} }
if err != nil { if err != nil && !errors.Is(err, storage.ErrNoAvailableMigrations) && !errors.Is(err, storage.ErrMigrateCurrentVersionSameAsTarget) {
if err.Error() == "cannot migrate to the same version as prior" {
fmt.Printf("No %s migrations found\n", directionStr)
return nil
}
return err return err
} }
@ -182,11 +344,15 @@ func newStorageMigrationRunE(up bool) func(cmd *cobra.Command, args []string) (e
ctx = context.Background() ctx = context.Background()
) )
provider, err = getStorageProvider() provider = getStorageProvider()
if err != nil { if provider == nil {
return err return errNoStorageProvider
} }
defer func() {
_ = provider.Close()
}()
target, err := cmd.Flags().GetInt("target") target, err := cmd.Flags().GetInt("target")
if err != nil { if err != nil {
return err return err
@ -253,11 +419,15 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
tablesStr string tablesStr string
) )
provider, err = getStorageProvider() provider = getStorageProvider()
if err != nil { if provider == nil {
return err return errNoStorageProvider
} }
defer func() {
_ = provider.Close()
}()
version, err := provider.SchemaVersion(ctx) version, err := provider.SchemaVersion(ctx)
if err != nil && err.Error() != "unknown schema state" { if err != nil && err.Error() != "unknown schema state" {
return err return err
@ -285,7 +455,37 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
upgradeStr = "no" 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 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. ## The available providers are: `local`, `mysql`, `postgres`. You must use one and only one of these providers.
storage: 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) ## 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+"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+"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+"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() val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) _, 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, "example_secret value", config.Session.Secret)
assert.Equal(t, "an env storage mysql password", config.Storage.MySQL.Password) 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, "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) { 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+"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+"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+"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() val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) _, 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.Session.Secret)
assert.Equal(t, "example_secret value", config.AuthenticationBackend.LDAP.Password) 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.MySQL.Password)
assert.Equal(t, "example_secret value", config.Storage.EncryptionKey)
} }
func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) { func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) {
@ -275,6 +279,7 @@ func testReset() {
testUnsetEnvName("PORT") testUnsetEnvName("PORT")
testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY") testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY")
testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_HMAC_SECRET") testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_HMAC_SECRET")
testUnsetEnvName("STORAGE_ENCRYPTION_KEY")
} }
func testUnsetEnvName(name string) { func testUnsetEnvName(name string) {

View File

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

View File

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

View File

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

View File

@ -20,6 +20,12 @@ func ValidateStorage(configuration schema.StorageConfiguration, validator *schem
case configuration.Local != nil: case configuration.Local != nil:
validateLocalStorageConfiguration(configuration.Local, validator) 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) { func validateMySQLConfiguration(configuration *schema.SQLStorageConfiguration, validator *schema.StructValidator) {

View File

@ -16,6 +16,7 @@ type StorageSuite struct {
func (suite *StorageSuite) SetupTest() { func (suite *StorageSuite) SetupTest() {
suite.validator = schema.NewStructValidator() suite.validator = schema.NewStructValidator()
suite.configuration.EncryptionKey = testEncryptionKey
suite.configuration.Local = &schema.LocalStorageConfiguration{ suite.configuration.Local = &schema.LocalStorageConfiguration{
Path: "/this/is/a/path", 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'") 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) { func TestShouldRunStorageSuite(t *testing.T) {
suite.Run(t, new(StorageSuite)) suite.Run(t, new(StorageSuite))
} }

View File

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

View File

@ -38,7 +38,7 @@ func (s *HandlerSignTOTPSuite) TearDownTest() {
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() { func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl) 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(). s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
@ -65,7 +65,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() { func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl) 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(). s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
@ -88,7 +88,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() { func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl) 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(). s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
@ -116,10 +116,10 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&models.TOTPConfiguration{Secret: "secret"}, nil) Return(&models.TOTPConfiguration{Secret: []byte("secret")}, nil)
verifier.EXPECT(). 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) Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
@ -136,7 +136,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFixation() { func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFixation() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl) 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(). s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). 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), 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) { func otpAlgoToString(algorithm otp.Algorithm) (out string) {

View File

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

View File

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

View File

@ -8,17 +8,31 @@ var (
// ErrNoU2FDeviceHandle error thrown when no U2F device handle has been found in DB. // ErrNoU2FDeviceHandle error thrown when no U2F device handle has been found in DB.
ErrNoU2FDeviceHandle = errors.New("no U2F device handle found") 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 error thrown when no TOTP secret has been found in DB.
ErrNoTOTPSecret = errors.New("no TOTP secret registered") ErrNoTOTPSecret = errors.New("no TOTP secret registered")
// ErrNoAvailableMigrations is returned when no available migrations can be found. // ErrNoAvailableMigrations is returned when no available migrations can be found.
ErrNoAvailableMigrations = errors.New("no available migrations") 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 is returned when the schema is already up to date.
ErrSchemaAlreadyUpToDate = errors.New("schema already up to date") ErrSchemaAlreadyUpToDate = errors.New("schema already up to date")
// ErrNoMigrationsFound is returned when no migrations were found. // ErrNoMigrationsFound is returned when no migrations were found.
ErrNoMigrationsFound = errors.New("no schema migrations 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. // 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. // this indicates the database zero state.
func loadMigrations(providerName string, prior, target int) (migrations []SchemaMigration, err error) { func loadMigrations(providerName string, prior, target int) (migrations []SchemaMigration, err error) {
if prior == target && (prior != -1 || target != -1) { 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") 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 u2f_devices;
DROP TABLE IF EXISTS user_preferences; DROP TABLE IF EXISTS user_preferences;
DROP TABLE IF EXISTS migrations; 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', algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6, digits INTEGER NOT NULL DEFAULT 6,
totp_period INTEGER NOT NULL DEFAULT 30, totp_period INTEGER NOT NULL DEFAULT 30,
secret VARCHAR(64) NOT NULL, secret BLOB NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY (username) UNIQUE KEY (username)
); );
@ -53,3 +53,11 @@ CREATE TABLE IF NOT EXISTS migrations (
application_version VARCHAR(128) NOT NULL, application_version VARCHAR(128) NOT NULL,
PRIMARY KEY (id) 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', algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6, digits INTEGER NOT NULL DEFAULT 6,
totp_period INTEGER NOT NULL DEFAULT 30, totp_period INTEGER NOT NULL DEFAULT 30,
secret VARCHAR(64) NOT NULL, secret BYTEA NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE (username) UNIQUE (username)
); );
@ -53,3 +53,11 @@ CREATE TABLE IF NOT EXISTS migrations (
application_version VARCHAR(128) NOT NULL, application_version VARCHAR(128) NOT NULL,
PRIMARY KEY (id) 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', algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER(1) NOT NULL DEFAULT 6, digits INTEGER(1) NOT NULL DEFAULT 6,
totp_period INTEGER NOT NULL DEFAULT 30, totp_period INTEGER NOT NULL DEFAULT 30,
secret VARCHAR(64) NOT NULL, secret BLOB NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE (username) UNIQUE (username)
); );
@ -52,3 +52,11 @@ CREATE TABLE IF NOT EXISTS migrations (
application_version VARCHAR(128) NOT NULL, application_version VARCHAR(128) NOT NULL,
PRIMARY KEY (id) 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) SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error)
DeleteTOTPConfiguration(ctx context.Context, username string) (err error) DeleteTOTPConfiguration(ctx context.Context, username string) (err error)
LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, 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) SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error)
LoadU2FDevice(ctx context.Context, username string) (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) SchemaMigrate(ctx context.Context, up bool, version int) (err error)
SchemaMigrationHistory(ctx context.Context) (migrations []models.Migration, 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) SchemaLatestVersion() (version int, err error)
SchemaMigrationsUp(ctx context.Context, version int) (migrations []SchemaMigration, err error) SchemaMigrationsUp(ctx context.Context, version int) (migrations []SchemaMigration, err error)
SchemaMigrationsDown(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. // 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 package storage
import ( import (
context "context" "context"
reflect "reflect" "reflect"
time "time" "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. // 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) 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. // DeleteTOTPConfiguration mocks base method.
func (m *MockProvider) DeleteTOTPConfiguration(arg0 context.Context, arg1 string) error { func (m *MockProvider) DeleteTOTPConfiguration(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper() 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) 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. // LoadU2FDevice mocks base method.
func (m *MockProvider) LoadU2FDevice(arg0 context.Context, arg1 string) (*models.U2FDevice, error) { func (m *MockProvider) LoadU2FDevice(arg0 context.Context, arg1 string) (*models.U2FDevice, error) {
m.ctrl.T.Helper() 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) 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. // SchemaLatestVersion mocks base method.
func (m *MockProvider) SchemaLatestVersion() (int, error) { func (m *MockProvider) SchemaLatestVersion() (int, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -341,3 +398,17 @@ func (mr *MockProviderMockRecorder) StartupCheck() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartupCheck", reflect.TypeOf((*MockProvider)(nil).StartupCheck)) 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 ( import (
"context" "context"
"crypto/sha256"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
@ -16,15 +17,16 @@ import (
) )
// NewSQLProvider generates a generic SQLProvider to be used with other SQL provider NewUp's. // 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) db, err := sqlx.Open(driverName, dataSourceName)
provider = SQLProvider{ provider = SQLProvider{
db: db,
key: sha256.Sum256([]byte(encryptionKey)),
name: name, name: name,
driverName: driverName, driverName: driverName,
db: db,
log: logging.Logger(),
errOpen: err, errOpen: err,
log: logging.Logger(),
sqlInsertAuthenticationAttempt: fmt.Sprintf(queryFmtInsertAuthenticationLogEntry, tableAuthenticationLogs), sqlInsertAuthenticationAttempt: fmt.Sprintf(queryFmtInsertAuthenticationLogEntry, tableAuthenticationLogs),
sqlSelectAuthenticationAttemptsByUsername: fmt.Sprintf(queryFmtSelect1FAAuthenticationLogEntryByUsername, tableAuthenticationLogs), sqlSelectAuthenticationAttemptsByUsername: fmt.Sprintf(queryFmtSelect1FAAuthenticationLogEntryByUsername, tableAuthenticationLogs),
@ -36,6 +38,10 @@ func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvid
sqlUpsertTOTPConfig: fmt.Sprintf(queryFmtUpsertTOTPConfiguration, tableTOTPConfigurations), sqlUpsertTOTPConfig: fmt.Sprintf(queryFmtUpsertTOTPConfiguration, tableTOTPConfigurations),
sqlDeleteTOTPConfig: fmt.Sprintf(queryFmtDeleteTOTPConfiguration, tableTOTPConfigurations), sqlDeleteTOTPConfig: fmt.Sprintf(queryFmtDeleteTOTPConfiguration, tableTOTPConfigurations),
sqlSelectTOTPConfig: fmt.Sprintf(queryFmtSelectTOTPConfiguration, 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), sqlUpsertU2FDevice: fmt.Sprintf(queryFmtUpsertU2FDevice, tableU2FDevices),
sqlSelectU2FDevice: fmt.Sprintf(queryFmtSelectU2FDevice, tableU2FDevices), sqlSelectU2FDevice: fmt.Sprintf(queryFmtSelectU2FDevice, tableU2FDevices),
@ -48,20 +54,29 @@ func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvid
sqlSelectMigrations: fmt.Sprintf(queryFmtSelectMigrations, tableMigrations), sqlSelectMigrations: fmt.Sprintf(queryFmtSelectMigrations, tableMigrations),
sqlSelectLatestMigration: fmt.Sprintf(queryFmtSelectLatestMigration, tableMigrations), sqlSelectLatestMigration: fmt.Sprintf(queryFmtSelectLatestMigration, tableMigrations),
sqlUpsertEncryptionValue: fmt.Sprintf(queryFmtUpsertEncryptionValue, tableEncryption),
sqlSelectEncryptionValue: fmt.Sprintf(queryFmtSelectEncryptionValue, tableEncryption),
sqlFmtRenameTable: queryFmtRenameTable, sqlFmtRenameTable: queryFmtRenameTable,
} }
key := sha256.Sum256([]byte(encryptionKey))
provider.key = key
return provider return provider
} }
// SQLProvider is a storage provider persisting data in a SQL database. // SQLProvider is a storage provider persisting data in a SQL database.
type SQLProvider struct { type SQLProvider struct {
db *sqlx.DB db *sqlx.DB
log *logrus.Logger key [32]byte
name string name string
driverName string driverName string
errOpen error errOpen error
log *logrus.Logger
// Table: authentication_logs. // Table: authentication_logs.
sqlInsertAuthenticationAttempt string sqlInsertAuthenticationAttempt string
sqlSelectAuthenticationAttemptsByUsername string sqlSelectAuthenticationAttemptsByUsername string
@ -75,6 +90,10 @@ type SQLProvider struct {
sqlUpsertTOTPConfig string sqlUpsertTOTPConfig string
sqlDeleteTOTPConfig string sqlDeleteTOTPConfig string
sqlSelectTOTPConfig string sqlSelectTOTPConfig string
sqlSelectTOTPConfigs string
sqlUpdateTOTPConfigSecret string
sqlUpdateTOTPConfigSecretByUsername string
// Table: u2f_devices. // Table: u2f_devices.
sqlUpsertU2FDevice string sqlUpsertU2FDevice string
@ -90,21 +109,29 @@ type SQLProvider struct {
sqlSelectMigrations string sqlSelectMigrations string
sqlSelectLatestMigration string sqlSelectLatestMigration string
// Table: encryption.
sqlUpsertEncryptionValue string
sqlSelectEncryptionValue string
// Utility. // Utility.
sqlSelectExistingTables string sqlSelectExistingTables string
sqlFmtRenameTable 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. // StartupCheck implements the provider startup check interface.
func (p *SQLProvider) StartupCheck() (err error) { func (p *SQLProvider) StartupCheck() (err error) {
if p.errOpen != nil { 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. // TODO: Decide if this is needed, or if it should be configurable.
for i := 0; i < 19; i++ { for i := 0; i < 19; i++ {
err = p.db.Ping() if err = p.db.Ping(); err == nil {
if err == nil {
break break
} }
@ -112,13 +139,17 @@ func (p *SQLProvider) StartupCheck() (err error) {
} }
if err != nil { if err != nil {
return err return fmt.Errorf("error pinging database: %w", err)
} }
p.log.Infof("Storage schema is being checked for updates") p.log.Infof("Storage schema is being checked for updates")
ctx := context.Background() ctx := context.Background()
if err = p.SchemaEncryptionCheckKey(ctx, false); err != nil && !errors.Is(err, ErrSchemaEncryptionVersionUnsupported) {
return err
}
err = p.SchemaMigrate(ctx, true, SchemaLatest) err = p.SchemaMigrate(ctx, true, SchemaLatest)
switch err { switch err {
@ -128,7 +159,7 @@ func (p *SQLProvider) StartupCheck() (err error) {
case nil: case nil:
return nil return nil
default: 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) { func (p *SQLProvider) LoadPreferred2FAMethod(ctx context.Context, username string) (method string, err error) {
err = p.db.GetContext(ctx, &method, p.sqlSelectPreferred2FAMethod, username) err = p.db.GetContext(ctx, &method, p.sqlSelectPreferred2FAMethod, username)
switch err { switch {
case sql.ErrNoRows: case err == nil:
return method, nil
case errors.Is(err, sql.ErrNoRows):
return "", nil return "", nil
case nil:
return method, err
default: 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: case err == nil:
return info, nil return info, nil
case errors.Is(err, sql.ErrNoRows): case errors.Is(err, sql.ErrNoRows):
_, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, authentication.PossibleMethods[0]) if _, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, authentication.PossibleMethods[0]); err != nil {
if err != nil { return models.UserInfo{}, fmt.Errorf("error upserting preferred two factor method while selecting user info for user '%s': %w", username, err)
return models.UserInfo{}, err
} }
err = p.db.GetContext(ctx, &info, p.sqlSelectUserInfo, username, username, username) if err = p.db.GetContext(ctx, &info, p.sqlSelectUserInfo, username, username, username); err != nil {
if err != nil { return models.UserInfo{}, fmt.Errorf("error selecting user info for user '%s': %w", username, err)
return models.UserInfo{}, err
} }
return info, nil return info, nil
default: 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. // SaveIdentityVerification save an identity verification record to the database.
func (p *SQLProvider) SaveIdentityVerification(ctx context.Context, verification models.IdentityVerification) (err error) { 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. // RemoveIdentityVerification remove an identity verification record from the database.
func (p *SQLProvider) RemoveIdentityVerification(ctx context.Context, token string) (err error) { 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. // 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) { func (p *SQLProvider) FindIdentityVerification(ctx context.Context, token string) (found bool, err error) {
err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, jti) if err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, token); err != nil {
if err != nil { return false, fmt.Errorf("error selecting identity verification exists: %w", err)
return false, err
} }
return found, nil 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) { func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error) {
// TODO: Encrypt config.Secret here. if config.Secret, err = p.encrypt(config.Secret); err != nil {
_, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig, return fmt.Errorf("error encrypting the TOTP configuration secret: %v", err)
config.Username,
config.Algorithm,
config.Digits,
config.Period,
config.Secret,
)
return err
} }
// DeleteTOTPConfiguration delete a TOTP secret from the database given a username. 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 configuration from the database given a username.
func (p *SQLProvider) DeleteTOTPConfiguration(ctx context.Context, username string) (err error) { 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
} }
// LoadTOTPConfiguration load a TOTP secret given a username from the database. return nil
}
// 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) { func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, err error) {
config = &models.TOTPConfiguration{} config = &models.TOTPConfiguration{}
err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config) if err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config); err != nil {
if err != nil { if errors.Is(err, sql.ErrNoRows) {
if err == sql.ErrNoRows {
return nil, ErrNoTOTPSecret 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 return config, nil
} }
// SaveU2FDevice saves a registered U2F device. // LoadTOTPConfigurations load a set of TOTP configurations.
func (p *SQLProvider) SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error) { func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error) {
_, err = p.db.ExecContext(ctx, p.sqlUpsertU2FDevice, device.Username, device.KeyHandle, device.PublicKey) rows, err := p.db.QueryxContext(ctx, p.sqlSelectTOTPConfigs, limit, limit*page)
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)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoU2FDeviceHandle return configs, nil
} }
return nil, err return nil, fmt.Errorf("error selecting TOTP configurations: %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) {
_, 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
} }
defer func() { 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) attempts = make([]models.AuthenticationAttempt, 0, limit)
for rows.Next() { for rows.Next() {
var attempt models.AuthenticationAttempt if err = rows.StructScan(&attempt); err != nil {
err = rows.StructScan(&attempt)
if err != nil {
return nil, err return nil, err
} }

View File

@ -15,9 +15,9 @@ type MySQLProvider struct {
} }
// NewMySQLProvider a MySQL provider. // NewMySQLProvider a MySQL provider.
func NewMySQLProvider(config schema.MySQLStorageConfiguration) (provider *MySQLProvider) { func NewMySQLProvider(config schema.MySQLStorageConfiguration, encryptionKey string) (provider *MySQLProvider) {
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. // All providers have differing SELECT existing table statements.

View File

@ -16,9 +16,9 @@ type PostgreSQLProvider struct {
} }
// NewPostgreSQLProvider a PostgreSQL provider. // NewPostgreSQLProvider a PostgreSQL provider.
func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration) (provider *PostgreSQLProvider) { func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration, encryptionKey string) (provider *PostgreSQLProvider) {
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. // 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.sqlUpsertU2FDevice = fmt.Sprintf(queryFmtPostgresUpsertU2FDevice, tableU2FDevices)
provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtPostgresUpsertTOTPConfiguration, tableTOTPConfigurations) provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtPostgresUpsertTOTPConfiguration, tableTOTPConfigurations)
provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtPostgresUpsertPreferred2FAMethod, tableUserPreferences) 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. // PostgreSQL requires rebinding of any query that contains a '?' placeholder to use the '$#' notation placeholders.
provider.sqlFmtRenameTable = provider.db.Rebind(provider.sqlFmtRenameTable) 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.sqlSelectTOTPConfig = provider.db.Rebind(provider.sqlSelectTOTPConfig)
provider.sqlUpsertTOTPConfig = provider.db.Rebind(provider.sqlUpsertTOTPConfig) provider.sqlUpsertTOTPConfig = provider.db.Rebind(provider.sqlUpsertTOTPConfig)
provider.sqlDeleteTOTPConfig = provider.db.Rebind(provider.sqlDeleteTOTPConfig) 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.sqlSelectU2FDevice = provider.db.Rebind(provider.sqlSelectU2FDevice)
provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt) provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt)
provider.sqlSelectAuthenticationAttemptsByUsername = provider.db.Rebind(provider.sqlSelectAuthenticationAttemptsByUsername) provider.sqlSelectAuthenticationAttemptsByUsername = provider.db.Rebind(provider.sqlSelectAuthenticationAttemptsByUsername)
provider.sqlInsertMigration = provider.db.Rebind(provider.sqlInsertMigration) provider.sqlInsertMigration = provider.db.Rebind(provider.sqlInsertMigration)
provider.sqlSelectEncryptionValue = provider.db.Rebind(provider.sqlSelectEncryptionValue)
return provider return provider
} }

View File

@ -10,9 +10,9 @@ type SQLiteProvider struct {
} }
// NewSQLiteProvider constructs a SQLite provider. // NewSQLiteProvider constructs a SQLite provider.
func NewSQLiteProvider(path string) (provider *SQLiteProvider) { func NewSQLiteProvider(path, encryptionKey string) (provider *SQLiteProvider) {
provider = &SQLiteProvider{ provider = &SQLiteProvider{
SQLProvider: NewSQLProvider(providerSQLite, "sqlite3", path), SQLProvider: NewSQLProvider(providerSQLite, "sqlite3", path, encryptionKey),
} }
// All providers have differing SELECT existing table statements. // 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 FROM %s
WHERE username = ?;` 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 = ` queryFmtUpsertTOTPConfiguration = `
REPLACE INTO %s (username, algorithm, digits, totp_period, secret) REPLACE INTO %s (username, algorithm, digits, totp_period, secret)
VALUES (?, ?, ?, ?, ?);` VALUES (?, ?, ?, ?, ?);`
@ -123,3 +141,20 @@ const (
LIMIT ? LIMIT ?
OFFSET ?;` 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 return -1, nil
} }
// TODO: Decide if we want to support external tables.
// return -2, ErrUnknownSchemaState
return 0, nil 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) return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err)
} }
if migration.Version == 1 {
// Skip the migration history insertion in a migration to v0. // 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 { if migration.Version == 1 && !migration.Up {
return nil 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) 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), if _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1InsertUserPreferencesFromSelect),
tableUserPreferences, tablePrefixBackup+tableUserPreferences)); err != nil { tableUserPreferences, tablePrefixBackup+tableUserPreferences)); err != nil {
return err return err
@ -213,8 +217,10 @@ func (p *SQLProvider) schemaMigratePre1To1TOTP(ctx context.Context) (err error)
return err return err
} }
// TODO: Add encryption migration here. encryptedSecret, err := p.encrypt([]byte(secret))
encryptedSecret := "encrypted:" + secret if err != nil {
return err
}
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: encryptedSecret}) totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: encryptedSecret})
} }
@ -288,6 +294,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1(ctx context.Context) (err error) {
tableDUODevices, tableDUODevices,
tableUserPreferences, tableUserPreferences,
tableAuthenticationLogs, tableAuthenticationLogs,
tableEncryption,
} }
if err = p.schemaMigratePre1Rename(ctx, tables, tablesRename); err != nil { 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() { 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 { if err != nil {
return err return err
} }
// TODO: Fix. secretClearText, err := p.decrypt(secretCipherText)
// TODO: Add DECRYPTION migration here. if err != nil {
decryptedSecret := strings.Replace(encryptedSecret, "encrypted:", "", 1) return err
}
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: decryptedSecret}) totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: secretClearText})
} }
for _, config := range totpConfigs { for _, config := range totpConfigs {

View File

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

View File

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

View File

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

View File

@ -4,6 +4,9 @@ services:
authelia-backend: authelia-backend:
volumes: volumes:
- './CLI/configuration.yml:/config/configuration.yml:ro' - './CLI/configuration.yml:/config/configuration.yml:ro'
- './CLI/storage.yml:/config/configuration.storage.yml:ro'
- './CLI/users.yml:/config/users.yml' - './CLI/users.yml:/config/users.yml'
- './common/ssl:/config/ssl:ro' - './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 remember_me_duration: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key
local: local:
path: /config/db.sqlite3 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:
encryption_key: a_not_so_secure_encryption_key
local: local:
path: /config/db.sqlite path: /config/db.sqlite

View File

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

View File

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

View File

@ -41,6 +41,7 @@ session:
remember_me_duration: 1y remember_me_duration: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key
local: local:
path: /config/db.sqlite3 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:
encryption_key: a_not_so_secure_encryption_key
mysql: mysql:
host: mariadb host: mariadb
port: 3306 port: 3306

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,6 +27,7 @@ session:
remember_me_duration: 1y remember_me_duration: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key
local: local:
path: /config/db.sqlite 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:
encryption_key: a_not_so_secure_encryption_key
postgres: postgres:
host: postgres host: postgres
port: 5432 port: 5432

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,18 @@
package suites package suites
import ( import (
"context"
"fmt"
"os" "os"
"regexp" "regexp"
"testing" "testing"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/storage"
) )
type CLISuite struct { type CLISuite struct {
@ -40,7 +47,7 @@ func (s *CLISuite) SetupTest() {
func (s *CLISuite) TestShouldPrintBuildInformation() { func (s *CLISuite) TestShouldPrintBuildInformation() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "build-info"}) 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, "Last Tag: ")
s.Assert().Contains(output, "State: ") s.Assert().Contains(output, "State: ")
s.Assert().Contains(output, "Branch: ") s.Assert().Contains(output, "Branch: ")
@ -55,13 +62,13 @@ func (s *CLISuite) TestShouldPrintBuildInformation() {
func (s *CLISuite) TestShouldPrintVersion() { func (s *CLISuite) TestShouldPrintVersion() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "--version"}) 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") s.Assert().Contains(output, "authelia version")
} }
func (s *CLISuite) TestShouldValidateConfig() { func (s *CLISuite) TestShouldValidateConfig() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "/config/configuration.yml"}) 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") s.Assert().Contains(output, "Configuration parsed successfully without errors")
} }
@ -73,33 +80,33 @@ func (s *CLISuite) TestShouldFailValidateConfig() {
func (s *CLISuite) TestShouldHashPasswordArgon2id() { func (s *CLISuite) TestShouldHashPasswordArgon2id() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-m", "32", "-s", "test1234"}) 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") s.Assert().Contains(output, "Password hash: $argon2id$v=19$m=32768,t=1,p=8")
} }
func (s *CLISuite) TestShouldHashPasswordSHA512() { func (s *CLISuite) TestShouldHashPasswordSHA512() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-z"}) 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") s.Assert().Contains(output, "Password hash: $6$rounds=50000")
} }
func (s *CLISuite) TestShouldGenerateCertificateRSA() { func (s *CLISuite) TestShouldGenerateCertificateRSA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/"}) 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 Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem") s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
} }
func (s *CLISuite) TestShouldGenerateCertificateRSAWithIPAddress() { 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/"}) 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 Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem") s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
} }
func (s *CLISuite) TestShouldGenerateCertificateRSAWithStartDate() { 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'"}) 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 Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.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() { func (s *CLISuite) TestShouldGenerateCertificateCA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ca"}) 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 Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem") s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
} }
func (s *CLISuite) TestShouldGenerateCertificateEd25519() { func (s *CLISuite) TestShouldGenerateCertificateEd25519() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ed25519"}) 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 Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.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() { 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"}) 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 Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem") s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
} }
func (s *CLISuite) TestShouldGenerateCertificateECDSAP256() { 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"}) 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 Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem") s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
} }
func (s *CLISuite) TestShouldGenerateCertificateECDSAP384() { 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"}) 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 Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem") s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
} }
func (s *CLISuite) TestShouldGenerateCertificateECDSAP521() { 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"}) 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 Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.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) { func TestCLISuite(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping suite test in short mode") t.Skip("skipping suite test in short mode")

View File

@ -121,7 +121,7 @@ func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice()
password := "password" password := "password"
// Clean up any TOTP secret already in DB. // 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.StartupCheck())
require.NoError(s.T(), provider.DeleteTOTPConfiguration(ctx, username)) require.NoError(s.T(), provider.DeleteTOTPConfiguration(ctx, username))