feat(totp): secret customization (#2681)
Allow customizing the shared secrets size specifically for apps which don't support 256bit shared secrets.pull/3134/head^2
parent
fe08bf56b0
commit
9b6bcca1ba
|
@ -127,6 +127,9 @@ totp:
|
||||||
skew: 1
|
skew: 1
|
||||||
## See: https://www.authelia.com/docs/configuration/one-time-password.html#input-validation to read the documentation.
|
## See: https://www.authelia.com/docs/configuration/one-time-password.html#input-validation to read the documentation.
|
||||||
|
|
||||||
|
## The size of the generated shared secrets. Default is 32 and is sufficient in most use cases, minimum is 20.
|
||||||
|
secret_size: 32
|
||||||
|
|
||||||
##
|
##
|
||||||
## WebAuthn Configuration
|
## WebAuthn Configuration
|
||||||
##
|
##
|
||||||
|
|
|
@ -7,9 +7,9 @@ nav_order: 16
|
||||||
|
|
||||||
# Time-based One-Time Password
|
# Time-based One-Time Password
|
||||||
|
|
||||||
Authelia uses time-based one-time passwords as the OTP method. You have
|
The OTP method _Authelia_ uses is the Time-Based One-Time Password Algorithm (TOTP) [RFC6238] which is an extension of
|
||||||
the option to tune the settings of the TOTP generation, and you can see a
|
HMAC-Based One-Time Password Algorithm (HOTP) [RFC4226]. You have the option to tune the settings of theTOTP generation,
|
||||||
full example of TOTP configuration below, as well as sections describing them.
|
and you can see a full example of TOTP configuration below, as well as sections describing them.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -20,6 +20,7 @@ totp:
|
||||||
digits: 6
|
digits: 6
|
||||||
period: 30
|
period: 30
|
||||||
skew: 1
|
skew: 1
|
||||||
|
secret_size: 32
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
@ -139,6 +140,21 @@ other.
|
||||||
|
|
||||||
Changing this value affects all TOTP validations, not just newly registered ones.
|
Changing this value affects all TOTP validations, not just newly registered ones.
|
||||||
|
|
||||||
|
### secret_size
|
||||||
|
<div markdown="1">
|
||||||
|
type: integer
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: 32
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The length in bytes of generated shared secrets. The minimum is 20 (or 160 bits), and the default is 32 (or 256 bits).
|
||||||
|
In most use cases 32 is sufficient. Though some authenticators may have issues with more than the minimum. Our minimum
|
||||||
|
is the recommended value in [RFC4226], though technically according to the specification 16 bytes (or 128 bits) is the
|
||||||
|
minimum.
|
||||||
|
|
||||||
## Registration
|
## Registration
|
||||||
When users register their TOTP device for the first time, the current [issuer](#issuer), [algorithm](#algorithm), and
|
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
|
[period](#period) are used to generate the TOTP link and QR code. These values are saved to the database for future
|
||||||
|
@ -153,9 +169,8 @@ users to register a new device, you can delete the old device for a particular u
|
||||||
The period and skew configuration parameters affect each other. The default values are a period of 30 and a skew of 1.
|
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.
|
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
|
These options affect security by changing the length of time a one-time password is valid for. The formula to calculate
|
||||||
password is valid for. The formula to calculate the effective validity period is
|
the effective validity period is `period + (period * skew * 2)`. For example period 30 and skew 1 would result in 90
|
||||||
`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.
|
seconds of validity, and period 30 and skew 2 would result in 150 seconds of validity.
|
||||||
|
|
||||||
## System time accuracy
|
## System time accuracy
|
||||||
|
@ -192,3 +207,6 @@ Help:
|
||||||
```shell
|
```shell
|
||||||
$ authelia storage totp export --help
|
$ authelia storage totp export --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[RFC4226]: https://datatracker.ietf.org/doc/html/rfc4226
|
||||||
|
[RFC6238]: https://datatracker.ietf.org/doc/html/rfc6238
|
|
@ -2,6 +2,8 @@ package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewStorageCmd returns a new storage *cobra.Command.
|
// NewStorageCmd returns a new storage *cobra.Command.
|
||||||
|
@ -107,6 +109,8 @@ func newStorageTOTPGenerateCmd() (cmd *cobra.Command) {
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().String("secret", "", "Optionally set the TOTP shared secret as base32 encoded bytes (no padding), it's recommended to not set this option unless you're restoring an TOTP config")
|
||||||
|
cmd.Flags().Uint("secret-size", schema.TOTPSecretSizeDefault, "set the TOTP secret size")
|
||||||
cmd.Flags().Uint("period", 30, "set the TOTP period")
|
cmd.Flags().Uint("period", 30, "set the TOTP period")
|
||||||
cmd.Flags().Uint("digits", 6, "set the TOTP digits")
|
cmd.Flags().Uint("digits", 6, "set the TOTP digits")
|
||||||
cmd.Flags().String("algorithm", "SHA1", "set the TOTP algorithm")
|
cmd.Flags().String("algorithm", "SHA1", "set the TOTP algorithm")
|
||||||
|
|
|
@ -2,6 +2,7 @@ package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base32"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
@ -11,6 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/configuration"
|
"github.com/authelia/authelia/v4/internal/configuration"
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
|
@ -63,10 +65,11 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
|
||||||
"postgres.ssl.certificate": "storage.postgres.ssl.certificate",
|
"postgres.ssl.certificate": "storage.postgres.ssl.certificate",
|
||||||
"postgres.ssl.key": "storage.postgres.ssl.key",
|
"postgres.ssl.key": "storage.postgres.ssl.key",
|
||||||
|
|
||||||
"period": "totp.period",
|
"period": "totp.period",
|
||||||
"digits": "totp.digits",
|
"digits": "totp.digits",
|
||||||
"algorithm": "totp.algorithm",
|
"algorithm": "totp.algorithm",
|
||||||
"issuer": "totp.issuer",
|
"issuer": "totp.issuer",
|
||||||
|
"secret-size": "totp.secret_size",
|
||||||
}
|
}
|
||||||
|
|
||||||
sources = append(sources, configuration.NewEnvironmentSource(configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter))
|
sources = append(sources, configuration.NewEnvironmentSource(configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter))
|
||||||
|
@ -201,13 +204,13 @@ func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (er
|
||||||
|
|
||||||
func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
|
func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
|
||||||
var (
|
var (
|
||||||
provider storage.Provider
|
provider storage.Provider
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
c *model.TOTPConfiguration
|
c *model.TOTPConfiguration
|
||||||
force bool
|
force bool
|
||||||
filename string
|
filename, secret string
|
||||||
file *os.File
|
file *os.File
|
||||||
img image.Image
|
img image.Image
|
||||||
)
|
)
|
||||||
|
|
||||||
provider = getStorageProvider()
|
provider = getStorageProvider()
|
||||||
|
@ -216,25 +219,19 @@ func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
|
||||||
_ = provider.Close()
|
_ = provider.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if force, err = cmd.Flags().GetBool("force"); err != nil {
|
if force, filename, secret, err = storageTOTPGenerateRunEOptsFromFlags(cmd.Flags()); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if filename, err = cmd.Flags().GetString("path"); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = provider.LoadTOTPConfiguration(ctx, args[0]); err == nil && !force {
|
if _, err = provider.LoadTOTPConfiguration(ctx, args[0]); err == nil && !force {
|
||||||
return fmt.Errorf("%s already has a TOTP configuration, use --force to overwrite", args[0])
|
return fmt.Errorf("%s already has a TOTP configuration, use --force to overwrite", args[0])
|
||||||
}
|
} else if err != nil && !errors.Is(err, storage.ErrNoTOTPConfiguration) {
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, storage.ErrNoTOTPConfiguration) {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
totpProvider := totp.NewTimeBasedProvider(config.TOTP)
|
totpProvider := totp.NewTimeBasedProvider(config.TOTP)
|
||||||
|
|
||||||
if c, err = totpProvider.Generate(args[0]); err != nil {
|
if c, err = totpProvider.GenerateCustom(args[0], config.TOTP.Algorithm, secret, config.TOTP.Digits, config.TOTP.Period, config.TOTP.SecretSize); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,6 +268,28 @@ func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func storageTOTPGenerateRunEOptsFromFlags(flags *pflag.FlagSet) (force bool, filename, secret string, err error) {
|
||||||
|
if force, err = flags.GetBool("force"); err != nil {
|
||||||
|
return force, filename, secret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if filename, err = flags.GetString("path"); err != nil {
|
||||||
|
return force, filename, secret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if secret, err = flags.GetString("secret"); err != nil {
|
||||||
|
return force, filename, secret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secretLength := base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(secret))
|
||||||
|
if secret != "" && secretLength < schema.TOTPSecretSizeMinimum {
|
||||||
|
return force, filename, secret, fmt.Errorf("decoded length of the base32 secret must have "+
|
||||||
|
"a length of more than %d but '%s' has a decoded length of %d", schema.TOTPSecretSizeMinimum, secret, secretLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
return force, filename, secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func storageTOTPDeleteRunE(cmd *cobra.Command, args []string) (err error) {
|
func storageTOTPDeleteRunE(cmd *cobra.Command, args []string) (err error) {
|
||||||
var (
|
var (
|
||||||
provider storage.Provider
|
provider storage.Provider
|
||||||
|
|
|
@ -127,6 +127,9 @@ totp:
|
||||||
skew: 1
|
skew: 1
|
||||||
## See: https://www.authelia.com/docs/configuration/one-time-password.html#input-validation to read the documentation.
|
## See: https://www.authelia.com/docs/configuration/one-time-password.html#input-validation to read the documentation.
|
||||||
|
|
||||||
|
## The size of the generated shared secrets. Default is 32 and is sufficient in most use cases, minimum is 20.
|
||||||
|
secret_size: 32
|
||||||
|
|
||||||
##
|
##
|
||||||
## WebAuthn Configuration
|
## WebAuthn Configuration
|
||||||
##
|
##
|
||||||
|
|
|
@ -44,3 +44,11 @@ var (
|
||||||
// TOTPPossibleAlgorithms is a list of valid TOTP Algorithms.
|
// TOTPPossibleAlgorithms is a list of valid TOTP Algorithms.
|
||||||
TOTPPossibleAlgorithms = []string{TOTPAlgorithmSHA1, TOTPAlgorithmSHA256, TOTPAlgorithmSHA512}
|
TOTPPossibleAlgorithms = []string{TOTPAlgorithmSHA1, TOTPAlgorithmSHA256, TOTPAlgorithmSHA512}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TOTPSecretSizeDefault is the default secret size.
|
||||||
|
TOTPSecretSizeDefault = 32
|
||||||
|
|
||||||
|
// TOTPSecretSizeMinimum is the minimum secret size.
|
||||||
|
TOTPSecretSizeMinimum = 20
|
||||||
|
)
|
||||||
|
|
|
@ -2,21 +2,23 @@ package schema
|
||||||
|
|
||||||
// TOTPConfiguration represents the configuration related to TOTP options.
|
// TOTPConfiguration represents the configuration related to TOTP options.
|
||||||
type TOTPConfiguration struct {
|
type TOTPConfiguration struct {
|
||||||
Disable bool `koanf:"disable"`
|
Disable bool `koanf:"disable"`
|
||||||
Issuer string `koanf:"issuer"`
|
Issuer string `koanf:"issuer"`
|
||||||
Algorithm string `koanf:"algorithm"`
|
Algorithm string `koanf:"algorithm"`
|
||||||
Digits uint `koanf:"digits"`
|
Digits uint `koanf:"digits"`
|
||||||
Period uint `koanf:"period"`
|
Period uint `koanf:"period"`
|
||||||
Skew *uint `koanf:"skew"`
|
Skew *uint `koanf:"skew"`
|
||||||
|
SecretSize uint `koanf:"secret_size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultOtpSkew = uint(1)
|
var defaultOtpSkew = uint(1)
|
||||||
|
|
||||||
// DefaultTOTPConfiguration represents default configuration parameters for TOTP generation.
|
// DefaultTOTPConfiguration represents default configuration parameters for TOTP generation.
|
||||||
var DefaultTOTPConfiguration = TOTPConfiguration{
|
var DefaultTOTPConfiguration = TOTPConfiguration{
|
||||||
Issuer: "Authelia",
|
Issuer: "Authelia",
|
||||||
Algorithm: TOTPAlgorithmSHA1,
|
Algorithm: TOTPAlgorithmSHA1,
|
||||||
Digits: 6,
|
Digits: 6,
|
||||||
Period: 30,
|
Period: 30,
|
||||||
Skew: &defaultOtpSkew,
|
Skew: &defaultOtpSkew,
|
||||||
|
SecretSize: TOTPSecretSizeDefault,
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,9 +104,10 @@ const (
|
||||||
|
|
||||||
// TOTP Error constants.
|
// TOTP Error constants.
|
||||||
const (
|
const (
|
||||||
errFmtTOTPInvalidAlgorithm = "totp: option 'algorithm' must be one of '%s' but it is configured as '%s'"
|
errFmtTOTPInvalidAlgorithm = "totp: option 'algorithm' must be one of '%s' but it is configured as '%s'"
|
||||||
errFmtTOTPInvalidPeriod = "totp: option 'period' option must be 15 or more but it is configured as '%d'"
|
errFmtTOTPInvalidPeriod = "totp: option 'period' option must be 15 or more but it is configured as '%d'"
|
||||||
errFmtTOTPInvalidDigits = "totp: option 'digits' must be 6 or 8 but it is configured as '%d'"
|
errFmtTOTPInvalidDigits = "totp: option 'digits' must be 6 or 8 but it is configured as '%d'"
|
||||||
|
errFmtTOTPInvalidSecretSize = "totp: option 'secret_size' must be %d or higher but it is configured as '%d'" //nolint:gosec
|
||||||
)
|
)
|
||||||
|
|
||||||
// Storage Error constants.
|
// Storage Error constants.
|
||||||
|
@ -114,7 +115,7 @@ const (
|
||||||
errStrStorage = "storage: configuration for a 'local', 'mysql' or 'postgres' database must be provided"
|
errStrStorage = "storage: configuration for a 'local', 'mysql' or 'postgres' database must be provided"
|
||||||
errStrStorageEncryptionKeyMustBeProvided = "storage: option 'encryption_key' must is required"
|
errStrStorageEncryptionKeyMustBeProvided = "storage: option 'encryption_key' must is required"
|
||||||
errStrStorageEncryptionKeyTooShort = "storage: option 'encryption_key' must be 20 characters or longer"
|
errStrStorageEncryptionKeyTooShort = "storage: option 'encryption_key' must be 20 characters or longer"
|
||||||
errFmtStorageUserPassMustBeProvided = "storage: %s: option 'username' and 'password' are required" //nolint: gosec
|
errFmtStorageUserPassMustBeProvided = "storage: %s: option 'username' and 'password' are required" //nolint:gosec
|
||||||
errFmtStorageOptionMustBeProvided = "storage: %s: option '%s' is required"
|
errFmtStorageOptionMustBeProvided = "storage: %s: option '%s' is required"
|
||||||
errFmtStoragePostgreSQLInvalidSSLMode = "storage: postgres: ssl: option 'mode' must be one of '%s' but it is configured as '%s'"
|
errFmtStoragePostgreSQLInvalidSSLMode = "storage: postgres: ssl: option 'mode' must be one of '%s' but it is configured as '%s'"
|
||||||
)
|
)
|
||||||
|
@ -325,6 +326,7 @@ var ValidKeys = []string{
|
||||||
"totp.digits",
|
"totp.digits",
|
||||||
"totp.period",
|
"totp.period",
|
||||||
"totp.skew",
|
"totp.skew",
|
||||||
|
"totp.secret_size",
|
||||||
|
|
||||||
// Webauthn Keys.
|
// Webauthn Keys.
|
||||||
"webauthn.disable",
|
"webauthn.disable",
|
||||||
|
|
|
@ -10,6 +10,10 @@ import (
|
||||||
|
|
||||||
// ValidateTOTP validates and update TOTP configuration.
|
// ValidateTOTP validates and update TOTP configuration.
|
||||||
func ValidateTOTP(config *schema.Configuration, validator *schema.StructValidator) {
|
func ValidateTOTP(config *schema.Configuration, validator *schema.StructValidator) {
|
||||||
|
if config.TOTP.Disable {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if config.TOTP.Issuer == "" {
|
if config.TOTP.Issuer == "" {
|
||||||
config.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer
|
config.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer
|
||||||
}
|
}
|
||||||
|
@ -39,4 +43,10 @@ func ValidateTOTP(config *schema.Configuration, validator *schema.StructValidato
|
||||||
if config.TOTP.Skew == nil {
|
if config.TOTP.Skew == nil {
|
||||||
config.TOTP.Skew = schema.DefaultTOTPConfiguration.Skew
|
config.TOTP.Skew = schema.DefaultTOTPConfiguration.Skew
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.TOTP.SecretSize == 0 {
|
||||||
|
config.TOTP.SecretSize = schema.DefaultTOTPConfiguration.SecretSize
|
||||||
|
} else if config.TOTP.SecretSize < schema.TOTPSecretSizeMinimum {
|
||||||
|
validator.Push(fmt.Errorf(errFmtTOTPInvalidSecretSize, schema.TOTPSecretSizeMinimum, config.TOTP.SecretSize))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -11,63 +10,112 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShouldSetDefaultTOTPValues(t *testing.T) {
|
func TestValidateTOTP(t *testing.T) {
|
||||||
validator := schema.NewStructValidator()
|
testCases := []struct {
|
||||||
config := &schema.Configuration{
|
desc string
|
||||||
TOTP: schema.TOTPConfiguration{},
|
have schema.TOTPConfiguration
|
||||||
}
|
expected schema.TOTPConfiguration
|
||||||
|
errs []string
|
||||||
ValidateTOTP(config, validator)
|
warns []string
|
||||||
|
}{
|
||||||
require.Len(t, validator.Errors(), 0)
|
{
|
||||||
assert.Equal(t, "Authelia", config.TOTP.Issuer)
|
desc: "ShouldSetDefaultTOTPValues",
|
||||||
assert.Equal(t, schema.DefaultTOTPConfiguration.Algorithm, config.TOTP.Algorithm)
|
expected: schema.DefaultTOTPConfiguration,
|
||||||
assert.Equal(t, schema.DefaultTOTPConfiguration.Skew, config.TOTP.Skew)
|
},
|
||||||
assert.Equal(t, schema.DefaultTOTPConfiguration.Period, config.TOTP.Period)
|
{
|
||||||
}
|
desc: "ShouldNotSetDefaultTOTPValuesWhenDisabled",
|
||||||
|
have: schema.TOTPConfiguration{Disable: true},
|
||||||
func TestShouldNormalizeTOTPAlgorithm(t *testing.T) {
|
expected: schema.TOTPConfiguration{Disable: true},
|
||||||
validator := schema.NewStructValidator()
|
},
|
||||||
|
{
|
||||||
config := &schema.Configuration{
|
desc: "ShouldNormalizeTOTPAlgorithm",
|
||||||
TOTP: schema.TOTPConfiguration{
|
have: schema.TOTPConfiguration{
|
||||||
Algorithm: "sha1",
|
Algorithm: "sha1",
|
||||||
|
Digits: 6,
|
||||||
|
Period: 30,
|
||||||
|
SecretSize: 32,
|
||||||
|
Skew: schema.DefaultTOTPConfiguration.Skew,
|
||||||
|
Issuer: "abc",
|
||||||
|
},
|
||||||
|
expected: schema.TOTPConfiguration{
|
||||||
|
Algorithm: "SHA1",
|
||||||
|
Digits: 6,
|
||||||
|
Period: 30,
|
||||||
|
SecretSize: 32,
|
||||||
|
Skew: schema.DefaultTOTPConfiguration.Skew,
|
||||||
|
Issuer: "abc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ShouldRaiseErrorWhenInvalidTOTPAlgorithm",
|
||||||
|
have: schema.TOTPConfiguration{
|
||||||
|
Algorithm: "sha3",
|
||||||
|
Digits: 6,
|
||||||
|
Period: 30,
|
||||||
|
SecretSize: 32,
|
||||||
|
Skew: schema.DefaultTOTPConfiguration.Skew,
|
||||||
|
Issuer: "abc",
|
||||||
|
},
|
||||||
|
errs: []string{"totp: option 'algorithm' must be one of 'SHA1', 'SHA256', 'SHA512' but it is configured as 'SHA3'"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ShouldRaiseErrorWhenInvalidTOTPValue",
|
||||||
|
have: schema.TOTPConfiguration{
|
||||||
|
Algorithm: "sha3",
|
||||||
|
Period: 5,
|
||||||
|
Digits: 20,
|
||||||
|
SecretSize: 10,
|
||||||
|
Skew: schema.DefaultTOTPConfiguration.Skew,
|
||||||
|
Issuer: "abc",
|
||||||
|
},
|
||||||
|
errs: []string{
|
||||||
|
"totp: option 'algorithm' must be one of 'SHA1', 'SHA256', 'SHA512' but it is configured as 'SHA3'",
|
||||||
|
"totp: option 'period' option must be 15 or more but it is configured as '5'",
|
||||||
|
"totp: option 'digits' must be 6 or 8 but it is configured as '20'",
|
||||||
|
"totp: option 'secret_size' must be 20 or higher but it is configured as '10'",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateTOTP(config, validator)
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
validator := schema.NewStructValidator()
|
||||||
|
config := &schema.Configuration{TOTP: tc.have}
|
||||||
|
|
||||||
assert.Len(t, validator.Errors(), 0)
|
ValidateTOTP(config, validator)
|
||||||
assert.Equal(t, "SHA1", config.TOTP.Algorithm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShouldRaiseErrorWhenInvalidTOTPAlgorithm(t *testing.T) {
|
errs := validator.Errors()
|
||||||
validator := schema.NewStructValidator()
|
warns := validator.Warnings()
|
||||||
|
|
||||||
config := &schema.Configuration{
|
if len(tc.errs) == 0 {
|
||||||
TOTP: schema.TOTPConfiguration{
|
assert.Len(t, errs, 0)
|
||||||
Algorithm: "sha3",
|
assert.Len(t, warns, 0)
|
||||||
},
|
assert.Equal(t, tc.expected.Disable, config.TOTP.Disable)
|
||||||
|
assert.Equal(t, tc.expected.Issuer, config.TOTP.Issuer)
|
||||||
|
assert.Equal(t, tc.expected.Algorithm, config.TOTP.Algorithm)
|
||||||
|
assert.Equal(t, tc.expected.Skew, config.TOTP.Skew)
|
||||||
|
assert.Equal(t, tc.expected.Period, config.TOTP.Period)
|
||||||
|
assert.Equal(t, tc.expected.SecretSize, config.TOTP.SecretSize)
|
||||||
|
} else {
|
||||||
|
expectedErrs := len(tc.errs)
|
||||||
|
|
||||||
|
require.Len(t, errs, expectedErrs)
|
||||||
|
|
||||||
|
for i := 0; i < expectedErrs; i++ {
|
||||||
|
t.Run(fmt.Sprintf("Err%d", i+1), func(t *testing.T) {
|
||||||
|
assert.EqualError(t, errs[i], tc.errs[i])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedWarns := len(tc.warns)
|
||||||
|
require.Len(t, warns, expectedWarns)
|
||||||
|
|
||||||
|
for i := 0; i < expectedWarns; i++ {
|
||||||
|
t.Run(fmt.Sprintf("Err%d", i+1), func(t *testing.T) {
|
||||||
|
assert.EqualError(t, warns[i], tc.warns[i])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateTOTP(config, validator)
|
|
||||||
|
|
||||||
require.Len(t, validator.Errors(), 1)
|
|
||||||
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtTOTPInvalidAlgorithm, strings.Join(schema.TOTPPossibleAlgorithms, "', '"), "SHA3"))
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,18 +51,18 @@ func (mr *MockTOTPMockRecorder) Generate(arg0 interface{}) *gomock.Call {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateCustom mocks base method.
|
// GenerateCustom mocks base method.
|
||||||
func (m *MockTOTP) GenerateCustom(arg0, arg1 string, arg2, arg3, arg4 uint) (*model.TOTPConfiguration, error) {
|
func (m *MockTOTP) GenerateCustom(arg0, arg1, arg2 string, arg3, arg4, arg5 uint) (*model.TOTPConfiguration, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "GenerateCustom", arg0, arg1, arg2, arg3, arg4)
|
ret := m.ctrl.Call(m, "GenerateCustom", arg0, arg1, arg2, arg3, arg4, arg5)
|
||||||
ret0, _ := ret[0].(*model.TOTPConfiguration)
|
ret0, _ := ret[0].(*model.TOTPConfiguration)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateCustom indicates an expected call of GenerateCustom.
|
// GenerateCustom indicates an expected call of GenerateCustom.
|
||||||
func (mr *MockTOTPMockRecorder) GenerateCustom(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
|
func (mr *MockTOTPMockRecorder) GenerateCustom(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateCustom", reflect.TypeOf((*MockTOTP)(nil).GenerateCustom), arg0, arg1, arg2, arg3, arg4)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateCustom", reflect.TypeOf((*MockTOTP)(nil).GenerateCustom), arg0, arg1, arg2, arg3, arg4, arg5)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate mocks base method.
|
// Validate mocks base method.
|
||||||
|
|
|
@ -7,6 +7,6 @@ import (
|
||||||
// Provider for TOTP functionality.
|
// Provider for TOTP functionality.
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
Generate(username string) (config *model.TOTPConfiguration, err error)
|
Generate(username string) (config *model.TOTPConfiguration, err error)
|
||||||
GenerateCustom(username string, algorithm string, digits, period, secretSize uint) (config *model.TOTPConfiguration, err error)
|
GenerateCustom(username string, algorithm, secret string, digits, period, secretSize uint) (config *model.TOTPConfiguration, err error)
|
||||||
Validate(token string, config *model.TOTPConfiguration) (valid bool, err error)
|
Validate(token string, config *model.TOTPConfiguration) (valid bool, err error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package totp
|
package totp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base32"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pquerna/otp"
|
"github.com/pquerna/otp"
|
||||||
|
@ -32,13 +34,22 @@ type TimeBased struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateCustom generates a TOTP with custom options.
|
// GenerateCustom generates a TOTP with custom options.
|
||||||
func (p TimeBased) GenerateCustom(username, algorithm string, digits, period, secretSize uint) (config *model.TOTPConfiguration, err error) {
|
func (p TimeBased) GenerateCustom(username, algorithm, secret string, digits, period, secretSize uint) (config *model.TOTPConfiguration, err error) {
|
||||||
var key *otp.Key
|
var key *otp.Key
|
||||||
|
|
||||||
|
var secretData []byte
|
||||||
|
|
||||||
|
if secret != "" {
|
||||||
|
if secretData, err = base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("totp generate failed: error decoding base32 string: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
opts := totp.GenerateOpts{
|
opts := totp.GenerateOpts{
|
||||||
Issuer: p.config.Issuer,
|
Issuer: p.config.Issuer,
|
||||||
AccountName: username,
|
AccountName: username,
|
||||||
Period: period,
|
Period: period,
|
||||||
|
Secret: secretData,
|
||||||
SecretSize: secretSize,
|
SecretSize: secretSize,
|
||||||
Digits: otp.Digits(digits),
|
Digits: otp.Digits(digits),
|
||||||
Algorithm: otpStringToAlgo(algorithm),
|
Algorithm: otpStringToAlgo(algorithm),
|
||||||
|
@ -63,7 +74,7 @@ func (p TimeBased) GenerateCustom(username, algorithm string, digits, period, se
|
||||||
|
|
||||||
// Generate generates a TOTP with default options.
|
// Generate generates a TOTP with default options.
|
||||||
func (p TimeBased) Generate(username string) (config *model.TOTPConfiguration, err error) {
|
func (p TimeBased) Generate(username string) (config *model.TOTPConfiguration, err error) {
|
||||||
return p.GenerateCustom(username, p.config.Algorithm, p.config.Digits, p.config.Period, 32)
|
return p.GenerateCustom(username, p.config.Algorithm, "", p.config.Digits, p.config.Period, p.config.SecretSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the token against the given configuration.
|
// Validate the token against the given configuration.
|
||||||
|
|
|
@ -6,65 +6,127 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTOTPGenerateCustom(t *testing.T) {
|
func TestTOTPGenerateCustom(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
username, algorithm, secret string
|
||||||
|
digits, period, secretSize uint
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "ShouldGenerateSHA1",
|
||||||
|
username: "john",
|
||||||
|
algorithm: "SHA1",
|
||||||
|
digits: 6,
|
||||||
|
period: 30,
|
||||||
|
secretSize: 32,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ShouldGenerateLongSecret",
|
||||||
|
username: "john",
|
||||||
|
algorithm: "SHA1",
|
||||||
|
digits: 6,
|
||||||
|
period: 30,
|
||||||
|
secretSize: 42,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ShouldGenerateSHA256",
|
||||||
|
username: "john",
|
||||||
|
algorithm: "SHA256",
|
||||||
|
digits: 6,
|
||||||
|
period: 30,
|
||||||
|
secretSize: 32,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ShouldGenerateSHA512",
|
||||||
|
username: "john",
|
||||||
|
algorithm: "SHA512",
|
||||||
|
digits: 6,
|
||||||
|
period: 30,
|
||||||
|
secretSize: 32,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ShouldGenerateWithSecret",
|
||||||
|
username: "john",
|
||||||
|
algorithm: "SHA512",
|
||||||
|
secret: "ONTGOYLTMZQXGZDBONSGC43EMFZWMZ3BONTWMYLTMRQXGZBSGMYTEMZRMFYXGZDBONSA",
|
||||||
|
digits: 6,
|
||||||
|
period: 30,
|
||||||
|
secretSize: 32,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ShouldGenerateWithBadSecretB32Data",
|
||||||
|
username: "john",
|
||||||
|
algorithm: "SHA512",
|
||||||
|
secret: "@#UNH$IK!J@N#EIKJ@U!NIJKUN@#WIK",
|
||||||
|
digits: 6,
|
||||||
|
period: 30,
|
||||||
|
secretSize: 32,
|
||||||
|
err: "totp generate failed: error decoding base32 string: illegal base32 data at input byte 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ShouldGenerateWithBadSecretLength",
|
||||||
|
username: "john",
|
||||||
|
algorithm: "SHA512",
|
||||||
|
secret: "ONTGOYLTMZQXGZD",
|
||||||
|
digits: 6,
|
||||||
|
period: 30,
|
||||||
|
secretSize: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
totp := NewTimeBasedProvider(schema.TOTPConfiguration{
|
totp := NewTimeBasedProvider(schema.TOTPConfiguration{
|
||||||
Issuer: "Authelia",
|
Issuer: "Authelia",
|
||||||
Algorithm: "SHA1",
|
Algorithm: "SHA1",
|
||||||
Digits: 6,
|
Digits: 6,
|
||||||
Period: 30,
|
Period: 30,
|
||||||
|
SecretSize: 32,
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, uint(1), totp.skew)
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
c, err := totp.GenerateCustom(tc.username, tc.algorithm, tc.secret, tc.digits, tc.period, tc.secretSize)
|
||||||
|
if tc.err == "" {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
require.NotNil(t, c)
|
||||||
|
assert.Equal(t, tc.period, c.Period)
|
||||||
|
assert.Equal(t, tc.digits, c.Digits)
|
||||||
|
assert.Equal(t, tc.algorithm, c.Algorithm)
|
||||||
|
|
||||||
config, err := totp.GenerateCustom("john", "SHA1", 6, 30, 32)
|
expectedSecretLen := int(tc.secretSize)
|
||||||
assert.NoError(t, err)
|
if tc.secret != "" {
|
||||||
|
expectedSecretLen = base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(tc.secret))
|
||||||
|
}
|
||||||
|
|
||||||
assert.Equal(t, uint(6), config.Digits)
|
secret := make([]byte, expectedSecretLen)
|
||||||
assert.Equal(t, uint(30), config.Period)
|
|
||||||
assert.Equal(t, "SHA1", config.Algorithm)
|
|
||||||
|
|
||||||
assert.Less(t, time.Since(config.CreatedAt), time.Second)
|
n, err := base32.StdEncoding.WithPadding(base32.NoPadding).Decode(secret, c.Secret)
|
||||||
assert.Greater(t, time.Since(config.CreatedAt), time.Second*-1)
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, secret, expectedSecretLen)
|
||||||
secret := make([]byte, base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(config.Secret)))
|
assert.Equal(t, expectedSecretLen, n)
|
||||||
|
} else {
|
||||||
_, err = base32.StdEncoding.WithPadding(base32.NoPadding).Decode(secret, config.Secret)
|
assert.Nil(t, c)
|
||||||
assert.NoError(t, err)
|
assert.EqualError(t, err, tc.err)
|
||||||
assert.Len(t, secret, 32)
|
}
|
||||||
|
})
|
||||||
config, err = totp.GenerateCustom("john", "SHA1", 6, 30, 42)
|
}
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, uint(6), config.Digits)
|
|
||||||
assert.Equal(t, uint(30), config.Period)
|
|
||||||
assert.Equal(t, "SHA1", config.Algorithm)
|
|
||||||
|
|
||||||
assert.Less(t, time.Since(config.CreatedAt), time.Second)
|
|
||||||
assert.Greater(t, time.Since(config.CreatedAt), time.Second*-1)
|
|
||||||
|
|
||||||
secret = make([]byte, base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(config.Secret)))
|
|
||||||
|
|
||||||
_, err = base32.StdEncoding.WithPadding(base32.NoPadding).Decode(secret, config.Secret)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, secret, 42)
|
|
||||||
|
|
||||||
_, err = totp.GenerateCustom("", "SHA1", 6, 30, 32)
|
|
||||||
assert.EqualError(t, err, "AccountName must be set")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTOTPGenerate(t *testing.T) {
|
func TestTOTPGenerate(t *testing.T) {
|
||||||
skew := uint(2)
|
skew := uint(2)
|
||||||
|
|
||||||
totp := NewTimeBasedProvider(schema.TOTPConfiguration{
|
totp := NewTimeBasedProvider(schema.TOTPConfiguration{
|
||||||
Issuer: "Authelia",
|
Issuer: "Authelia",
|
||||||
Algorithm: "SHA256",
|
Algorithm: "SHA256",
|
||||||
Digits: 8,
|
Digits: 8,
|
||||||
Period: 60,
|
Period: 60,
|
||||||
Skew: &skew,
|
Skew: &skew,
|
||||||
|
SecretSize: 32,
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, uint(2), totp.skew)
|
assert.Equal(t, uint(2), totp.skew)
|
||||||
|
|
Loading…
Reference in New Issue