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
|
||||
## 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
|
||||
##
|
||||
|
|
|
@ -7,9 +7,9 @@ nav_order: 16
|
|||
|
||||
# Time-based One-Time Password
|
||||
|
||||
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.
|
||||
The OTP method _Authelia_ uses is the Time-Based One-Time Password Algorithm (TOTP) [RFC6238] which is an extension of
|
||||
HMAC-Based One-Time Password Algorithm (HOTP) [RFC4226]. You have the option to tune the settings of theTOTP generation,
|
||||
and you can see a full example of TOTP configuration below, as well as sections describing them.
|
||||
|
||||
## Configuration
|
||||
```yaml
|
||||
|
@ -20,6 +20,7 @@ totp:
|
|||
digits: 6
|
||||
period: 30
|
||||
skew: 1
|
||||
secret_size: 32
|
||||
```
|
||||
|
||||
## Options
|
||||
|
@ -139,6 +140,21 @@ other.
|
|||
|
||||
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
|
||||
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
|
||||
|
@ -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.
|
||||
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
|
||||
These options affect 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
|
||||
|
@ -192,3 +207,6 @@ Help:
|
|||
```shell
|
||||
$ 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 (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
)
|
||||
|
||||
// NewStorageCmd returns a new storage *cobra.Command.
|
||||
|
@ -107,6 +109,8 @@ func newStorageTOTPGenerateCmd() (cmd *cobra.Command) {
|
|||
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("digits", 6, "set the TOTP digits")
|
||||
cmd.Flags().String("algorithm", "SHA1", "set the TOTP algorithm")
|
||||
|
|
|
@ -2,6 +2,7 @@ package commands
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base32"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration"
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
|
@ -67,6 +69,7 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
|
|||
"digits": "totp.digits",
|
||||
"algorithm": "totp.algorithm",
|
||||
"issuer": "totp.issuer",
|
||||
"secret-size": "totp.secret_size",
|
||||
}
|
||||
|
||||
sources = append(sources, configuration.NewEnvironmentSource(configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter))
|
||||
|
@ -205,7 +208,7 @@ func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
|
|||
ctx = context.Background()
|
||||
c *model.TOTPConfiguration
|
||||
force bool
|
||||
filename string
|
||||
filename, secret string
|
||||
file *os.File
|
||||
img image.Image
|
||||
)
|
||||
|
@ -216,25 +219,19 @@ func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
|
|||
_ = provider.Close()
|
||||
}()
|
||||
|
||||
if force, err = cmd.Flags().GetBool("force"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if filename, err = cmd.Flags().GetString("path"); err != nil {
|
||||
if force, filename, secret, err = storageTOTPGenerateRunEOptsFromFlags(cmd.Flags()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
if err != nil && !errors.Is(err, storage.ErrNoTOTPConfiguration) {
|
||||
} else if err != nil && !errors.Is(err, storage.ErrNoTOTPConfiguration) {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -271,6 +268,28 @@ func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
|
|||
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) {
|
||||
var (
|
||||
provider storage.Provider
|
||||
|
|
|
@ -127,6 +127,9 @@ totp:
|
|||
skew: 1
|
||||
## 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
|
||||
##
|
||||
|
|
|
@ -44,3 +44,11 @@ var (
|
|||
// TOTPPossibleAlgorithms is a list of valid TOTP Algorithms.
|
||||
TOTPPossibleAlgorithms = []string{TOTPAlgorithmSHA1, TOTPAlgorithmSHA256, TOTPAlgorithmSHA512}
|
||||
)
|
||||
|
||||
const (
|
||||
// TOTPSecretSizeDefault is the default secret size.
|
||||
TOTPSecretSizeDefault = 32
|
||||
|
||||
// TOTPSecretSizeMinimum is the minimum secret size.
|
||||
TOTPSecretSizeMinimum = 20
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@ type TOTPConfiguration struct {
|
|||
Digits uint `koanf:"digits"`
|
||||
Period uint `koanf:"period"`
|
||||
Skew *uint `koanf:"skew"`
|
||||
SecretSize uint `koanf:"secret_size"`
|
||||
}
|
||||
|
||||
var defaultOtpSkew = uint(1)
|
||||
|
@ -19,4 +20,5 @@ var DefaultTOTPConfiguration = TOTPConfiguration{
|
|||
Digits: 6,
|
||||
Period: 30,
|
||||
Skew: &defaultOtpSkew,
|
||||
SecretSize: TOTPSecretSizeDefault,
|
||||
}
|
||||
|
|
|
@ -107,6 +107,7 @@ const (
|
|||
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'"
|
||||
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.
|
||||
|
@ -114,7 +115,7 @@ const (
|
|||
errStrStorage = "storage: configuration for a 'local', 'mysql' or 'postgres' database must be provided"
|
||||
errStrStorageEncryptionKeyMustBeProvided = "storage: option 'encryption_key' must is required"
|
||||
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"
|
||||
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.period",
|
||||
"totp.skew",
|
||||
"totp.secret_size",
|
||||
|
||||
// Webauthn Keys.
|
||||
"webauthn.disable",
|
||||
|
|
|
@ -10,6 +10,10 @@ import (
|
|||
|
||||
// ValidateTOTP validates and update TOTP configuration.
|
||||
func ValidateTOTP(config *schema.Configuration, validator *schema.StructValidator) {
|
||||
if config.TOTP.Disable {
|
||||
return
|
||||
}
|
||||
|
||||
if config.TOTP.Issuer == "" {
|
||||
config.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer
|
||||
}
|
||||
|
@ -39,4 +43,10 @@ func ValidateTOTP(config *schema.Configuration, validator *schema.StructValidato
|
|||
if config.TOTP.Skew == nil {
|
||||
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 (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -11,63 +10,112 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
)
|
||||
|
||||
func TestShouldSetDefaultTOTPValues(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := &schema.Configuration{
|
||||
TOTP: schema.TOTPConfiguration{},
|
||||
}
|
||||
|
||||
ValidateTOTP(config, validator)
|
||||
|
||||
require.Len(t, validator.Errors(), 0)
|
||||
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 TestShouldNormalizeTOTPAlgorithm(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
|
||||
config := &schema.Configuration{
|
||||
TOTP: schema.TOTPConfiguration{
|
||||
func TestValidateTOTP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
have schema.TOTPConfiguration
|
||||
expected schema.TOTPConfiguration
|
||||
errs []string
|
||||
warns []string
|
||||
}{
|
||||
{
|
||||
desc: "ShouldSetDefaultTOTPValues",
|
||||
expected: schema.DefaultTOTPConfiguration,
|
||||
},
|
||||
{
|
||||
desc: "ShouldNotSetDefaultTOTPValuesWhenDisabled",
|
||||
have: schema.TOTPConfiguration{Disable: true},
|
||||
expected: schema.TOTPConfiguration{Disable: true},
|
||||
},
|
||||
{
|
||||
desc: "ShouldNormalizeTOTPAlgorithm",
|
||||
have: schema.TOTPConfiguration{
|
||||
Algorithm: "sha1",
|
||||
Digits: 6,
|
||||
Period: 30,
|
||||
SecretSize: 32,
|
||||
Skew: schema.DefaultTOTPConfiguration.Skew,
|
||||
Issuer: "abc",
|
||||
},
|
||||
}
|
||||
|
||||
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{
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
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{
|
||||
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'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := &schema.Configuration{TOTP: tc.have}
|
||||
|
||||
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))
|
||||
errs := validator.Errors()
|
||||
warns := validator.Warnings()
|
||||
|
||||
if len(tc.errs) == 0 {
|
||||
assert.Len(t, errs, 0)
|
||||
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])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,18 +51,18 @@ func (mr *MockTOTPMockRecorder) Generate(arg0 interface{}) *gomock.Call {
|
|||
}
|
||||
|
||||
// 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()
|
||||
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)
|
||||
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 {
|
||||
func (mr *MockTOTPMockRecorder) GenerateCustom(arg0, arg1, arg2, arg3, arg4, arg5 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)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateCustom", reflect.TypeOf((*MockTOTP)(nil).GenerateCustom), arg0, arg1, arg2, arg3, arg4, arg5)
|
||||
}
|
||||
|
||||
// Validate mocks base method.
|
||||
|
|
|
@ -7,6 +7,6 @@ import (
|
|||
// Provider for TOTP functionality.
|
||||
type Provider interface {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package totp
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
|
@ -32,13 +34,22 @@ type TimeBased struct {
|
|||
}
|
||||
|
||||
// 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 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{
|
||||
Issuer: p.config.Issuer,
|
||||
AccountName: username,
|
||||
Period: period,
|
||||
Secret: secretData,
|
||||
SecretSize: secretSize,
|
||||
Digits: otp.Digits(digits),
|
||||
Algorithm: otpStringToAlgo(algorithm),
|
||||
|
@ -63,7 +74,7 @@ func (p TimeBased) GenerateCustom(username, algorithm string, digits, period, se
|
|||
|
||||
// Generate generates a TOTP with default options.
|
||||
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.
|
||||
|
|
|
@ -6,54 +6,115 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
)
|
||||
|
||||
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{
|
||||
Issuer: "Authelia",
|
||||
Algorithm: "SHA1",
|
||||
Digits: 6,
|
||||
Period: 30,
|
||||
SecretSize: 32,
|
||||
})
|
||||
|
||||
assert.Equal(t, uint(1), totp.skew)
|
||||
|
||||
config, err := totp.GenerateCustom("john", "SHA1", 6, 30, 32)
|
||||
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)
|
||||
|
||||
assert.Equal(t, uint(6), config.Digits)
|
||||
assert.Equal(t, uint(30), config.Period)
|
||||
assert.Equal(t, "SHA1", config.Algorithm)
|
||||
expectedSecretLen := int(tc.secretSize)
|
||||
if tc.secret != "" {
|
||||
expectedSecretLen = base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(tc.secret))
|
||||
}
|
||||
|
||||
assert.Less(t, time.Since(config.CreatedAt), time.Second)
|
||||
assert.Greater(t, time.Since(config.CreatedAt), time.Second*-1)
|
||||
secret := make([]byte, expectedSecretLen)
|
||||
|
||||
secret := make([]byte, base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(config.Secret)))
|
||||
|
||||
_, err = base32.StdEncoding.WithPadding(base32.NoPadding).Decode(secret, config.Secret)
|
||||
n, err := base32.StdEncoding.WithPadding(base32.NoPadding).Decode(secret, c.Secret)
|
||||
assert.NoError(t, 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")
|
||||
assert.Len(t, secret, expectedSecretLen)
|
||||
assert.Equal(t, expectedSecretLen, n)
|
||||
} else {
|
||||
assert.Nil(t, c)
|
||||
assert.EqualError(t, err, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOTPGenerate(t *testing.T) {
|
||||
|
@ -65,6 +126,7 @@ func TestTOTPGenerate(t *testing.T) {
|
|||
Digits: 8,
|
||||
Period: 60,
|
||||
Skew: &skew,
|
||||
SecretSize: 32,
|
||||
})
|
||||
|
||||
assert.Equal(t, uint(2), totp.skew)
|
||||
|
|
Loading…
Reference in New Issue