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.
This commit is contained in:
Clement Michaud 2019-12-07 18:14:26 +01:00 committed by Clément Michaud
parent 5f8726fe87
commit df33bef478
8 changed files with 70 additions and 5 deletions

View File

@ -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),

View File

@ -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),

View File

@ -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)

View File

@ -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,

View File

@ -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),

View File

@ -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)

View File

@ -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())
}

View File

@ -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 = <AlreadyAuthenticatedContainer />
stateClass = "state-already-authenticated";
break;
case State.NOT_REGISTERED:
container = <NotRegisteredContainer />
stateClass = "state-not-registered";
break;
case State.METHOD:
container = <MethodContainer explanation={props.explanation}>
{props.children}
</MethodContainer>
stateClass = "state-method";
break;
}
return (
<div id={props.id}>
<Typography variant="h6">{props.title}</Typography>
<div className={style.container} id="2fa-container">
<div className={classnames(style.container, stateClass)} id="2fa-container">
<div className={style.containerFlex}>
{container}
</div>