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.pull/482/head
parent
5f8726fe87
commit
df33bef478
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue