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 = + stateClass = "state-already-authenticated"; break; case State.NOT_REGISTERED: container = + stateClass = "state-not-registered"; break; case State.METHOD: container = {props.children} + stateClass = "state-method"; break; } return (
{props.title} -
+
{container}