feat(totp): secret customization (#2681)

Allow customizing the shared secrets size specifically for apps which don't support 256bit shared secrets.
This commit is contained in:
James Elliott 2022-04-08 09:01:01 +10:00 committed by GitHub
parent fe08bf56b0
commit 9b6bcca1ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 333 additions and 143 deletions

View File

@ -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
##

View File

@ -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

View File

@ -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")

View File

@ -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"
@ -63,10 +65,11 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
"postgres.ssl.certificate": "storage.postgres.ssl.certificate",
"postgres.ssl.key": "storage.postgres.ssl.key",
"period": "totp.period",
"digits": "totp.digits",
"algorithm": "totp.algorithm",
"issuer": "totp.issuer",
"period": "totp.period",
"digits": "totp.digits",
"algorithm": "totp.algorithm",
"issuer": "totp.issuer",
"secret-size": "totp.secret_size",
}
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) {
var (
provider storage.Provider
ctx = context.Background()
c *model.TOTPConfiguration
force bool
filename string
file *os.File
img image.Image
provider storage.Provider
ctx = context.Background()
c *model.TOTPConfiguration
force bool
filename, secret string
file *os.File
img image.Image
)
provider = getStorageProvider()
@ -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

View File

@ -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
##

View File

@ -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
)

View File

@ -2,21 +2,23 @@ package schema
// TOTPConfiguration represents the configuration related to TOTP options.
type TOTPConfiguration struct {
Disable bool `koanf:"disable"`
Issuer string `koanf:"issuer"`
Algorithm string `koanf:"algorithm"`
Digits uint `koanf:"digits"`
Period uint `koanf:"period"`
Skew *uint `koanf:"skew"`
Disable bool `koanf:"disable"`
Issuer string `koanf:"issuer"`
Algorithm string `koanf:"algorithm"`
Digits uint `koanf:"digits"`
Period uint `koanf:"period"`
Skew *uint `koanf:"skew"`
SecretSize uint `koanf:"secret_size"`
}
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,
Issuer: "Authelia",
Algorithm: TOTPAlgorithmSHA1,
Digits: 6,
Period: 30,
Skew: &defaultOtpSkew,
SecretSize: TOTPSecretSizeDefault,
}

View File

@ -104,9 +104,10 @@ const (
// TOTP Error constants.
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'"
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",

View File

@ -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))
}
}

View File

@ -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{
Algorithm: "sha1",
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",
},
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)
assert.Equal(t, "SHA1", config.TOTP.Algorithm)
}
ValidateTOTP(config, validator)
func TestShouldRaiseErrorWhenInvalidTOTPAlgorithm(t *testing.T) {
validator := schema.NewStructValidator()
errs := validator.Errors()
warns := validator.Warnings()
config := &schema.Configuration{
TOTP: schema.TOTPConfiguration{
Algorithm: "sha3",
},
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])
})
}
})
}
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))
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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.

View File

@ -6,65 +6,127 @@ 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,
Issuer: "Authelia",
Algorithm: "SHA1",
Digits: 6,
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)
assert.NoError(t, err)
expectedSecretLen := int(tc.secretSize)
if tc.secret != "" {
expectedSecretLen = base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(tc.secret))
}
assert.Equal(t, uint(6), config.Digits)
assert.Equal(t, uint(30), config.Period)
assert.Equal(t, "SHA1", config.Algorithm)
secret := make([]byte, expectedSecretLen)
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, 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")
n, err := base32.StdEncoding.WithPadding(base32.NoPadding).Decode(secret, c.Secret)
assert.NoError(t, err)
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) {
skew := uint(2)
totp := NewTimeBasedProvider(schema.TOTPConfiguration{
Issuer: "Authelia",
Algorithm: "SHA256",
Digits: 8,
Period: 60,
Skew: &skew,
Issuer: "Authelia",
Algorithm: "SHA256",
Digits: 8,
Period: 60,
Skew: &skew,
SecretSize: 32,
})
assert.Equal(t, uint(2), totp.skew)