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} />