diff --git a/config.template.yml b/config.template.yml
index cd6db1f9b..0ed769971 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -35,12 +35,21 @@ default_redirection_url: https://home.example.com:8080/
#
## google_analytics: UA-00000-01
-# TOTP Issuer Name
+# TOTP Settings
#
-# This will be the issuer name displayed in Google Authenticator
-# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names
+# Parameters used for TOTP generation
totp:
+ # The issuer name displayed in the Authenticator application of your choice
+ # See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names
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
#
diff --git a/docs/configuration/one-time-password.md b/docs/configuration/one-time-password.md
index 08c7c5f81..9aa383791 100644
--- a/docs/configuration/one-time-password.md
+++ b/docs/configuration/one-time-password.md
@@ -7,11 +7,47 @@ nav_order: 6
# One-Time Password
-Applications generating one-time passwords usually displays an issuer to
-differentiate the various applications registered by the user.
-
-Authelia allows to customize the issuer to differentiate the entry created
-by Authelia from others.
+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.
totp:
- issuer: authelia.com
\ No newline at end of file
+ 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.
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 753c5a33c..e36640372 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fasthttp/router v0.7.0
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/golang/mock v1.4.3
github.com/golang/snappy v0.0.1 // indirect
diff --git a/go.sum b/go.sum
index 249eb774f..18603dc00 100644
--- a/go.sum
+++ b/go.sum
@@ -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/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.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
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/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.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
diff --git a/internal/configuration/schema/totp.go b/internal/configuration/schema/totp.go
index bcdc29a20..45c38d0f8 100644
--- a/internal/configuration/schema/totp.go
+++ b/internal/configuration/schema/totp.go
@@ -3,4 +3,6 @@ package schema
// TOTPConfiguration represents the configuration related to TOTP options.
type TOTPConfiguration struct {
Issuer string `mapstructure:"issuer"`
+ Period int `mapstructure:"period"`
+ Skew *int `mapstructure:"skew"`
}
diff --git a/internal/configuration/validator/configuration.go b/internal/configuration/validator/configuration.go
index 890b2bdc6..e44b7d3d1 100644
--- a/internal/configuration/validator/configuration.go
+++ b/internal/configuration/validator/configuration.go
@@ -46,8 +46,8 @@ func Validate(configuration *schema.Configuration, validator *schema.StructValid
if configuration.TOTP == nil {
configuration.TOTP = &schema.TOTPConfiguration{}
- ValidateTOTP(configuration.TOTP, validator)
}
+ ValidateTOTP(configuration.TOTP, validator)
if configuration.Notifier == nil {
validator.Push(fmt.Errorf("A notifier configuration must be provided"))
diff --git a/internal/configuration/validator/totp.go b/internal/configuration/validator/totp.go
index edd7ad756..5dfa9cf3c 100644
--- a/internal/configuration/validator/totp.go
+++ b/internal/configuration/validator/totp.go
@@ -1,14 +1,29 @@
package validator
import (
+ "fmt"
"github.com/authelia/authelia/internal/configuration/schema"
)
const defaultTOTPIssuer = "Authelia"
+const DefaultTOTPPeriod = 30
+const DefaultTOTPSkew = 1
// ValidateTOTP validates and update TOTP configuration.
func ValidateTOTP(configuration *schema.TOTPConfiguration, validator *schema.StructValidator) {
if configuration.Issuer == "" {
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"))
+ }
}
diff --git a/internal/configuration/validator/totp_test.go b/internal/configuration/validator/totp_test.go
index ff32354c3..aae98c5b4 100644
--- a/internal/configuration/validator/totp_test.go
+++ b/internal/configuration/validator/totp_test.go
@@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/require"
)
-func TestShouldSetDefaultIssuer(t *testing.T) {
+func TestShouldSetDefaultTOTPValues(t *testing.T) {
validator := schema.NewStructValidator()
config := schema.TOTPConfiguration{}
@@ -16,4 +16,20 @@ func TestShouldSetDefaultIssuer(t *testing.T) {
require.Len(t, validator.Errors(), 0)
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")
+
}
diff --git a/internal/handlers/handler_extended_configuration.go b/internal/handlers/handler_extended_configuration.go
index 09085ee2f..0ac352a70 100644
--- a/internal/handlers/handler_extended_configuration.go
+++ b/internal/handlers/handler_extended_configuration.go
@@ -11,12 +11,16 @@ type ExtendedConfigurationBody struct {
// SecondFactorEnabled whether second factor is enabled
SecondFactorEnabled bool `json:"second_factor_enabled"`
+
+ // TOTP Period
+ TOTPPeriod int `json:"totp_period"`
}
// ExtendedConfigurationGet get the extended configuration accessible to authenticated users.
func ExtendedConfigurationGet(ctx *middlewares.AutheliaCtx) {
body := ExtendedConfigurationBody{}
body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
+ body.TOTPPeriod = ctx.Configuration.TOTP.Period
if ctx.Configuration.DuoAPI != nil {
body.AvailableMethods = append(body.AvailableMethods, authentication.Push)
diff --git a/internal/handlers/handler_extended_configuration_test.go b/internal/handlers/handler_extended_configuration_test.go
index 27c761419..507e5d4fe 100644
--- a/internal/handlers/handler_extended_configuration_test.go
+++ b/internal/handlers/handler_extended_configuration_test.go
@@ -4,9 +4,10 @@ import (
"testing"
"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/configuration/schema"
"github.com/stretchr/testify/suite"
)
@@ -28,9 +29,15 @@ func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
+ s.mock.Ctx.Configuration = schema.Configuration{
+ TOTP: &schema.TOTPConfiguration{
+ Period: validator.DefaultTOTPPeriod,
+ },
+ }
expectedBody := ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: false,
+ TOTPPeriod: validator.DefaultTOTPPeriod,
}
ExtendedConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
@@ -39,16 +46,25 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{},
+ TOTP: &schema.TOTPConfiguration{
+ Period: validator.DefaultTOTPPeriod,
+ },
}
expectedBody := ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f", "mobile_push"},
SecondFactorEnabled: false,
+ TOTPPeriod: validator.DefaultTOTPPeriod,
}
ExtendedConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
}
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{
DefaultPolicy: "bypass",
Rules: []schema.ACLRule{
@@ -70,10 +86,16 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisab
s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: false,
+ TOTPPeriod: validator.DefaultTOTPPeriod,
})
}
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{
DefaultPolicy: "two_factor",
Rules: []schema.ACLRule{
@@ -95,10 +117,16 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabl
s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: true,
+ TOTPPeriod: validator.DefaultTOTPPeriod,
})
}
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{
DefaultPolicy: "bypass",
Rules: []schema.ACLRule{
@@ -120,6 +148,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabl
s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: true,
+ TOTPPeriod: validator.DefaultTOTPPeriod,
})
}
diff --git a/internal/handlers/handler_register_totp.go b/internal/handlers/handler_register_totp.go
index 63ea2dc35..9dee14652 100644
--- a/internal/handlers/handler_register_totp.go
+++ b/internal/handlers/handler_register_totp.go
@@ -41,6 +41,7 @@ func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username strin
Issuer: ctx.Configuration.TOTP.Issuer,
AccountName: username,
SecretSize: 32,
+ Period: uint(ctx.Configuration.TOTP.Period),
})
if err != nil {
diff --git a/internal/handlers/handler_sign_totp.go b/internal/handlers/handler_sign_totp.go
index e1b11b6e4..fe91a657c 100644
--- a/internal/handlers/handler_sign_totp.go
+++ b/internal/handlers/handler_sign_totp.go
@@ -25,7 +25,11 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
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 {
ctx.Error(fmt.Errorf("Wrong passcode during TOTP validation for user %s", userSession.Username), mfaValidationFailedMessage)
diff --git a/internal/handlers/handler_sign_totp_test.go b/internal/handlers/handler_sign_totp_test.go
index 6f2ce2531..baf8ae3d3 100644
--- a/internal/handlers/handler_sign_totp_test.go
+++ b/internal/handlers/handler_sign_totp_test.go
@@ -40,7 +40,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")).
- Return(true)
+ Return(true, nil)
s.mock.Ctx.Configuration.DefaultRedirectionURL = "http://redirection.local"
@@ -65,7 +65,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")).
- Return(true)
+ Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc",
@@ -86,7 +86,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")).
- Return(true)
+ Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc",
@@ -110,7 +110,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")).
- Return(true)
+ Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc",
@@ -132,7 +132,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFi
verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")).
- Return(true)
+ Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc",
diff --git a/internal/handlers/totp.go b/internal/handlers/totp.go
index 814afc56c..83476d4dd 100644
--- a/internal/handlers/totp.go
+++ b/internal/handlers/totp.go
@@ -1,15 +1,26 @@
package handlers
import (
+ "github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
+ "time"
)
type TOTPVerifier interface {
- Verify(token, secret string) bool
+ Verify(token, secret string) (bool, error)
}
-type TOTPVerifierImpl struct{}
-
-func (tv *TOTPVerifierImpl) Verify(token, secret string) bool {
- return totp.Validate(token, secret)
+type TOTPVerifierImpl struct {
+ Period uint
+ Skew uint
+}
+
+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)
}
diff --git a/internal/handlers/totp_mock.go b/internal/handlers/totp_mock.go
index 80e5596e6..09d2641ea 100644
--- a/internal/handlers/totp_mock.go
+++ b/internal/handlers/totp_mock.go
@@ -33,11 +33,12 @@ func (m *MockTOTPVerifier) EXPECT() *MockTOTPVerifierMockRecorder {
}
// 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()
ret := m.ctrl.Call(m, "Verify", token, secret)
ret0, _ := ret[0].(bool)
- return ret0
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
}
// Verify indicates an expected call of Verify
diff --git a/internal/server/server.go b/internal/server/server.go
index 1beb38de7..cec8dbb04 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -61,7 +61,10 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
router.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish)))
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
router.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware(
diff --git a/web/src/components/PieChartIcon.tsx b/web/src/components/PieChartIcon.tsx
index 3316d3f22..a8fd4176f 100644
--- a/web/src/components/PieChartIcon.tsx
+++ b/web/src/components/PieChartIcon.tsx
@@ -26,7 +26,7 @@ export default function (props: Props) {
)
diff --git a/web/src/components/TimerIcon.tsx b/web/src/components/TimerIcon.tsx
index d82423ba0..1c480d78c 100644
--- a/web/src/components/TimerIcon.tsx
+++ b/web/src/components/TimerIcon.tsx
@@ -4,32 +4,31 @@ import PieChartIcon from "./PieChartIcon";
export interface Props {
width: number;
height: number;
+ period: number;
color?: string;
backgroundColor?: string;
}
export default function (props: Props) {
- const maxTimeProgress = 1000;
+ const radius = 31.6;
const [timeProgress, setTimeProgress] = useState(0);
useEffect(() => {
// 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);
const interval = setInterval(() => {
- const ms = new Date().getSeconds() * 1000.0 + new Date().getMilliseconds();
- const value = (ms % 30000) / 30000 * maxTimeProgress;
+ const value = (new Date().getTime() / 1000) % props.period / props.period * radius;
setTimeProgress(value);
}, 100);
return () => clearInterval(interval);
- }, []);
+ }, [props]);
return (
)
}
diff --git a/web/src/models/Configuration.ts b/web/src/models/Configuration.ts
index 6d2559d6c..709efa604 100644
--- a/web/src/models/Configuration.ts
+++ b/web/src/models/Configuration.ts
@@ -7,4 +7,5 @@ export interface Configuration {
export interface ExtendedConfiguration {
available_methods: Set;
second_factor_enabled: boolean;
+ totp_period: number;
}
\ No newline at end of file
diff --git a/web/src/services/Configuration.ts b/web/src/services/Configuration.ts
index 123ab99a0..a0c7530d2 100644
--- a/web/src/services/Configuration.ts
+++ b/web/src/services/Configuration.ts
@@ -10,6 +10,7 @@ export async function getConfiguration(): Promise {
interface ExtendedConfigurationPayload {
available_methods: Method2FA[];
second_factor_enabled: boolean;
+ totp_period: number;
}
export async function getExtendedConfiguration(): Promise {
diff --git a/web/src/views/LoginPortal/SecondFactor/OTPDial.tsx b/web/src/views/LoginPortal/SecondFactor/OTPDial.tsx
index 90fcad989..b3669d55a 100644
--- a/web/src/views/LoginPortal/SecondFactor/OTPDial.tsx
+++ b/web/src/views/LoginPortal/SecondFactor/OTPDial.tsx
@@ -10,13 +10,13 @@ import SuccessIcon from "../../../components/SuccessIcon";
export interface Props {
passcode: string;
state: State;
+ period: number
onChange: (passcode: string) => void;
}
export default function (props: Props) {
const style = useStyles();
-
const dial = (
}
+ icon={}
context={dial} />
)
}
@@ -61,12 +61,13 @@ const useStyles = makeStyles(theme => ({
interface IconProps {
state: State;
+ period: number;
}
function Icon(props: IconProps) {
return (
- {props.state !== State.Success ? : null}
+ {props.state !== State.Success ? : null}
{props.state === State.Success ? : null}
)
diff --git a/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx b/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx
index 43c021fc0..5196c722d 100644
--- a/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx
+++ b/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx
@@ -16,6 +16,7 @@ export interface Props {
id: string;
authenticationLevel: AuthenticationLevel;
registered: boolean;
+ totp_period: number
onRegisterClick: () => void;
onSignInError: (err: Error) => void;
@@ -83,7 +84,8 @@ export default function (props: Props) {
+ state={state}
+ period={props.totp_period} />
)
}
\ No newline at end of file
diff --git a/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx b/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx
index 62b4994cc..7f8d25768 100644
--- a/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx
+++ b/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx
@@ -111,6 +111,7 @@ export default function (props: Props) {
authenticationLevel={props.authenticationLevel}
// Whether the user has a TOTP secret registered already
registered={props.userInfo.has_totp}
+ totp_period={props.configuration.totp_period}
onRegisterClick={initiateRegistration(initiateTOTPRegistrationProcess)}
onSignInError={err => createErrorNotification(err.message)}
onSignInSuccess={props.onAuthenticationSuccess} />