[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 graphicpull/779/head
parent
c057c917f6
commit
40fb13ba3c
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
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
2
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -26,7 +26,7 @@ export default function (props: Props) {
|
|||
<circle r="5" cx="13" cy="13" fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="10"
|
||||
strokeDasharray={`calc(${props.progress} * 31.6 / ${maxProgress}) 31.6`}
|
||||
strokeDasharray={`${props.progress} ${maxProgress}`}
|
||||
transform="rotate(-90) translate(-26)" />
|
||||
</svg>
|
||||
)
|
||||
|
|
|
@ -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 (
|
||||
<PieChartIcon width={props.width} height={props.height}
|
||||
maxProgress={maxTimeProgress}
|
||||
progress={timeProgress}
|
||||
progress={timeProgress} maxProgress={radius}
|
||||
backgroundColor={props.backgroundColor} color={props.color} />
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ export interface Configuration {
|
|||
export interface ExtendedConfiguration {
|
||||
available_methods: Set<SecondFactorMethod>;
|
||||
second_factor_enabled: boolean;
|
||||
totp_period: number;
|
||||
}
|
|
@ -10,6 +10,7 @@ export async function getConfiguration(): Promise<Configuration> {
|
|||
interface ExtendedConfigurationPayload {
|
||||
available_methods: Method2FA[];
|
||||
second_factor_enabled: boolean;
|
||||
totp_period: number;
|
||||
}
|
||||
|
||||
export async function getExtendedConfiguration(): Promise<ExtendedConfiguration> {
|
||||
|
|
|
@ -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 = (
|
||||
<span className={style.otpInput} id="otp-input">
|
||||
<OtpInput
|
||||
|
@ -31,7 +31,7 @@ export default function (props: Props) {
|
|||
|
||||
return (
|
||||
<IconWithContext
|
||||
icon={<Icon state={props.state} />}
|
||||
icon={<Icon state={props.state} period={props.period} />}
|
||||
context={dial} />
|
||||
)
|
||||
}
|
||||
|
@ -61,12 +61,13 @@ const useStyles = makeStyles(theme => ({
|
|||
|
||||
interface IconProps {
|
||||
state: State;
|
||||
period: number;
|
||||
}
|
||||
|
||||
function Icon(props: IconProps) {
|
||||
return (
|
||||
<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}
|
||||
</Fragment>
|
||||
)
|
||||
|
|
|
@ -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) {
|
|||
<OTPDial
|
||||
passcode={passcode}
|
||||
onChange={setPasscode}
|
||||
state={state} />
|
||||
state={state}
|
||||
period={props.totp_period} />
|
||||
</MethodContainer>
|
||||
)
|
||||
}
|
|
@ -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} />
|
||||
|
|
Loading…
Reference in New Issue