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.
This commit is contained in:
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
security:
- 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:
post:
tags:
@ -640,9 +659,6 @@ components:
second_factor_enabled:
type: boolean
description: If second factor is enabled.
totp_period:
type: integer
example: 30
handlers.DuoDeviceBody:
required:
- device
@ -841,6 +857,25 @@ components:
has_duo:
type: boolean
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:
required:
- method

View File

@ -86,7 +86,7 @@ func buildFrontend(branch string) {
}
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")
err := cmd.Run()

View File

@ -90,16 +90,28 @@ log:
##
## Parameters used for TOTP generation.
totp:
## 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
## The issuer name displayed in the Authenticator application of your choice.
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
## 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.
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

View File

@ -7,7 +7,7 @@ nav_order: 16
# 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
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
totp:
issuer: authelia.com
algorithm: sha1
digits: 6
period: 30
skew: 1
```
@ -37,17 +39,56 @@ differentiate applications registered by the user.
Authelia allows customisation of the issuer to differentiate the entry created
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
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.
_**Important Note:** Many 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 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.
The algorithm used for the TOTP key.
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
<div markdown="1">
@ -59,10 +100,13 @@ required: no
{: .label .label-config .label-green }
</div>
Configures the period of time in seconds a one-time password is current for. It is important
to note that changing this value will require your users to register their application again.
The period of time in seconds between key rotations or the time element of TOTP. Please see the
[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
<div markdown="1">
@ -74,15 +118,34 @@ required: no
{: .label .label-config .label-green }
</div>
Configures the number of one-time passwords either side of the current one that are
considered valid, each time you increase this it makes two more one-time passwords valid.
For example the default of 1 has a total of 3 keys valid. A value of 2 has 5 one-time passwords
valid.
The number of one time passwords either side of the current valid one time password that should also be considered 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
this would result in 90 and 150 seconds of valid one time passwords respectively. Please see the
[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
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
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
a time synchronization issue on the server being an issue. There is however no effective and reliable way to check the
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
* suites
* templates
* totp
* utils
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/session"
"github.com/authelia/authelia/v4/internal/storage"
"github.com/authelia/authelia/v4/internal/totp"
"github.com/authelia/authelia/v4/internal/utils"
)
func getStorageProvider() (provider storage.Provider) {
switch {
case config.Storage.PostgreSQL != nil:
return storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL, config.Storage.EncryptionKey)
return storage.NewPostgreSQLProvider(config)
case config.Storage.MySQL != nil:
return storage.NewMySQLProvider(*config.Storage.MySQL, config.Storage.EncryptionKey)
return storage.NewMySQLProvider(config)
case config.Storage.Local != nil:
return storage.NewSQLiteProvider(config.Storage.Local.Path, config.Storage.EncryptionKey)
return storage.NewSQLiteProvider(config)
default:
return nil
}
@ -71,6 +72,8 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [
errors = append(errors, err)
}
totpProvider := totp.NewTimeBasedProvider(config.TOTP)
return middlewares.Providers{
Authorizer: authorizer,
UserProvider: userProvider,
@ -80,5 +83,6 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [
NTP: ntpProvider,
Notifier: notifier,
SessionProvider: sessionProvider,
TOTP: totpProvider,
}, warnings, errors
}

View File

@ -35,7 +35,7 @@ func NewStorageCmd() (cmd *cobra.Command) {
newStorageMigrateCmd(),
newStorageSchemaInfoCmd(),
newStorageEncryptionCmd(),
newStorageExportCmd(),
newStorageTOTPCmd(),
)
return cmd
@ -79,25 +79,57 @@ func newStorageEncryptionChangeKeyCmd() (cmd *cobra.Command) {
return cmd
}
func newStorageExportCmd() (cmd *cobra.Command) {
func newStorageTOTPCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "export",
Short: "Performs exports",
Use: "totp",
Short: "Manage TOTP configurations",
}
cmd.AddCommand(newStorageExportTOTPConfigurationsCmd())
cmd.AddCommand(
newStorageTOTPGenerateCmd(),
newStorageTOTPDeleteCmd(),
newStorageTOTPExportCmd(),
)
return cmd
}
func newStorageExportTOTPConfigurationsCmd() (cmd *cobra.Command) {
func newStorageTOTPGenerateCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "totp-configurations",
Short: "Performs exports of the totp configurations",
RunE: storageExportTOTPConfigurationsRunE,
Use: "generate username",
Short: "Generate a TOTP configuration for a user",
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
}

View File

@ -14,6 +14,7 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/validator"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/storage"
"github.com/authelia/authelia/v4/internal/totp"
)
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.password": "storage.postgres.password",
"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))
@ -62,7 +67,7 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
config = &schema.Configuration{}
_, err = configuration.LoadAdvanced(val, "storage", &config.Storage, sources...)
_, err = configuration.LoadAdvanced(val, "", &config, sources...)
if err != nil {
return err
}
@ -84,6 +89,8 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
validator.ValidateStorage(config.Storage, val)
validator.ValidateTOTP(config, val)
if val.HasErrors() {
var finalErr error
@ -109,9 +116,6 @@ func storageSchemaEncryptionCheckRunE(cmd *cobra.Command, args []string) (err er
)
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
@ -145,9 +149,6 @@ func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (er
)
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
@ -188,16 +189,83 @@ func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (er
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 (
provider storage.Provider
ctx = context.Background()
)
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
@ -236,9 +304,9 @@ func storageExportTOTPConfigurationsRunE(cmd *cobra.Command, args []string) (err
for _, c := range configurations {
switch format {
case storageExportFormatCSV:
fmt.Printf("%s,%s,%s,%d,%d,%s\n", "Authelia", c.Username, c.Algorithm, c.Digits, c.Period, string(c.Secret))
fmt.Printf("%s,%s,%s,%d,%d,%s\n", c.Issuer, c.Username, c.Algorithm, c.Digits, c.Period, string(c.Secret))
case storageExportFormatURI:
fmt.Printf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d\n", "Authelia", c.Username, string(c.Secret), "Authelia", c.Algorithm, c.Digits, c.Period)
fmt.Println(c.URI())
}
}
@ -298,14 +366,11 @@ func newStorageMigrateListRunE(up bool) func(cmd *cobra.Command, args []string)
var (
provider storage.Provider
ctx = context.Background()
migrations []storage.SchemaMigration
migrations []models.SchemaMigration
directionStr string
)
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
@ -345,9 +410,6 @@ func newStorageMigrationRunE(up bool) func(cmd *cobra.Command, args []string) (e
)
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()
@ -420,9 +482,6 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
)
provider = getStorageProvider()
if provider == nil {
return errNoStorageProvider
}
defer func() {
_ = provider.Close()

View File

@ -90,16 +90,28 @@ log:
##
## Parameters used for TOTP generation.
totp:
## 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
## The issuer name displayed in the Authenticator application of your choice.
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
## 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.
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

View File

@ -23,3 +23,15 @@ const LDAPImplementationCustom = "custom"
// LDAPImplementationActiveDirectory is the string for the Active Directory LDAP implementation.
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.
type TOTPConfiguration struct {
Issuer string `koanf:"issuer"`
Period int `koanf:"period"`
Skew *int `koanf:"skew"`
Algorithm string `koanf:"algorithm"`
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.
var DefaultTOTPConfiguration = TOTPConfiguration{
Issuer: "Authelia",
Algorithm: TOTPAlgorithmSHA1,
Digits: 6,
Period: 30,
Skew: &defaultOtpSkew,
}

View File

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

View File

@ -54,6 +54,13 @@ const (
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.
const (
errFmtOIDCClientsDuplicateID = "openid connect provider: one or more clients have the same ID"
@ -157,6 +164,8 @@ var ValidKeys = []string{
// TOTP Keys.
"totp.issuer",
"totp.algorithm",
"totp.digits",
"totp.period",
"totp.skew",

View File

@ -2,25 +2,47 @@ package validator
import (
"fmt"
"strings"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
)
// ValidateTOTP validates and update TOTP configuration.
func ValidateTOTP(configuration *schema.TOTPConfiguration, validator *schema.StructValidator) {
if configuration.Issuer == "" {
configuration.Issuer = schema.DefaultTOTPConfiguration.Issuer
func ValidateTOTP(configuration *schema.Configuration, validator *schema.StructValidator) {
if configuration.TOTP == nil {
configuration.TOTP = &schema.DefaultTOTPConfiguration
return
}
if configuration.Period == 0 {
configuration.Period = schema.DefaultTOTPConfiguration.Period
} else if configuration.Period < 0 {
validator.Push(fmt.Errorf("TOTP Period must be 1 or more"))
if configuration.TOTP.Issuer == "" {
configuration.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer
}
if configuration.Skew == nil {
configuration.Skew = schema.DefaultTOTPConfiguration.Skew
} else if *configuration.Skew < 0 {
validator.Push(fmt.Errorf("TOTP Skew must be 0 or more"))
if configuration.TOTP.Algorithm == "" {
configuration.TOTP.Algorithm = schema.DefaultTOTPConfiguration.Algorithm
} else {
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
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -11,26 +13,61 @@ import (
func TestShouldSetDefaultTOTPValues(t *testing.T) {
validator := schema.NewStructValidator()
config := schema.TOTPConfiguration{}
config := &schema.Configuration{
TOTP: &schema.TOTPConfiguration{},
}
ValidateTOTP(&config, validator)
ValidateTOTP(config, validator)
require.Len(t, validator.Errors(), 0)
assert.Equal(t, "Authelia", config.Issuer)
assert.Equal(t, *schema.DefaultTOTPConfiguration.Skew, *config.Skew)
assert.Equal(t, schema.DefaultTOTPConfiguration.Period, config.Period)
assert.Equal(t, "Authelia", config.TOTP.Issuer)
assert.Equal(t, schema.DefaultTOTPConfiguration.Algorithm, config.TOTP.Algorithm)
assert.Equal(t, schema.DefaultTOTPConfiguration.Skew, config.TOTP.Skew)
assert.Equal(t, schema.DefaultTOTPConfiguration.Period, config.TOTP.Period)
}
func TestShouldRaiseErrorWhenInvalidTOTPMinimumValues(t *testing.T) {
var badSkew = -1
func TestShouldNormalizeTOTPAlgorithm(t *testing.T) {
validator := schema.NewStructValidator()
config := schema.TOTPConfiguration{
Period: -5,
Skew: &badSkew,
config := &schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Algorithm: "sha1",
},
}
ValidateTOTP(&config, validator)
assert.Len(t, validator.Errors(), 2)
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")
ValidateTOTP(config, validator)
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"
)
const (
totpAlgoSHA1 = "SHA1"
totpAlgoSHA256 = "SHA256"
totpAlgoSHA512 = "SHA512"
)
const (
accept = "accept"
reject = "reject"

View File

@ -5,18 +5,10 @@ import (
"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.
func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
body := ConfigurationBody{}
body := configurationBody{}
body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
body.TOTPPeriod = ctx.Configuration.TOTP.Period
if ctx.Configuration.DuoAPI != nil {
body.AvailableMethods = append(body.AvailableMethods, authentication.Push)

View File

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

View File

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

View File

@ -128,7 +128,7 @@ func (s *RegisterDuoDeviceSuite) TestShouldRespondWithDeny() {
func (s *RegisterDuoDeviceSuite) TestShouldRespondOK() {
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"})).
Return(nil)

View File

@ -3,9 +3,6 @@ package handlers
import (
"fmt"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/session"
@ -39,39 +36,24 @@ var SecondFactorTOTPIdentityStart = middlewares.IdentityVerificationStart(middle
})
func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
algorithm := otp.AlgorithmSHA1
var (
config *models.TOTPConfiguration
err error
)
key, err := totp.Generate(totp.GenerateOpts{
Issuer: ctx.Configuration.TOTP.Issuer,
AccountName: username,
Period: uint(ctx.Configuration.TOTP.Period),
SecretSize: 32,
Digits: otp.Digits(6),
Algorithm: algorithm,
})
if err != nil {
if config, err = ctx.Providers.TOTP.Generate(username); err != nil {
ctx.Error(fmt.Errorf("unable to generate TOTP key: %s", err), messageUnableToRegisterOneTimePassword)
return
}
config := models.TOTPConfiguration{
Username: username,
Algorithm: otpAlgoToString(algorithm),
Digits: 6,
Secret: []byte(key.Secret()),
Period: key.Period(),
}
err = ctx.Providers.StorageProvider.SaveTOTPConfiguration(ctx, config)
err = ctx.Providers.StorageProvider.SaveTOTPConfiguration(ctx, *config)
if err != nil {
ctx.Error(fmt.Errorf("unable to save TOTP secret in DB: %s", err), messageUnableToRegisterOneTimePassword)
return
}
response := TOTPKeyResponse{
OTPAuthURL: key.URL(),
Base32Secret: key.Secret(),
OTPAuthURL: config.URI(),
Base32Secret: string(config.Secret),
}
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() {
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))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())).
s.mock.StorageMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil)
s.mock.StorageProviderMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())).
s.mock.StorageMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(nil)
SecondFactorU2FIdentityFinish(s.mock.Ctx)
@ -68,16 +68,16 @@ func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissi
func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() {
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))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())).
s.mock.StorageMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil)
s.mock.StorageProviderMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())).
s.mock.StorageMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(nil)
SecondFactorU2FIdentityFinish(s.mock.Ctx)

View File

@ -39,7 +39,7 @@ func (s *SecondFactorDuoPostSuite) TearDownTest() {
func (s *SecondFactorDuoPostSuite) TestShouldEnroll() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(nil, errors.New("no Duo device and method saved"))
@ -69,7 +69,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldEnroll() {
func (s *SecondFactorDuoPostSuite) TestShouldAutoSelect() {
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{
{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)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
SavePreferredDuoDevice(s.mock.Ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}).
Return(nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -124,7 +124,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldAutoSelect() {
func (s *SecondFactorDuoPostSuite) TestShouldDenyAutoSelect() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(nil, errors.New("no Duo device and method saved"))
@ -156,7 +156,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDenyAutoSelect() {
func (s *SecondFactorDuoPostSuite) TestShouldFailAutoSelect() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(nil, errors.New("no Duo device and method saved"))
@ -174,7 +174,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldFailAutoSelect() {
func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndEnroll() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
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)
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{})
s.Require().NoError(err)
@ -206,7 +206,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndEnroll() {
func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndCallPreauthAPIWithInvalidDevicesAndEnroll() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
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)
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{})
s.Require().NoError(err)
@ -239,7 +239,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndCallPreauthAPIWit
func (s *SecondFactorDuoPostSuite) TestShouldUseOldDeviceAndSelect() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "NOTEXISTENT", Method: "push"}, nil)
@ -274,11 +274,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseOldDeviceAndSelect() {
func (s *SecondFactorDuoPostSuite) TestShouldUseInvalidMethodAndAutoSelect() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "invalidmethod"}, nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -303,7 +303,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseInvalidMethodAndAutoSelect() {
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"}).
Return(nil)
@ -330,7 +330,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseInvalidMethodAndAutoSelect() {
func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndAllowAccess() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
@ -354,7 +354,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndAllowAccess() {
func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndDenyAccess() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
@ -384,7 +384,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndDenyAccess() {
func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndFail() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
@ -402,11 +402,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndFail() {
func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndDenyAccess() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -454,7 +454,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndDenyAccess() {
func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndFail() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
@ -485,11 +485,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndFail() {
func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToDefaultURL() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -534,11 +534,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToDefaultURL() {
func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -579,11 +579,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() {
func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -628,11 +628,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -675,11 +675,11 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() {
func (s *SecondFactorDuoPostSuite) TestShouldRegenerateSessionForPreventingSessionFixation() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&models.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",

View File

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

View File

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

View File

@ -37,13 +37,13 @@ func (s *HandlerSignU2FStep2Suite) TearDownTest() {
}
func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToDefaultURL() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -69,13 +69,13 @@ func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToDefaultURL() {
}
func (s *HandlerSignU2FStep2Suite) TestShouldNotReturnRedirectURL() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -97,13 +97,13 @@ func (s *HandlerSignU2FStep2Suite) TestShouldNotReturnRedirectURL() {
}
func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToSafeTargetURL() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -128,13 +128,13 @@ func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToSafeTargetURL() {
}
func (s *HandlerSignU2FStep2Suite) TestShouldNotRedirectToUnsafeURL() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
@ -157,13 +157,13 @@ func (s *HandlerSignU2FStep2Suite) TestShouldNotRedirectToUnsafeURL() {
}
func (s *HandlerSignU2FStep2Suite) TestShouldRegenerateSessionForPreventingSessionFixation() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
s.mock.StorageProviderMock.
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
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.
func MethodPreferencePost(ctx *middlewares.AutheliaCtx) {
bodyJSON := MethodBody{}
bodyJSON := preferred2FAMethodBody{}
err := ctx.ParseBody(&bodyJSON)
if err != nil {

View File

@ -96,7 +96,7 @@ func TestMethodSetToU2F(t *testing.T) {
err := mock.Ctx.SaveSession(userSession)
require.NoError(t, err)
mock.StorageProviderMock.
mock.StorageMock.
EXPECT().
LoadUserInfo(mock.Ctx, gomock.Eq("john")).
Return(resp.db, resp.err)
@ -139,7 +139,7 @@ func TestMethodSetToU2F(t *testing.T) {
}
func (s *FetchSuite) TestShouldReturnError500WhenStorageFailsToLoad() {
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
LoadUserInfo(s.mock.Ctx, gomock.Eq("john")).
Return(models.UserInfo{}, fmt.Errorf("failure"))
@ -211,7 +211,7 @@ func (s *SaveSuite) TestShouldReturnError500WhenBadMethodProvided() {
func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() {
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")).
Return(fmt.Errorf("Failure"))
@ -224,7 +224,7 @@ func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() {
func (s *SaveSuite) TestShouldReturn200WhenMethodIsSuccessfullySaved() {
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")).
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
// UserInfo is the model of user info and second factor preferences.
type UserInfo struct {
// The users display name.
DisplayName string `json:"display_name"`
// 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"`
// 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.
}
// signTOTPRequestBody model of the request body received by TOTP authentication endpoint.
@ -46,6 +34,11 @@ type signDuoRequestBody struct {
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.
type firstFactorRequestBody struct {
Username string `json:"username" valid:"required"`

View File

@ -55,7 +55,7 @@ func TestShouldFailIfJWTCannotBeSaved(t *testing.T) {
mock.Ctx.Configuration.JWTSecret = testJWTSecret
mock.StorageProviderMock.EXPECT().
mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()).
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-Host", "host")
mock.StorageProviderMock.EXPECT().
mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()).
Return(nil)
@ -96,7 +96,7 @@ func TestShouldFailWhenXForwardedProtoHeaderIsMissing(t *testing.T) {
mock.Ctx.Configuration.JWTSecret = testJWTSecret
mock.Ctx.Request.Header.Add("X-Forwarded-Host", "host")
mock.StorageProviderMock.EXPECT().
mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()).
Return(nil)
@ -114,7 +114,7 @@ func TestShouldFailWhenXForwardedHostHeaderIsMissing(t *testing.T) {
mock.Ctx.Configuration.JWTSecret = testJWTSecret
mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http")
mock.StorageProviderMock.EXPECT().
mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()).
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-Host", "host")
mock.StorageProviderMock.EXPECT().
mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()).
Return(nil)
@ -208,7 +208,7 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsNotFoundInDB(
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())).
Return(false, nil)
@ -244,7 +244,7 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongAction() {
time.Now().Add(1*time.Minute))
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())).
Return(true, nil)
@ -259,7 +259,7 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongUser() {
time.Now().Add(1*time.Minute))
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())).
Return(true, nil)
@ -276,11 +276,11 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenCannotBeRemoved
time.Now().Add(1*time.Minute))
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())).
Return(true, nil)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(fmt.Errorf("cannot remove"))
@ -295,11 +295,11 @@ func (s *IdentityVerificationFinishProcess) TestShouldReturn200OnFinishComplete(
time.Now().Add(1*time.Minute))
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())).
Return(true, nil)
s.mock.StorageProviderMock.EXPECT().
s.mock.StorageMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(nil)

View File

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

View File

@ -1,8 +1,8 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/authelia/authelia/v4/internal/storage (interfaces: Provider)
// Package storage is a generated GoMock package.
package storage
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
@ -14,31 +14,31 @@ import (
models "github.com/authelia/authelia/v4/internal/models"
)
// MockProvider is a mock of Provider interface.
type MockProvider struct {
// MockStorage is a mock of Provider interface.
type MockStorage struct {
ctrl *gomock.Controller
recorder *MockProviderMockRecorder
recorder *MockStorageMockRecorder
}
// MockProviderMockRecorder is the mock recorder for MockProvider.
type MockProviderMockRecorder struct {
mock *MockProvider
// MockStorageMockRecorder is the mock recorder for MockStorage.
type MockStorageMockRecorder struct {
mock *MockStorage
}
// NewMockProvider creates a new mock instance.
func NewMockProvider(ctrl *gomock.Controller) *MockProvider {
mock := &MockProvider{ctrl: ctrl}
mock.recorder = &MockProviderMockRecorder{mock}
// NewMockStorage creates a new mock instance.
func NewMockStorage(ctrl *gomock.Controller) *MockStorage {
mock := &MockStorage{ctrl: ctrl}
mock.recorder = &MockStorageMockRecorder{mock}
return mock
}
// 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
}
// 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()
ret := m.ctrl.Call(m, "AppendAuthenticationLog", arg0, arg1)
ret0, _ := ret[0].(error)
@ -46,13 +46,13 @@ func (m *MockProvider) AppendAuthenticationLog(arg0 context.Context, arg1 models
}
// 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()
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.
func (m *MockProvider) Close() error {
func (m *MockStorage) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
@ -60,13 +60,13 @@ func (m *MockProvider) Close() error {
}
// Close indicates an expected call of Close.
func (mr *MockProviderMockRecorder) Close() *gomock.Call {
func (mr *MockStorageMockRecorder) Close() *gomock.Call {
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.
func (m *MockProvider) DeletePreferredDuoDevice(arg0 context.Context, arg1 string) error {
func (m *MockStorage) DeletePreferredDuoDevice(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeletePreferredDuoDevice", arg0, arg1)
ret0, _ := ret[0].(error)
@ -74,13 +74,13 @@ func (m *MockProvider) DeletePreferredDuoDevice(arg0 context.Context, arg1 strin
}
// 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()
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.
func (m *MockProvider) DeleteTOTPConfiguration(arg0 context.Context, arg1 string) error {
func (m *MockStorage) DeleteTOTPConfiguration(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteTOTPConfiguration", arg0, arg1)
ret0, _ := ret[0].(error)
@ -88,13 +88,13 @@ func (m *MockProvider) DeleteTOTPConfiguration(arg0 context.Context, arg1 string
}
// 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()
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.
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()
ret := m.ctrl.Call(m, "FindIdentityVerification", arg0, arg1)
ret0, _ := ret[0].(bool)
@ -103,13 +103,13 @@ func (m *MockProvider) FindIdentityVerification(arg0 context.Context, arg1 strin
}
// 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()
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.
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()
ret := m.ctrl.Call(m, "LoadAuthenticationLogs", arg0, arg1, arg2, arg3, arg4)
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.
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()
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.
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()
ret := m.ctrl.Call(m, "LoadPreferred2FAMethod", arg0, arg1)
ret0, _ := ret[0].(string)
@ -133,13 +133,13 @@ func (m *MockProvider) LoadPreferred2FAMethod(arg0 context.Context, arg1 string)
}
// 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()
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.
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()
ret := m.ctrl.Call(m, "LoadPreferredDuoDevice", arg0, arg1)
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.
func (mr *MockProviderMockRecorder) LoadPreferredDuoDevice(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) LoadPreferredDuoDevice(arg0, arg1 interface{}) *gomock.Call {
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.
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()
ret := m.ctrl.Call(m, "LoadTOTPConfiguration", arg0, arg1)
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.
func (mr *MockProviderMockRecorder) LoadTOTPConfiguration(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) LoadTOTPConfiguration(arg0, arg1 interface{}) *gomock.Call {
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.
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()
ret := m.ctrl.Call(m, "LoadTOTPConfigurations", arg0, arg1, arg2)
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.
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()
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.
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()
ret := m.ctrl.Call(m, "LoadU2FDevice", arg0, arg1)
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.
func (mr *MockProviderMockRecorder) LoadU2FDevice(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) LoadU2FDevice(arg0, arg1 interface{}) *gomock.Call {
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.
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()
ret := m.ctrl.Call(m, "LoadUserInfo", arg0, arg1)
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.
func (mr *MockProviderMockRecorder) LoadUserInfo(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) LoadUserInfo(arg0, arg1 interface{}) *gomock.Call {
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.
func (m *MockProvider) RemoveIdentityVerification(arg0 context.Context, arg1 string) error {
func (m *MockStorage) RemoveIdentityVerification(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveIdentityVerification", arg0, arg1)
ret0, _ := ret[0].(error)
@ -222,13 +222,13 @@ func (m *MockProvider) RemoveIdentityVerification(arg0 context.Context, arg1 str
}
// 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()
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.
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()
ret := m.ctrl.Call(m, "SaveIdentityVerification", arg0, arg1)
ret0, _ := ret[0].(error)
@ -236,13 +236,13 @@ func (m *MockProvider) SaveIdentityVerification(arg0 context.Context, arg1 model
}
// 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()
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.
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()
ret := m.ctrl.Call(m, "SavePreferred2FAMethod", arg0, arg1, arg2)
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.
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()
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.
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()
ret := m.ctrl.Call(m, "SavePreferredDuoDevice", arg0, arg1)
ret0, _ := ret[0].(error)
@ -264,13 +264,13 @@ func (m *MockProvider) SavePreferredDuoDevice(arg0 context.Context, arg1 models.
}
// 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()
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.
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()
ret := m.ctrl.Call(m, "SaveTOTPConfiguration", arg0, arg1)
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.
func (mr *MockProviderMockRecorder) SaveTOTPConfiguration(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) SaveTOTPConfiguration(arg0, arg1 interface{}) *gomock.Call {
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.
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()
ret := m.ctrl.Call(m, "SaveU2FDevice", arg0, arg1)
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.
func (mr *MockProviderMockRecorder) SaveU2FDevice(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) SaveU2FDevice(arg0, arg1 interface{}) *gomock.Call {
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.
func (m *MockProvider) SchemaEncryptionChangeKey(arg0 context.Context, arg1 string) error {
func (m *MockStorage) SchemaEncryptionChangeKey(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaEncryptionChangeKey", arg0, arg1)
ret0, _ := ret[0].(error)
@ -306,13 +306,13 @@ func (m *MockProvider) SchemaEncryptionChangeKey(arg0 context.Context, arg1 stri
}
// 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()
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.
func (m *MockProvider) SchemaEncryptionCheckKey(arg0 context.Context, arg1 bool) error {
func (m *MockStorage) SchemaEncryptionCheckKey(arg0 context.Context, arg1 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaEncryptionCheckKey", arg0, arg1)
ret0, _ := ret[0].(error)
@ -320,13 +320,13 @@ func (m *MockProvider) SchemaEncryptionCheckKey(arg0 context.Context, arg1 bool)
}
// 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()
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.
func (m *MockProvider) SchemaLatestVersion() (int, error) {
func (m *MockStorage) SchemaLatestVersion() (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaLatestVersion")
ret0, _ := ret[0].(int)
@ -335,13 +335,13 @@ func (m *MockProvider) SchemaLatestVersion() (int, error) {
}
// SchemaLatestVersion indicates an expected call of SchemaLatestVersion.
func (mr *MockProviderMockRecorder) SchemaLatestVersion() *gomock.Call {
func (mr *MockStorageMockRecorder) SchemaLatestVersion() *gomock.Call {
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.
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()
ret := m.ctrl.Call(m, "SchemaMigrate", arg0, arg1, arg2)
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.
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()
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.
func (m *MockProvider) SchemaMigrationHistory(arg0 context.Context) ([]models.Migration, error) {
func (m *MockStorage) SchemaMigrationHistory(arg0 context.Context) ([]models.Migration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaMigrationHistory", arg0)
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.
func (mr *MockProviderMockRecorder) SchemaMigrationHistory(arg0 interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) SchemaMigrationHistory(arg0 interface{}) *gomock.Call {
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.
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()
ret := m.ctrl.Call(m, "SchemaMigrationsDown", arg0, arg1)
ret0, _ := ret[0].([]SchemaMigration)
ret0, _ := ret[0].([]models.SchemaMigration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// 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()
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.
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()
ret := m.ctrl.Call(m, "SchemaMigrationsUp", arg0, arg1)
ret0, _ := ret[0].([]SchemaMigration)
ret0, _ := ret[0].([]models.SchemaMigration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// 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()
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.
func (m *MockProvider) SchemaTables(arg0 context.Context) ([]string, error) {
func (m *MockStorage) SchemaTables(arg0 context.Context) ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaTables", arg0)
ret0, _ := ret[0].([]string)
@ -409,13 +409,13 @@ func (m *MockProvider) SchemaTables(arg0 context.Context) ([]string, error) {
}
// 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()
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.
func (m *MockProvider) SchemaVersion(arg0 context.Context) (int, error) {
func (m *MockStorage) SchemaVersion(arg0 context.Context) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SchemaVersion", arg0)
ret0, _ := ret[0].(int)
@ -424,13 +424,13 @@ func (m *MockProvider) SchemaVersion(arg0 context.Context) (int, error) {
}
// 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()
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.
func (m *MockProvider) StartupCheck() error {
func (m *MockStorage) StartupCheck() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StartupCheck")
ret0, _ := ret[0].(error)
@ -438,13 +438,13 @@ func (m *MockProvider) StartupCheck() error {
}
// StartupCheck indicates an expected call of StartupCheck.
func (mr *MockProviderMockRecorder) StartupCheck() *gomock.Call {
func (mr *MockStorageMockRecorder) StartupCheck() *gomock.Call {
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.
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()
ret := m.ctrl.Call(m, "UpdateTOTPConfigurationSecret", arg0, arg1)
ret0, _ := ret[0].(error)
@ -452,7 +452,7 @@ func (m *MockProvider) UpdateTOTPConfigurationSecret(arg0 context.Context, arg1
}
// 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()
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)
}

81
internal/mocks/totp.go Normal file
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.
// Source: internal/handlers/u2f.go
// Source: github.com/authelia/authelia/v4/internal/handlers (interfaces: U2FVerifier)
// Package handlers is a generated GoMock package.
package handlers
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
@ -11,39 +11,39 @@ import (
u2f "github.com/tstranex/u2f"
)
// MockU2FVerifier is a mock of U2FVerifier interface
// MockU2FVerifier is a mock of U2FVerifier interface.
type MockU2FVerifier struct {
ctrl *gomock.Controller
recorder *MockU2FVerifierMockRecorder
}
// MockU2FVerifierMockRecorder is the mock recorder for MockU2FVerifier
// MockU2FVerifierMockRecorder is the mock recorder for MockU2FVerifier.
type MockU2FVerifierMockRecorder struct {
mock *MockU2FVerifier
}
// NewMockU2FVerifier creates a new mock instance
// NewMockU2FVerifier creates a new mock instance.
func NewMockU2FVerifier(ctrl *gomock.Controller) *MockU2FVerifier {
mock := &MockU2FVerifier{ctrl: ctrl}
mock.recorder = &MockU2FVerifierMockRecorder{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 {
return m.recorder
}
// Verify mocks base method
func (m *MockU2FVerifier) Verify(keyHandle, publicKey []byte, signResponse u2f.SignResponse, challenge u2f.Challenge) error {
// Verify mocks base method.
func (m *MockU2FVerifier) Verify(arg0, arg1 []byte, arg2 u2f.SignResponse, arg3 u2f.Challenge) error {
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)
return ret0
}
// Verify indicates an expected call of Verify
func (mr *MockU2FVerifierMockRecorder) Verify(keyHandle, publicKey, signResponse, challenge interface{}) *gomock.Call {
// Verify indicates an expected call of Verify.
func (mr *MockU2FVerifierMockRecorder) Verify(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
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.
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
}
// 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/models"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/storage"
)
type RegulatorSuite struct {
@ -21,14 +20,14 @@ type RegulatorSuite struct {
ctx context.Context
ctrl *gomock.Controller
storageMock *storage.MockProvider
storageMock *mocks.MockStorage
configuration schema.RegulationConfiguration
clock mocks.TestingClock
}
func (s *RegulatorSuite) SetupTest() {
s.ctrl = gomock.NewController(s.T())
s.storageMock = storage.NewMockProvider(s.ctrl)
s.storageMock = mocks.NewMockStorage(s.ctrl)
s.ctx = context.Background()
s.configuration = schema.RegulationConfiguration{

View File

@ -88,6 +88,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
middlewares.RequireFirstFactor(handlers.UserInfoGet)))
r.POST("/api/user/info/2fa_method", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.MethodPreferencePost)))
r.GET("/api/user/info/totp", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.UserTOTPGet)))
// TOTP related endpoints.
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(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish)))
r.POST("/api/secondfactor/totp", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost(&handlers.TOTPVerifierImpl{
Period: uint(configuration.TOTP.Period),
Skew: uint(*configuration.TOTP.Skew),
}))))
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost)))
// U2F related endpoints.
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 = errors.New("no matching authentication logs found")
// ErrNoTOTPSecret error thrown when no TOTP secret has been found in DB.
ErrNoTOTPSecret = errors.New("no TOTP secret registered")
// ErrNoTOTPConfiguration error thrown when no TOTP configuration has been found in DB.
ErrNoTOTPConfiguration = errors.New("no TOTP configuration for user")
// ErrNoU2FDeviceHandle error thrown when no U2F device handle has been found in DB.
ErrNoU2FDeviceHandle = errors.New("no U2F device handle found")

View File

@ -7,6 +7,8 @@ import (
"sort"
"strconv"
"strings"
"github.com/authelia/authelia/v4/internal/models"
)
//go:embed migrations/*
@ -44,7 +46,7 @@ func latestMigrationVersion(providerName string) (version int, err error) {
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")
if err != nil {
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
// 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.
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) {
return nil, ErrMigrateCurrentVersionSameAsTarget
}
@ -125,7 +127,7 @@ func loadMigrations(providerName string, prior, target int) (migrations []Schema
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 {
// Skip if migration.Provider is not a match.
return true
@ -163,21 +165,21 @@ func skipMigration(providerName string, up bool, target, prior int, migration *S
return false
}
func scanMigration(m string) (migration SchemaMigration, err error) {
func scanMigration(m string) (migration models.SchemaMigration, err error) {
result := reMigration.FindStringSubmatch(m)
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], "_", " "),
Provider: result[3],
}
data, err := migrationsFS.ReadFile(fmt.Sprintf("migrations/%s", m))
if err != nil {
return SchemaMigration{}, err
return models.SchemaMigration{}, err
}
migration.Query = string(data)
@ -188,7 +190,7 @@ func scanMigration(m string) (migration SchemaMigration, err error) {
case "down":
migration.Up = false
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])
@ -197,7 +199,7 @@ func scanMigration(m string) (migration SchemaMigration, err error) {
case providerAll, providerSQLite, providerMySQL, providerPostgres:
break
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

View File

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

View File

@ -12,19 +12,21 @@ import (
"github.com/sirupsen/logrus"
"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/models"
)
// 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)
provider = SQLProvider{
db: db,
key: sha256.Sum256([]byte(encryptionKey)),
key: sha256.Sum256([]byte(config.Storage.EncryptionKey)),
name: name,
driverName: driverName,
config: config,
errOpen: err,
log: logging.Logger(),
@ -64,10 +66,6 @@ func NewSQLProvider(name, driverName, dataSourceName, encryptionKey string) (pro
sqlFmtRenameTable: queryFmtRenameTable,
}
key := sha256.Sum256([]byte(encryptionKey))
provider.key = key
return provider
}
@ -77,6 +75,7 @@ type SQLProvider struct {
key [32]byte
name string
driverName string
config *schema.Configuration
errOpen error
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,
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)
}
@ -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 errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoTOTPSecret
return nil, ErrNoTOTPConfiguration
}
return nil, fmt.Errorf("error selecting TOTP configuration: %w", err)

View File

@ -15,9 +15,9 @@ type MySQLProvider struct {
}
// NewMySQLProvider a MySQL provider.
func NewMySQLProvider(config schema.MySQLStorageConfiguration, encryptionKey string) (provider *MySQLProvider) {
func NewMySQLProvider(config *schema.Configuration) (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.

View File

@ -16,9 +16,9 @@ type PostgreSQLProvider struct {
}
// NewPostgreSQLProvider a PostgreSQL provider.
func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration, encryptionKey string) (provider *PostgreSQLProvider) {
func NewPostgreSQLProvider(config *schema.Configuration) (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.

View File

@ -2,6 +2,8 @@ package storage
import (
_ "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.
@ -10,9 +12,9 @@ type SQLiteProvider struct {
}
// NewSQLiteProvider constructs a SQLite provider.
func NewSQLiteProvider(path, encryptionKey string) (provider *SQLiteProvider) {
func NewSQLiteProvider(config *schema.Configuration) (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.

View File

@ -75,12 +75,12 @@ const (
const (
queryFmtSelectTOTPConfiguration = `
SELECT id, username, algorithm, digits, totp_period, secret
SELECT id, username, issuer, algorithm, digits, totp_period, secret
FROM %s
WHERE username = ?;`
queryFmtSelectTOTPConfigurations = `
SELECT id, username, algorithm, digits, totp_period, secret
SELECT id, username, issuer, algorithm, digits, totp_period, secret
FROM %s
LIMIT ?
OFFSET ?;`
@ -98,14 +98,14 @@ const (
WHERE username = ?;`
queryFmtUpsertTOTPConfiguration = `
REPLACE INTO %s (username, algorithm, digits, totp_period, secret)
VALUES (?, ?, ?, ?, ?);`
REPLACE INTO %s (username, issuer, algorithm, digits, totp_period, secret)
VALUES (?, ?, ?, ?, ?, ?);`
queryFmtPostgresUpsertTOTPConfiguration = `
INSERT INTO %s (username, algorithm, digits, totp_period, secret)
VALUES ($1, $2, $3, $4, $5)
INSERT INTO %s (username, issuer, algorithm, digits, totp_period, secret)
VALUES ($1, $2, $3, $4, $5, $6)
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 = `
DELETE FROM %s

View File

@ -35,7 +35,11 @@ const (
FROM %s
ORDER BY username ASC;`
queryFmtPre1InsertTOTPConfiguration = `
queryFmtPre1To1InsertTOTPConfiguration = `
INSERT INTO %s (username, issuer, totp_period, secret)
VALUES (?, ?, ?, ?);`
queryFmt1ToPre1InsertTOTPConfiguration = `
INSERT INTO %s (username, secret)
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)
}
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)
if err != nil {
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)
}
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())
}
@ -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.
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)
if err != nil {
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.
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)
if err != nil {
return migrations, err

View File

@ -226,7 +226,7 @@ func (p *SQLProvider) schemaMigratePre1To1TOTP(ctx context.Context) (err error)
}
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 {
return err
}
@ -414,7 +414,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1TOTP(ctx context.Context) (err error)
}
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 {
return err
}

View File

@ -1,6 +1,6 @@
---
storage:
encryption_key: a_cli_encryption_key_which_isnt_secure
encryption_key: a_not_so_secure_encryption_key
local:
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) {
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]))
}
}

View File

@ -3,6 +3,8 @@ package suites
import (
"fmt"
"os"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
// BaseDomain the base domain.
@ -55,3 +57,18 @@ const (
testUsername = "john"
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"
"os"
"regexp"
"strconv"
"testing"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/models"
@ -178,6 +177,8 @@ func (s *CLISuite) TestStorageShouldShowErrWithoutConfig() {
}
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"})
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`)
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().Regexp(patternOutdated, output)
@ -267,16 +268,14 @@ func (s *CLISuite) TestStorage02ShouldShowSchemaInfo() {
}
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()
var (
err error
key *otp.Key
config models.TOTPConfiguration
)
var (
@ -287,39 +286,53 @@ func (s *CLISuite) TestStorage03ShouldExportTOTP() {
expectedLinesCSV = append(expectedLinesCSV, "issuer,username,algorithm,digits,period,secret")
for _, name := range []string{"john", "mary", "fred"} {
key, err = totp.Generate(totp.GenerateOpts{
Issuer: "Authelia",
AccountName: name,
Period: uint(30),
SecretSize: 32,
Digits: otp.Digits(6),
Algorithm: otp.AlgorithmSHA1,
})
s.Require().NoError(err)
config = models.TOTPConfiguration{
Username: name,
Algorithm: "SHA1",
configs := []*models.TOTPConfiguration{
{
Username: "john",
Period: 30,
Digits: 6,
Secret: []byte(key.Secret()),
Period: key.Period(),
Algorithm: "SHA1",
},
{
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)))
expectedLines = append(expectedLines, fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d", "Authelia", config.Username, string(config.Secret), "Authelia", config.Algorithm, config.Digits, config.Period))
s.Require().NoError(provider.SaveTOTPConfiguration(ctx, config))
expectedLines = append(expectedLines, config.URI())
}
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)
for _, expectedLine := range expectedLines {
s.Assert().Contains(output, expectedLine)
}
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "export", "totp-configurations", "--format", "csv", "--config", "/config/configuration.storage.yml"})
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)
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"})
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"})
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.
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.DeletePreferredDuoDevice(ctx, "john"))
}
@ -152,7 +152,7 @@ func (s *DuoPushWebDriverSuite) TestShouldSelectDevice() {
defer cancel()
// 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"}))
var PreAuthAPIResponse = duo.PreAuthResponse{
@ -231,7 +231,7 @@ func (s *DuoPushWebDriverSuite) TestShouldSelectNewDeviceAfterSavedDeviceMethodI
}
// 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"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Allow)
@ -257,7 +257,7 @@ func (s *DuoPushWebDriverSuite) TestShouldAutoSelectNewDeviceAfterSavedDeviceIsN
}
// 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"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Allow)
@ -276,7 +276,7 @@ func (s *DuoPushWebDriverSuite) TestShouldFailSelectionBecauseOfSelectionBypasse
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"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Deny)
@ -296,7 +296,7 @@ func (s *DuoPushWebDriverSuite) TestShouldFailSelectionBecauseOfSelectionDenied(
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"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Deny)
@ -317,7 +317,7 @@ func (s *DuoPushWebDriverSuite) TestShouldFailAuthenticationBecausePreauthDenied
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"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
@ -344,7 +344,7 @@ func (s *DuoPushWebDriverSuite) TestShouldSucceedAuthentication() {
}
// 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"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Allow)
@ -371,7 +371,7 @@ func (s *DuoPushWebDriverSuite) TestShouldFailAuthentication() {
}
// 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"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Deny)
@ -430,7 +430,7 @@ func (s *DuoPushDefaultRedirectionSuite) TestUserIsRedirectedToDefaultURL() {
}
// 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"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Allow)
@ -475,7 +475,7 @@ func (s *DuoPushSuite) TestUserPreferencesScenario() {
ctx := context.Background()
// 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"}))
ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse)
ConfigureDuo(s.T(), Allow)

View File

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

View File

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

20
internal/totp/helpers.go Normal file
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
}
}

12
internal/totp/provider.go Normal file
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)
}

78
internal/totp/totp.go Normal file
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",
"suites",
"templates",
"totp",
"utils",
"web",
],

View File

@ -1,6 +1,6 @@
import { useRemoteCall } from "@hooks/RemoteCall";
import { getUserPreferences } from "@services/UserPreferences";
import { getUserInfo } from "@services/UserInfo";
export function useUserPreferences() {
return useRemoteCall(getUserPreferences, []);
export function useUserInfo() {
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 {
available_methods: Set<SecondFactorMethod>;
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 UserInfoPath = basePath + "/api/user/info";
export const UserInfo2FAMethodPath = basePath + "/api/user/info/2fa_method";
export const UserInfoTOTPConfigurationPath = basePath + "/api/user/info/totp";
export const ConfigurationPath = basePath + "/api/configuration";

View File

@ -1,12 +1,11 @@
import { Configuration } from "@models/Configuration";
import { ConfigurationPath } from "@services/Api";
import { Get } from "@services/Client";
import { toEnum, Method2FA } from "@services/UserPreferences";
import { toEnum, Method2FA } from "@services/UserInfo";
interface ConfigurationPayload {
available_methods: Method2FA[];
second_factor_enabled: boolean;
totp_period: number;
}
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);
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 { useRequestMethod } from "@hooks/RequestMethod";
import { useAutheliaState } from "@hooks/State";
import { useUserPreferences as userUserInfo } from "@hooks/UserInfo";
import { useUserInfo } from "@hooks/UserInfo";
import { SecondFactorMethod } from "@models/Methods";
import { checkSafeRedirection } from "@services/SafeRedirection";
import { AuthenticationLevel } from "@services/State";
@ -44,7 +44,7 @@ const LoginPortal = function (props: Props) {
const redirector = useRedirector();
const [state, fetchState, , fetchStateError] = useAutheliaState();
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo();
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfo();
const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration();
const redirect = useCallback((url: string) => navigate(url), [navigate]);

View File

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

View File

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

View File

@ -1,8 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useRedirectionURL } from "@hooks/RedirectionURL";
import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration";
import { completeTOTPSignIn } from "@services/OneTimePassword";
import { AuthenticationLevel } from "@services/State";
import LoadingPage from "@views/LoadingPage/LoadingPage";
import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer";
import OTPDial from "@views/LoginPortal/SecondFactor/OTPDial";
@ -17,7 +19,6 @@ export interface Props {
id: string;
authenticationLevel: AuthenticationLevel;
registered: boolean;
totp_period: number;
onRegisterClick: () => void;
onSignInError: (err: Error) => void;
@ -35,6 +36,20 @@ const OneTimePasswordMethod = function (props: Props) {
const onSignInErrorCallback = useRef(onSignInError).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 () => {
if (!props.registered || props.authenticationLevel === AuthenticationLevel.TwoFactor) {
return;
@ -42,7 +57,7 @@ const OneTimePasswordMethod = function (props: Props) {
const passcodeStr = `${passcode}`;
if (!passcode || passcodeStr.length !== 6) {
if (!passcode || passcodeStr.length !== (resp?.digits || 6)) {
return;
}
@ -62,6 +77,7 @@ const OneTimePasswordMethod = function (props: Props) {
onSignInSuccessCallback,
passcode,
redirectionURL,
resp,
props.authenticationLevel,
props.registered,
]);
@ -94,7 +110,19 @@ const OneTimePasswordMethod = function (props: Props) {
state={methodState}
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>
);
};

View File

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

View File

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