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 #682pull/2632/head
parent
eb94960348
commit
347bd1be77
|
@ -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)
|
||||||
##
|
##
|
||||||
|
|
|
@ -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 |
|
||||||
|
|
|
@ -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).
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
)
|
||||||
|
|
|
@ -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) ||
|
|
||||||
(config.Storage.MySQL != nil && config.Storage.Local != nil) ||
|
func getProviders() (providers middlewares.Providers, warnings []error, errors []error) {
|
||||||
(config.Storage.PostgreSQL != nil && config.Storage.Local != nil) {
|
// TODO: Adjust this so the CertPool can be used like a provider.
|
||||||
return nil, errors.New("multiple storage providers are configured but should only configure one")
|
autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
|
||||||
}
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
##
|
##
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()).
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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;
|
|
@ -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)
|
||||||
|
);
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
|
@ -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)
|
||||||
|
);
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
if _, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig,
|
||||||
|
config.Username, config.Algorithm, config.Digits, config.Period, config.Secret); err != nil {
|
||||||
|
return fmt.Errorf("error upserting TOTP configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTOTPConfiguration delete a TOTP secret from the database given a username.
|
// DeleteTOTPConfiguration delete a TOTP configuration from the database given a username.
|
||||||
func (p *SQLProvider) DeleteTOTPConfiguration(ctx context.Context, username string) (err error) {
|
func (p *SQLProvider) DeleteTOTPConfiguration(ctx context.Context, username string) (err error) {
|
||||||
_, err = p.db.ExecContext(ctx, p.sqlDeleteTOTPConfig, username)
|
if _, err = p.db.ExecContext(ctx, p.sqlDeleteTOTPConfig, username); err != nil {
|
||||||
|
return fmt.Errorf("error deleting TOTP configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadTOTPConfiguration load a TOTP secret given a username from the database.
|
// LoadTOTPConfiguration load a TOTP configuration given a username from the database.
|
||||||
func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, err error) {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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;`
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
...
|
...
|
||||||
|
|
|
@ -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}
|
||||||
...
|
...
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
storage:
|
||||||
|
encryption_key: a_cli_encryption_key_which_isnt_secure
|
||||||
|
local:
|
||||||
|
path: /tmp/db.sqlite3
|
||||||
|
...
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
...
|
...
|
||||||
|
|
|
@ -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
|
||||||
...
|
...
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in New Issue