diff --git a/internal/storage/mysql_provider.go b/internal/storage/mysql_provider.go
index c4cd45f95..8ea2800af 100644
--- a/internal/storage/mysql_provider.go
+++ b/internal/storage/mysql_provider.go
@@ -52,6 +52,7 @@ func NewMySQLProvider(configuration schema.MySQLStorageConfiguration) *MySQLProv
sqlGetTOTPSecretByUsername: fmt.Sprintf("SELECT secret FROM %s WHERE username=?", totpSecretsTableName),
sqlUpsertTOTPSecret: fmt.Sprintf("REPLACE INTO %s (username, secret) VALUES (?, ?)", totpSecretsTableName),
+ sqlDeleteTOTPSecret: fmt.Sprintf("DELETE FROM %s WHERE username=?", totpSecretsTableName),
sqlGetU2FDeviceHandleByUsername: fmt.Sprintf("SELECT keyHandle, publicKey FROM %s WHERE username=?", u2fDeviceHandlesTableName),
sqlUpsertU2FDeviceHandle: fmt.Sprintf("REPLACE INTO %s (username, keyHandle, publicKey) VALUES (?, ?, ?)", u2fDeviceHandlesTableName),
diff --git a/internal/storage/postgres_provider.go b/internal/storage/postgres_provider.go
index ada6cbbb3..f1b62e0d1 100644
--- a/internal/storage/postgres_provider.go
+++ b/internal/storage/postgres_provider.go
@@ -60,6 +60,7 @@ func NewPostgreSQLProvider(configuration schema.PostgreSQLStorageConfiguration)
sqlGetTOTPSecretByUsername: fmt.Sprintf("SELECT secret FROM %s WHERE username=$1", totpSecretsTableName),
sqlUpsertTOTPSecret: fmt.Sprintf("INSERT INTO %s (username, secret) VALUES ($1, $2) ON CONFLICT (username) DO UPDATE SET secret=$2", totpSecretsTableName),
+ sqlDeleteTOTPSecret: fmt.Sprintf("DELETE FROM %s WHERE username=$1", totpSecretsTableName),
sqlGetU2FDeviceHandleByUsername: fmt.Sprintf("SELECT keyHandle, publicKey FROM %s WHERE username=$1", u2fDeviceHandlesTableName),
sqlUpsertU2FDeviceHandle: fmt.Sprintf("INSERT INTO %s (username, keyHandle, publicKey) VALUES ($1, $2, $3) ON CONFLICT (username) DO UPDATE SET keyHandle=$2, publicKey=$3", u2fDeviceHandlesTableName),
diff --git a/internal/storage/provider.go b/internal/storage/provider.go
index 02593974f..4942b0b8b 100644
--- a/internal/storage/provider.go
+++ b/internal/storage/provider.go
@@ -18,6 +18,7 @@ type Provider interface {
SaveTOTPSecret(username string, secret string) error
LoadTOTPSecret(username string) (string, error)
+ DeleteTOTPSecret(username string) error
SaveU2FDeviceHandle(username string, keyHandle []byte, publicKey []byte) error
LoadU2FDeviceHandle(username string) (keyHandle []byte, publicKey []byte, err error)
diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go
index 563cc11e0..83850133f 100644
--- a/internal/storage/sql_provider.go
+++ b/internal/storage/sql_provider.go
@@ -22,6 +22,7 @@ type SQLProvider struct {
sqlGetTOTPSecretByUsername string
sqlUpsertTOTPSecret string
+ sqlDeleteTOTPSecret string
sqlGetU2FDeviceHandleByUsername string
sqlUpsertU2FDeviceHandle string
@@ -135,6 +136,12 @@ func (p *SQLProvider) LoadTOTPSecret(username string) (string, error) {
return secret, nil
}
+// DeleteTOTPSecret delete a TOTP secret given a username.
+func (p *SQLProvider) DeleteTOTPSecret(username string) error {
+ _, err := p.db.Exec(p.sqlDeleteTOTPSecret, username)
+ return err
+}
+
// SaveU2FDeviceHandle save a registered U2F device registration blob.
func (p *SQLProvider) SaveU2FDeviceHandle(username string, keyHandle []byte, publicKey []byte) error {
_, err := p.db.Exec(p.sqlUpsertU2FDeviceHandle,
diff --git a/internal/storage/sqlite_provider.go b/internal/storage/sqlite_provider.go
index 72718be2a..33fa8fdbc 100644
--- a/internal/storage/sqlite_provider.go
+++ b/internal/storage/sqlite_provider.go
@@ -31,6 +31,7 @@ func NewSQLiteProvider(path string) *SQLiteProvider {
sqlGetTOTPSecretByUsername: fmt.Sprintf("SELECT secret FROM %s WHERE username=?", totpSecretsTableName),
sqlUpsertTOTPSecret: fmt.Sprintf("REPLACE INTO %s (username, secret) VALUES (?, ?)", totpSecretsTableName),
+ sqlDeleteTOTPSecret: fmt.Sprintf("DELETE FROM %s WHERE username=?", totpSecretsTableName),
sqlGetU2FDeviceHandleByUsername: fmt.Sprintf("SELECT keyHandle, publicKey FROM %s WHERE username=?", u2fDeviceHandlesTableName),
sqlUpsertU2FDeviceHandle: fmt.Sprintf("REPLACE INTO %s (username, keyHandle, publicKey) VALUES (?, ?, ?)", u2fDeviceHandlesTableName),
diff --git a/internal/suites/scenario_backend_protection_test.go b/internal/suites/scenario_backend_protection_test.go
index 62bc07b34..d990c4615 100644
--- a/internal/suites/scenario_backend_protection_test.go
+++ b/internal/suites/scenario_backend_protection_test.go
@@ -42,8 +42,9 @@ func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() {
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/register", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign_request", AutheliaBaseURL), 403)
- s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/user/info/2fa_method", AutheliaBaseURL), 403)
- s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/user/info", AutheliaBaseURL), 403)
+ s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/user/info/2fa_method", AutheliaBaseURL), 403)
+
+ s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/user/info", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/available", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", AutheliaBaseURL), 403)
diff --git a/internal/suites/scenario_two_factor_test.go b/internal/suites/scenario_two_factor_test.go
index 4302daddf..900aab575 100644
--- a/internal/suites/scenario_two_factor_test.go
+++ b/internal/suites/scenario_two_factor_test.go
@@ -7,6 +7,8 @@ import (
"testing"
"time"
+ "github.com/clems4ever/authelia/internal/storage"
+ "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@@ -47,20 +49,66 @@ func (s *TwoFactorSuite) SetupTest() {
s.verifyIsHome(ctx, s.T())
}
+func (s *TwoFactorSuite) TestShouldCheckUserIsAskedToRegisterDevice() {
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer cancel()
+
+ username := "john"
+ password := "password"
+
+ // Clean up any TOTP secret already in DB
+ provider := storage.NewSQLiteProvider("/tmp/authelia/db.sqlite3")
+ require.NoError(s.T(), provider.DeleteTOTPSecret(username))
+
+ // Login one factor
+ s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
+
+ // Check the user is asked to register a new device
+ s.WaitElementLocatedByClassName(ctx, s.T(), "state-not-registered")
+
+ // Then register the TOTP factor
+ s.doRegisterTOTP(ctx, s.T())
+ // And logout
+ s.doLogout(ctx, s.T())
+
+ // Login one factor again
+ s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
+
+ // now the user should be asked to perform 2FA
+ s.WaitElementLocatedByClassName(ctx, s.T(), "state-method")
+}
+
func (s *TwoFactorSuite) TestShouldAuthorizeSecretAfterTwoFactor() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
- // Register TOTP secret and logout.
- secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
+ username := "john"
+ password := "password"
+ // Login one factor
+ s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
+
+ // Check he reaches the 2FA stage
+ s.verifyIsSecondFactorPage(ctx, s.T())
+
+ // Then register the TOTP factor
+ secret := s.doRegisterTOTP(ctx, s.T())
+
+ // And logout
+ s.doLogout(ctx, s.T())
+
+ // Login again with 1FA & 2FA
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, targetURL)
+
+ // And check if the user is redirected to the secret.
s.verifySecretAuthorized(ctx, s.T())
+ // Leave the secret
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
+ // And try to reload it again to check the session is kept
s.doVisit(s.T(), targetURL)
s.verifySecretAuthorized(ctx, s.T())
}
diff --git a/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx b/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx
index 0d1362210..085104a27 100644
--- a/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx
+++ b/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx
@@ -2,6 +2,7 @@ import React, { ReactNode, Fragment } from "react";
import { makeStyles, Typography, Link, useTheme } from "@material-ui/core";
import SuccessIcon from "../../../components/SuccessIcon";
import InformationIcon from "../../../components/InformationIcon";
+import classnames from "classnames";
export enum State {
ALREADY_AUTHENTICATED = 1,
@@ -23,24 +24,28 @@ export default function (props: Props) {
const style = useStyles();
let container: ReactNode;
+ let stateClass: string = '';
switch (props.state) {
case State.ALREADY_AUTHENTICATED:
container =