From df33bef478d1d3b8dabacfc40db93eb89f70f3fd Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 7 Dec 2019 18:14:26 +0100 Subject: [PATCH] Test user does see the not registered message. When a user use Authelia for the first time no device is enrolled in DB. Now we test that the user does see the "not registered" message when no device is enrolled and see the standard 2FA method when a device is already enrolled. --- internal/storage/mysql_provider.go | 1 + internal/storage/postgres_provider.go | 1 + internal/storage/provider.go | 1 + internal/storage/sql_provider.go | 7 +++ internal/storage/sqlite_provider.go | 1 + .../scenario_backend_protection_test.go | 5 +- internal/suites/scenario_two_factor_test.go | 52 ++++++++++++++++++- .../SecondFactor/MethodContainer.tsx | 7 ++- 8 files changed, 70 insertions(+), 5 deletions(-) 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}