[FEATURE] TOTP Tuning Configuration Options and Fix Timer Graphic (#773)

* Add period TOPT config key to define the time in seconds each OTP is rotated
* Add skew TOTP config to define how many keys either side of the current one should be considered valid
* Add tests and set minimum values
* Update config template
* Use unix epoch for position calculation and Fix QR gen
  * This resolves the timer resetting improperly at the 0 seconds mark and allows for periods longer than 1 minute
* Generate QR based on period
* Fix OTP timer graphic
pull/779/head
James Elliott 2020-03-25 12:48:20 +11:00 committed by GitHub
parent c057c917f6
commit 40fb13ba3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 177 additions and 39 deletions

View File

@ -35,12 +35,21 @@ default_redirection_url: https://home.example.com:8080/
# #
## google_analytics: UA-00000-01 ## google_analytics: UA-00000-01
# TOTP Issuer Name # TOTP Settings
# #
# This will be the issuer name displayed in Google Authenticator # Parameters used for TOTP generation
# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names
totp: 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
issuer: authelia.com issuer: authelia.com
# The period in seconds a one-time password is current for. Changing this will require all users to register
# their TOTP applications again.
# Warning: before changing period read the docs link below.
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://docs.authelia.com/configuration/one-time-password.html#period-and-skew to read the documentation.
# Duo Push API # Duo Push API
# #

View File

@ -7,11 +7,47 @@ nav_order: 6
# One-Time Password # One-Time Password
Applications generating one-time passwords usually displays an issuer to Authelia uses time based one-time passwords as the OTP method. You have
differentiate the various applications registered by the user. 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.
Authelia allows to customize the issuer to differentiate the entry created
by Authelia from others.
totp: totp:
issuer: authelia.com issuer: authelia.com
period: 30
skew: 1
## Issuer
Applications generating one-time passwords usually display an issuer to
differentiate applications registered by the user.
Authelia allows customisation of the issuer to differentiate the entry created
by Authelia from others.
## Period and Skew
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.
### Period
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.
It is recommended to keep this value set to 30, the minimum is 1.
### Skew
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.
It is recommended to keep this value set to 0 or 1, the minimum is 0.

2
go.mod
View File

@ -13,7 +13,7 @@ require (
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fasthttp/router v0.7.0 github.com/fasthttp/router v0.7.0
github.com/fasthttp/session v1.1.7 github.com/fasthttp/session v1.1.7
github.com/go-ldap/ldap/v3 v3.1.7 // indirect github.com/go-ldap/ldap/v3 v3.1.7
github.com/go-sql-driver/mysql v1.5.0 github.com/go-sql-driver/mysql v1.5.0
github.com/golang/mock v1.4.3 github.com/golang/mock v1.4.3
github.com/golang/snappy v0.0.1 // indirect github.com/golang/snappy v0.0.1 // indirect

2
go.sum
View File

@ -223,10 +223,12 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

View File

@ -3,4 +3,6 @@ package schema
// TOTPConfiguration represents the configuration related to TOTP options. // TOTPConfiguration represents the configuration related to TOTP options.
type TOTPConfiguration struct { type TOTPConfiguration struct {
Issuer string `mapstructure:"issuer"` Issuer string `mapstructure:"issuer"`
Period int `mapstructure:"period"`
Skew *int `mapstructure:"skew"`
} }

View File

@ -46,8 +46,8 @@ func Validate(configuration *schema.Configuration, validator *schema.StructValid
if configuration.TOTP == nil { if configuration.TOTP == nil {
configuration.TOTP = &schema.TOTPConfiguration{} configuration.TOTP = &schema.TOTPConfiguration{}
ValidateTOTP(configuration.TOTP, validator)
} }
ValidateTOTP(configuration.TOTP, validator)
if configuration.Notifier == nil { if configuration.Notifier == nil {
validator.Push(fmt.Errorf("A notifier configuration must be provided")) validator.Push(fmt.Errorf("A notifier configuration must be provided"))

View File

@ -1,14 +1,29 @@
package validator package validator
import ( import (
"fmt"
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
) )
const defaultTOTPIssuer = "Authelia" const defaultTOTPIssuer = "Authelia"
const DefaultTOTPPeriod = 30
const DefaultTOTPSkew = 1
// ValidateTOTP validates and update TOTP configuration. // ValidateTOTP validates and update TOTP configuration.
func ValidateTOTP(configuration *schema.TOTPConfiguration, validator *schema.StructValidator) { func ValidateTOTP(configuration *schema.TOTPConfiguration, validator *schema.StructValidator) {
if configuration.Issuer == "" { if configuration.Issuer == "" {
configuration.Issuer = defaultTOTPIssuer configuration.Issuer = defaultTOTPIssuer
} }
if configuration.Period == 0 {
configuration.Period = DefaultTOTPPeriod
} else if configuration.Period < 0 {
validator.Push(fmt.Errorf("TOTP Period must be 1 or more"))
}
if configuration.Skew == nil {
var skew = DefaultTOTPSkew
configuration.Skew = &skew
} else if *configuration.Skew < 0 {
validator.Push(fmt.Errorf("TOTP Skew must be 0 or more"))
}
} }

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestShouldSetDefaultIssuer(t *testing.T) { func TestShouldSetDefaultTOTPValues(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := schema.TOTPConfiguration{} config := schema.TOTPConfiguration{}
@ -16,4 +16,20 @@ func TestShouldSetDefaultIssuer(t *testing.T) {
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, "Authelia", config.Issuer) assert.Equal(t, "Authelia", config.Issuer)
assert.Equal(t, DefaultTOTPSkew, *config.Skew)
assert.Equal(t, DefaultTOTPPeriod, config.Period)
}
func TestShouldRaiseErrorWhenInvalidTOTPMinimumValues(t *testing.T) {
var badSkew = -1
validator := schema.NewStructValidator()
config := schema.TOTPConfiguration{
Period: -5,
Skew: &badSkew,
}
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")
} }

View File

@ -11,12 +11,16 @@ type ExtendedConfigurationBody struct {
// SecondFactorEnabled whether second factor is enabled // SecondFactorEnabled whether second factor is enabled
SecondFactorEnabled bool `json:"second_factor_enabled"` SecondFactorEnabled bool `json:"second_factor_enabled"`
// TOTP Period
TOTPPeriod int `json:"totp_period"`
} }
// ExtendedConfigurationGet get the extended configuration accessible to authenticated users. // ExtendedConfigurationGet get the extended configuration accessible to authenticated users.
func ExtendedConfigurationGet(ctx *middlewares.AutheliaCtx) { func ExtendedConfigurationGet(ctx *middlewares.AutheliaCtx) {
body := ExtendedConfigurationBody{} body := ExtendedConfigurationBody{}
body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F} body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
body.TOTPPeriod = ctx.Configuration.TOTP.Period
if ctx.Configuration.DuoAPI != nil { if ctx.Configuration.DuoAPI != nil {
body.AvailableMethods = append(body.AvailableMethods, authentication.Push) body.AvailableMethods = append(body.AvailableMethods, authentication.Push)

View File

@ -4,9 +4,10 @@ import (
"testing" "testing"
"github.com/authelia/authelia/internal/authorization" "github.com/authelia/authelia/internal/authorization"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/configuration/validator"
"github.com/authelia/authelia/internal/mocks" "github.com/authelia/authelia/internal/mocks"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@ -28,9 +29,15 @@ func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() { func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
s.mock.Ctx.Configuration = schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Period: validator.DefaultTOTPPeriod,
},
}
expectedBody := ExtendedConfigurationBody{ expectedBody := ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f"}, AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: false, SecondFactorEnabled: false,
TOTPPeriod: validator.DefaultTOTPPeriod,
} }
ExtendedConfigurationGet(s.mock.Ctx) ExtendedConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody) s.mock.Assert200OK(s.T(), expectedBody)
@ -39,16 +46,25 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() { func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
s.mock.Ctx.Configuration = schema.Configuration{ s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{}, DuoAPI: &schema.DuoAPIConfiguration{},
TOTP: &schema.TOTPConfiguration{
Period: validator.DefaultTOTPPeriod,
},
} }
expectedBody := ExtendedConfigurationBody{ expectedBody := ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f", "mobile_push"}, AvailableMethods: []string{"totp", "u2f", "mobile_push"},
SecondFactorEnabled: false, SecondFactorEnabled: false,
TOTPPeriod: validator.DefaultTOTPPeriod,
} }
ExtendedConfigurationGet(s.mock.Ctx) ExtendedConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody) s.mock.Assert200OK(s.T(), expectedBody)
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisabledWhenNoRuleIsSetToTwoFactor() { func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisabledWhenNoRuleIsSetToTwoFactor() {
s.mock.Ctx.Configuration = schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Period: validator.DefaultTOTPPeriod,
},
}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
DefaultPolicy: "bypass", DefaultPolicy: "bypass",
Rules: []schema.ACLRule{ Rules: []schema.ACLRule{
@ -70,10 +86,16 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisab
s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{ s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f"}, AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: false, SecondFactorEnabled: false,
TOTPPeriod: validator.DefaultTOTPPeriod,
}) })
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenDefaultPolicySetToTwoFactor() { func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenDefaultPolicySetToTwoFactor() {
s.mock.Ctx.Configuration = schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Period: validator.DefaultTOTPPeriod,
},
}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
DefaultPolicy: "two_factor", DefaultPolicy: "two_factor",
Rules: []schema.ACLRule{ Rules: []schema.ACLRule{
@ -95,10 +117,16 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabl
s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{ s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f"}, AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: true, SecondFactorEnabled: true,
TOTPPeriod: validator.DefaultTOTPPeriod,
}) })
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenSomePolicySetToTwoFactor() { func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenSomePolicySetToTwoFactor() {
s.mock.Ctx.Configuration = schema.Configuration{
TOTP: &schema.TOTPConfiguration{
Period: validator.DefaultTOTPPeriod,
},
}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
DefaultPolicy: "bypass", DefaultPolicy: "bypass",
Rules: []schema.ACLRule{ Rules: []schema.ACLRule{
@ -120,6 +148,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabl
s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{ s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f"}, AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: true, SecondFactorEnabled: true,
TOTPPeriod: validator.DefaultTOTPPeriod,
}) })
} }

View File

@ -41,6 +41,7 @@ func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username strin
Issuer: ctx.Configuration.TOTP.Issuer, Issuer: ctx.Configuration.TOTP.Issuer,
AccountName: username, AccountName: username,
SecretSize: 32, SecretSize: 32,
Period: uint(ctx.Configuration.TOTP.Period),
}) })
if err != nil { if err != nil {

View File

@ -25,7 +25,11 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
return return
} }
isValid := totpVerifier.Verify(bodyJSON.Token, secret) isValid, err := totpVerifier.Verify(bodyJSON.Token, secret)
if err != nil {
ctx.Error(fmt.Errorf("Error occurred during OTP validation for user %s: %s", userSession.Username, err), mfaValidationFailedMessage)
return
}
if !isValid { if !isValid {
ctx.Error(fmt.Errorf("Wrong passcode during TOTP validation for user %s", userSession.Username), mfaValidationFailedMessage) ctx.Error(fmt.Errorf("Wrong passcode during TOTP validation for user %s", userSession.Username), mfaValidationFailedMessage)

View File

@ -40,7 +40,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
verifier.EXPECT(). verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")). Verify(gomock.Eq("abc"), gomock.Eq("secret")).
Return(true) Return(true, nil)
s.mock.Ctx.Configuration.DefaultRedirectionURL = "http://redirection.local" s.mock.Ctx.Configuration.DefaultRedirectionURL = "http://redirection.local"
@ -65,7 +65,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
verifier.EXPECT(). verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")). Verify(gomock.Eq("abc"), gomock.Eq("secret")).
Return(true) Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
@ -86,7 +86,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
verifier.EXPECT(). verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")). Verify(gomock.Eq("abc"), gomock.Eq("secret")).
Return(true) Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
@ -110,7 +110,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
verifier.EXPECT(). verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")). Verify(gomock.Eq("abc"), gomock.Eq("secret")).
Return(true) Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
@ -132,7 +132,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFi
verifier.EXPECT(). verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")). Verify(gomock.Eq("abc"), gomock.Eq("secret")).
Return(true) Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",

View File

@ -1,15 +1,26 @@
package handlers package handlers
import ( import (
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
"time"
) )
type TOTPVerifier interface { type TOTPVerifier interface {
Verify(token, secret string) bool Verify(token, secret string) (bool, error)
} }
type TOTPVerifierImpl struct{} type TOTPVerifierImpl struct {
Period uint
func (tv *TOTPVerifierImpl) Verify(token, secret string) bool { Skew uint
return totp.Validate(token, secret) }
func (tv *TOTPVerifierImpl) Verify(token, secret string) (bool, error) {
opts := totp.ValidateOpts{
Period: tv.Period,
Skew: tv.Skew,
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
}
return totp.ValidateCustom(token, secret, time.Now().UTC(), opts)
} }

View File

@ -33,11 +33,12 @@ func (m *MockTOTPVerifier) EXPECT() *MockTOTPVerifierMockRecorder {
} }
// Verify mocks base method // Verify mocks base method
func (m *MockTOTPVerifier) Verify(token, secret string) bool { func (m *MockTOTPVerifier) Verify(token, secret string) (bool, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Verify", token, secret) ret := m.ctrl.Call(m, "Verify", token, secret)
ret0, _ := ret[0].(bool) ret0, _ := ret[0].(bool)
return ret0 ret1, _ := ret[1].(error)
return ret0, ret1
} }
// Verify indicates an expected call of Verify // Verify indicates an expected call of Verify

View File

@ -61,7 +61,10 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
router.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware( router.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish))) middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish)))
router.POST("/api/secondfactor/totp", autheliaMiddleware( router.POST("/api/secondfactor/totp", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost(&handlers.TOTPVerifierImpl{})))) middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost(&handlers.TOTPVerifierImpl{
Period: uint(configuration.TOTP.Period),
Skew: uint(*configuration.TOTP.Skew),
}))))
// U2F related endpoints // U2F related endpoints
router.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware( router.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware(

View File

@ -26,7 +26,7 @@ export default function (props: Props) {
<circle r="5" cx="13" cy="13" fill="none" <circle r="5" cx="13" cy="13" fill="none"
stroke={color} stroke={color}
strokeWidth="10" strokeWidth="10"
strokeDasharray={`calc(${props.progress} * 31.6 / ${maxProgress}) 31.6`} strokeDasharray={`${props.progress} ${maxProgress}`}
transform="rotate(-90) translate(-26)" /> transform="rotate(-90) translate(-26)" />
</svg> </svg>
) )

View File

@ -4,32 +4,31 @@ import PieChartIcon from "./PieChartIcon";
export interface Props { export interface Props {
width: number; width: number;
height: number; height: number;
period: number;
color?: string; color?: string;
backgroundColor?: string; backgroundColor?: string;
} }
export default function (props: Props) { export default function (props: Props) {
const maxTimeProgress = 1000; const radius = 31.6;
const [timeProgress, setTimeProgress] = useState(0); const [timeProgress, setTimeProgress] = useState(0);
useEffect(() => { useEffect(() => {
// Get the current number of seconds to initialize timer. // Get the current number of seconds to initialize timer.
const initialValue = Math.floor((new Date().getSeconds() % 30) / 30 * maxTimeProgress); const initialValue = (new Date().getTime() / 1000) % props.period / props.period * radius;
setTimeProgress(initialValue); setTimeProgress(initialValue);
const interval = setInterval(() => { const interval = setInterval(() => {
const ms = new Date().getSeconds() * 1000.0 + new Date().getMilliseconds(); const value = (new Date().getTime() / 1000) % props.period / props.period * radius;
const value = (ms % 30000) / 30000 * maxTimeProgress;
setTimeProgress(value); setTimeProgress(value);
}, 100); }, 100);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, [props]);
return ( return (
<PieChartIcon width={props.width} height={props.height} <PieChartIcon width={props.width} height={props.height}
maxProgress={maxTimeProgress} progress={timeProgress} maxProgress={radius}
progress={timeProgress}
backgroundColor={props.backgroundColor} color={props.color} /> backgroundColor={props.backgroundColor} color={props.color} />
) )
} }

View File

@ -7,4 +7,5 @@ export interface Configuration {
export interface ExtendedConfiguration { export interface ExtendedConfiguration {
available_methods: Set<SecondFactorMethod>; available_methods: Set<SecondFactorMethod>;
second_factor_enabled: boolean; second_factor_enabled: boolean;
totp_period: number;
} }

View File

@ -10,6 +10,7 @@ export async function getConfiguration(): Promise<Configuration> {
interface ExtendedConfigurationPayload { interface ExtendedConfigurationPayload {
available_methods: Method2FA[]; available_methods: Method2FA[];
second_factor_enabled: boolean; second_factor_enabled: boolean;
totp_period: number;
} }
export async function getExtendedConfiguration(): Promise<ExtendedConfiguration> { export async function getExtendedConfiguration(): Promise<ExtendedConfiguration> {

View File

@ -10,13 +10,13 @@ import SuccessIcon from "../../../components/SuccessIcon";
export interface Props { export interface Props {
passcode: string; passcode: string;
state: State; state: State;
period: number
onChange: (passcode: string) => void; onChange: (passcode: string) => void;
} }
export default function (props: Props) { export default function (props: Props) {
const style = useStyles(); const style = useStyles();
const dial = ( const dial = (
<span className={style.otpInput} id="otp-input"> <span className={style.otpInput} id="otp-input">
<OtpInput <OtpInput
@ -31,7 +31,7 @@ export default function (props: Props) {
return ( return (
<IconWithContext <IconWithContext
icon={<Icon state={props.state} />} icon={<Icon state={props.state} period={props.period} />}
context={dial} /> context={dial} />
) )
} }
@ -61,12 +61,13 @@ const useStyles = makeStyles(theme => ({
interface IconProps { interface IconProps {
state: State; state: State;
period: number;
} }
function Icon(props: IconProps) { function Icon(props: IconProps) {
return ( return (
<Fragment> <Fragment>
{props.state !== State.Success ? <TimerIcon backgroundColor="#000" color="#FFFFFF" width={64} height={64} /> : null} {props.state !== State.Success ? <TimerIcon backgroundColor="#000" color="#FFFFFF" width={64} height={64} period={props.period} /> : null}
{props.state === State.Success ? <SuccessIcon /> : null} {props.state === State.Success ? <SuccessIcon /> : null}
</Fragment> </Fragment>
) )

View File

@ -16,6 +16,7 @@ export interface Props {
id: string; id: string;
authenticationLevel: AuthenticationLevel; authenticationLevel: AuthenticationLevel;
registered: boolean; registered: boolean;
totp_period: number
onRegisterClick: () => void; onRegisterClick: () => void;
onSignInError: (err: Error) => void; onSignInError: (err: Error) => void;
@ -83,7 +84,8 @@ export default function (props: Props) {
<OTPDial <OTPDial
passcode={passcode} passcode={passcode}
onChange={setPasscode} onChange={setPasscode}
state={state} /> state={state}
period={props.totp_period} />
</MethodContainer> </MethodContainer>
) )
} }

View File

@ -111,6 +111,7 @@ export default function (props: Props) {
authenticationLevel={props.authenticationLevel} authenticationLevel={props.authenticationLevel}
// Whether the user has a TOTP secret registered already // Whether the user has a TOTP secret registered already
registered={props.userInfo.has_totp} registered={props.userInfo.has_totp}
totp_period={props.configuration.totp_period}
onRegisterClick={initiateRegistration(initiateTOTPRegistrationProcess)} onRegisterClick={initiateRegistration(initiateTOTPRegistrationProcess)}
onSignInError={err => createErrorNotification(err.message)} onSignInError={err => createErrorNotification(err.message)}
onSignInSuccess={props.onAuthenticationSuccess} /> onSignInSuccess={props.onAuthenticationSuccess} />