feat(totp): algorithm and digits config (#2634)

Allow users to configure the TOTP Algorithm and Digits. This should be used with caution as many TOTP applications do not support it. Some will also fail to notify the user that there is an issue. i.e. if the algorithm in the QR code is sha512, they continue to generate one time passwords with sha1. In addition this drastically refactors TOTP in general to be more user friendly by not forcing them to register a new device if the administrator changes the period (or algorithm).

Fixes #1226.
pull/2629/head^2
James Elliott 2021-12-01 23:11:29 +11:00 committed by GitHub
parent 01b77384f9
commit ad8e844af6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 1285 additions and 773 deletions

View File

@ -317,6 +317,25 @@ paths:
description: Forbidden description: Forbidden
security: security:
- authelia_auth: [] - authelia_auth: []
/api/user/info/totp:
get:
tags:
- User TOTP Information
summary: User TOTP Configuration
description: >
The user TOTP info endpoint provides information necessary to display the TOTP component to validate their
TOTP input such as the period/frequency and number of digits.
responses:
"200":
description: Successful Operation
content:
application/json:
schema:
$ref: '#/components/schemas/handlers.UserInfoTOTP'
"403":
description: Forbidden
security:
- authelia_auth: []
/api/user/info/2fa_method: /api/user/info/2fa_method:
post: post:
tags: tags:
@ -640,9 +659,6 @@ components:
second_factor_enabled: second_factor_enabled:
type: boolean type: boolean
description: If second factor is enabled. description: If second factor is enabled.
totp_period:
type: integer
example: 30
handlers.DuoDeviceBody: handlers.DuoDeviceBody:
required: required:
- device - device
@ -841,6 +857,25 @@ components:
has_duo: has_duo:
type: boolean type: boolean
example: true example: true
handlers.UserInfoTOTP:
type: object
properties:
status:
type: string
example: OK
data:
type: object
properties:
period:
default: 30
description: The period defined in the users TOTP configuration
type: integer
example: 30
digits:
default: 6
description: The number of digits defined in the users TOTP configuration
type: integer
example: 6
handlers.UserInfo.MethodBody: handlers.UserInfo.MethodBody:
required: required:
- method - method

View File

@ -86,7 +86,7 @@ func buildFrontend(branch string) {
} }
func buildSwagger() { func buildSwagger() {
swaggerVer := "4.1.0" swaggerVer := "4.1.2"
cmd := utils.CommandWithStdout("bash", "-c", "wget -q https://github.com/swagger-api/swagger-ui/archive/v"+swaggerVer+".tar.gz -O ./v"+swaggerVer+".tar.gz") cmd := utils.CommandWithStdout("bash", "-c", "wget -q https://github.com/swagger-api/swagger-ui/archive/v"+swaggerVer+".tar.gz -O ./v"+swaggerVer+".tar.gz")
err := cmd.Run() err := cmd.Run()

View File

@ -90,16 +90,28 @@ log:
## ##
## Parameters used for TOTP generation. ## Parameters used for TOTP generation.
totp: totp:
## The issuer name displayed in the Authenticator application of your choice ## The issuer name displayed in the Authenticator application of your choice.
## See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names
issuer: authelia.com issuer: authelia.com
## The period in seconds a one-time password is current for. Changing this will require all users to register
## their TOTP applications again. Warning: before changing period read the docs link below. ## The TOTP algorithm to use.
## It is CRITICAL you read the documentation before changing this option:
## https://www.authelia.com/docs/configuration/one-time-password.html#algorithm
algorithm: sha1
## The number of digits a user has to input. Must either be 6 or 8.
## Changing this option only affects newly generated TOTP configurations.
## It is CRITICAL you read the documentation before changing this option:
## https://www.authelia.com/docs/configuration/one-time-password.html#digits
digits: 6
## The period in seconds a one-time password is valid for.
## Changing this option only affects newly generated TOTP configurations.
period: 30 period: 30
## The skew controls number of one-time passwords either side of the current one that are valid. ## The skew controls number of one-time passwords either side of the current one that are valid.
## Warning: before changing skew read the docs link below. ## Warning: before changing skew read the docs link below.
skew: 1 skew: 1
## See: https://www.authelia.com/docs/configuration/one-time-password.html#period-and-skew to read the documentation. ## See: https://www.authelia.com/docs/configuration/one-time-password.html#input-validation to read the documentation.
## ##
## Duo Push API Configuration ## Duo Push API Configuration

View File

@ -7,7 +7,7 @@ nav_order: 16
# Time-based One-Time Password # Time-based One-Time Password
Authelia uses time based one-time passwords as the OTP method. You have Authelia uses time-based one-time passwords as the OTP method. You have
the option to tune the settings of the TOTP generation, and you can see a the option to tune the settings of the TOTP generation, and you can see a
full example of TOTP configuration below, as well as sections describing them. full example of TOTP configuration below, as well as sections describing them.
@ -15,6 +15,8 @@ full example of TOTP configuration below, as well as sections describing them.
```yaml ```yaml
totp: totp:
issuer: authelia.com issuer: authelia.com
algorithm: sha1
digits: 6
period: 30 period: 30
skew: 1 skew: 1
``` ```
@ -37,17 +39,56 @@ differentiate applications registered by the user.
Authelia allows customisation of the issuer to differentiate the entry created Authelia allows customisation of the issuer to differentiate the entry created
by Authelia from others. by Authelia from others.
## Period and Skew ### algorithm
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: sha1
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
The period and skew configuration parameters affect each other. The default values are _**Important Note:** Many TOTP applications do not support this option. It is strongly advised you find out which
a period of 30 and a skew of 1. It is highly recommended you do not change these unless applications your users use and test them before changing this option. It is insufficient to test that the application
you wish to set skew to 0. can add the key, it must also authenticate with Authelia as some applications silently ignore these options. Bitwarden
is the only one that has been tested at this time. If you'd like to contribute to documenting support for this option
please see [Issue 2650](https://github.com/authelia/authelia/issues/2650)._
The way you configure these affects security by changing the length of time a one-time The algorithm used for the TOTP key.
password is valid for. The formula to calculate the effective validity period is
`period + (period * skew * 2)`. For example period 30 and skew 1 would result in 90
seconds of validity, and period 30 and skew 2 would result in 150 seconds of validity.
Possible Values (case-insensitive):
- `sha1`
- `sha256`
- `sha512`
Changing this value only affects newly registered TOTP keys. See the [Registration](#registration) section for more
information.
### digits
<div markdown="1">
type: integer
{: .label .label-config .label-purple }
default: 6
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
_**Important Note:** Some TOTP applications do not support this option. It is strongly advised you find out which
applications your users use and test them before changing this option. It is insufficient to test that the application
can add the key, it must also authenticate with Authelia as some applications silently ignore these options. Bitwarden
is the only one that has been tested at this time. If you'd like to contribute to documenting support for this option
please see [Issue 2650](https://github.com/authelia/authelia/issues/2650)._
The number of digits a user needs to input to perform authentication. It's generally not recommended for this to be
altered as many TOTP applications do not support anything other than 6. What's worse is some TOTP applications allow
you to add the key, but do not use the correct number of digits specified by the key.
The valid values are `6` or `8`.
Changing this value only affects newly registered TOTP keys. See the [Registration](#registration) section for more
information.
### period ### period
<div markdown="1"> <div markdown="1">
@ -59,10 +100,13 @@ required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
Configures the period of time in seconds a one-time password is current for. It is important The period of time in seconds between key rotations or the time element of TOTP. Please see the
to note that changing this value will require your users to register their application again. [input validation](#input-validation) section for how this option and the [skew](#skew) option interact with each other.
It is recommended to keep this value set to 30, the minimum is 1. It is recommended to keep this value set to 30, the minimum is 15.
Changing this value only affects newly registered TOTP keys. See the [Registration](#registration) section for more
information.
### skew ### skew
<div markdown="1"> <div markdown="1">
@ -74,15 +118,34 @@ required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
Configures the number of one-time passwords either side of the current one that are The number of one time passwords either side of the current valid one time password that should also be considered valid.
considered valid, each time you increase this it makes two more one-time passwords valid. The default of 1 results in 3 one time passwords valid. A setting of 2 would result in 5. With the default period of 30
For example the default of 1 has a total of 3 keys valid. A value of 2 has 5 one-time passwords this would result in 90 and 150 seconds of valid one time passwords respectively. Please see the
valid. [input validation](#input-validation) section for how this option and the [period](#period) option interact with each
other.
It is recommended to keep this value set to 0 or 1, the minimum is 0. Changing this value affects all TOTP validations, not just newly registered ones.
## Registration
When users register their TOTP device for the first time, the current [issuer](#issuer), [algorithm](#algorithm), and
[period](#period) are used to generate the TOTP link and QR code. These values are saved to the database for future
validations.
This means if the configuration options are changed, users will not need to regenerate their keys. This functionality
takes effect from 4.33.0 onwards, previously the effect was the keys would just fail to validate. If you'd like to force
users to register a new device, you can delete the old device for a particular user by using the
`authelia storage totp delete <username>` command regardless of if you change the settings or not.
## Input Validation
The period and skew configuration parameters affect each other. The default values are a period of 30 and a skew of 1.
It is highly recommended you do not change these unless you wish to set skew to 0.
The way you configure these affects security by changing the length of time a one-time
password is valid for. The formula to calculate the effective validity period is
`period + (period * skew * 2)`. For example period 30 and skew 1 would result in 90
seconds of validity, and period 30 and skew 2 would result in 150 seconds of validity.
## System time accuracy ## System time accuracy
It's important to note that if the system time is not accurate enough then clients will seemingly not generate valid It's important to note that if the system time is not accurate enough then clients will seemingly not generate valid
passwords for TOTP. Conversely this is the same when the client time is not accurate enough. This is due to the Time-based passwords for TOTP. Conversely this is the same when the client time is not accurate enough. This is due to the Time-based
One Time Passwords being time-based. One Time Passwords being time-based.
@ -90,3 +153,29 @@ One Time Passwords being time-based.
Authelia by default checks the system time against an [NTP server](./ntp.md#address) on startup. This helps to prevent Authelia by default checks the system time against an [NTP server](./ntp.md#address) on startup. This helps to prevent
a time synchronization issue on the server being an issue. There is however no effective and reliable way to check the a time synchronization issue on the server being an issue. There is however no effective and reliable way to check the
clients. clients.
## Encryption
The TOTP secret is [encrypted](storage/index.md#encryption_key) in the database in version 4.33.0 and above. This is so
a user having access to only the database cannot easily compromise your two-factor authentication method.
This may be inconvenient for some users who wish to export TOTP keys from Authelia to other services. As such there is
a command specifically for exporting TOTP configurations from the database. These commands require the configuration or
at least a minimal configuration that has the storage backend connection details and the encryption key.
Export in [Key URI Format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format):
```shell
$ authelia storage totp export --format uri
```
Export as CSV:
```shell
$ authelia storage totp export --format csv
```
Help:
```shell
$ authelia storage totp export --help
```

View File

@ -86,6 +86,7 @@ The scope should be the name of the package affected
* storage * storage
* suites * suites
* templates * templates
* totp
* utils * utils
There are currently a few exceptions to the "use package name" rule: There are currently a few exceptions to the "use package name" rule:

View File

@ -10,17 +10,18 @@ import (
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session" "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/totp"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
func getStorageProvider() (provider storage.Provider) { func getStorageProvider() (provider storage.Provider) {
switch { switch {
case config.Storage.PostgreSQL != nil: case config.Storage.PostgreSQL != nil:
return storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL, config.Storage.EncryptionKey) return storage.NewPostgreSQLProvider(config)
case config.Storage.MySQL != nil: case config.Storage.MySQL != nil:
return storage.NewMySQLProvider(*config.Storage.MySQL, config.Storage.EncryptionKey) return storage.NewMySQLProvider(config)
case config.Storage.Local != nil: case config.Storage.Local != nil:
return storage.NewSQLiteProvider(config.Storage.Local.Path, config.Storage.EncryptionKey) return storage.NewSQLiteProvider(config)
default: default:
return nil return nil
} }
@ -71,6 +72,8 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [
errors = append(errors, err) errors = append(errors, err)
} }
totpProvider := totp.NewTimeBasedProvider(config.TOTP)
return middlewares.Providers{ return middlewares.Providers{
Authorizer: authorizer, Authorizer: authorizer,
UserProvider: userProvider, UserProvider: userProvider,
@ -80,5 +83,6 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [
NTP: ntpProvider, NTP: ntpProvider,
Notifier: notifier, Notifier: notifier,
SessionProvider: sessionProvider, SessionProvider: sessionProvider,
TOTP: totpProvider,
}, warnings, errors }, warnings, errors
} }

View File

@ -35,7 +35,7 @@ func NewStorageCmd() (cmd *cobra.Command) {
newStorageMigrateCmd(), newStorageMigrateCmd(),
newStorageSchemaInfoCmd(), newStorageSchemaInfoCmd(),
newStorageEncryptionCmd(), newStorageEncryptionCmd(),
newStorageExportCmd(), newStorageTOTPCmd(),
) )
return cmd return cmd
@ -79,25 +79,57 @@ func newStorageEncryptionChangeKeyCmd() (cmd *cobra.Command) {
return cmd return cmd
} }
func newStorageExportCmd() (cmd *cobra.Command) { func newStorageTOTPCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{ cmd = &cobra.Command{
Use: "export", Use: "totp",
Short: "Performs exports", Short: "Manage TOTP configurations",
} }
cmd.AddCommand(newStorageExportTOTPConfigurationsCmd()) cmd.AddCommand(
newStorageTOTPGenerateCmd(),
newStorageTOTPDeleteCmd(),
newStorageTOTPExportCmd(),
)
return cmd return cmd
} }
func newStorageExportTOTPConfigurationsCmd() (cmd *cobra.Command) { func newStorageTOTPGenerateCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{ cmd = &cobra.Command{
Use: "totp-configurations", Use: "generate username",
Short: "Performs exports of the totp configurations", Short: "Generate a TOTP configuration for a user",
RunE: storageExportTOTPConfigurationsRunE, RunE: storageTOTPGenerateRunE,
Args: cobra.ExactArgs(1),
} }
cmd.Flags().String("format", storageExportFormatCSV, "changes the format of the export, options are csv and uri") cmd.Flags().Uint("period", 30, "set the TOTP period")
cmd.Flags().Uint("digits", 6, "set the TOTP digits")
cmd.Flags().String("algorithm", "SHA1", "set the TOTP algorithm")
cmd.Flags().String("issuer", "Authelia", "set the TOTP issuer")
cmd.Flags().BoolP("force", "f", false, "forces the TOTP configuration to be generated regardless if it exists or not")
return cmd
}
func newStorageTOTPDeleteCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "delete username",
Short: "Delete a TOTP configuration for a user",
RunE: storageTOTPDeleteRunE,
Args: cobra.ExactArgs(1),
}
return cmd
}
func newStorageTOTPExportCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "export",
Short: "Performs exports of the TOTP configurations",
RunE: storageTOTPExportRunE,
}
cmd.Flags().String("format", storageExportFormatURI, "sets the output format")
return cmd return cmd
} }

View File

@ -14,6 +14,7 @@ import (
"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/models"
"github.com/authelia/authelia/v4/internal/storage" "github.com/authelia/authelia/v4/internal/storage"
"github.com/authelia/authelia/v4/internal/totp"
) )
func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) { func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
@ -52,6 +53,10 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
"postgres.username": "storage.postgres.username", "postgres.username": "storage.postgres.username",
"postgres.password": "storage.postgres.password", "postgres.password": "storage.postgres.password",
"postgres.schema": "storage.postgres.schema", "postgres.schema": "storage.postgres.schema",
"period": "totp.period",
"digits": "totp.digits",
"algorithm": "totp.algorithm",
"issuer": "totp.issuer",
} }
sources = append(sources, configuration.NewEnvironmentSource(configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter)) sources = append(sources, configuration.NewEnvironmentSource(configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter))
@ -62,7 +67,7 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
config = &schema.Configuration{} config = &schema.Configuration{}
_, err = configuration.LoadAdvanced(val, "storage", &config.Storage, sources...) _, err = configuration.LoadAdvanced(val, "", &config, sources...)
if err != nil { if err != nil {
return err return err
} }
@ -84,6 +89,8 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
validator.ValidateStorage(config.Storage, val) validator.ValidateStorage(config.Storage, val)
validator.ValidateTOTP(config, val)
if val.HasErrors() { if val.HasErrors() {
var finalErr error var finalErr error
@ -109,9 +116,6 @@ func storageSchemaEncryptionCheckRunE(cmd *cobra.Command, args []string) (err er
) )
provider = getStorageProvider() provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() { defer func() {
_ = provider.Close() _ = provider.Close()
@ -145,9 +149,6 @@ func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (er
) )
provider = getStorageProvider() provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() { defer func() {
_ = provider.Close() _ = provider.Close()
@ -188,16 +189,83 @@ func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (er
return nil return nil
} }
func storageExportTOTPConfigurationsRunE(cmd *cobra.Command, args []string) (err error) { func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
c *models.TOTPConfiguration
force bool
)
provider = getStorageProvider()
defer func() {
_ = provider.Close()
}()
force, err = cmd.Flags().GetBool("force")
_, err = provider.LoadTOTPConfiguration(ctx, args[0])
if err == nil && !force {
return fmt.Errorf("%s already has a TOTP configuration, use --force to overwrite", args[0])
}
if err != nil && !errors.Is(err, storage.ErrNoTOTPConfiguration) {
return err
}
totpProvider := totp.NewTimeBasedProvider(config.TOTP)
if c, err = totpProvider.Generate(args[0]); err != nil {
return err
}
err = provider.SaveTOTPConfiguration(ctx, *c)
if err != nil {
return err
}
fmt.Printf("Generated TOTP configuration for user '%s': %s", args[0], c.URI())
return nil
}
func storageTOTPDeleteRunE(cmd *cobra.Command, args []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
)
user := args[0]
provider = getStorageProvider()
defer func() {
_ = provider.Close()
}()
_, err = provider.LoadTOTPConfiguration(ctx, user)
if err != nil {
return fmt.Errorf("can't delete configuration for user '%s': %+v", user, err)
}
err = provider.DeleteTOTPConfiguration(ctx, user)
if err != nil {
return fmt.Errorf("can't delete configuration for user '%s': %+v", user, err)
}
fmt.Printf("Deleted TOTP configuration for user '%s'.", user)
return nil
}
func storageTOTPExportRunE(cmd *cobra.Command, args []string) (err error) {
var ( var (
provider storage.Provider provider storage.Provider
ctx = context.Background() ctx = context.Background()
) )
provider = getStorageProvider() provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() { defer func() {
_ = provider.Close() _ = provider.Close()
@ -236,9 +304,9 @@ func storageExportTOTPConfigurationsRunE(cmd *cobra.Command, args []string) (err
for _, c := range configurations { for _, c := range configurations {
switch format { switch format {
case storageExportFormatCSV: case storageExportFormatCSV:
fmt.Printf("%s,%s,%s,%d,%d,%s\n", "Authelia", c.Username, c.Algorithm, c.Digits, c.Period, string(c.Secret)) fmt.Printf("%s,%s,%s,%d,%d,%s\n", c.Issuer, c.Username, c.Algorithm, c.Digits, c.Period, string(c.Secret))
case storageExportFormatURI: 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) fmt.Println(c.URI())
} }
} }
@ -298,14 +366,11 @@ func newStorageMigrateListRunE(up bool) func(cmd *cobra.Command, args []string)
var ( var (
provider storage.Provider provider storage.Provider
ctx = context.Background() ctx = context.Background()
migrations []storage.SchemaMigration migrations []models.SchemaMigration
directionStr string directionStr string
) )
provider = getStorageProvider() provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() { defer func() {
_ = provider.Close() _ = provider.Close()
@ -345,9 +410,6 @@ func newStorageMigrationRunE(up bool) func(cmd *cobra.Command, args []string) (e
) )
provider = getStorageProvider() provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() { defer func() {
_ = provider.Close() _ = provider.Close()
@ -420,9 +482,6 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
) )
provider = getStorageProvider() provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() { defer func() {
_ = provider.Close() _ = provider.Close()

View File

@ -90,16 +90,28 @@ log:
## ##
## Parameters used for TOTP generation. ## Parameters used for TOTP generation.
totp: totp:
## The issuer name displayed in the Authenticator application of your choice ## The issuer name displayed in the Authenticator application of your choice.
## See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names
issuer: authelia.com issuer: authelia.com
## The period in seconds a one-time password is current for. Changing this will require all users to register
## their TOTP applications again. Warning: before changing period read the docs link below. ## The TOTP algorithm to use.
## It is CRITICAL you read the documentation before changing this option:
## https://www.authelia.com/docs/configuration/one-time-password.html#algorithm
algorithm: sha1
## The number of digits a user has to input. Must either be 6 or 8.
## Changing this option only affects newly generated TOTP configurations.
## It is CRITICAL you read the documentation before changing this option:
## https://www.authelia.com/docs/configuration/one-time-password.html#digits
digits: 6
## The period in seconds a one-time password is valid for.
## Changing this option only affects newly generated TOTP configurations.
period: 30 period: 30
## The skew controls number of one-time passwords either side of the current one that are valid. ## The skew controls number of one-time passwords either side of the current one that are valid.
## Warning: before changing skew read the docs link below. ## Warning: before changing skew read the docs link below.
skew: 1 skew: 1
## See: https://www.authelia.com/docs/configuration/one-time-password.html#period-and-skew to read the documentation. ## See: https://www.authelia.com/docs/configuration/one-time-password.html#input-validation to read the documentation.
## ##
## Duo Push API Configuration ## Duo Push API Configuration

View File

@ -23,3 +23,15 @@ const LDAPImplementationCustom = "custom"
// LDAPImplementationActiveDirectory is the string for the Active Directory LDAP implementation. // LDAPImplementationActiveDirectory is the string for the Active Directory LDAP implementation.
const LDAPImplementationActiveDirectory = "activedirectory" const LDAPImplementationActiveDirectory = "activedirectory"
// TOTP Algorithm.
const (
TOTPAlgorithmSHA1 = "SHA1"
TOTPAlgorithmSHA256 = "SHA256"
TOTPAlgorithmSHA512 = "SHA512"
)
var (
// TOTPPossibleAlgorithms is a list of valid TOTP Algorithms.
TOTPPossibleAlgorithms = []string{TOTPAlgorithmSHA1, TOTPAlgorithmSHA256, TOTPAlgorithmSHA512}
)

View File

@ -3,15 +3,19 @@ package schema
// TOTPConfiguration represents the configuration related to TOTP options. // TOTPConfiguration represents the configuration related to TOTP options.
type TOTPConfiguration struct { type TOTPConfiguration struct {
Issuer string `koanf:"issuer"` Issuer string `koanf:"issuer"`
Period int `koanf:"period"` Algorithm string `koanf:"algorithm"`
Skew *int `koanf:"skew"` Digits uint `koanf:"digits"`
Period uint `koanf:"period"`
Skew *uint `koanf:"skew"`
} }
var defaultOtpSkew = 1 var defaultOtpSkew = uint(1)
// DefaultTOTPConfiguration represents default configuration parameters for TOTP generation. // DefaultTOTPConfiguration represents default configuration parameters for TOTP generation.
var DefaultTOTPConfiguration = TOTPConfiguration{ var DefaultTOTPConfiguration = TOTPConfiguration{
Issuer: "Authelia", Issuer: "Authelia",
Algorithm: TOTPAlgorithmSHA1,
Digits: 6,
Period: 30, Period: 30,
Skew: &defaultOtpSkew, Skew: &defaultOtpSkew,
} }

View File

@ -32,13 +32,9 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
ValidateTheme(configuration, validator) ValidateTheme(configuration, validator)
if configuration.TOTP == nil {
configuration.TOTP = &schema.DefaultTOTPConfiguration
}
ValidateLogging(configuration, validator) ValidateLogging(configuration, validator)
ValidateTOTP(configuration.TOTP, validator) ValidateTOTP(configuration, validator)
ValidateAuthenticationBackend(&configuration.AuthenticationBackend, validator) ValidateAuthenticationBackend(&configuration.AuthenticationBackend, validator)

View File

@ -54,6 +54,13 @@ const (
errFmtNotifierSMTPNotConfigured = "smtp notifier: the '%s' must be configured" errFmtNotifierSMTPNotConfigured = "smtp notifier: the '%s' must be configured"
) )
// TOTP Error constants.
const (
errFmtTOTPInvalidAlgorithm = "totp: algorithm '%s' is invalid: must be one of %s"
errFmtTOTPInvalidPeriod = "totp: period '%d' is invalid: must be 15 or more"
errFmtTOTPInvalidDigits = "totp: digits '%d' is invalid: must be 6 or 8"
)
// OpenID Error constants. // OpenID Error constants.
const ( const (
errFmtOIDCClientsDuplicateID = "openid connect provider: one or more clients have the same ID" errFmtOIDCClientsDuplicateID = "openid connect provider: one or more clients have the same ID"
@ -157,6 +164,8 @@ var ValidKeys = []string{
// TOTP Keys. // TOTP Keys.
"totp.issuer", "totp.issuer",
"totp.algorithm",
"totp.digits",
"totp.period", "totp.period",
"totp.skew", "totp.skew",

View File

@ -2,25 +2,47 @@ package validator
import ( import (
"fmt" "fmt"
"strings"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
) )
// ValidateTOTP validates and update TOTP configuration. // ValidateTOTP validates and update TOTP configuration.
func ValidateTOTP(configuration *schema.TOTPConfiguration, validator *schema.StructValidator) { func ValidateTOTP(configuration *schema.Configuration, validator *schema.StructValidator) {
if configuration.Issuer == "" { if configuration.TOTP == nil {
configuration.Issuer = schema.DefaultTOTPConfiguration.Issuer configuration.TOTP = &schema.DefaultTOTPConfiguration
return
} }
if configuration.Period == 0 { if configuration.TOTP.Issuer == "" {
configuration.Period = schema.DefaultTOTPConfiguration.Period configuration.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer
} else if configuration.Period < 0 {
validator.Push(fmt.Errorf("TOTP Period must be 1 or more"))
} }
if configuration.Skew == nil { if configuration.TOTP.Algorithm == "" {
configuration.Skew = schema.DefaultTOTPConfiguration.Skew configuration.TOTP.Algorithm = schema.DefaultTOTPConfiguration.Algorithm
} else if *configuration.Skew < 0 { } else {
validator.Push(fmt.Errorf("TOTP Skew must be 0 or more")) configuration.TOTP.Algorithm = strings.ToUpper(configuration.TOTP.Algorithm)
if !utils.IsStringInSlice(configuration.TOTP.Algorithm, schema.TOTPPossibleAlgorithms) {
validator.Push(fmt.Errorf(errFmtTOTPInvalidAlgorithm, configuration.TOTP.Algorithm, strings.Join(schema.TOTPPossibleAlgorithms, ", ")))
}
}
if configuration.TOTP.Period == 0 {
configuration.TOTP.Period = schema.DefaultTOTPConfiguration.Period
} else if configuration.TOTP.Period < 15 {
validator.Push(fmt.Errorf(errFmtTOTPInvalidPeriod, configuration.TOTP.Period))
}
if configuration.TOTP.Digits == 0 {
configuration.TOTP.Digits = schema.DefaultTOTPConfiguration.Digits
} else if configuration.TOTP.Digits != 6 && configuration.TOTP.Digits != 8 {
validator.Push(fmt.Errorf(errFmtTOTPInvalidDigits, configuration.TOTP.Digits))
}
if configuration.TOTP.Skew == nil {
configuration.TOTP.Skew = schema.DefaultTOTPConfiguration.Skew
} }
} }

View File

@ -1,6 +1,8 @@
package validator package validator
import ( import (
"fmt"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -11,26 +13,61 @@ import (
func TestShouldSetDefaultTOTPValues(t *testing.T) { func TestShouldSetDefaultTOTPValues(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := schema.TOTPConfiguration{} config := &schema.Configuration{
TOTP: &schema.TOTPConfiguration{},
}
ValidateTOTP(&config, validator) ValidateTOTP(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, "Authelia", config.Issuer) assert.Equal(t, "Authelia", config.TOTP.Issuer)
assert.Equal(t, *schema.DefaultTOTPConfiguration.Skew, *config.Skew) assert.Equal(t, schema.DefaultTOTPConfiguration.Algorithm, config.TOTP.Algorithm)
assert.Equal(t, schema.DefaultTOTPConfiguration.Period, config.Period) assert.Equal(t, schema.DefaultTOTPConfiguration.Skew, config.TOTP.Skew)
assert.Equal(t, schema.DefaultTOTPConfiguration.Period, config.TOTP.Period)
} }
func TestShouldRaiseErrorWhenInvalidTOTPMinimumValues(t *testing.T) { func TestShouldNormalizeTOTPAlgorithm(t *testing.T) {
var badSkew = -1
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := schema.TOTPConfiguration{
Period: -5, config := &schema.Configuration{
Skew: &badSkew, TOTP: &schema.TOTPConfiguration{
Algorithm: "sha1",
},
} }
ValidateTOTP(&config, validator)
assert.Len(t, validator.Errors(), 2) ValidateTOTP(config, validator)
assert.EqualError(t, validator.Errors()[0], "TOTP Period must be 1 or more")
assert.EqualError(t, validator.Errors()[1], "TOTP Skew must be 0 or more") assert.Len(t, validator.Errors(), 0)
assert.Equal(t, "SHA1", config.TOTP.Algorithm)
}
func TestShouldRaiseErrorWhenInvalidTOTPAlgorithm(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Algorithm: "sha3",
},
}
ValidateTOTP(config, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtTOTPInvalidAlgorithm, "SHA3", strings.Join(schema.TOTPPossibleAlgorithms, ", ")))
}
func TestShouldRaiseErrorWhenInvalidTOTPValues(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Period: 5,
Digits: 20,
},
}
ValidateTOTP(config, validator)
require.Len(t, validator.Errors(), 2)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtTOTPInvalidPeriod, 5))
assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errFmtTOTPInvalidDigits, 20))
} }

View File

@ -91,12 +91,6 @@ const (
pathOpenIDConnectConsent = "/api/oidc/consent" pathOpenIDConnectConsent = "/api/oidc/consent"
) )
const (
totpAlgoSHA1 = "SHA1"
totpAlgoSHA256 = "SHA256"
totpAlgoSHA512 = "SHA512"
)
const ( const (
accept = "accept" accept = "accept"
reject = "reject" reject = "reject"

View File

@ -5,18 +5,10 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
) )
// ConfigurationBody the content returned by the configuration endpoint.
type ConfigurationBody struct {
AvailableMethods MethodList `json:"available_methods"`
SecondFactorEnabled bool `json:"second_factor_enabled"` // whether second factor is enabled or not.
TOTPPeriod int `json:"totp_period"`
}
// ConfigurationGet get the configuration accessible to authenticated users. // ConfigurationGet get the configuration accessible to authenticated users.
func ConfigurationGet(ctx *middlewares.AutheliaCtx) { func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
body := ConfigurationBody{} body := configurationBody{}
body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F} body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
body.TOTPPeriod = ctx.Configuration.TOTP.Period
if ctx.Configuration.DuoAPI != nil { if ctx.Configuration.DuoAPI != nil {
body.AvailableMethods = append(body.AvailableMethods, authentication.Push) body.AvailableMethods = append(body.AvailableMethods, authentication.Push)

View File

@ -29,15 +29,9 @@ func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() { func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
s.mock.Ctx.Configuration = schema.Configuration{ expectedBody := configurationBody{
TOTP: &schema.TOTPConfiguration{
Period: schema.DefaultTOTPConfiguration.Period,
},
}
expectedBody := ConfigurationBody{
AvailableMethods: []string{"totp", "u2f"}, AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: false, SecondFactorEnabled: false,
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
} }
ConfigurationGet(s.mock.Ctx) ConfigurationGet(s.mock.Ctx)
@ -47,14 +41,10 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() { func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
s.mock.Ctx.Configuration = schema.Configuration{ s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{}, DuoAPI: &schema.DuoAPIConfiguration{},
TOTP: &schema.TOTPConfiguration{
Period: schema.DefaultTOTPConfiguration.Period,
},
} }
expectedBody := ConfigurationBody{ expectedBody := configurationBody{
AvailableMethods: []string{"totp", "u2f", "mobile_push"}, AvailableMethods: []string{"totp", "u2f", "mobile_push"},
SecondFactorEnabled: false, SecondFactorEnabled: false,
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
} }
ConfigurationGet(s.mock.Ctx) ConfigurationGet(s.mock.Ctx)
@ -62,11 +52,6 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMo
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisabledWhenNoRuleIsSetToTwoFactor() { func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisabledWhenNoRuleIsSetToTwoFactor() {
s.mock.Ctx.Configuration = schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Period: schema.DefaultTOTPConfiguration.Period,
},
}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer( s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(
&schema.Configuration{ &schema.Configuration{
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -87,19 +72,13 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisab
}, },
}}) }})
ConfigurationGet(s.mock.Ctx) ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), ConfigurationBody{ s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{"totp", "u2f"}, AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: false, SecondFactorEnabled: false,
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
}) })
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenDefaultPolicySetToTwoFactor() { func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenDefaultPolicySetToTwoFactor() {
s.mock.Ctx.Configuration = schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Period: schema.DefaultTOTPConfiguration.Period,
},
}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
DefaultPolicy: "two_factor", DefaultPolicy: "two_factor",
@ -119,19 +98,13 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabl
}, },
}}) }})
ConfigurationGet(s.mock.Ctx) ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), ConfigurationBody{ s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{"totp", "u2f"}, AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: true, SecondFactorEnabled: true,
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
}) })
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenSomePolicySetToTwoFactor() { func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenSomePolicySetToTwoFactor() {
s.mock.Ctx.Configuration = schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Period: schema.DefaultTOTPConfiguration.Period,
},
}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer( s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(
&schema.Configuration{ &schema.Configuration{
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -152,10 +125,9 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabl
}, },
}}) }})
ConfigurationGet(s.mock.Ctx) ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), ConfigurationBody{ s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{"totp", "u2f"}, AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: true, SecondFactorEnabled: true,
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
}) })
} }

View File

@ -57,7 +57,7 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() {
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
Return(false, fmt.Errorf("failed")) Return(false, fmt.Errorf("failed"))
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "test", Username: "test",
@ -85,7 +85,7 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsNotMarkedWhenProviderC
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
Return(false, fmt.Errorf("invalid credentials")) Return(false, fmt.Errorf("invalid credentials"))
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "test", Username: "test",
@ -111,7 +111,7 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCrede
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
Return(false, nil) Return(false, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "test", Username: "test",
@ -137,7 +137,7 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() {
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
Return(true, nil) Return(true, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Any()). AppendAuthenticationLog(s.mock.Ctx, gomock.Any()).
Return(nil) Return(nil)
@ -164,7 +164,7 @@ func (s *FirstFactorSuite) TestShouldFailIfAuthenticationMarkFail() {
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
Return(true, nil) Return(true, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Any()). AppendAuthenticationLog(s.mock.Ctx, gomock.Any()).
Return(fmt.Errorf("failed")) Return(fmt.Errorf("failed"))
@ -195,7 +195,7 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() {
Groups: []string{"dev", "admins"}, Groups: []string{"dev", "admins"},
}, nil) }, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Any()). AppendAuthenticationLog(s.mock.Ctx, gomock.Any()).
Return(nil) Return(nil)
@ -235,7 +235,7 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() {
Groups: []string{"dev", "admins"}, Groups: []string{"dev", "admins"},
}, nil) }, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Any()). AppendAuthenticationLog(s.mock.Ctx, gomock.Any()).
Return(nil) Return(nil)
@ -279,7 +279,7 @@ func (s *FirstFactorSuite) TestShouldSaveUsernameFromAuthenticationBackendInSess
Groups: []string{"dev", "admins"}, Groups: []string{"dev", "admins"},
}, nil) }, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Any()). AppendAuthenticationLog(s.mock.Ctx, gomock.Any()).
Return(nil) Return(nil)
@ -337,7 +337,7 @@ func (s *FirstFactorRedirectionSuite) SetupTest() {
Groups: []string{"dev", "admins"}, Groups: []string{"dev", "admins"},
}, nil) }, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Any()). AppendAuthenticationLog(s.mock.Ctx, gomock.Any()).
Return(nil) Return(nil)

View File

@ -128,7 +128,7 @@ func (s *RegisterDuoDeviceSuite) TestShouldRespondWithDeny() {
func (s *RegisterDuoDeviceSuite) TestShouldRespondOK() { func (s *RegisterDuoDeviceSuite) TestShouldRespondOK() {
s.mock.Ctx.Request.SetBodyString("{\"device\":\"1234567890123456\", \"method\":\"push\"}") s.mock.Ctx.Request.SetBodyString("{\"device\":\"1234567890123456\", \"method\":\"push\"}")
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
SavePreferredDuoDevice(gomock.Eq(s.mock.Ctx), gomock.Eq(models.DuoDevice{Username: "john", Device: "1234567890123456", Method: "push"})). SavePreferredDuoDevice(gomock.Eq(s.mock.Ctx), gomock.Eq(models.DuoDevice{Username: "john", Device: "1234567890123456", Method: "push"})).
Return(nil) Return(nil)

View File

@ -3,9 +3,6 @@ package handlers
import ( import (
"fmt" "fmt"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"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/session" "github.com/authelia/authelia/v4/internal/session"
@ -39,39 +36,24 @@ var SecondFactorTOTPIdentityStart = middlewares.IdentityVerificationStart(middle
}) })
func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username string) { func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
algorithm := otp.AlgorithmSHA1 var (
config *models.TOTPConfiguration
err error
)
key, err := totp.Generate(totp.GenerateOpts{ if config, err = ctx.Providers.TOTP.Generate(username); err != nil {
Issuer: ctx.Configuration.TOTP.Issuer,
AccountName: username,
Period: uint(ctx.Configuration.TOTP.Period),
SecretSize: 32,
Digits: otp.Digits(6),
Algorithm: algorithm,
})
if err != nil {
ctx.Error(fmt.Errorf("unable to generate TOTP key: %s", err), messageUnableToRegisterOneTimePassword) ctx.Error(fmt.Errorf("unable to generate TOTP key: %s", err), messageUnableToRegisterOneTimePassword)
return
} }
config := models.TOTPConfiguration{ err = ctx.Providers.StorageProvider.SaveTOTPConfiguration(ctx, *config)
Username: username,
Algorithm: otpAlgoToString(algorithm),
Digits: 6,
Secret: []byte(key.Secret()),
Period: key.Period(),
}
err = ctx.Providers.StorageProvider.SaveTOTPConfiguration(ctx, config)
if err != nil { if err != nil {
ctx.Error(fmt.Errorf("unable to save TOTP secret in DB: %s", err), messageUnableToRegisterOneTimePassword) ctx.Error(fmt.Errorf("unable to save TOTP secret in DB: %s", err), messageUnableToRegisterOneTimePassword)
return return
} }
response := TOTPKeyResponse{ response := TOTPKeyResponse{
OTPAuthURL: key.URL(), OTPAuthURL: config.URI(),
Base32Secret: key.Secret(), Base32Secret: string(config.Secret),
} }
err = ctx.SetJSONBody(response) err = ctx.SetJSONBody(response)

View File

@ -48,16 +48,16 @@ func createToken(secret, username, action string, expiresAt time.Time) (data str
} }
func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissing() { func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissing() {
token, v := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", ActionU2FRegistration, token, verification := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", ActionU2FRegistration,
time.Now().Add(1*time.Minute)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil) Return(true, nil)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())). RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(nil) Return(nil)
SecondFactorU2FIdentityFinish(s.mock.Ctx) SecondFactorU2FIdentityFinish(s.mock.Ctx)
@ -68,16 +68,16 @@ func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissi
func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() { func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() {
s.mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http") s.mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http")
token, v := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", ActionU2FRegistration, token, verification := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", ActionU2FRegistration,
time.Now().Add(1*time.Minute)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil) Return(true, nil)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())). RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(nil) Return(nil)
SecondFactorU2FIdentityFinish(s.mock.Ctx) SecondFactorU2FIdentityFinish(s.mock.Ctx)

View File

@ -39,7 +39,7 @@ func (s *SecondFactorDuoPostSuite) TearDownTest() {
func (s *SecondFactorDuoPostSuite) TestShouldEnroll() { func (s *SecondFactorDuoPostSuite) TestShouldEnroll() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(nil, errors.New("no Duo device and method saved")) Return(nil, errors.New("no Duo device and method saved"))
@ -69,7 +69,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldEnroll() {
func (s *SecondFactorDuoPostSuite) TestShouldAutoSelect() { func (s *SecondFactorDuoPostSuite) TestShouldAutoSelect() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().LoadPreferredDuoDevice(s.mock.Ctx, "john").Return(nil, errors.New("no Duo device and method saved")) s.mock.StorageMock.EXPECT().LoadPreferredDuoDevice(s.mock.Ctx, "john").Return(nil, errors.New("no Duo device and method saved"))
var duoDevices = []duo.Device{ var duoDevices = []duo.Device{
{Capabilities: []string{"auto", "push", "sms", "mobile_otp"}, Number: " ", Device: "12345ABCDEFGHIJ67890", DisplayName: "Test Device 1"}, {Capabilities: []string{"auto", "push", "sms", "mobile_otp"}, Number: " ", Device: "12345ABCDEFGHIJ67890", DisplayName: "Test Device 1"},
@ -85,11 +85,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldAutoSelect() {
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil) duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
SavePreferredDuoDevice(s.mock.Ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}). SavePreferredDuoDevice(s.mock.Ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}).
Return(nil) Return(nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -124,7 +124,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldAutoSelect() {
func (s *SecondFactorDuoPostSuite) TestShouldDenyAutoSelect() { func (s *SecondFactorDuoPostSuite) TestShouldDenyAutoSelect() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(nil, errors.New("no Duo device and method saved")) Return(nil, errors.New("no Duo device and method saved"))
@ -156,7 +156,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDenyAutoSelect() {
func (s *SecondFactorDuoPostSuite) TestShouldFailAutoSelect() { func (s *SecondFactorDuoPostSuite) TestShouldFailAutoSelect() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(nil, errors.New("no Duo device and method saved")) Return(nil, errors.New("no Duo device and method saved"))
@ -174,7 +174,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldFailAutoSelect() {
func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndEnroll() { func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndEnroll() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "NOTEXISTENT", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "NOTEXISTENT", Method: "push"}, nil)
@ -189,7 +189,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndEnroll() {
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil) duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
s.mock.StorageProviderMock.EXPECT().DeletePreferredDuoDevice(s.mock.Ctx, "john").Return(nil) s.mock.StorageMock.EXPECT().DeletePreferredDuoDevice(s.mock.Ctx, "john").Return(nil)
bodyBytes, err := json.Marshal(signDuoRequestBody{}) bodyBytes, err := json.Marshal(signDuoRequestBody{})
s.Require().NoError(err) s.Require().NoError(err)
@ -206,7 +206,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndEnroll() {
func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndCallPreauthAPIWithInvalidDevicesAndEnroll() { func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndCallPreauthAPIWithInvalidDevicesAndEnroll() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "NOTEXISTENT", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "NOTEXISTENT", Method: "push"}, nil)
@ -223,7 +223,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndCallPreauthAPIWit
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil) duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
s.mock.StorageProviderMock.EXPECT().DeletePreferredDuoDevice(s.mock.Ctx, "john").Return(nil) s.mock.StorageMock.EXPECT().DeletePreferredDuoDevice(s.mock.Ctx, "john").Return(nil)
bodyBytes, err := json.Marshal(signDuoRequestBody{}) bodyBytes, err := json.Marshal(signDuoRequestBody{})
s.Require().NoError(err) s.Require().NoError(err)
@ -239,7 +239,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndCallPreauthAPIWit
func (s *SecondFactorDuoPostSuite) TestShouldUseOldDeviceAndSelect() { func (s *SecondFactorDuoPostSuite) TestShouldUseOldDeviceAndSelect() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "NOTEXISTENT", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "NOTEXISTENT", Method: "push"}, nil)
@ -274,11 +274,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseOldDeviceAndSelect() {
func (s *SecondFactorDuoPostSuite) TestShouldUseInvalidMethodAndAutoSelect() { func (s *SecondFactorDuoPostSuite) TestShouldUseInvalidMethodAndAutoSelect() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "invalidmethod"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "invalidmethod"}, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -303,7 +303,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseInvalidMethodAndAutoSelect() {
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil) duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
SavePreferredDuoDevice(s.mock.Ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}). SavePreferredDuoDevice(s.mock.Ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}).
Return(nil) Return(nil)
@ -330,7 +330,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseInvalidMethodAndAutoSelect() {
func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndAllowAccess() { func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndAllowAccess() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
@ -354,7 +354,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndAllowAccess() {
func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndDenyAccess() { func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndDenyAccess() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
@ -384,7 +384,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndDenyAccess() {
func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndFail() { func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndFail() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
@ -402,11 +402,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndFail() {
func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndDenyAccess() { func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndDenyAccess() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -454,7 +454,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndDenyAccess() {
func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndFail() { func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndFail() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
@ -485,11 +485,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndFail() {
func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToDefaultURL() { func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToDefaultURL() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -534,11 +534,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToDefaultURL() {
func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() { func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -579,11 +579,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() {
func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() { func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -628,11 +628,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() { func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -675,11 +675,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() {
func (s *SecondFactorDuoPostSuite) TestShouldRegenerateSessionForPreventingSessionFixation() { func (s *SecondFactorDuoPostSuite) TestShouldRegenerateSessionForPreventingSessionFixation() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",

View File

@ -6,8 +6,7 @@ import (
) )
// SecondFactorTOTPPost validate the TOTP passcode provided by the user. // SecondFactorTOTPPost validate the TOTP passcode provided by the user.
func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler { func SecondFactorTOTPPost(ctx *middlewares.AutheliaCtx) {
return func(ctx *middlewares.AutheliaCtx) {
requestBody := signTOTPRequestBody{} requestBody := signTOTPRequestBody{}
if err := ctx.ParseBody(&requestBody); err != nil { if err := ctx.ParseBody(&requestBody); err != nil {
@ -29,7 +28,7 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
return return
} }
isValid, err := totpVerifier.Verify(config, requestBody.Token) isValid, err := ctx.Providers.TOTP.Validate(requestBody.Token, config)
if err != nil { if err != nil {
ctx.Logger.Errorf("Failed to perform TOTP verification: %+v", err) ctx.Logger.Errorf("Failed to perform TOTP verification: %+v", err)
@ -74,5 +73,4 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
} else { } else {
Handle2FAResponse(ctx, requestBody.TargetURL) Handle2FAResponse(ctx, requestBody.TargetURL)
} }
}
} }

View File

@ -37,15 +37,13 @@ func (s *HandlerSignTOTPSuite) TearDownTest() {
} }
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() { func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("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.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil) Return(&config, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -56,9 +54,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
RemoteIP: models.NewIPAddressFromString("0.0.0.0"), RemoteIP: models.NewIPAddressFromString("0.0.0.0"),
})) }))
verifier.EXPECT(). s.mock.TOTPMock.EXPECT().Validate(gomock.Eq("abc"), gomock.Eq(&config)).Return(true, nil)
Verify(gomock.Eq(&config), gomock.Eq("abc")).
Return(true, nil)
s.mock.Ctx.Configuration.DefaultRedirectionURL = testRedirectionURL s.mock.Ctx.Configuration.DefaultRedirectionURL = testRedirectionURL
@ -68,22 +64,20 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
s.Require().NoError(err) s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes) s.mock.Ctx.Request.SetBody(bodyBytes)
SecondFactorTOTPPost(verifier)(s.mock.Ctx) SecondFactorTOTPPost(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{ s.mock.Assert200OK(s.T(), redirectResponse{
Redirect: testRedirectionURL, Redirect: testRedirectionURL,
}) })
} }
func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() { func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("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.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil) Return(&config, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -94,9 +88,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
RemoteIP: models.NewIPAddressFromString("0.0.0.0"), RemoteIP: models.NewIPAddressFromString("0.0.0.0"),
})) }))
verifier.EXPECT(). s.mock.TOTPMock.EXPECT().Validate(gomock.Eq("abc"), gomock.Eq(&config)).Return(true, nil)
Verify(gomock.Eq(&config), gomock.Eq("abc")).
Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
@ -104,20 +96,18 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
s.Require().NoError(err) s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes) s.mock.Ctx.Request.SetBody(bodyBytes)
SecondFactorTOTPPost(verifier)(s.mock.Ctx) SecondFactorTOTPPost(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil) s.mock.Assert200OK(s.T(), nil)
} }
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() { func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("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.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil) Return(&config, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -128,9 +118,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
RemoteIP: models.NewIPAddressFromString("0.0.0.0"), RemoteIP: models.NewIPAddressFromString("0.0.0.0"),
})) }))
verifier.EXPECT(). s.mock.TOTPMock.EXPECT().Validate(gomock.Eq("abc"), gomock.Eq(&config)).Return(true, nil)
Verify(gomock.Eq(&config), gomock.Eq("abc")).
Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
@ -139,20 +127,18 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
s.Require().NoError(err) s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes) s.mock.Ctx.Request.SetBody(bodyBytes)
SecondFactorTOTPPost(verifier)(s.mock.Ctx) SecondFactorTOTPPost(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{ s.mock.Assert200OK(s.T(), redirectResponse{
Redirect: "https://mydomain.local", Redirect: "https://mydomain.local",
}) })
} }
func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() { func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl) s.mock.StorageMock.EXPECT().
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&models.TOTPConfiguration{Secret: []byte("secret")}, nil) Return(&models.TOTPConfiguration{Secret: []byte("secret")}, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -163,31 +149,30 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
RemoteIP: models.NewIPAddressFromString("0.0.0.0"), RemoteIP: models.NewIPAddressFromString("0.0.0.0"),
})) }))
verifier.EXPECT(). s.mock.TOTPMock.EXPECT().
Verify(gomock.Eq(&models.TOTPConfiguration{Secret: []byte("secret")}), gomock.Eq("abc")). Validate(gomock.Eq("abc"), gomock.Eq(&models.TOTPConfiguration{Secret: []byte("secret")})).
Return(true, nil) Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
TargetURL: "http://mydomain.local", TargetURL: "http://mydomain.local",
}) })
s.Require().NoError(err) s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes) s.mock.Ctx.Request.SetBody(bodyBytes)
SecondFactorTOTPPost(verifier)(s.mock.Ctx) SecondFactorTOTPPost(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil) s.mock.Assert200OK(s.T(), nil)
} }
func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFixation() { func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFixation() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("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.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil) Return(&config, nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -198,8 +183,8 @@ func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFi
RemoteIP: models.NewIPAddressFromString("0.0.0.0"), RemoteIP: models.NewIPAddressFromString("0.0.0.0"),
})) }))
verifier.EXPECT(). s.mock.TOTPMock.EXPECT().
Verify(gomock.Eq(&config), gomock.Eq("abc")). Validate(gomock.Eq("abc"), gomock.Eq(&config)).
Return(true, nil) Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
@ -211,7 +196,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFi
r := regexp.MustCompile("^authelia_session=(.*); path=") r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1) res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
SecondFactorTOTPPost(verifier)(s.mock.Ctx) SecondFactorTOTPPost(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil) s.mock.Assert200OK(s.T(), nil)
s.Assert().NotEqual( s.Assert().NotEqual(

View File

@ -37,13 +37,13 @@ func (s *HandlerSignU2FStep2Suite) TearDownTest() {
} }
func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToDefaultURL() { func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToDefaultURL() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl) u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT(). u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil) Return(nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -69,13 +69,13 @@ func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToDefaultURL() {
} }
func (s *HandlerSignU2FStep2Suite) TestShouldNotReturnRedirectURL() { func (s *HandlerSignU2FStep2Suite) TestShouldNotReturnRedirectURL() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl) u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT(). u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil) Return(nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -97,13 +97,13 @@ func (s *HandlerSignU2FStep2Suite) TestShouldNotReturnRedirectURL() {
} }
func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToSafeTargetURL() { func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToSafeTargetURL() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl) u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT(). u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil) Return(nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -128,13 +128,13 @@ func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToSafeTargetURL() {
} }
func (s *HandlerSignU2FStep2Suite) TestShouldNotRedirectToUnsafeURL() { func (s *HandlerSignU2FStep2Suite) TestShouldNotRedirectToUnsafeURL() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl) u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT(). u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil) Return(nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",
@ -157,13 +157,13 @@ func (s *HandlerSignU2FStep2Suite) TestShouldNotRedirectToUnsafeURL() {
} }
func (s *HandlerSignU2FStep2Suite) TestShouldRegenerateSessionForPreventingSessionFixation() { func (s *HandlerSignU2FStep2Suite) TestShouldRegenerateSessionForPreventingSessionFixation() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl) u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT(). u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil) Return(nil)
s.mock.StorageProviderMock. s.mock.StorageMock.
EXPECT(). EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john", Username: "john",

View File

@ -27,14 +27,9 @@ func UserInfoGet(ctx *middlewares.AutheliaCtx) {
} }
} }
// MethodBody the selected 2FA method.
type MethodBody struct {
Method string `json:"method" valid:"required"`
}
// MethodPreferencePost update the user preferences regarding 2FA method. // MethodPreferencePost update the user preferences regarding 2FA method.
func MethodPreferencePost(ctx *middlewares.AutheliaCtx) { func MethodPreferencePost(ctx *middlewares.AutheliaCtx) {
bodyJSON := MethodBody{} bodyJSON := preferred2FAMethodBody{}
err := ctx.ParseBody(&bodyJSON) err := ctx.ParseBody(&bodyJSON)
if err != nil { if err != nil {

View File

@ -96,7 +96,7 @@ func TestMethodSetToU2F(t *testing.T) {
err := mock.Ctx.SaveSession(userSession) err := mock.Ctx.SaveSession(userSession)
require.NoError(t, err) require.NoError(t, err)
mock.StorageProviderMock. mock.StorageMock.
EXPECT(). EXPECT().
LoadUserInfo(mock.Ctx, gomock.Eq("john")). LoadUserInfo(mock.Ctx, gomock.Eq("john")).
Return(resp.db, resp.err) Return(resp.db, resp.err)
@ -139,7 +139,7 @@ func TestMethodSetToU2F(t *testing.T) {
} }
func (s *FetchSuite) TestShouldReturnError500WhenStorageFailsToLoad() { func (s *FetchSuite) TestShouldReturnError500WhenStorageFailsToLoad() {
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadUserInfo(s.mock.Ctx, gomock.Eq("john")). LoadUserInfo(s.mock.Ctx, gomock.Eq("john")).
Return(models.UserInfo{}, fmt.Errorf("failure")) Return(models.UserInfo{}, fmt.Errorf("failure"))
@ -211,7 +211,7 @@ func (s *SaveSuite) TestShouldReturnError500WhenBadMethodProvided() {
func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() { func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() {
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}")) s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}"))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
SavePreferred2FAMethod(s.mock.Ctx, gomock.Eq("john"), gomock.Eq("u2f")). SavePreferred2FAMethod(s.mock.Ctx, gomock.Eq("john"), gomock.Eq("u2f")).
Return(fmt.Errorf("Failure")) Return(fmt.Errorf("Failure"))
@ -224,7 +224,7 @@ func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() {
func (s *SaveSuite) TestShouldReturn200WhenMethodIsSuccessfullySaved() { func (s *SaveSuite) TestShouldReturn200WhenMethodIsSuccessfullySaved() {
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}")) s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}"))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
SavePreferred2FAMethod(s.mock.Ctx, gomock.Eq("john"), gomock.Eq("u2f")). SavePreferred2FAMethod(s.mock.Ctx, gomock.Eq("john"), gomock.Eq("u2f")).
Return(nil) Return(nil)

View File

@ -0,0 +1,34 @@
package handlers
import (
"errors"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/storage"
)
// UserTOTPGet returns the users TOTP configuration.
func UserTOTPGet(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
config, err := ctx.Providers.StorageProvider.LoadTOTPConfiguration(ctx, userSession.Username)
if err != nil {
if errors.Is(err, storage.ErrNoTOTPConfiguration) {
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.Error(err, "No TOTP Configuration.")
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.Error(err, "Unknown Error.")
}
return
}
if err = ctx.SetJSONBody(config); err != nil {
ctx.Logger.Errorf("Unable to perform TOTP configuration response: %s", err)
}
ctx.SetStatusCode(fasthttp.StatusOK)
}

View File

@ -1,64 +0,0 @@
package handlers
import (
"errors"
"time"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/authelia/authelia/v4/internal/models"
)
// TOTPVerifier is the interface for verifying TOTPs.
type TOTPVerifier interface {
Verify(config *models.TOTPConfiguration, token string) (bool, error)
}
// TOTPVerifierImpl the production implementation for TOTP verification.
type TOTPVerifierImpl struct {
Period uint
Skew uint
}
// Verify verifies TOTPs.
func (tv *TOTPVerifierImpl) Verify(config *models.TOTPConfiguration, token string) (bool, error) {
if config == nil {
return false, errors.New("config not provided")
}
opts := totp.ValidateOpts{
Period: uint(config.Period),
Skew: tv.Skew,
Digits: otp.Digits(config.Digits),
Algorithm: otpStringToAlgo(config.Algorithm),
}
return totp.ValidateCustom(token, string(config.Secret), time.Now().UTC(), opts)
}
func otpAlgoToString(algorithm otp.Algorithm) (out string) {
switch algorithm {
case otp.AlgorithmSHA1:
return totpAlgoSHA1
case otp.AlgorithmSHA256:
return totpAlgoSHA256
case otp.AlgorithmSHA512:
return totpAlgoSHA512
default:
return ""
}
}
func otpStringToAlgo(in string) (algorithm otp.Algorithm) {
switch in {
case totpAlgoSHA1:
return otp.AlgorithmSHA1
case totpAlgoSHA256:
return otp.AlgorithmSHA256
case totpAlgoSHA512:
return otp.AlgorithmSHA512
default:
return otp.AlgorithmSHA1
}
}

View File

@ -1,51 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: internal/handlers/totp.go
// Package handlers is a generated GoMock package.
package handlers
import (
"reflect"
"github.com/golang/mock/gomock"
"github.com/authelia/authelia/v4/internal/models"
)
// MockTOTPVerifier is a mock of TOTPVerifier interface.
type MockTOTPVerifier struct {
ctrl *gomock.Controller
recorder *MockTOTPVerifierMockRecorder
}
// MockTOTPVerifierMockRecorder is the mock recorder for MockTOTPVerifier.
type MockTOTPVerifierMockRecorder struct {
mock *MockTOTPVerifier
}
// NewMockTOTPVerifier creates a new mock instance.
func NewMockTOTPVerifier(ctrl *gomock.Controller) *MockTOTPVerifier {
mock := &MockTOTPVerifier{ctrl: ctrl}
mock.recorder = &MockTOTPVerifierMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTOTPVerifier) EXPECT() *MockTOTPVerifierMockRecorder {
return m.recorder
}
// Verify mocks base method.
func (m *MockTOTPVerifier) Verify(arg0 *models.TOTPConfiguration, arg1 string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Verify indicates an expected call of Verify.
func (mr *MockTOTPVerifierMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockTOTPVerifier)(nil).Verify), arg0, arg1)
}

View File

@ -11,22 +11,10 @@ type MethodList = []string
type authorizationMatching int type authorizationMatching int
// UserInfo is the model of user info and second factor preferences. // configurationBody the content returned by the configuration endpoint.
type UserInfo struct { type configurationBody struct {
// The users display name. AvailableMethods MethodList `json:"available_methods"`
DisplayName string `json:"display_name"` SecondFactorEnabled bool `json:"second_factor_enabled"` // whether second factor is enabled or not.
// The preferred 2FA method.
Method string `json:"method" valid:"required"`
// True if a security key has been registered.
HasU2F bool `json:"has_u2f" valid:"required"`
// True if a TOTP device has been registered.
HasTOTP bool `json:"has_totp" valid:"required"`
// True if a Duo device and method has been enrolled.
HasDuo bool `json:"has_duo" valid:"required"`
} }
// signTOTPRequestBody model of the request body received by TOTP authentication endpoint. // signTOTPRequestBody model of the request body received by TOTP authentication endpoint.
@ -46,6 +34,11 @@ type signDuoRequestBody struct {
Passcode string `json:"passcode"` Passcode string `json:"passcode"`
} }
// preferred2FAMethodBody the selected 2FA method.
type preferred2FAMethodBody struct {
Method string `json:"method" valid:"required"`
}
// firstFactorRequestBody represents the JSON body received by the endpoint. // firstFactorRequestBody represents the JSON body received by the endpoint.
type firstFactorRequestBody struct { type firstFactorRequestBody struct {
Username string `json:"username" valid:"required"` Username string `json:"username" valid:"required"`

View File

@ -55,7 +55,7 @@ func TestShouldFailIfJWTCannotBeSaved(t *testing.T) {
mock.Ctx.Configuration.JWTSecret = testJWTSecret mock.Ctx.Configuration.JWTSecret = testJWTSecret
mock.StorageProviderMock.EXPECT(). mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()). SaveIdentityVerification(mock.Ctx, gomock.Any()).
Return(fmt.Errorf("cannot save")) Return(fmt.Errorf("cannot save"))
@ -74,7 +74,7 @@ func TestShouldFailSendingAnEmail(t *testing.T) {
mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http") mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http")
mock.Ctx.Request.Header.Add("X-Forwarded-Host", "host") mock.Ctx.Request.Header.Add("X-Forwarded-Host", "host")
mock.StorageProviderMock.EXPECT(). mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()). SaveIdentityVerification(mock.Ctx, gomock.Any()).
Return(nil) Return(nil)
@ -96,7 +96,7 @@ func TestShouldFailWhenXForwardedProtoHeaderIsMissing(t *testing.T) {
mock.Ctx.Configuration.JWTSecret = testJWTSecret mock.Ctx.Configuration.JWTSecret = testJWTSecret
mock.Ctx.Request.Header.Add("X-Forwarded-Host", "host") mock.Ctx.Request.Header.Add("X-Forwarded-Host", "host")
mock.StorageProviderMock.EXPECT(). mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()). SaveIdentityVerification(mock.Ctx, gomock.Any()).
Return(nil) Return(nil)
@ -114,7 +114,7 @@ func TestShouldFailWhenXForwardedHostHeaderIsMissing(t *testing.T) {
mock.Ctx.Configuration.JWTSecret = testJWTSecret mock.Ctx.Configuration.JWTSecret = testJWTSecret
mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http") mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http")
mock.StorageProviderMock.EXPECT(). mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()). SaveIdentityVerification(mock.Ctx, gomock.Any()).
Return(nil) Return(nil)
@ -132,7 +132,7 @@ func TestShouldSucceedIdentityVerificationStartProcess(t *testing.T) {
mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http") mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http")
mock.Ctx.Request.Header.Add("X-Forwarded-Host", "host") mock.Ctx.Request.Header.Add("X-Forwarded-Host", "host")
mock.StorageProviderMock.EXPECT(). mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()). SaveIdentityVerification(mock.Ctx, gomock.Any()).
Return(nil) Return(nil)
@ -208,7 +208,7 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsNotFoundInDB(
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(false, nil) Return(false, nil)
@ -244,7 +244,7 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongAction() {
time.Now().Add(1*time.Minute)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil) Return(true, nil)
@ -259,7 +259,7 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongUser() {
time.Now().Add(1*time.Minute)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil) Return(true, nil)
@ -276,11 +276,11 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenCannotBeRemoved
time.Now().Add(1*time.Minute)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil) Return(true, nil)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(fmt.Errorf("cannot remove")) Return(fmt.Errorf("cannot remove"))
@ -295,11 +295,11 @@ func (s *IdentityVerificationFinishProcess) TestShouldReturn200OnFinishComplete(
time.Now().Add(1*time.Minute)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil) Return(true, nil)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(nil) Return(nil)

View File

@ -13,6 +13,7 @@ import (
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session" "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/totp"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
@ -37,6 +38,7 @@ type Providers struct {
UserProvider authentication.UserProvider UserProvider authentication.UserProvider
StorageProvider storage.Provider StorageProvider storage.Provider
Notifier notification.Notifier Notifier notification.Notifier
TOTP totp.Provider
} }
// RequestHandler represents an Authelia request handler. // RequestHandler represents an Authelia request handler.

View File

@ -18,7 +18,6 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage"
) )
// MockAutheliaCtx a mock of AutheliaCtx. // MockAutheliaCtx a mock of AutheliaCtx.
@ -30,8 +29,9 @@ type MockAutheliaCtx struct {
// Providers. // Providers.
UserProviderMock *MockUserProvider UserProviderMock *MockUserProvider
StorageProviderMock *storage.MockProvider StorageMock *MockStorage
NotifierMock *MockNotifier NotifierMock *MockNotifier
TOTPMock *MockTOTP
UserSession *session.UserSession UserSession *session.UserSession
@ -98,8 +98,8 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
mockAuthelia.UserProviderMock = NewMockUserProvider(mockAuthelia.Ctrl) mockAuthelia.UserProviderMock = NewMockUserProvider(mockAuthelia.Ctrl)
providers.UserProvider = mockAuthelia.UserProviderMock providers.UserProvider = mockAuthelia.UserProviderMock
mockAuthelia.StorageProviderMock = storage.NewMockProvider(mockAuthelia.Ctrl) mockAuthelia.StorageMock = NewMockStorage(mockAuthelia.Ctrl)
providers.StorageProvider = mockAuthelia.StorageProviderMock providers.StorageProvider = mockAuthelia.StorageMock
mockAuthelia.NotifierMock = NewMockNotifier(mockAuthelia.Ctrl) mockAuthelia.NotifierMock = NewMockNotifier(mockAuthelia.Ctrl)
providers.Notifier = mockAuthelia.NotifierMock providers.Notifier = mockAuthelia.NotifierMock
@ -112,6 +112,9 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
providers.Regulator = regulation.NewRegulator(configuration.Regulation, providers.StorageProvider, &mockAuthelia.Clock) providers.Regulator = regulation.NewRegulator(configuration.Regulation, providers.StorageProvider, &mockAuthelia.Clock)
mockAuthelia.TOTPMock = NewMockTOTP(mockAuthelia.Ctrl)
providers.TOTP = mockAuthelia.TOTPMock
request := &fasthttp.RequestCtx{} request := &fasthttp.RequestCtx{}
// Set a cookie to identify this client throughout the test. // Set a cookie to identify this client throughout the test.
// request.Request.Header.SetCookie("authelia_session", "client_cookie") // request.Request.Header.SetCookie("authelia_session", "client_cookie")

View File

@ -0,0 +1,11 @@
package mocks
// This file is used to generate mocks. You can generate all mocks using the
// command `go generate github.com/authelia/authelia/v4/internal/mocks`.
//go:generate mockgen -package mocks -destination user_provider.go -mock_names UserProvider=MockUserProvider github.com/authelia/authelia/v4/internal/authentication UserProvider
//go:generate mockgen -package mocks -destination notifier.go -mock_names Notifier=MockNotifier github.com/authelia/authelia/v4/internal/notification Notifier
//go:generate mockgen -package mocks -destination totp.go -mock_names Provider=MockTOTP github.com/authelia/authelia/v4/internal/totp Provider
//go:generate mockgen -package mocks -destination u2f_verifier.go -mock_names U2FVerifier=MockU2FVerifier github.com/authelia/authelia/v4/internal/handlers U2FVerifier
//go:generate mockgen -package mocks -destination storage.go -mock_names Provider=MockStorage github.com/authelia/authelia/v4/internal/storage Provider
//go:generate mockgen -package mocks -destination duo_api.go -mock_names API=MockAPI github.com/authelia/authelia/v4/internal/duo API

View File

@ -1,7 +1,7 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/authelia/authelia/v4/internal/notification (interfaces: Notifier) // Source: github.com/authelia/authelia/v4/internal/notification (interfaces: Notifier)
// Package mock_notification is a generated GoMock package. // Package mocks is a generated GoMock package.
package mocks package mocks
import ( import (

View File

@ -1,8 +1,8 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/authelia/authelia/v4/internal/storage (interfaces: Provider) // Source: github.com/authelia/authelia/v4/internal/storage (interfaces: Provider)
// Package storage is a generated GoMock package. // Package mocks is a generated GoMock package.
package storage package mocks
import ( import (
context "context" context "context"
@ -14,31 +14,31 @@ import (
models "github.com/authelia/authelia/v4/internal/models" models "github.com/authelia/authelia/v4/internal/models"
) )
// MockProvider is a mock of Provider interface. // MockStorage is a mock of Provider interface.
type MockProvider struct { type MockStorage struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockProviderMockRecorder recorder *MockStorageMockRecorder
} }
// MockProviderMockRecorder is the mock recorder for MockProvider. // MockStorageMockRecorder is the mock recorder for MockStorage.
type MockProviderMockRecorder struct { type MockStorageMockRecorder struct {
mock *MockProvider mock *MockStorage
} }
// NewMockProvider creates a new mock instance. // NewMockStorage creates a new mock instance.
func NewMockProvider(ctrl *gomock.Controller) *MockProvider { func NewMockStorage(ctrl *gomock.Controller) *MockStorage {
mock := &MockProvider{ctrl: ctrl} mock := &MockStorage{ctrl: ctrl}
mock.recorder = &MockProviderMockRecorder{mock} mock.recorder = &MockStorageMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use. // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockProvider) EXPECT() *MockProviderMockRecorder { func (m *MockStorage) EXPECT() *MockStorageMockRecorder {
return m.recorder return m.recorder
} }
// AppendAuthenticationLog mocks base method. // AppendAuthenticationLog mocks base method.
func (m *MockProvider) AppendAuthenticationLog(arg0 context.Context, arg1 models.AuthenticationAttempt) error { func (m *MockStorage) AppendAuthenticationLog(arg0 context.Context, arg1 models.AuthenticationAttempt) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AppendAuthenticationLog", arg0, arg1) ret := m.ctrl.Call(m, "AppendAuthenticationLog", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -46,13 +46,13 @@ func (m *MockProvider) AppendAuthenticationLog(arg0 context.Context, arg1 models
} }
// AppendAuthenticationLog indicates an expected call of AppendAuthenticationLog. // AppendAuthenticationLog indicates an expected call of AppendAuthenticationLog.
func (mr *MockProviderMockRecorder) AppendAuthenticationLog(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) AppendAuthenticationLog(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
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((*MockStorage)(nil).AppendAuthenticationLog), arg0, arg1)
} }
// Close mocks base method. // Close mocks base method.
func (m *MockProvider) Close() error { func (m *MockStorage) Close() error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close") ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -60,13 +60,13 @@ func (m *MockProvider) Close() error {
} }
// Close indicates an expected call of Close. // Close indicates an expected call of Close.
func (mr *MockProviderMockRecorder) Close() *gomock.Call { func (mr *MockStorageMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockProvider)(nil).Close)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockStorage)(nil).Close))
} }
// DeletePreferredDuoDevice mocks base method. // DeletePreferredDuoDevice mocks base method.
func (m *MockProvider) DeletePreferredDuoDevice(arg0 context.Context, arg1 string) error { func (m *MockStorage) DeletePreferredDuoDevice(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeletePreferredDuoDevice", arg0, arg1) ret := m.ctrl.Call(m, "DeletePreferredDuoDevice", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -74,13 +74,13 @@ func (m *MockProvider) DeletePreferredDuoDevice(arg0 context.Context, arg1 strin
} }
// DeletePreferredDuoDevice indicates an expected call of DeletePreferredDuoDevice. // DeletePreferredDuoDevice indicates an expected call of DeletePreferredDuoDevice.
func (mr *MockProviderMockRecorder) DeletePreferredDuoDevice(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) DeletePreferredDuoDevice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePreferredDuoDevice", reflect.TypeOf((*MockProvider)(nil).DeletePreferredDuoDevice), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePreferredDuoDevice", reflect.TypeOf((*MockStorage)(nil).DeletePreferredDuoDevice), arg0, arg1)
} }
// DeleteTOTPConfiguration mocks base method. // DeleteTOTPConfiguration mocks base method.
func (m *MockProvider) DeleteTOTPConfiguration(arg0 context.Context, arg1 string) error { func (m *MockStorage) DeleteTOTPConfiguration(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteTOTPConfiguration", arg0, arg1) ret := m.ctrl.Call(m, "DeleteTOTPConfiguration", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -88,13 +88,13 @@ func (m *MockProvider) DeleteTOTPConfiguration(arg0 context.Context, arg1 string
} }
// DeleteTOTPConfiguration indicates an expected call of DeleteTOTPConfiguration. // DeleteTOTPConfiguration indicates an expected call of DeleteTOTPConfiguration.
func (mr *MockProviderMockRecorder) DeleteTOTPConfiguration(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) DeleteTOTPConfiguration(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTOTPConfiguration", reflect.TypeOf((*MockProvider)(nil).DeleteTOTPConfiguration), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTOTPConfiguration", reflect.TypeOf((*MockStorage)(nil).DeleteTOTPConfiguration), arg0, arg1)
} }
// FindIdentityVerification mocks base method. // FindIdentityVerification mocks base method.
func (m *MockProvider) FindIdentityVerification(arg0 context.Context, arg1 string) (bool, error) { func (m *MockStorage) FindIdentityVerification(arg0 context.Context, arg1 string) (bool, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindIdentityVerification", arg0, arg1) ret := m.ctrl.Call(m, "FindIdentityVerification", arg0, arg1)
ret0, _ := ret[0].(bool) ret0, _ := ret[0].(bool)
@ -103,13 +103,13 @@ func (m *MockProvider) FindIdentityVerification(arg0 context.Context, arg1 strin
} }
// FindIdentityVerification indicates an expected call of FindIdentityVerification. // FindIdentityVerification indicates an expected call of FindIdentityVerification.
func (mr *MockProviderMockRecorder) FindIdentityVerification(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) FindIdentityVerification(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindIdentityVerification", reflect.TypeOf((*MockProvider)(nil).FindIdentityVerification), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindIdentityVerification", reflect.TypeOf((*MockStorage)(nil).FindIdentityVerification), arg0, arg1)
} }
// LoadAuthenticationLogs mocks base method. // LoadAuthenticationLogs mocks base method.
func (m *MockProvider) LoadAuthenticationLogs(arg0 context.Context, arg1 string, arg2 time.Time, arg3, arg4 int) ([]models.AuthenticationAttempt, error) { func (m *MockStorage) LoadAuthenticationLogs(arg0 context.Context, arg1 string, arg2 time.Time, arg3, arg4 int) ([]models.AuthenticationAttempt, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadAuthenticationLogs", arg0, arg1, arg2, arg3, arg4) ret := m.ctrl.Call(m, "LoadAuthenticationLogs", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].([]models.AuthenticationAttempt) ret0, _ := ret[0].([]models.AuthenticationAttempt)
@ -118,13 +118,13 @@ func (m *MockProvider) LoadAuthenticationLogs(arg0 context.Context, arg1 string,
} }
// LoadAuthenticationLogs indicates an expected call of LoadAuthenticationLogs. // LoadAuthenticationLogs indicates an expected call of LoadAuthenticationLogs.
func (mr *MockProviderMockRecorder) LoadAuthenticationLogs(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadAuthenticationLogs(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadAuthenticationLogs", reflect.TypeOf((*MockProvider)(nil).LoadAuthenticationLogs), arg0, arg1, arg2, arg3, arg4) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadAuthenticationLogs", reflect.TypeOf((*MockStorage)(nil).LoadAuthenticationLogs), arg0, arg1, arg2, arg3, arg4)
} }
// LoadPreferred2FAMethod mocks base method. // LoadPreferred2FAMethod mocks base method.
func (m *MockProvider) LoadPreferred2FAMethod(arg0 context.Context, arg1 string) (string, error) { func (m *MockStorage) LoadPreferred2FAMethod(arg0 context.Context, arg1 string) (string, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadPreferred2FAMethod", arg0, arg1) ret := m.ctrl.Call(m, "LoadPreferred2FAMethod", arg0, arg1)
ret0, _ := ret[0].(string) ret0, _ := ret[0].(string)
@ -133,13 +133,13 @@ func (m *MockProvider) LoadPreferred2FAMethod(arg0 context.Context, arg1 string)
} }
// LoadPreferred2FAMethod indicates an expected call of LoadPreferred2FAMethod. // LoadPreferred2FAMethod indicates an expected call of LoadPreferred2FAMethod.
func (mr *MockProviderMockRecorder) LoadPreferred2FAMethod(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadPreferred2FAMethod(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadPreferred2FAMethod", reflect.TypeOf((*MockProvider)(nil).LoadPreferred2FAMethod), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadPreferred2FAMethod", reflect.TypeOf((*MockStorage)(nil).LoadPreferred2FAMethod), arg0, arg1)
} }
// LoadPreferredDuoDevice mocks base method. // LoadPreferredDuoDevice mocks base method.
func (m *MockProvider) LoadPreferredDuoDevice(arg0 context.Context, arg1 string) (*models.DuoDevice, error) { func (m *MockStorage) LoadPreferredDuoDevice(arg0 context.Context, arg1 string) (*models.DuoDevice, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadPreferredDuoDevice", arg0, arg1) ret := m.ctrl.Call(m, "LoadPreferredDuoDevice", arg0, arg1)
ret0, _ := ret[0].(*models.DuoDevice) ret0, _ := ret[0].(*models.DuoDevice)
@ -148,13 +148,13 @@ func (m *MockProvider) LoadPreferredDuoDevice(arg0 context.Context, arg1 string)
} }
// LoadPreferredDuoDevice indicates an expected call of LoadPreferredDuoDevice. // LoadPreferredDuoDevice indicates an expected call of LoadPreferredDuoDevice.
func (mr *MockProviderMockRecorder) LoadPreferredDuoDevice(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadPreferredDuoDevice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadPreferredDuoDevice", reflect.TypeOf((*MockProvider)(nil).LoadPreferredDuoDevice), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadPreferredDuoDevice", reflect.TypeOf((*MockStorage)(nil).LoadPreferredDuoDevice), arg0, arg1)
} }
// LoadTOTPConfiguration mocks base method. // LoadTOTPConfiguration mocks base method.
func (m *MockProvider) LoadTOTPConfiguration(arg0 context.Context, arg1 string) (*models.TOTPConfiguration, error) { func (m *MockStorage) LoadTOTPConfiguration(arg0 context.Context, arg1 string) (*models.TOTPConfiguration, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadTOTPConfiguration", arg0, arg1) ret := m.ctrl.Call(m, "LoadTOTPConfiguration", arg0, arg1)
ret0, _ := ret[0].(*models.TOTPConfiguration) ret0, _ := ret[0].(*models.TOTPConfiguration)
@ -163,13 +163,13 @@ func (m *MockProvider) LoadTOTPConfiguration(arg0 context.Context, arg1 string)
} }
// LoadTOTPConfiguration indicates an expected call of LoadTOTPConfiguration. // LoadTOTPConfiguration indicates an expected call of LoadTOTPConfiguration.
func (mr *MockProviderMockRecorder) LoadTOTPConfiguration(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadTOTPConfiguration(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
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((*MockStorage)(nil).LoadTOTPConfiguration), arg0, arg1)
} }
// LoadTOTPConfigurations mocks base method. // LoadTOTPConfigurations mocks base method.
func (m *MockProvider) LoadTOTPConfigurations(arg0 context.Context, arg1, arg2 int) ([]models.TOTPConfiguration, error) { func (m *MockStorage) LoadTOTPConfigurations(arg0 context.Context, arg1, arg2 int) ([]models.TOTPConfiguration, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadTOTPConfigurations", arg0, arg1, arg2) ret := m.ctrl.Call(m, "LoadTOTPConfigurations", arg0, arg1, arg2)
ret0, _ := ret[0].([]models.TOTPConfiguration) ret0, _ := ret[0].([]models.TOTPConfiguration)
@ -178,13 +178,13 @@ func (m *MockProvider) LoadTOTPConfigurations(arg0 context.Context, arg1, arg2 i
} }
// LoadTOTPConfigurations indicates an expected call of LoadTOTPConfigurations. // LoadTOTPConfigurations indicates an expected call of LoadTOTPConfigurations.
func (mr *MockProviderMockRecorder) LoadTOTPConfigurations(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadTOTPConfigurations(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadTOTPConfigurations", reflect.TypeOf((*MockProvider)(nil).LoadTOTPConfigurations), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadTOTPConfigurations", reflect.TypeOf((*MockStorage)(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 *MockStorage) LoadU2FDevice(arg0 context.Context, arg1 string) (*models.U2FDevice, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadU2FDevice", arg0, arg1) ret := m.ctrl.Call(m, "LoadU2FDevice", arg0, arg1)
ret0, _ := ret[0].(*models.U2FDevice) ret0, _ := ret[0].(*models.U2FDevice)
@ -193,13 +193,13 @@ func (m *MockProvider) LoadU2FDevice(arg0 context.Context, arg1 string) (*models
} }
// LoadU2FDevice indicates an expected call of LoadU2FDevice. // LoadU2FDevice indicates an expected call of LoadU2FDevice.
func (mr *MockProviderMockRecorder) LoadU2FDevice(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadU2FDevice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadU2FDevice", reflect.TypeOf((*MockProvider)(nil).LoadU2FDevice), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadU2FDevice", reflect.TypeOf((*MockStorage)(nil).LoadU2FDevice), arg0, arg1)
} }
// LoadUserInfo mocks base method. // LoadUserInfo mocks base method.
func (m *MockProvider) LoadUserInfo(arg0 context.Context, arg1 string) (models.UserInfo, error) { func (m *MockStorage) LoadUserInfo(arg0 context.Context, arg1 string) (models.UserInfo, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadUserInfo", arg0, arg1) ret := m.ctrl.Call(m, "LoadUserInfo", arg0, arg1)
ret0, _ := ret[0].(models.UserInfo) ret0, _ := ret[0].(models.UserInfo)
@ -208,13 +208,13 @@ func (m *MockProvider) LoadUserInfo(arg0 context.Context, arg1 string) (models.U
} }
// LoadUserInfo indicates an expected call of LoadUserInfo. // LoadUserInfo indicates an expected call of LoadUserInfo.
func (mr *MockProviderMockRecorder) LoadUserInfo(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadUserInfo(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserInfo", reflect.TypeOf((*MockProvider)(nil).LoadUserInfo), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserInfo", reflect.TypeOf((*MockStorage)(nil).LoadUserInfo), arg0, arg1)
} }
// RemoveIdentityVerification mocks base method. // RemoveIdentityVerification mocks base method.
func (m *MockProvider) RemoveIdentityVerification(arg0 context.Context, arg1 string) error { func (m *MockStorage) RemoveIdentityVerification(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveIdentityVerification", arg0, arg1) ret := m.ctrl.Call(m, "RemoveIdentityVerification", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -222,13 +222,13 @@ func (m *MockProvider) RemoveIdentityVerification(arg0 context.Context, arg1 str
} }
// RemoveIdentityVerification indicates an expected call of RemoveIdentityVerification. // RemoveIdentityVerification indicates an expected call of RemoveIdentityVerification.
func (mr *MockProviderMockRecorder) RemoveIdentityVerification(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) RemoveIdentityVerification(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveIdentityVerification", reflect.TypeOf((*MockProvider)(nil).RemoveIdentityVerification), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveIdentityVerification", reflect.TypeOf((*MockStorage)(nil).RemoveIdentityVerification), arg0, arg1)
} }
// SaveIdentityVerification mocks base method. // SaveIdentityVerification mocks base method.
func (m *MockProvider) SaveIdentityVerification(arg0 context.Context, arg1 models.IdentityVerification) error { func (m *MockStorage) SaveIdentityVerification(arg0 context.Context, arg1 models.IdentityVerification) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveIdentityVerification", arg0, arg1) ret := m.ctrl.Call(m, "SaveIdentityVerification", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -236,13 +236,13 @@ func (m *MockProvider) SaveIdentityVerification(arg0 context.Context, arg1 model
} }
// SaveIdentityVerification indicates an expected call of SaveIdentityVerification. // SaveIdentityVerification indicates an expected call of SaveIdentityVerification.
func (mr *MockProviderMockRecorder) SaveIdentityVerification(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SaveIdentityVerification(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveIdentityVerification", reflect.TypeOf((*MockProvider)(nil).SaveIdentityVerification), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveIdentityVerification", reflect.TypeOf((*MockStorage)(nil).SaveIdentityVerification), arg0, arg1)
} }
// SavePreferred2FAMethod mocks base method. // SavePreferred2FAMethod mocks base method.
func (m *MockProvider) SavePreferred2FAMethod(arg0 context.Context, arg1, arg2 string) error { func (m *MockStorage) SavePreferred2FAMethod(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SavePreferred2FAMethod", arg0, arg1, arg2) ret := m.ctrl.Call(m, "SavePreferred2FAMethod", arg0, arg1, arg2)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -250,13 +250,13 @@ func (m *MockProvider) SavePreferred2FAMethod(arg0 context.Context, arg1, arg2 s
} }
// SavePreferred2FAMethod indicates an expected call of SavePreferred2FAMethod. // SavePreferred2FAMethod indicates an expected call of SavePreferred2FAMethod.
func (mr *MockProviderMockRecorder) SavePreferred2FAMethod(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SavePreferred2FAMethod(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePreferred2FAMethod", reflect.TypeOf((*MockProvider)(nil).SavePreferred2FAMethod), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePreferred2FAMethod", reflect.TypeOf((*MockStorage)(nil).SavePreferred2FAMethod), arg0, arg1, arg2)
} }
// SavePreferredDuoDevice mocks base method. // SavePreferredDuoDevice mocks base method.
func (m *MockProvider) SavePreferredDuoDevice(arg0 context.Context, arg1 models.DuoDevice) error { func (m *MockStorage) SavePreferredDuoDevice(arg0 context.Context, arg1 models.DuoDevice) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SavePreferredDuoDevice", arg0, arg1) ret := m.ctrl.Call(m, "SavePreferredDuoDevice", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -264,13 +264,13 @@ func (m *MockProvider) SavePreferredDuoDevice(arg0 context.Context, arg1 models.
} }
// SavePreferredDuoDevice indicates an expected call of SavePreferredDuoDevice. // SavePreferredDuoDevice indicates an expected call of SavePreferredDuoDevice.
func (mr *MockProviderMockRecorder) SavePreferredDuoDevice(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SavePreferredDuoDevice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePreferredDuoDevice", reflect.TypeOf((*MockProvider)(nil).SavePreferredDuoDevice), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePreferredDuoDevice", reflect.TypeOf((*MockStorage)(nil).SavePreferredDuoDevice), arg0, arg1)
} }
// SaveTOTPConfiguration mocks base method. // SaveTOTPConfiguration mocks base method.
func (m *MockProvider) SaveTOTPConfiguration(arg0 context.Context, arg1 models.TOTPConfiguration) error { func (m *MockStorage) SaveTOTPConfiguration(arg0 context.Context, arg1 models.TOTPConfiguration) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveTOTPConfiguration", arg0, arg1) ret := m.ctrl.Call(m, "SaveTOTPConfiguration", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -278,13 +278,13 @@ func (m *MockProvider) SaveTOTPConfiguration(arg0 context.Context, arg1 models.T
} }
// SaveTOTPConfiguration indicates an expected call of SaveTOTPConfiguration. // SaveTOTPConfiguration indicates an expected call of SaveTOTPConfiguration.
func (mr *MockProviderMockRecorder) SaveTOTPConfiguration(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SaveTOTPConfiguration(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTOTPConfiguration", reflect.TypeOf((*MockProvider)(nil).SaveTOTPConfiguration), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTOTPConfiguration", reflect.TypeOf((*MockStorage)(nil).SaveTOTPConfiguration), arg0, arg1)
} }
// SaveU2FDevice mocks base method. // SaveU2FDevice mocks base method.
func (m *MockProvider) SaveU2FDevice(arg0 context.Context, arg1 models.U2FDevice) error { func (m *MockStorage) SaveU2FDevice(arg0 context.Context, arg1 models.U2FDevice) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveU2FDevice", arg0, arg1) ret := m.ctrl.Call(m, "SaveU2FDevice", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -292,13 +292,13 @@ func (m *MockProvider) SaveU2FDevice(arg0 context.Context, arg1 models.U2FDevice
} }
// SaveU2FDevice indicates an expected call of SaveU2FDevice. // SaveU2FDevice indicates an expected call of SaveU2FDevice.
func (mr *MockProviderMockRecorder) SaveU2FDevice(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SaveU2FDevice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
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((*MockStorage)(nil).SaveU2FDevice), arg0, arg1)
} }
// SchemaEncryptionChangeKey mocks base method. // SchemaEncryptionChangeKey mocks base method.
func (m *MockProvider) SchemaEncryptionChangeKey(arg0 context.Context, arg1 string) error { func (m *MockStorage) SchemaEncryptionChangeKey(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaEncryptionChangeKey", arg0, arg1) ret := m.ctrl.Call(m, "SchemaEncryptionChangeKey", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -306,13 +306,13 @@ func (m *MockProvider) SchemaEncryptionChangeKey(arg0 context.Context, arg1 stri
} }
// SchemaEncryptionChangeKey indicates an expected call of SchemaEncryptionChangeKey. // SchemaEncryptionChangeKey indicates an expected call of SchemaEncryptionChangeKey.
func (mr *MockProviderMockRecorder) SchemaEncryptionChangeKey(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SchemaEncryptionChangeKey(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaEncryptionChangeKey", reflect.TypeOf((*MockProvider)(nil).SchemaEncryptionChangeKey), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaEncryptionChangeKey", reflect.TypeOf((*MockStorage)(nil).SchemaEncryptionChangeKey), arg0, arg1)
} }
// SchemaEncryptionCheckKey mocks base method. // SchemaEncryptionCheckKey mocks base method.
func (m *MockProvider) SchemaEncryptionCheckKey(arg0 context.Context, arg1 bool) error { func (m *MockStorage) SchemaEncryptionCheckKey(arg0 context.Context, arg1 bool) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaEncryptionCheckKey", arg0, arg1) ret := m.ctrl.Call(m, "SchemaEncryptionCheckKey", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -320,13 +320,13 @@ func (m *MockProvider) SchemaEncryptionCheckKey(arg0 context.Context, arg1 bool)
} }
// SchemaEncryptionCheckKey indicates an expected call of SchemaEncryptionCheckKey. // SchemaEncryptionCheckKey indicates an expected call of SchemaEncryptionCheckKey.
func (mr *MockProviderMockRecorder) SchemaEncryptionCheckKey(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SchemaEncryptionCheckKey(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaEncryptionCheckKey", reflect.TypeOf((*MockProvider)(nil).SchemaEncryptionCheckKey), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaEncryptionCheckKey", reflect.TypeOf((*MockStorage)(nil).SchemaEncryptionCheckKey), arg0, arg1)
} }
// SchemaLatestVersion mocks base method. // SchemaLatestVersion mocks base method.
func (m *MockProvider) SchemaLatestVersion() (int, error) { func (m *MockStorage) SchemaLatestVersion() (int, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaLatestVersion") ret := m.ctrl.Call(m, "SchemaLatestVersion")
ret0, _ := ret[0].(int) ret0, _ := ret[0].(int)
@ -335,13 +335,13 @@ func (m *MockProvider) SchemaLatestVersion() (int, error) {
} }
// SchemaLatestVersion indicates an expected call of SchemaLatestVersion. // SchemaLatestVersion indicates an expected call of SchemaLatestVersion.
func (mr *MockProviderMockRecorder) SchemaLatestVersion() *gomock.Call { func (mr *MockStorageMockRecorder) SchemaLatestVersion() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaLatestVersion", reflect.TypeOf((*MockProvider)(nil).SchemaLatestVersion)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaLatestVersion", reflect.TypeOf((*MockStorage)(nil).SchemaLatestVersion))
} }
// SchemaMigrate mocks base method. // SchemaMigrate mocks base method.
func (m *MockProvider) SchemaMigrate(arg0 context.Context, arg1 bool, arg2 int) error { func (m *MockStorage) SchemaMigrate(arg0 context.Context, arg1 bool, arg2 int) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaMigrate", arg0, arg1, arg2) ret := m.ctrl.Call(m, "SchemaMigrate", arg0, arg1, arg2)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -349,13 +349,13 @@ func (m *MockProvider) SchemaMigrate(arg0 context.Context, arg1 bool, arg2 int)
} }
// SchemaMigrate indicates an expected call of SchemaMigrate. // SchemaMigrate indicates an expected call of SchemaMigrate.
func (mr *MockProviderMockRecorder) SchemaMigrate(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SchemaMigrate(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaMigrate", reflect.TypeOf((*MockProvider)(nil).SchemaMigrate), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaMigrate", reflect.TypeOf((*MockStorage)(nil).SchemaMigrate), arg0, arg1, arg2)
} }
// SchemaMigrationHistory mocks base method. // SchemaMigrationHistory mocks base method.
func (m *MockProvider) SchemaMigrationHistory(arg0 context.Context) ([]models.Migration, error) { func (m *MockStorage) SchemaMigrationHistory(arg0 context.Context) ([]models.Migration, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaMigrationHistory", arg0) ret := m.ctrl.Call(m, "SchemaMigrationHistory", arg0)
ret0, _ := ret[0].([]models.Migration) ret0, _ := ret[0].([]models.Migration)
@ -364,43 +364,43 @@ func (m *MockProvider) SchemaMigrationHistory(arg0 context.Context) ([]models.Mi
} }
// SchemaMigrationHistory indicates an expected call of SchemaMigrationHistory. // SchemaMigrationHistory indicates an expected call of SchemaMigrationHistory.
func (mr *MockProviderMockRecorder) SchemaMigrationHistory(arg0 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SchemaMigrationHistory(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaMigrationHistory", reflect.TypeOf((*MockProvider)(nil).SchemaMigrationHistory), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaMigrationHistory", reflect.TypeOf((*MockStorage)(nil).SchemaMigrationHistory), arg0)
} }
// SchemaMigrationsDown mocks base method. // SchemaMigrationsDown mocks base method.
func (m *MockProvider) SchemaMigrationsDown(arg0 context.Context, arg1 int) ([]SchemaMigration, error) { func (m *MockStorage) SchemaMigrationsDown(arg0 context.Context, arg1 int) ([]models.SchemaMigration, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaMigrationsDown", arg0, arg1) ret := m.ctrl.Call(m, "SchemaMigrationsDown", arg0, arg1)
ret0, _ := ret[0].([]SchemaMigration) ret0, _ := ret[0].([]models.SchemaMigration)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// SchemaMigrationsDown indicates an expected call of SchemaMigrationsDown. // SchemaMigrationsDown indicates an expected call of SchemaMigrationsDown.
func (mr *MockProviderMockRecorder) SchemaMigrationsDown(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SchemaMigrationsDown(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaMigrationsDown", reflect.TypeOf((*MockProvider)(nil).SchemaMigrationsDown), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaMigrationsDown", reflect.TypeOf((*MockStorage)(nil).SchemaMigrationsDown), arg0, arg1)
} }
// SchemaMigrationsUp mocks base method. // SchemaMigrationsUp mocks base method.
func (m *MockProvider) SchemaMigrationsUp(arg0 context.Context, arg1 int) ([]SchemaMigration, error) { func (m *MockStorage) SchemaMigrationsUp(arg0 context.Context, arg1 int) ([]models.SchemaMigration, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaMigrationsUp", arg0, arg1) ret := m.ctrl.Call(m, "SchemaMigrationsUp", arg0, arg1)
ret0, _ := ret[0].([]SchemaMigration) ret0, _ := ret[0].([]models.SchemaMigration)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// SchemaMigrationsUp indicates an expected call of SchemaMigrationsUp. // SchemaMigrationsUp indicates an expected call of SchemaMigrationsUp.
func (mr *MockProviderMockRecorder) SchemaMigrationsUp(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SchemaMigrationsUp(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaMigrationsUp", reflect.TypeOf((*MockProvider)(nil).SchemaMigrationsUp), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaMigrationsUp", reflect.TypeOf((*MockStorage)(nil).SchemaMigrationsUp), arg0, arg1)
} }
// SchemaTables mocks base method. // SchemaTables mocks base method.
func (m *MockProvider) SchemaTables(arg0 context.Context) ([]string, error) { func (m *MockStorage) SchemaTables(arg0 context.Context) ([]string, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaTables", arg0) ret := m.ctrl.Call(m, "SchemaTables", arg0)
ret0, _ := ret[0].([]string) ret0, _ := ret[0].([]string)
@ -409,13 +409,13 @@ func (m *MockProvider) SchemaTables(arg0 context.Context) ([]string, error) {
} }
// SchemaTables indicates an expected call of SchemaTables. // SchemaTables indicates an expected call of SchemaTables.
func (mr *MockProviderMockRecorder) SchemaTables(arg0 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SchemaTables(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaTables", reflect.TypeOf((*MockProvider)(nil).SchemaTables), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaTables", reflect.TypeOf((*MockStorage)(nil).SchemaTables), arg0)
} }
// SchemaVersion mocks base method. // SchemaVersion mocks base method.
func (m *MockProvider) SchemaVersion(arg0 context.Context) (int, error) { func (m *MockStorage) SchemaVersion(arg0 context.Context) (int, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaVersion", arg0) ret := m.ctrl.Call(m, "SchemaVersion", arg0)
ret0, _ := ret[0].(int) ret0, _ := ret[0].(int)
@ -424,13 +424,13 @@ func (m *MockProvider) SchemaVersion(arg0 context.Context) (int, error) {
} }
// SchemaVersion indicates an expected call of SchemaVersion. // SchemaVersion indicates an expected call of SchemaVersion.
func (mr *MockProviderMockRecorder) SchemaVersion(arg0 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SchemaVersion(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaVersion", reflect.TypeOf((*MockProvider)(nil).SchemaVersion), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaVersion", reflect.TypeOf((*MockStorage)(nil).SchemaVersion), arg0)
} }
// StartupCheck mocks base method. // StartupCheck mocks base method.
func (m *MockProvider) StartupCheck() error { func (m *MockStorage) StartupCheck() error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StartupCheck") ret := m.ctrl.Call(m, "StartupCheck")
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -438,13 +438,13 @@ func (m *MockProvider) StartupCheck() error {
} }
// StartupCheck indicates an expected call of StartupCheck. // StartupCheck indicates an expected call of StartupCheck.
func (mr *MockProviderMockRecorder) StartupCheck() *gomock.Call { func (mr *MockStorageMockRecorder) 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((*MockStorage)(nil).StartupCheck))
} }
// UpdateTOTPConfigurationSecret mocks base method. // UpdateTOTPConfigurationSecret mocks base method.
func (m *MockProvider) UpdateTOTPConfigurationSecret(arg0 context.Context, arg1 models.TOTPConfiguration) error { func (m *MockStorage) UpdateTOTPConfigurationSecret(arg0 context.Context, arg1 models.TOTPConfiguration) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateTOTPConfigurationSecret", arg0, arg1) ret := m.ctrl.Call(m, "UpdateTOTPConfigurationSecret", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -452,7 +452,7 @@ func (m *MockProvider) UpdateTOTPConfigurationSecret(arg0 context.Context, arg1
} }
// UpdateTOTPConfigurationSecret indicates an expected call of UpdateTOTPConfigurationSecret. // UpdateTOTPConfigurationSecret indicates an expected call of UpdateTOTPConfigurationSecret.
func (mr *MockProviderMockRecorder) UpdateTOTPConfigurationSecret(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) UpdateTOTPConfigurationSecret(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTOTPConfigurationSecret", reflect.TypeOf((*MockProvider)(nil).UpdateTOTPConfigurationSecret), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTOTPConfigurationSecret", reflect.TypeOf((*MockStorage)(nil).UpdateTOTPConfigurationSecret), arg0, arg1)
} }

View File

@ -0,0 +1,81 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/authelia/authelia/v4/internal/totp (interfaces: Provider)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
models "github.com/authelia/authelia/v4/internal/models"
)
// MockTOTP is a mock of Provider interface.
type MockTOTP struct {
ctrl *gomock.Controller
recorder *MockTOTPMockRecorder
}
// MockTOTPMockRecorder is the mock recorder for MockTOTP.
type MockTOTPMockRecorder struct {
mock *MockTOTP
}
// NewMockTOTP creates a new mock instance.
func NewMockTOTP(ctrl *gomock.Controller) *MockTOTP {
mock := &MockTOTP{ctrl: ctrl}
mock.recorder = &MockTOTPMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTOTP) EXPECT() *MockTOTPMockRecorder {
return m.recorder
}
// Generate mocks base method.
func (m *MockTOTP) Generate(arg0 string) (*models.TOTPConfiguration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Generate", arg0)
ret0, _ := ret[0].(*models.TOTPConfiguration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Generate indicates an expected call of Generate.
func (mr *MockTOTPMockRecorder) Generate(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockTOTP)(nil).Generate), arg0)
}
// GenerateCustom mocks base method.
func (m *MockTOTP) GenerateCustom(arg0, arg1 string, arg2, arg3, arg4 uint) (*models.TOTPConfiguration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GenerateCustom", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(*models.TOTPConfiguration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GenerateCustom indicates an expected call of GenerateCustom.
func (mr *MockTOTPMockRecorder) GenerateCustom(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateCustom", reflect.TypeOf((*MockTOTP)(nil).GenerateCustom), arg0, arg1, arg2, arg3, arg4)
}
// Validate mocks base method.
func (m *MockTOTP) Validate(arg0 string, arg1 *models.TOTPConfiguration) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Validate", arg0, arg1)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Validate indicates an expected call of Validate.
func (mr *MockTOTPMockRecorder) Validate(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockTOTP)(nil).Validate), arg0, arg1)
}

View File

@ -1,8 +1,8 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: internal/handlers/u2f.go // Source: github.com/authelia/authelia/v4/internal/handlers (interfaces: U2FVerifier)
// Package handlers is a generated GoMock package. // Package mocks is a generated GoMock package.
package handlers package mocks
import ( import (
reflect "reflect" reflect "reflect"
@ -11,39 +11,39 @@ import (
u2f "github.com/tstranex/u2f" u2f "github.com/tstranex/u2f"
) )
// MockU2FVerifier is a mock of U2FVerifier interface // MockU2FVerifier is a mock of U2FVerifier interface.
type MockU2FVerifier struct { type MockU2FVerifier struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockU2FVerifierMockRecorder recorder *MockU2FVerifierMockRecorder
} }
// MockU2FVerifierMockRecorder is the mock recorder for MockU2FVerifier // MockU2FVerifierMockRecorder is the mock recorder for MockU2FVerifier.
type MockU2FVerifierMockRecorder struct { type MockU2FVerifierMockRecorder struct {
mock *MockU2FVerifier mock *MockU2FVerifier
} }
// NewMockU2FVerifier creates a new mock instance // NewMockU2FVerifier creates a new mock instance.
func NewMockU2FVerifier(ctrl *gomock.Controller) *MockU2FVerifier { func NewMockU2FVerifier(ctrl *gomock.Controller) *MockU2FVerifier {
mock := &MockU2FVerifier{ctrl: ctrl} mock := &MockU2FVerifier{ctrl: ctrl}
mock.recorder = &MockU2FVerifierMockRecorder{mock} mock.recorder = &MockU2FVerifierMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockU2FVerifier) EXPECT() *MockU2FVerifierMockRecorder { func (m *MockU2FVerifier) EXPECT() *MockU2FVerifierMockRecorder {
return m.recorder return m.recorder
} }
// Verify mocks base method // Verify mocks base method.
func (m *MockU2FVerifier) Verify(keyHandle, publicKey []byte, signResponse u2f.SignResponse, challenge u2f.Challenge) error { func (m *MockU2FVerifier) Verify(arg0, arg1 []byte, arg2 u2f.SignResponse, arg3 u2f.Challenge) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Verify", keyHandle, publicKey, signResponse, challenge) ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// Verify indicates an expected call of Verify // Verify indicates an expected call of Verify.
func (mr *MockU2FVerifierMockRecorder) Verify(keyHandle, publicKey, signResponse, challenge interface{}) *gomock.Call { func (mr *MockU2FVerifierMockRecorder) Verify(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockU2FVerifier)(nil).Verify), keyHandle, publicKey, signResponse, challenge) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockU2FVerifier)(nil).Verify), arg0, arg1, arg2, arg3)
} }

View File

@ -1,11 +0,0 @@
package models
// TOTPConfiguration represents a users TOTP configuration row in the database.
type TOTPConfiguration struct {
ID int `db:"id"`
Username string `db:"username"`
Algorithm string `db:"algorithm"`
Digits int `db:"digits"`
Period uint64 `db:"totp_period"`
Secret []byte `db:"secret"`
}

View File

@ -1,4 +1,4 @@
package storage package models
// SchemaMigration represents an intended migration. // SchemaMigration represents an intended migration.
type SchemaMigration struct { type SchemaMigration struct {

View File

@ -0,0 +1,36 @@
package models
import (
"net/url"
"strconv"
)
// TOTPConfiguration represents a users TOTP configuration row in the database.
type TOTPConfiguration struct {
ID int `db:"id" json:"-"`
Username string `db:"username" json:"-"`
Issuer string `db:"issuer" json:"-"`
Algorithm string `db:"algorithm" json:"-"`
Digits uint `db:"digits" json:"digits"`
Period uint `db:"totp_period" json:"period"`
Secret []byte `db:"secret" json:"-"`
}
// URI shows the configuration in the URI representation.
func (c TOTPConfiguration) URI() (uri string) {
v := url.Values{}
v.Set("secret", string(c.Secret))
v.Set("issuer", c.Issuer)
v.Set("period", strconv.FormatUint(uint64(c.Period), 10))
v.Set("algorithm", c.Algorithm)
v.Set("digits", strconv.Itoa(int(c.Digits)))
u := url.URL{
Scheme: "otpauth",
Host: "totp",
Path: "/" + c.Issuer + ":" + c.Username,
RawQuery: v.Encode(),
}
return u.String()
}

View File

@ -0,0 +1,40 @@
package models
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
/*
TestShouldOnlyMarshalPeriodAndDigitsAndAbsolutelyNeverSecret.
This test is vital to ensuring the TOTP configuration is marshalled correctly. If encoding/json suddenly changes
upstream and the json tag value of '-' doesn't exclude the field from marshalling then this test will pickup this
issue prior to code being shipped.
For this reason it's essential that the marshalled object contains all values populated, especially the secret.
*/
func TestShouldOnlyMarshalPeriodAndDigitsAndAbsolutelyNeverSecret(t *testing.T) {
object := TOTPConfiguration{
ID: 1,
Username: "john",
Issuer: "Authelia",
Algorithm: "SHA1",
Digits: 6,
Period: 30,
// DO NOT CHANGE THIS VALUE UNLESS YOU FULLY UNDERSTAND THE COMMENT AT THE TOP OF THIS TEST.
Secret: []byte("ABC123"),
}
data, err := json.Marshal(object)
assert.NoError(t, err)
assert.Equal(t, "{\"digits\":6,\"period\":30}", string(data))
// DO NOT REMOVE OR CHANGE THESE TESTS UNLESS YOU FULLY UNDERSTAND THE COMMENT AT THE TOP OF THIS TEST.
require.NotContains(t, string(data), "secret")
require.NotContains(t, string(data), "ABC123")
}

View File

@ -1,6 +0,0 @@
package models
// StartupCheck represents a provider that has a startup check.
type StartupCheck interface {
StartupCheck() (err error)
}

View File

@ -46,3 +46,8 @@ func (ip *IPAddress) Scan(src interface{}) (err error) {
return nil return nil
} }
// StartupCheck represents a provider that has a startup check.
type StartupCheck interface {
StartupCheck() (err error)
}

View File

@ -13,7 +13,6 @@ import (
"github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/models" "github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/storage"
) )
type RegulatorSuite struct { type RegulatorSuite struct {
@ -21,14 +20,14 @@ type RegulatorSuite struct {
ctx context.Context ctx context.Context
ctrl *gomock.Controller ctrl *gomock.Controller
storageMock *storage.MockProvider storageMock *mocks.MockStorage
configuration schema.RegulationConfiguration configuration schema.RegulationConfiguration
clock mocks.TestingClock clock mocks.TestingClock
} }
func (s *RegulatorSuite) SetupTest() { func (s *RegulatorSuite) SetupTest() {
s.ctrl = gomock.NewController(s.T()) s.ctrl = gomock.NewController(s.T())
s.storageMock = storage.NewMockProvider(s.ctrl) s.storageMock = mocks.NewMockStorage(s.ctrl)
s.ctx = context.Background() s.ctx = context.Background()
s.configuration = schema.RegulationConfiguration{ s.configuration = schema.RegulationConfiguration{

View File

@ -88,6 +88,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
middlewares.RequireFirstFactor(handlers.UserInfoGet))) middlewares.RequireFirstFactor(handlers.UserInfoGet)))
r.POST("/api/user/info/2fa_method", autheliaMiddleware( r.POST("/api/user/info/2fa_method", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.MethodPreferencePost))) middlewares.RequireFirstFactor(handlers.MethodPreferencePost)))
r.GET("/api/user/info/totp", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.UserTOTPGet)))
// TOTP related endpoints. // TOTP related endpoints.
r.POST("/api/secondfactor/totp/identity/start", autheliaMiddleware( r.POST("/api/secondfactor/totp/identity/start", autheliaMiddleware(
@ -95,10 +97,7 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
r.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware( r.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish))) middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish)))
r.POST("/api/secondfactor/totp", autheliaMiddleware( r.POST("/api/secondfactor/totp", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost(&handlers.TOTPVerifierImpl{ middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost)))
Period: uint(configuration.TOTP.Period),
Skew: uint(*configuration.TOTP.Skew),
}))))
// U2F related endpoints. // U2F related endpoints.
r.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware( r.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware(

View File

@ -8,8 +8,8 @@ var (
// ErrNoAuthenticationLogs error thrown when no matching authentication logs hve been found in DB. // ErrNoAuthenticationLogs error thrown when no matching authentication logs hve been found in DB.
ErrNoAuthenticationLogs = errors.New("no matching authentication logs found") ErrNoAuthenticationLogs = errors.New("no matching authentication logs found")
// ErrNoTOTPSecret error thrown when no TOTP secret has been found in DB. // ErrNoTOTPConfiguration error thrown when no TOTP configuration has been found in DB.
ErrNoTOTPSecret = errors.New("no TOTP secret registered") ErrNoTOTPConfiguration = errors.New("no TOTP configuration for user")
// 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")

View File

@ -7,6 +7,8 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"github.com/authelia/authelia/v4/internal/models"
) )
//go:embed migrations/* //go:embed migrations/*
@ -44,7 +46,7 @@ func latestMigrationVersion(providerName string) (version int, err error) {
return version, nil return version, nil
} }
func loadMigration(providerName string, version int, up bool) (migration *SchemaMigration, err error) { func loadMigration(providerName string, version int, up bool) (migration *models.SchemaMigration, err error) {
entries, err := migrationsFS.ReadDir("migrations") entries, err := migrationsFS.ReadDir("migrations")
if err != nil { if err != nil {
return nil, err return nil, err
@ -83,7 +85,7 @@ func loadMigration(providerName string, version int, up bool) (migration *Schema
// loadMigrations scans the migrations fs and loads the appropriate migrations for a given providerName, prior and // loadMigrations scans the migrations fs and loads the appropriate migrations for a given providerName, prior and
// target versions. If the target version is -1 this indicates the latest version. If the target version is 0 // target versions. If the target version is -1 this indicates the latest version. If the target version is 0
// 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 []models.SchemaMigration, err error) {
if prior == target && (prior != -1 || target != -1) { if prior == target && (prior != -1 || target != -1) {
return nil, ErrMigrateCurrentVersionSameAsTarget return nil, ErrMigrateCurrentVersionSameAsTarget
} }
@ -125,7 +127,7 @@ func loadMigrations(providerName string, prior, target int) (migrations []Schema
return migrations, nil return migrations, nil
} }
func skipMigration(providerName string, up bool, target, prior int, migration *SchemaMigration) (skip bool) { func skipMigration(providerName string, up bool, target, prior int, migration *models.SchemaMigration) (skip bool) {
if migration.Provider != providerAll && migration.Provider != providerName { if migration.Provider != providerAll && migration.Provider != providerName {
// Skip if migration.Provider is not a match. // Skip if migration.Provider is not a match.
return true return true
@ -163,21 +165,21 @@ func skipMigration(providerName string, up bool, target, prior int, migration *S
return false return false
} }
func scanMigration(m string) (migration SchemaMigration, err error) { func scanMigration(m string) (migration models.SchemaMigration, err error) {
result := reMigration.FindStringSubmatch(m) result := reMigration.FindStringSubmatch(m)
if result == nil || len(result) != 5 { if result == nil || len(result) != 5 {
return SchemaMigration{}, errors.New("invalid migration: could not parse the format") return models.SchemaMigration{}, errors.New("invalid migration: could not parse the format")
} }
migration = SchemaMigration{ migration = models.SchemaMigration{
Name: strings.ReplaceAll(result[2], "_", " "), Name: strings.ReplaceAll(result[2], "_", " "),
Provider: result[3], Provider: result[3],
} }
data, err := migrationsFS.ReadFile(fmt.Sprintf("migrations/%s", m)) data, err := migrationsFS.ReadFile(fmt.Sprintf("migrations/%s", m))
if err != nil { if err != nil {
return SchemaMigration{}, err return models.SchemaMigration{}, err
} }
migration.Query = string(data) migration.Query = string(data)
@ -188,7 +190,7 @@ func scanMigration(m string) (migration SchemaMigration, err error) {
case "down": case "down":
migration.Up = false migration.Up = false
default: default:
return SchemaMigration{}, fmt.Errorf("invalid migration: value in position 4 '%s' must be up or down", result[4]) return models.SchemaMigration{}, fmt.Errorf("invalid migration: value in position 4 '%s' must be up or down", result[4])
} }
migration.Version, _ = strconv.Atoi(result[1]) migration.Version, _ = strconv.Atoi(result[1])
@ -197,7 +199,7 @@ func scanMigration(m string) (migration SchemaMigration, err error) {
case providerAll, providerSQLite, providerMySQL, providerPostgres: case providerAll, providerSQLite, providerMySQL, providerPostgres:
break break
default: default:
return SchemaMigration{}, fmt.Errorf("invalid migration: value in position 3 '%s' must be all, sqlite, postgres, or mysql", result[3]) return models.SchemaMigration{}, fmt.Errorf("invalid migration: value in position 3 '%s' must be all, sqlite, postgres, or mysql", result[3])
} }
return migration, nil return migration, nil

View File

@ -40,8 +40,8 @@ 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)
SchemaMigrationsUp(ctx context.Context, version int) (migrations []SchemaMigration, err error) SchemaMigrationsUp(ctx context.Context, version int) (migrations []models.SchemaMigration, err error)
SchemaMigrationsDown(ctx context.Context, version int) (migrations []SchemaMigration, err error) SchemaMigrationsDown(ctx context.Context, version int) (migrations []models.SchemaMigration, err error)
SchemaEncryptionChangeKey(ctx context.Context, encryptionKey string) (err error) SchemaEncryptionChangeKey(ctx context.Context, encryptionKey string) (err error)
SchemaEncryptionCheckKey(ctx context.Context, verbose bool) (err error) SchemaEncryptionCheckKey(ctx context.Context, verbose bool) (err error)

View File

@ -12,19 +12,21 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authentication"
"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/models" "github.com/authelia/authelia/v4/internal/models"
) )
// 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, encryptionKey string) (provider SQLProvider) { func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceName string) (provider SQLProvider) {
db, err := sqlx.Open(driverName, dataSourceName) db, err := sqlx.Open(driverName, dataSourceName)
provider = SQLProvider{ provider = SQLProvider{
db: db, db: db,
key: sha256.Sum256([]byte(encryptionKey)), key: sha256.Sum256([]byte(config.Storage.EncryptionKey)),
name: name, name: name,
driverName: driverName, driverName: driverName,
config: config,
errOpen: err, errOpen: err,
log: logging.Logger(), log: logging.Logger(),
@ -64,10 +66,6 @@ func NewSQLProvider(name, driverName, dataSourceName, encryptionKey string) (pro
sqlFmtRenameTable: queryFmtRenameTable, sqlFmtRenameTable: queryFmtRenameTable,
} }
key := sha256.Sum256([]byte(encryptionKey))
provider.key = key
return provider return provider
} }
@ -77,6 +75,7 @@ type SQLProvider struct {
key [32]byte key [32]byte
name string name string
driverName string driverName string
config *schema.Configuration
errOpen error errOpen error
log *logrus.Logger log *logrus.Logger
@ -251,7 +250,7 @@ func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config models.T
} }
if _, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig, if _, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig,
config.Username, config.Algorithm, config.Digits, config.Period, config.Secret); err != nil { config.Username, config.Issuer, config.Algorithm, config.Digits, config.Period, config.Secret); err != nil {
return fmt.Errorf("error upserting TOTP configuration: %w", err) return fmt.Errorf("error upserting TOTP configuration: %w", err)
} }
@ -273,7 +272,7 @@ func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string
if err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config); err != nil { if err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoTOTPSecret return nil, ErrNoTOTPConfiguration
} }
return nil, fmt.Errorf("error selecting TOTP configuration: %w", err) return nil, fmt.Errorf("error selecting TOTP configuration: %w", err)

View File

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

View File

@ -16,9 +16,9 @@ type PostgreSQLProvider struct {
} }
// NewPostgreSQLProvider a PostgreSQL provider. // NewPostgreSQLProvider a PostgreSQL provider.
func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration, encryptionKey string) (provider *PostgreSQLProvider) { func NewPostgreSQLProvider(config *schema.Configuration) (provider *PostgreSQLProvider) {
provider = &PostgreSQLProvider{ provider = &PostgreSQLProvider{
SQLProvider: NewSQLProvider(providerPostgres, "pgx", dataSourceNamePostgreSQL(config), encryptionKey), SQLProvider: NewSQLProvider(config, providerPostgres, "pgx", dataSourceNamePostgreSQL(*config.Storage.PostgreSQL)),
} }
// All providers have differing SELECT existing table statements. // All providers have differing SELECT existing table statements.

View File

@ -2,6 +2,8 @@ package storage
import ( import (
_ "github.com/mattn/go-sqlite3" // Load the SQLite Driver used in the connection string. _ "github.com/mattn/go-sqlite3" // Load the SQLite Driver used in the connection string.
"github.com/authelia/authelia/v4/internal/configuration/schema"
) )
// SQLiteProvider is a SQLite3 provider. // SQLiteProvider is a SQLite3 provider.
@ -10,9 +12,9 @@ type SQLiteProvider struct {
} }
// NewSQLiteProvider constructs a SQLite provider. // NewSQLiteProvider constructs a SQLite provider.
func NewSQLiteProvider(path, encryptionKey string) (provider *SQLiteProvider) { func NewSQLiteProvider(config *schema.Configuration) (provider *SQLiteProvider) {
provider = &SQLiteProvider{ provider = &SQLiteProvider{
SQLProvider: NewSQLProvider(providerSQLite, "sqlite3", path, encryptionKey), SQLProvider: NewSQLProvider(config, providerSQLite, "sqlite3", config.Storage.Local.Path),
} }
// All providers have differing SELECT existing table statements. // All providers have differing SELECT existing table statements.

View File

@ -75,12 +75,12 @@ const (
const ( const (
queryFmtSelectTOTPConfiguration = ` queryFmtSelectTOTPConfiguration = `
SELECT id, username, algorithm, digits, totp_period, secret SELECT id, username, issuer, algorithm, digits, totp_period, secret
FROM %s FROM %s
WHERE username = ?;` WHERE username = ?;`
queryFmtSelectTOTPConfigurations = ` queryFmtSelectTOTPConfigurations = `
SELECT id, username, algorithm, digits, totp_period, secret SELECT id, username, issuer, algorithm, digits, totp_period, secret
FROM %s FROM %s
LIMIT ? LIMIT ?
OFFSET ?;` OFFSET ?;`
@ -98,14 +98,14 @@ const (
WHERE username = ?;` WHERE username = ?;`
queryFmtUpsertTOTPConfiguration = ` queryFmtUpsertTOTPConfiguration = `
REPLACE INTO %s (username, algorithm, digits, totp_period, secret) REPLACE INTO %s (username, issuer, algorithm, digits, totp_period, secret)
VALUES (?, ?, ?, ?, ?);` VALUES (?, ?, ?, ?, ?, ?);`
queryFmtPostgresUpsertTOTPConfiguration = ` queryFmtPostgresUpsertTOTPConfiguration = `
INSERT INTO %s (username, algorithm, digits, totp_period, secret) INSERT INTO %s (username, issuer, algorithm, digits, totp_period, secret)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (username) ON CONFLICT (username)
DO UPDATE SET algorithm = $2, digits = $3, totp_period = $4, secret = $5;` DO UPDATE SET issuer = $2, algorithm = $3, digits = $4, totp_period = $5, secret = $6;`
queryFmtDeleteTOTPConfiguration = ` queryFmtDeleteTOTPConfiguration = `
DELETE FROM %s DELETE FROM %s

View File

@ -35,7 +35,11 @@ const (
FROM %s FROM %s
ORDER BY username ASC;` ORDER BY username ASC;`
queryFmtPre1InsertTOTPConfiguration = ` queryFmtPre1To1InsertTOTPConfiguration = `
INSERT INTO %s (username, issuer, totp_period, secret)
VALUES (?, ?, ?, ?);`
queryFmt1ToPre1InsertTOTPConfiguration = `
INSERT INTO %s (username, secret) INSERT INTO %s (username, secret)
VALUES (?, ?);` VALUES (?, ?);`

View File

@ -206,7 +206,7 @@ func (p *SQLProvider) schemaMigrateRollback(ctx context.Context, prior, after in
return fmt.Errorf("migration rollback complete. rollback caused by: %+v", migrateErr) return fmt.Errorf("migration rollback complete. rollback caused by: %+v", migrateErr)
} }
func (p *SQLProvider) schemaMigrateApply(ctx context.Context, migration SchemaMigration) (err error) { func (p *SQLProvider) schemaMigrateApply(ctx context.Context, migration models.SchemaMigration) (err error) {
_, err = p.db.ExecContext(ctx, migration.Query) _, err = p.db.ExecContext(ctx, migration.Query)
if err != nil { if err != nil {
return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err) return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err)
@ -231,7 +231,7 @@ func (p *SQLProvider) schemaMigrateApply(ctx context.Context, migration SchemaMi
return p.schemaMigrateFinalize(ctx, migration) return p.schemaMigrateFinalize(ctx, migration)
} }
func (p SQLProvider) schemaMigrateFinalize(ctx context.Context, migration SchemaMigration) (err error) { func (p SQLProvider) schemaMigrateFinalize(ctx context.Context, migration models.SchemaMigration) (err error) {
return p.schemaMigrateFinalizeAdvanced(ctx, migration.Before(), migration.After()) return p.schemaMigrateFinalizeAdvanced(ctx, migration.Before(), migration.After())
} }
@ -247,7 +247,7 @@ func (p *SQLProvider) schemaMigrateFinalizeAdvanced(ctx context.Context, before,
} }
// SchemaMigrationsUp returns a list of migrations up available between the current version and the provided version. // SchemaMigrationsUp returns a list of migrations up available between the current version and the provided version.
func (p *SQLProvider) SchemaMigrationsUp(ctx context.Context, version int) (migrations []SchemaMigration, err error) { func (p *SQLProvider) SchemaMigrationsUp(ctx context.Context, version int) (migrations []models.SchemaMigration, err error) {
current, err := p.SchemaVersion(ctx) current, err := p.SchemaVersion(ctx)
if err != nil { if err != nil {
return migrations, err return migrations, err
@ -265,7 +265,7 @@ func (p *SQLProvider) SchemaMigrationsUp(ctx context.Context, version int) (migr
} }
// SchemaMigrationsDown returns a list of migrations down available between the current version and the provided version. // SchemaMigrationsDown returns a list of migrations down available between the current version and the provided version.
func (p *SQLProvider) SchemaMigrationsDown(ctx context.Context, version int) (migrations []SchemaMigration, err error) { func (p *SQLProvider) SchemaMigrationsDown(ctx context.Context, version int) (migrations []models.SchemaMigration, err error) {
current, err := p.SchemaVersion(ctx) current, err := p.SchemaVersion(ctx)
if err != nil { if err != nil {
return migrations, err return migrations, err

View File

@ -226,7 +226,7 @@ func (p *SQLProvider) schemaMigratePre1To1TOTP(ctx context.Context) (err error)
} }
for _, config := range totpConfigs { for _, config := range totpConfigs {
_, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1InsertTOTPConfiguration), tableTOTPConfigurations), config.Username, config.Secret) _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1To1InsertTOTPConfiguration), tableTOTPConfigurations), config.Username, p.config.TOTP.Issuer, p.config.TOTP.Period, config.Secret)
if err != nil { if err != nil {
return err return err
} }
@ -414,7 +414,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1TOTP(ctx context.Context) (err error)
} }
for _, config := range totpConfigs { for _, config := range totpConfigs {
_, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1InsertTOTPConfiguration), tablePre1TOTPSecrets), config.Username, config.Secret) _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmt1ToPre1InsertTOTPConfiguration), tablePre1TOTPSecrets), config.Username, config.Secret)
if err != nil { if err != nil {
return err return err
} }

View File

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

View File

@ -0,0 +1,9 @@
---
version: '3'
services:
authelia-backend:
volumes:
- './MariaDB/configuration.yml:/config/configuration.yml:ro'
- './MariaDB/users.yml:/config/users.yml'
- './common/ssl:/config/ssl:ro'
...

View File

@ -1,9 +0,0 @@
---
version: '3'
services:
authelia-backend:
volumes:
- './Mariadb/configuration.yml:/config/configuration.yml:ro'
- './Mariadb/users.yml:/config/users.yml'
- './common/ssl:/config/ssl:ro'
...

View File

@ -30,7 +30,7 @@ func (rs *RodSession) doRegisterTOTP(t *testing.T, page *rod.Page) string {
func (rs *RodSession) doEnterOTP(t *testing.T, page *rod.Page, code string) { func (rs *RodSession) doEnterOTP(t *testing.T, page *rod.Page, code string) {
inputs := rs.WaitElementsLocatedByCSSSelector(t, page, "otp-input input") inputs := rs.WaitElementsLocatedByCSSSelector(t, page, "otp-input input")
for i := 0; i < 6; i++ { for i := 0; i < len(code); i++ {
_ = inputs[i].Input(string(code[i])) _ = inputs[i].Input(string(code[i]))
} }
} }

View File

@ -3,6 +3,8 @@ package suites
import ( import (
"fmt" "fmt"
"os" "os"
"github.com/authelia/authelia/v4/internal/configuration/schema"
) )
// BaseDomain the base domain. // BaseDomain the base domain.
@ -55,3 +57,18 @@ const (
testUsername = "john" testUsername = "john"
testPassword = "password" testPassword = "password"
) )
var (
storageLocalTmpConfig = schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Issuer: "Authelia",
Period: 6,
},
Storage: schema.StorageConfiguration{
EncryptionKey: "a_not_so_secure_encryption_key",
Local: &schema.LocalStorageConfiguration{
Path: "/tmp/db.sqlite3",
},
},
}
)

View File

@ -5,10 +5,9 @@ import (
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"strconv"
"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/models"
@ -178,6 +177,8 @@ func (s *CLISuite) TestStorageShouldShowErrWithoutConfig() {
} }
func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() { func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() {
_ = os.Remove("/tmp/db.sqlite3")
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"}) output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err) s.Assert().NoError(err)
@ -187,7 +188,7 @@ func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() {
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`) 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"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--config", "/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1") s.Assert().EqualError(err, "exit status 1")
s.Assert().Regexp(patternOutdated, output) s.Assert().Regexp(patternOutdated, output)
@ -267,16 +268,14 @@ func (s *CLISuite) TestStorage02ShouldShowSchemaInfo() {
} }
func (s *CLISuite) TestStorage03ShouldExportTOTP() { func (s *CLISuite) TestStorage03ShouldExportTOTP() {
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_cli_encryption_key_which_isnt_secure") storageProvider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
s.Require().NoError(provider.StartupCheck()) s.Require().NoError(storageProvider.StartupCheck())
ctx := context.Background() ctx := context.Background()
var ( var (
err error err error
key *otp.Key
config models.TOTPConfiguration
) )
var ( var (
@ -287,39 +286,53 @@ func (s *CLISuite) TestStorage03ShouldExportTOTP() {
expectedLinesCSV = append(expectedLinesCSV, "issuer,username,algorithm,digits,period,secret") expectedLinesCSV = append(expectedLinesCSV, "issuer,username,algorithm,digits,period,secret")
for _, name := range []string{"john", "mary", "fred"} { configs := []*models.TOTPConfiguration{
key, err = totp.Generate(totp.GenerateOpts{ {
Issuer: "Authelia", Username: "john",
AccountName: name, Period: 30,
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, Digits: 6,
Secret: []byte(key.Secret()), Algorithm: "SHA1",
Period: key.Period(), },
{
Username: "mary",
Period: 45,
Digits: 6,
Algorithm: "SHA1",
},
{
Username: "fred",
Period: 30,
Digits: 8,
Algorithm: "SHA1",
},
{
Username: "jone",
Period: 30,
Digits: 6,
Algorithm: "SHA512",
},
} }
for _, config := range configs {
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "generate", config.Username, "--period", strconv.Itoa(int(config.Period)), "--algorithm", config.Algorithm, "--digits", strconv.Itoa(int(config.Digits)), "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err)
config, err = storageProvider.LoadTOTPConfiguration(ctx, config.Username)
s.Assert().NoError(err)
s.Assert().Contains(output, config.URI())
expectedLinesCSV = append(expectedLinesCSV, fmt.Sprintf("%s,%s,%s,%d,%d,%s", "Authelia", config.Username, config.Algorithm, config.Digits, config.Period, string(config.Secret))) 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)) expectedLines = append(expectedLines, config.URI())
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"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format", "uri", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err) s.Assert().NoError(err)
for _, expectedLine := range expectedLines { for _, expectedLine := range expectedLines {
s.Assert().Contains(output, expectedLine) 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"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format", "csv", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err) s.Assert().NoError(err)
for _, expectedLine := range expectedLinesCSV { for _, expectedLine := range expectedLinesCSV {
@ -347,7 +360,7 @@ func (s *CLISuite) TestStorage04ShouldChangeEncryptionKey() {
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--config", "/config/configuration.storage.yml"}) 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().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") s.Assert().Contains(output, "Encryption key validation: failed.\n\nError: the encryption key is not valid against the schema check value, 4 of 4 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"}) 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().NoError(err)

View File

@ -57,7 +57,7 @@ func (s *DuoPushWebDriverSuite) TearDownTest() {
}() }()
// Set default 2FA preference and clean up any Duo device already in DB. // Set default 2FA preference and clean up any Duo device already in DB.
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferred2FAMethod(ctx, "john", "totp")) require.NoError(s.T(), provider.SavePreferred2FAMethod(ctx, "john", "totp"))
require.NoError(s.T(), provider.DeletePreferredDuoDevice(ctx, "john")) require.NoError(s.T(), provider.DeletePreferredDuoDevice(ctx, "john"))
} }
@ -152,7 +152,7 @@ func (s *DuoPushWebDriverSuite) TestShouldSelectDevice() {
defer cancel() defer cancel()
// Set default 2FA preference to enable Select Device link in frontend. // Set default 2FA preference to enable Select Device link in frontend.
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "ABCDEFGHIJ1234567890", Method: "push"})) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "ABCDEFGHIJ1234567890", Method: "push"}))
var PreAuthAPIResponse = duo.PreAuthResponse{ var PreAuthAPIResponse = duo.PreAuthResponse{
@ -231,7 +231,7 @@ func (s *DuoPushWebDriverSuite) TestShouldSelectNewDeviceAfterSavedDeviceMethodI
} }
// Setup unsupported Duo device in DB. // Setup unsupported Duo device in DB.
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "ABCDEFGHIJ1234567890", Method: "sms"})) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "ABCDEFGHIJ1234567890", Method: "sms"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Allow) ConfigureDuo(s.T(), Allow)
@ -257,7 +257,7 @@ func (s *DuoPushWebDriverSuite) TestShouldAutoSelectNewDeviceAfterSavedDeviceIsN
} }
// Setup unsupported Duo device in DB. // Setup unsupported Duo device in DB.
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "ABCDEFGHIJ1234567890", Method: "push"})) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "ABCDEFGHIJ1234567890", Method: "push"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Allow) ConfigureDuo(s.T(), Allow)
@ -276,7 +276,7 @@ func (s *DuoPushWebDriverSuite) TestShouldFailSelectionBecauseOfSelectionBypasse
StatusMessage: "Allowing unknown user", StatusMessage: "Allowing unknown user",
} }
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Deny) ConfigureDuo(s.T(), Deny)
@ -296,7 +296,7 @@ func (s *DuoPushWebDriverSuite) TestShouldFailSelectionBecauseOfSelectionDenied(
StatusMessage: "We're sorry, access is not allowed.", StatusMessage: "We're sorry, access is not allowed.",
} }
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Deny) ConfigureDuo(s.T(), Deny)
@ -317,7 +317,7 @@ func (s *DuoPushWebDriverSuite) TestShouldFailAuthenticationBecausePreauthDenied
StatusMessage: "We're sorry, access is not allowed.", StatusMessage: "We're sorry, access is not allowed.",
} }
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
@ -344,7 +344,7 @@ func (s *DuoPushWebDriverSuite) TestShouldSucceedAuthentication() {
} }
// Setup Duo device in DB. // Setup Duo device in DB.
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Allow) ConfigureDuo(s.T(), Allow)
@ -371,7 +371,7 @@ func (s *DuoPushWebDriverSuite) TestShouldFailAuthentication() {
} }
// Setup Duo device in DB. // Setup Duo device in DB.
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Deny) ConfigureDuo(s.T(), Deny)
@ -430,7 +430,7 @@ func (s *DuoPushDefaultRedirectionSuite) TestUserIsRedirectedToDefaultURL() {
} }
// Setup Duo device in DB. // Setup Duo device in DB.
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Allow) ConfigureDuo(s.T(), Allow)
@ -475,7 +475,7 @@ func (s *DuoPushSuite) TestUserPreferencesScenario() {
ctx := context.Background() ctx := context.Background()
// Setup Duo device in DB. // Setup Duo device in DB.
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Allow) ConfigureDuo(s.T(), Allow)

View File

@ -5,12 +5,12 @@ import (
"time" "time"
) )
var mariadbSuiteName = "Mariadb" var mariadbSuiteName = "MariaDB"
func init() { func init() {
dockerEnvironment := NewDockerEnvironment([]string{ dockerEnvironment := NewDockerEnvironment([]string{
"internal/suites/docker-compose.yml", "internal/suites/docker-compose.yml",
"internal/suites/Mariadb/docker-compose.yml", "internal/suites/MariaDB/docker-compose.yml",
"internal/suites/example/compose/authelia/docker-compose.backend.{}.yml", "internal/suites/example/compose/authelia/docker-compose.backend.{}.yml",
"internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml", "internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml",
"internal/suites/example/compose/nginx/backend/docker-compose.yml", "internal/suites/example/compose/nginx/backend/docker-compose.yml",

View File

@ -6,26 +6,26 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
type MariadbSuite struct { type MariaDBSuite struct {
*RodSuite *RodSuite
} }
func NewMariadbSuite() *MariadbSuite { func NewMariaDBSuite() *MariaDBSuite {
return &MariadbSuite{RodSuite: new(RodSuite)} return &MariaDBSuite{RodSuite: new(RodSuite)}
} }
func (s *MariadbSuite) TestOneFactorScenario() { func (s *MariaDBSuite) TestOneFactorScenario() {
suite.Run(s.T(), NewOneFactorScenario()) suite.Run(s.T(), NewOneFactorScenario())
} }
func (s *MariadbSuite) TestTwoFactorScenario() { func (s *MariaDBSuite) TestTwoFactorScenario() {
suite.Run(s.T(), NewTwoFactorScenario()) suite.Run(s.T(), NewTwoFactorScenario())
} }
func TestMariadbSuite(t *testing.T) { func TestMariaDBSuite(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")
} }
suite.Run(t, NewMariadbSuite()) suite.Run(t, NewMariaDBSuite())
} }

View File

@ -121,7 +121,7 @@ func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice()
password := "password" password := "password"
// Clean up any TOTP secret already in DB. // Clean up any TOTP secret already in DB.
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key") provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
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))

View File

@ -0,0 +1,20 @@
package totp
import (
"github.com/pquerna/otp"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
func otpStringToAlgo(in string) (algorithm otp.Algorithm) {
switch in {
case schema.TOTPAlgorithmSHA1:
return otp.AlgorithmSHA1
case schema.TOTPAlgorithmSHA256:
return otp.AlgorithmSHA256
case schema.TOTPAlgorithmSHA512:
return otp.AlgorithmSHA512
default:
return otp.AlgorithmSHA1
}
}

View File

@ -0,0 +1,12 @@
package totp
import (
"github.com/authelia/authelia/v4/internal/models"
)
// Provider for TOTP functionality.
type Provider interface {
Generate(username string) (config *models.TOTPConfiguration, err error)
GenerateCustom(username, algorithm string, digits, period, secretSize uint) (config *models.TOTPConfiguration, err error)
Validate(token string, config *models.TOTPConfiguration) (valid bool, err error)
}

View File

@ -0,0 +1,78 @@
package totp
import (
"time"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/models"
)
// NewTimeBasedProvider creates a new totp.TimeBased which implements the totp.Provider.
func NewTimeBasedProvider(config *schema.TOTPConfiguration) (provider *TimeBased) {
provider = &TimeBased{
config: config,
}
if config.Skew != nil {
provider.skew = *config.Skew
} else {
provider.skew = 1
}
return provider
}
// TimeBased totp.Provider for production use.
type TimeBased struct {
config *schema.TOTPConfiguration
skew uint
}
// GenerateCustom generates a TOTP with custom options.
func (p TimeBased) GenerateCustom(username, algorithm string, digits, period, secretSize uint) (config *models.TOTPConfiguration, err error) {
var key *otp.Key
opts := totp.GenerateOpts{
Issuer: p.config.Issuer,
AccountName: username,
Period: period,
SecretSize: secretSize,
Digits: otp.Digits(digits),
Algorithm: otpStringToAlgo(algorithm),
}
if key, err = totp.Generate(opts); err != nil {
return nil, err
}
config = &models.TOTPConfiguration{
Username: username,
Issuer: p.config.Issuer,
Algorithm: algorithm,
Digits: digits,
Secret: []byte(key.Secret()),
Period: period,
}
return config, nil
}
// Generate generates a TOTP with default options.
func (p TimeBased) Generate(username string) (config *models.TOTPConfiguration, err error) {
return p.GenerateCustom(username, p.config.Algorithm, p.config.Digits, p.config.Period, 32)
}
// Validate the token against the given configuration.
func (p TimeBased) Validate(token string, config *models.TOTPConfiguration) (valid bool, err error) {
opts := totp.ValidateOpts{
Period: config.Period,
Skew: p.skew,
Digits: otp.Digits(config.Digits),
Algorithm: otpStringToAlgo(config.Algorithm),
}
return totp.ValidateCustom(token, string(config.Secret), time.Now().UTC(), opts)
}

View File

@ -45,6 +45,7 @@ module.exports = {
"storage", "storage",
"suites", "suites",
"templates", "templates",
"totp",
"utils", "utils",
"web", "web",
], ],

View File

@ -1,6 +1,6 @@
import { useRemoteCall } from "@hooks/RemoteCall"; import { useRemoteCall } from "@hooks/RemoteCall";
import { getUserPreferences } from "@services/UserPreferences"; import { getUserInfo } from "@services/UserInfo";
export function useUserPreferences() { export function useUserInfo() {
return useRemoteCall(getUserPreferences, []); return useRemoteCall(getUserInfo, []);
} }

View File

@ -0,0 +1,6 @@
import { useRemoteCall } from "@hooks/RemoteCall";
import { getUserInfoTOTPConfiguration } from "@services/UserInfoTOTPConfiguration";
export function useUserInfoTOTPConfiguration() {
return useRemoteCall(getUserInfoTOTPConfiguration, []);
}

View File

@ -3,5 +3,4 @@ import { SecondFactorMethod } from "@models/Methods";
export interface Configuration { export interface Configuration {
available_methods: Set<SecondFactorMethod>; available_methods: Set<SecondFactorMethod>;
second_factor_enabled: boolean; second_factor_enabled: boolean;
totp_period: number;
} }

View File

@ -0,0 +1,4 @@
export interface UserInfoTOTPConfiguration {
period: number;
digits: number;
}

View File

@ -34,6 +34,7 @@ export const LogoutPath = basePath + "/api/logout";
export const StatePath = basePath + "/api/state"; export const StatePath = basePath + "/api/state";
export const UserInfoPath = basePath + "/api/user/info"; export const UserInfoPath = basePath + "/api/user/info";
export const UserInfo2FAMethodPath = basePath + "/api/user/info/2fa_method"; export const UserInfo2FAMethodPath = basePath + "/api/user/info/2fa_method";
export const UserInfoTOTPConfigurationPath = basePath + "/api/user/info/totp";
export const ConfigurationPath = basePath + "/api/configuration"; export const ConfigurationPath = basePath + "/api/configuration";

View File

@ -1,12 +1,11 @@
import { Configuration } from "@models/Configuration"; import { Configuration } from "@models/Configuration";
import { ConfigurationPath } from "@services/Api"; import { ConfigurationPath } from "@services/Api";
import { Get } from "@services/Client"; import { Get } from "@services/Client";
import { toEnum, Method2FA } from "@services/UserPreferences"; import { toEnum, Method2FA } from "@services/UserInfo";
interface ConfigurationPayload { interface ConfigurationPayload {
available_methods: Method2FA[]; available_methods: Method2FA[];
second_factor_enabled: boolean; second_factor_enabled: boolean;
totp_period: number;
} }
export async function getConfiguration(): Promise<Configuration> { export async function getConfiguration(): Promise<Configuration> {

View File

@ -39,7 +39,7 @@ export function toString(method: SecondFactorMethod): Method2FA {
} }
} }
export async function getUserPreferences(): Promise<UserInfo> { export async function getUserInfo(): Promise<UserInfo> {
const res = await Get<UserInfoPayload>(UserInfoPath); const res = await Get<UserInfoPayload>(UserInfoPath);
return { ...res, method: toEnum(res.method) }; return { ...res, method: toEnum(res.method) };
} }

View File

@ -0,0 +1,15 @@
import { UserInfoTOTPConfiguration } from "@models/UserInfoTOTPConfiguration";
import { UserInfoTOTPConfigurationPath } from "@services/Api";
import { Get } from "@services/Client";
export type TOTPDigits = 6 | 8;
export interface UserInfoTOTPConfigurationPayload {
period: number;
digits: TOTPDigits;
}
export async function getUserInfoTOTPConfiguration(): Promise<UserInfoTOTPConfiguration> {
const res = await Get<UserInfoTOTPConfigurationPayload>(UserInfoTOTPConfigurationPath);
return { ...res };
}

View File

@ -16,7 +16,7 @@ import { useRedirectionURL } from "@hooks/RedirectionURL";
import { useRedirector } from "@hooks/Redirector"; import { useRedirector } from "@hooks/Redirector";
import { useRequestMethod } from "@hooks/RequestMethod"; import { useRequestMethod } from "@hooks/RequestMethod";
import { useAutheliaState } from "@hooks/State"; import { useAutheliaState } from "@hooks/State";
import { useUserPreferences as userUserInfo } from "@hooks/UserInfo"; import { useUserInfo } from "@hooks/UserInfo";
import { SecondFactorMethod } from "@models/Methods"; import { SecondFactorMethod } from "@models/Methods";
import { checkSafeRedirection } from "@services/SafeRedirection"; import { checkSafeRedirection } from "@services/SafeRedirection";
import { AuthenticationLevel } from "@services/State"; import { AuthenticationLevel } from "@services/State";
@ -44,7 +44,7 @@ const LoginPortal = function (props: Props) {
const redirector = useRedirector(); const redirector = useRedirector();
const [state, fetchState, , fetchStateError] = useAutheliaState(); const [state, fetchState, , fetchStateError] = useAutheliaState();
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo(); const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfo();
const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration(); const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration();
const redirect = useCallback((url: string) => navigate(url), [navigate]); const redirect = useCallback((url: string) => navigate(url), [navigate]);

View File

@ -5,7 +5,7 @@ import classnames from "classnames";
interface IconWithContextProps { interface IconWithContextProps {
icon: ReactNode; icon: ReactNode;
context: ReactNode; children: ReactNode;
className?: string; className?: string;
} }
@ -33,7 +33,7 @@ const IconWithContext = function (props: IconWithContextProps) {
<div className={style.iconContainer}> <div className={style.iconContainer}>
<div className={style.icon}>{props.icon}</div> <div className={style.icon}>{props.icon}</div>
</div> </div>
<div className={style.context}>{props.context}</div> <div className={style.context}>{props.children}</div>
</div> </div>
); );
}; };

View File

@ -12,6 +12,8 @@ import { State } from "@views/LoginPortal/SecondFactor/OneTimePasswordMethod";
export interface Props { export interface Props {
passcode: string; passcode: string;
state: State; state: State;
digits: number;
period: number; period: number;
onChange: (passcode: string) => void; onChange: (passcode: string) => void;
@ -19,22 +21,23 @@ export interface Props {
const OTPDial = function (props: Props) { const OTPDial = function (props: Props) {
const style = useStyles(); const style = useStyles();
const dial = (
return (
<IconWithContext icon={<Icon state={props.state} period={props.period} />}>
<span className={style.otpInput} id="otp-input"> <span className={style.otpInput} id="otp-input">
<OtpInput <OtpInput
shouldAutoFocus shouldAutoFocus
onChange={props.onChange} onChange={props.onChange}
value={props.passcode} value={props.passcode}
numInputs={6} numInputs={props.digits}
isDisabled={props.state === State.InProgress || props.state === State.Success} isDisabled={props.state === State.InProgress || props.state === State.Success}
isInputNum isInputNum
hasErrored={props.state === State.Failure} hasErrored={props.state === State.Failure}
inputStyle={classnames(style.otpDigitInput, props.state === State.Failure ? style.inputError : "")} inputStyle={classnames(style.otpDigitInput, props.state === State.Failure ? style.inputError : "")}
/> />
</span> </span>
</IconWithContext>
); );
return <IconWithContext icon={<Icon state={props.state} period={props.period} />} context={dial} />;
}; };
export default OTPDial; export default OTPDial;

View File

@ -1,8 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { useRedirectionURL } from "@hooks/RedirectionURL";
import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration";
import { completeTOTPSignIn } from "@services/OneTimePassword"; import { completeTOTPSignIn } from "@services/OneTimePassword";
import { AuthenticationLevel } from "@services/State"; import { AuthenticationLevel } from "@services/State";
import LoadingPage from "@views/LoadingPage/LoadingPage";
import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer"; import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer";
import OTPDial from "@views/LoginPortal/SecondFactor/OTPDial"; import OTPDial from "@views/LoginPortal/SecondFactor/OTPDial";
@ -17,7 +19,6 @@ export interface Props {
id: string; id: string;
authenticationLevel: AuthenticationLevel; authenticationLevel: AuthenticationLevel;
registered: boolean; registered: boolean;
totp_period: number;
onRegisterClick: () => void; onRegisterClick: () => void;
onSignInError: (err: Error) => void; onSignInError: (err: Error) => void;
@ -35,6 +36,20 @@ const OneTimePasswordMethod = function (props: Props) {
const onSignInErrorCallback = useRef(onSignInError).current; const onSignInErrorCallback = useRef(onSignInError).current;
const onSignInSuccessCallback = useRef(onSignInSuccess).current; const onSignInSuccessCallback = useRef(onSignInSuccess).current;
const [resp, fetch, , err] = useUserInfoTOTPConfiguration();
useEffect(() => {
if (err) {
console.error(err);
onSignInErrorCallback(new Error("Could not obtain user settings"));
setState(State.Failure);
}
}, [onSignInErrorCallback, err]);
useEffect(() => {
fetch();
}, [fetch]);
const signInFunc = useCallback(async () => { const signInFunc = useCallback(async () => {
if (!props.registered || props.authenticationLevel === AuthenticationLevel.TwoFactor) { if (!props.registered || props.authenticationLevel === AuthenticationLevel.TwoFactor) {
return; return;
@ -42,7 +57,7 @@ const OneTimePasswordMethod = function (props: Props) {
const passcodeStr = `${passcode}`; const passcodeStr = `${passcode}`;
if (!passcode || passcodeStr.length !== 6) { if (!passcode || passcodeStr.length !== (resp?.digits || 6)) {
return; return;
} }
@ -62,6 +77,7 @@ const OneTimePasswordMethod = function (props: Props) {
onSignInSuccessCallback, onSignInSuccessCallback,
passcode, passcode,
redirectionURL, redirectionURL,
resp,
props.authenticationLevel, props.authenticationLevel,
props.registered, props.registered,
]); ]);
@ -94,7 +110,19 @@ const OneTimePasswordMethod = function (props: Props) {
state={methodState} state={methodState}
onRegisterClick={props.onRegisterClick} onRegisterClick={props.onRegisterClick}
> >
<OTPDial passcode={passcode} onChange={setPasscode} state={state} period={props.totp_period} /> <div>
{resp !== undefined || err !== undefined ? (
<OTPDial
passcode={passcode}
period={resp?.period || 30}
digits={resp?.digits || 6}
onChange={setPasscode}
state={state}
/>
) : (
<LoadingPage />
)}
</div>
</MethodContainer> </MethodContainer>
); );
}; };

View File

@ -17,7 +17,7 @@ import { SecondFactorMethod } from "@models/Methods";
import { UserInfo } from "@models/UserInfo"; import { UserInfo } from "@models/UserInfo";
import { initiateTOTPRegistrationProcess, initiateU2FRegistrationProcess } from "@services/RegisterDevice"; import { initiateTOTPRegistrationProcess, initiateU2FRegistrationProcess } from "@services/RegisterDevice";
import { AuthenticationLevel } from "@services/State"; import { AuthenticationLevel } from "@services/State";
import { setPreferred2FAMethod } from "@services/UserPreferences"; import { setPreferred2FAMethod } from "@services/UserInfo";
import MethodSelectionDialog from "@views/LoginPortal/SecondFactor/MethodSelectionDialog"; import MethodSelectionDialog from "@views/LoginPortal/SecondFactor/MethodSelectionDialog";
import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswordMethod"; import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswordMethod";
import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod"; import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod";
@ -116,7 +116,6 @@ const SecondFactorForm = function (props: Props) {
authenticationLevel={props.authenticationLevel} authenticationLevel={props.authenticationLevel}
// Whether the user has a TOTP secret registered already // Whether the user has a TOTP secret registered already
registered={props.userInfo.has_totp} registered={props.userInfo.has_totp}
totp_period={props.configuration.totp_period}
onRegisterClick={initiateRegistration(initiateTOTPRegistrationProcess)} onRegisterClick={initiateRegistration(initiateTOTPRegistrationProcess)}
onSignInError={(err) => createErrorNotification(err.message)} onSignInError={(err) => createErrorNotification(err.message)}
onSignInSuccess={props.onAuthenticationSuccess} onSignInSuccess={props.onAuthenticationSuccess}

View File

@ -143,21 +143,18 @@ function Icon(props: IconProps) {
const touch = ( const touch = (
<IconWithContext <IconWithContext
icon={<FingerTouchIcon size={64} animated strong />} icon={<FingerTouchIcon size={64} animated strong />}
context={<LinearProgressBar value={props.timer} style={progressBarStyle} height={theme.spacing(2)} />}
className={state === State.WaitTouch ? undefined : "hidden"} className={state === State.WaitTouch ? undefined : "hidden"}
/> >
<LinearProgressBar value={props.timer} style={progressBarStyle} height={theme.spacing(2)} />
</IconWithContext>
); );
const failure = ( const failure = (
<IconWithContext <IconWithContext icon={<FailureIcon />} className={state === State.Failure ? undefined : "hidden"}>
icon={<FailureIcon />}
context={
<Button color="secondary" onClick={props.onRetryClick}> <Button color="secondary" onClick={props.onRetryClick}>
Retry Retry
</Button> </Button>
} </IconWithContext>
className={state === State.Failure ? undefined : "hidden"}
/>
); );
return ( return (