Allow administrator to provide a Google Analytics tracking ID.
Providing a GA tracking ID allows administrators to analyze how the portal is used by their users in large environments, i.e., with many users. This will make even more sense when we have users and admins management interfaces.pull/482/head
parent
3faa63e8ed
commit
3d20142292
|
@ -26,6 +26,11 @@ jwt_secret: a_very_important_secret
|
||||||
# be redirected upon successful authentication.
|
# be redirected upon successful authentication.
|
||||||
default_redirection_url: https://home.example.com:8080/
|
default_redirection_url: https://home.example.com:8080/
|
||||||
|
|
||||||
|
# Google Analytics Tracking ID to track the usage of the portal
|
||||||
|
# using a Google Analytics dashboard.
|
||||||
|
#
|
||||||
|
## google_analytics: UA-00000-01
|
||||||
|
|
||||||
# TOTP Issuer Name
|
# TOTP Issuer Name
|
||||||
#
|
#
|
||||||
# This will be the issuer name displayed in Google Authenticator
|
# This will be the issuer name displayed in Google Authenticator
|
||||||
|
|
|
@ -7,4 +7,11 @@ ARG GROUP_ID
|
||||||
|
|
||||||
RUN addgroup --gid ${GROUP_ID} dev && \
|
RUN addgroup --gid ${GROUP_ID} dev && \
|
||||||
adduser --uid ${USER_ID} -G dev -D dev
|
adduser --uid ${USER_ID} -G dev -D dev
|
||||||
USER dev
|
|
||||||
|
RUN mkdir -p /etc/authelia && chown dev:dev /etc/authelia
|
||||||
|
RUN mkdir -p /var/lib/authelia && chown dev:dev /var/lib/authelia
|
||||||
|
|
||||||
|
USER dev
|
||||||
|
|
||||||
|
VOLUME /etc/authelia
|
||||||
|
VOLUME /var/lib/authelia
|
||||||
|
|
|
@ -4,8 +4,6 @@ services:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
volumes:
|
|
||||||
- "/tmp/authelia:/tmp/authelia"
|
|
||||||
environment:
|
environment:
|
||||||
- ENVIRONMENT=dev
|
- ENVIRONMENT=dev
|
||||||
restart: always
|
restart: always
|
||||||
|
|
|
@ -13,7 +13,6 @@ services:
|
||||||
- "./example/compose/authelia/resources/:/resources"
|
- "./example/compose/authelia/resources/:/resources"
|
||||||
- ".:/app"
|
- ".:/app"
|
||||||
- "${GOPATH}:/go"
|
- "${GOPATH}:/go"
|
||||||
- "/tmp/authelia:/tmp/authelia"
|
|
||||||
environment:
|
environment:
|
||||||
- ENVIRONMENT=dev
|
- ENVIRONMENT=dev
|
||||||
networks:
|
networks:
|
||||||
|
|
|
@ -4,9 +4,6 @@ set -x
|
||||||
|
|
||||||
go get github.com/cespare/reflex
|
go get github.com/cespare/reflex
|
||||||
|
|
||||||
mkdir -p /var/lib/authelia
|
|
||||||
mkdir -p /etc/authelia
|
|
||||||
|
|
||||||
# Sleep 10 seconds to wait the end of npm install updating web directory
|
# Sleep 10 seconds to wait the end of npm install updating web directory
|
||||||
# and making reflex reload multiple times.
|
# and making reflex reload multiple times.
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
|
@ -5,8 +5,6 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./example/compose/nginx/portal/nginx.conf:/etc/nginx/nginx.conf
|
- ./example/compose/nginx/portal/nginx.conf:/etc/nginx/nginx.conf
|
||||||
- ./example/compose/nginx/portal/ssl:/etc/ssl
|
- ./example/compose/nginx/portal/ssl:/etc/ssl
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
networks:
|
networks:
|
||||||
authelianet:
|
authelianet:
|
||||||
aliases:
|
aliases:
|
||||||
|
|
|
@ -2,10 +2,12 @@ package schema
|
||||||
|
|
||||||
// Configuration object extracted from YAML configuration file.
|
// Configuration object extracted from YAML configuration file.
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
LogsLevel string `yaml:"logs_level"`
|
LogsLevel string `yaml:"logs_level"`
|
||||||
JWTSecret string `yaml:"jwt_secret"`
|
JWTSecret string `yaml:"jwt_secret"`
|
||||||
DefaultRedirectionURL string `yaml:"default_redirection_url"`
|
DefaultRedirectionURL string `yaml:"default_redirection_url"`
|
||||||
|
GoogleAnalyticsTrackingID string `yaml:"google_analytics"`
|
||||||
|
|
||||||
AuthenticationBackend AuthenticationBackendConfiguration `yaml:"authentication_backend"`
|
AuthenticationBackend AuthenticationBackendConfiguration `yaml:"authentication_backend"`
|
||||||
Session SessionConfiguration `yaml:"session"`
|
Session SessionConfiguration `yaml:"session"`
|
||||||
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/clems4ever/authelia/internal/authentication"
|
|
||||||
"github.com/clems4ever/authelia/internal/middlewares"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SecondFactorAvailableMethodsGet retrieve available 2FA methods.
|
|
||||||
// The supported methods are: "totp", "u2f", "duo"
|
|
||||||
func SecondFactorAvailableMethodsGet(ctx *middlewares.AutheliaCtx) {
|
|
||||||
availableMethods := MethodList{authentication.TOTP, authentication.U2F}
|
|
||||||
|
|
||||||
if ctx.Configuration.DuoAPI != nil {
|
|
||||||
availableMethods = append(availableMethods, authentication.Push)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Logger.Debugf("Available methods are %s", availableMethods)
|
|
||||||
ctx.SetJSONBody(availableMethods)
|
|
||||||
}
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import "github.com/clems4ever/authelia/internal/middlewares"
|
||||||
|
|
||||||
|
type ConfigurationBody struct {
|
||||||
|
GoogleAnalyticsTrackingID string `json:"ga_tracking_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
|
||||||
|
body := ConfigurationBody{
|
||||||
|
GoogleAnalyticsTrackingID: ctx.Configuration.GoogleAnalyticsTrackingID,
|
||||||
|
}
|
||||||
|
ctx.SetJSONBody(body)
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/clems4ever/authelia/internal/mocks"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigurationSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
mock *mocks.MockAutheliaCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigurationSuite) SetupTest() {
|
||||||
|
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigurationSuite) TearDownTest() {
|
||||||
|
s.mock.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigurationSuite) TestShouldReturnConfiguredGATrackingID() {
|
||||||
|
GATrackingID := "ABC"
|
||||||
|
s.mock.Ctx.Configuration.GoogleAnalyticsTrackingID = GATrackingID
|
||||||
|
|
||||||
|
expectedBody := ConfigurationBody{
|
||||||
|
GoogleAnalyticsTrackingID: GATrackingID,
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationGet(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), expectedBody)
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/clems4ever/authelia/internal/authentication"
|
||||||
|
"github.com/clems4ever/authelia/internal/middlewares"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExtendedConfigurationBody struct {
|
||||||
|
AvailableMethods MethodList `json:"available_methods"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedConfigurationGet get the extended configuration accessbile to authenticated users.
|
||||||
|
func ExtendedConfigurationGet(ctx *middlewares.AutheliaCtx) {
|
||||||
|
body := ExtendedConfigurationBody{}
|
||||||
|
body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
|
||||||
|
|
||||||
|
if ctx.Configuration.DuoAPI != nil {
|
||||||
|
body.AvailableMethods = append(body.AvailableMethods, authentication.Push)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Logger.Debugf("Available methods are %s", body.AvailableMethods)
|
||||||
|
ctx.SetJSONBody(body)
|
||||||
|
}
|
|
@ -11,7 +11,6 @@ import (
|
||||||
|
|
||||||
type SecondFactorAvailableMethodsFixture struct {
|
type SecondFactorAvailableMethodsFixture struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
|
|
||||||
mock *mocks.MockAutheliaCtx
|
mock *mocks.MockAutheliaCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,16 +23,22 @@ func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
|
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
|
||||||
SecondFactorAvailableMethodsGet(s.mock.Ctx)
|
expectedBody := ExtendedConfigurationBody{
|
||||||
s.mock.Assert200OK(s.T(), []string{"totp", "u2f"})
|
AvailableMethods: []string{"totp", "u2f"},
|
||||||
|
}
|
||||||
|
ExtendedConfigurationGet(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), expectedBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
|
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
|
||||||
s.mock.Ctx.Configuration = schema.Configuration{
|
s.mock.Ctx.Configuration = schema.Configuration{
|
||||||
DuoAPI: &schema.DuoAPIConfiguration{},
|
DuoAPI: &schema.DuoAPIConfiguration{},
|
||||||
}
|
}
|
||||||
SecondFactorAvailableMethodsGet(s.mock.Ctx)
|
expectedBody := ExtendedConfigurationBody{
|
||||||
s.mock.Assert200OK(s.T(), []string{"totp", "u2f", "mobile_push"})
|
AvailableMethods: []string{"totp", "u2f", "mobile_push"},
|
||||||
|
}
|
||||||
|
ExtendedConfigurationGet(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), expectedBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunSuite(t *testing.T) {
|
func TestRunSuite(t *testing.T) {
|
|
@ -32,6 +32,10 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
|
||||||
|
|
||||||
router.GET("/api/state", autheliaMiddleware(handlers.StateGet))
|
router.GET("/api/state", autheliaMiddleware(handlers.StateGet))
|
||||||
|
|
||||||
|
router.GET("/api/configuration", autheliaMiddleware(handlers.ConfigurationGet))
|
||||||
|
router.GET("/api/configuration/extended", autheliaMiddleware(
|
||||||
|
middlewares.RequireFirstFactor(handlers.ExtendedConfigurationGet)))
|
||||||
|
|
||||||
router.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet))
|
router.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet))
|
||||||
|
|
||||||
router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost))
|
router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost))
|
||||||
|
@ -45,10 +49,6 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
|
||||||
router.POST("/api/reset-password", autheliaMiddleware(
|
router.POST("/api/reset-password", autheliaMiddleware(
|
||||||
handlers.ResetPasswordPost))
|
handlers.ResetPasswordPost))
|
||||||
|
|
||||||
// 2FA preferences and settings related endpoints.
|
|
||||||
router.GET("/api/secondfactor/available", autheliaMiddleware(
|
|
||||||
middlewares.RequireFirstFactor(handlers.SecondFactorAvailableMethodsGet)))
|
|
||||||
|
|
||||||
// Information about the user
|
// Information about the user
|
||||||
router.GET("/api/user/info", autheliaMiddleware(
|
router.GET("/api/user/info", autheliaMiddleware(
|
||||||
middlewares.RequireFirstFactor(handlers.UserInfoGet)))
|
middlewares.RequireFirstFactor(handlers.UserInfoGet)))
|
||||||
|
|
|
@ -20,7 +20,7 @@ session:
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
path: /tmp/authelia/db.sqlite
|
path: /var/lib/authelia/db.sqlite
|
||||||
|
|
||||||
# The Duo Push Notification API configuration
|
# The Duo Push Notification API configuration
|
||||||
duo_api:
|
duo_api:
|
||||||
|
|
|
@ -22,7 +22,7 @@ session:
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
path: /tmp/authelia/db.sqlite3
|
path: /var/lib/authelia/db.sqlite3
|
||||||
|
|
||||||
totp:
|
totp:
|
||||||
issuer: example.com
|
issuer: example.com
|
||||||
|
|
|
@ -21,7 +21,7 @@ session:
|
||||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
path: /tmp/authelia/db.sqlite
|
path: /var/lib/authelia/db.sqlite
|
||||||
|
|
||||||
# TOTP Issuer Name
|
# TOTP Issuer Name
|
||||||
#
|
#
|
||||||
|
|
|
@ -57,7 +57,7 @@ session:
|
||||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
path: /tmp/authelia/db.sqlite3
|
path: /var/lib/authelia/db.sqlite3
|
||||||
|
|
||||||
# TOTP Issuer Name
|
# TOTP Issuer Name
|
||||||
#
|
#
|
||||||
|
|
|
@ -21,7 +21,7 @@ session:
|
||||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
path: /tmp/authelia/db.sqlite
|
path: /var/lib/authelia/db.sqlite
|
||||||
|
|
||||||
# Access Control
|
# Access Control
|
||||||
#
|
#
|
||||||
|
|
|
@ -20,24 +20,14 @@ session:
|
||||||
inactivity: 5
|
inactivity: 5
|
||||||
expiration: 8
|
expiration: 8
|
||||||
|
|
||||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
path: /tmp/authelia/db.sqlite
|
path: /var/lib/authelia/db.sqlite
|
||||||
|
|
||||||
# TOTP Issuer Name
|
|
||||||
#
|
|
||||||
# This will be the issuer name displayed in Google Authenticator
|
|
||||||
# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names
|
|
||||||
totp:
|
totp:
|
||||||
issuer: example.com
|
issuer: example.com
|
||||||
|
|
||||||
# Access Control
|
|
||||||
#
|
|
||||||
# Access control is a set of rules you can use to restrict user access to certain
|
|
||||||
# resources.
|
|
||||||
access_control:
|
access_control:
|
||||||
# Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
|
|
||||||
default_policy: deny
|
default_policy: deny
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
|
@ -70,37 +60,12 @@ access_control:
|
||||||
subject: "user:bob"
|
subject: "user:bob"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
# Configuration of the authentication regulation mechanism.
|
|
||||||
regulation:
|
regulation:
|
||||||
# Set it to 0 to disable max_retries.
|
|
||||||
max_retries: 3
|
max_retries: 3
|
||||||
|
|
||||||
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
|
|
||||||
find_time: 3
|
find_time: 3
|
||||||
|
|
||||||
# The length of time before a banned user can login again.
|
|
||||||
ban_time: 5
|
ban_time: 5
|
||||||
|
|
||||||
# Default redirection URL
|
|
||||||
#
|
|
||||||
# Note: this parameter is optional. If not provided, user won't
|
|
||||||
# be redirected upon successful authentication.
|
|
||||||
#default_redirection_url: https://authelia.example.domain
|
|
||||||
|
|
||||||
notifier:
|
notifier:
|
||||||
# For testing purpose, notifications can be sent in a file
|
|
||||||
# filesystem:
|
|
||||||
# filename: /tmp/authelia/notification.txt
|
|
||||||
|
|
||||||
# Use your email account to send the notifications. You can use an app password.
|
|
||||||
# List of valid services can be found here: https://nodemailer.com/smtp/well-known/
|
|
||||||
## email:
|
|
||||||
## username: user@example.com
|
|
||||||
## password: yourpassword
|
|
||||||
## sender: admin@example.com
|
|
||||||
## service: gmail
|
|
||||||
|
|
||||||
# Use a SMTP server for sending notifications
|
|
||||||
smtp:
|
smtp:
|
||||||
host: smtp
|
host: smtp
|
||||||
port: 1025
|
port: 1025
|
||||||
|
|
|
@ -4,3 +4,4 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- "./internal/suites/Standalone/configuration.yml:/etc/authelia/configuration.yml:ro"
|
- "./internal/suites/Standalone/configuration.yml:/etc/authelia/configuration.yml:ro"
|
||||||
- "./internal/suites/Standalone/users.yml:/var/lib/authelia/users.yml"
|
- "./internal/suites/Standalone/users.yml:/var/lib/authelia/users.yml"
|
||||||
|
- "/tmp/authelia:/tmp/authelia"
|
||||||
|
|
|
@ -20,7 +20,7 @@ session:
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
path: /tmp/authelia/db.sqlite
|
path: /var/lib/authelia/db.sqlite
|
||||||
|
|
||||||
access_control:
|
access_control:
|
||||||
default_policy: bypass
|
default_policy: bypass
|
||||||
|
|
|
@ -45,7 +45,10 @@ func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() {
|
||||||
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/user/info/2fa_method", 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/user/info", AutheliaBaseURL), 403)
|
||||||
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/available", AutheliaBaseURL), 403)
|
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration/extended", AutheliaBaseURL), 403)
|
||||||
|
|
||||||
|
// This is the global configuration, it's safe to let it open.
|
||||||
|
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration", AutheliaBaseURL), 200)
|
||||||
|
|
||||||
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", AutheliaBaseURL), 403)
|
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", AutheliaBaseURL), 403)
|
||||||
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/finish", AutheliaBaseURL), 403)
|
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/finish", AutheliaBaseURL), 403)
|
||||||
|
|
|
@ -7,8 +7,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/clems4ever/authelia/internal/storage"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -49,35 +47,6 @@ func (s *TwoFactorSuite) SetupTest() {
|
||||||
s.verifyIsHome(ctx, s.T())
|
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() {
|
func (s *TwoFactorSuite) TestShouldAuthorizeSecretAfterTwoFactor() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
|
@ -49,15 +49,15 @@ func (s *HighAvailabilityWebDriverSuite) SetupTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserDataInDB() {
|
func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserDataInDB() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
|
secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
|
||||||
|
|
||||||
err := haDockerEnvironment.Restart("mariadb")
|
err := haDockerEnvironment.Restart("mariadb")
|
||||||
s.Assert().NoError(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(20 * time.Second)
|
||||||
|
|
||||||
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, "")
|
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, "")
|
||||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||||
|
@ -229,6 +229,10 @@ func (s *HighAvailabilitySuite) TestHighAvailabilityWebDriverSuite() {
|
||||||
suite.Run(s.T(), NewHighAvailabilityWebDriverSuite())
|
suite.Run(s.T(), NewHighAvailabilityWebDriverSuite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHighAvailabilityWebDriverSuite(t *testing.T) {
|
||||||
|
suite.Run(t, NewHighAvailabilityWebDriverSuite())
|
||||||
|
}
|
||||||
|
|
||||||
func TestHighAvailabilitySuite(t *testing.T) {
|
func TestHighAvailabilitySuite(t *testing.T) {
|
||||||
suite.Run(t, NewHighAvailabilitySuite())
|
suite.Run(t, NewHighAvailabilitySuite())
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ func init() {
|
||||||
SetUpTimeout: 5 * time.Minute,
|
SetUpTimeout: 5 * time.Minute,
|
||||||
OnSetupTimeout: onSetupTimeout,
|
OnSetupTimeout: onSetupTimeout,
|
||||||
TearDown: teardown,
|
TearDown: teardown,
|
||||||
TestTimeout: 2 * time.Minute,
|
TestTimeout: 3 * time.Minute,
|
||||||
TearDownTimeout: 2 * time.Minute,
|
TearDownTimeout: 2 * time.Minute,
|
||||||
Description: `This suite is used to test Authelia in a standalone
|
Description: `This suite is used to test Authelia in a standalone
|
||||||
configuration with in-memory sessions and a local sqlite db stored on disk`,
|
configuration with in-memory sessions and a local sqlite db stored on disk`,
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/clems4ever/authelia/internal/storage"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,6 +67,35 @@ func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated
|
||||||
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
|
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StandaloneWebDriverSuite) 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")
|
||||||
|
}
|
||||||
|
|
||||||
type StandaloneSuite struct {
|
type StandaloneSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
}
|
}
|
||||||
|
|
|
@ -1566,6 +1566,14 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-ga": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-ga/-/react-ga-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-7Vkv6wH1Kem4vkjuJxRYxDgLfokm0shugDk0W5p9C28POrsPAXezLbgP5C2tyFZ7lKARdyrCxwkdRTC1UV0dHg==",
|
||||||
|
"requires": {
|
||||||
|
"react-ga": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-router": {
|
"@types/react-router": {
|
||||||
"version": "5.1.3",
|
"version": "5.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.3.tgz",
|
||||||
|
@ -11311,6 +11319,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.3.tgz",
|
||||||
"integrity": "sha512-bOUvMWFQVk5oz8Ded9Xb7WVdEi3QGLC8tH7HmYP0Fdp4Bn3qw0tRFmr5TW6mvahzvmrK4a6bqWGfCevBflP+Xw=="
|
"integrity": "sha512-bOUvMWFQVk5oz8Ded9Xb7WVdEi3QGLC8tH7HmYP0Fdp4Bn3qw0tRFmr5TW6mvahzvmrK4a6bqWGfCevBflP+Xw=="
|
||||||
},
|
},
|
||||||
|
"react-ga": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-ga/-/react-ga-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-AjC7UOZMvygrWTc2hKxTDvlMXEtbmA0IgJjmkhgmQQ3RkXrWR11xEagLGFGaNyaPnmg24oaIiaNPnEoftUhfXA=="
|
||||||
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.12.0",
|
"version": "16.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"@types/query-string": "^6.3.0",
|
"@types/query-string": "^6.3.0",
|
||||||
"@types/react": "16.9.12",
|
"@types/react": "16.9.12",
|
||||||
"@types/react-dom": "16.9.4",
|
"@types/react-dom": "16.9.4",
|
||||||
|
"@types/react-ga": "^2.3.0",
|
||||||
"@types/react-router-dom": "^5.1.2",
|
"@types/react-router-dom": "^5.1.2",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
"query-string": "^6.9.0",
|
"query-string": "^6.9.0",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
|
"react-ga": "^2.7.0",
|
||||||
"react-loading": "^2.0.3",
|
"react-loading": "^2.0.3",
|
||||||
"react-otp-input": "^1.0.1",
|
"react-otp-input": "^1.0.1",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router, Route, Switch, Redirect
|
BrowserRouter as Router, Route, Switch, Redirect
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
@ -17,36 +17,52 @@ import NotificationsContext from './hooks/NotificationsContext';
|
||||||
import { Notification } from './models/Notifications';
|
import { Notification } from './models/Notifications';
|
||||||
import NotificationBar from './components/NotificationBar';
|
import NotificationBar from './components/NotificationBar';
|
||||||
import SignOut from './views/LoginPortal/SignOut/SignOut';
|
import SignOut from './views/LoginPortal/SignOut/SignOut';
|
||||||
|
import { useConfiguration } from './hooks/Configuration';
|
||||||
|
import Tracker from "./components/Tracker";
|
||||||
|
import { useTracking } from "./hooks/Tracking";
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [notification, setNotification] = useState(null as Notification | null);
|
const [notification, setNotification] = useState(null as Notification | null);
|
||||||
|
const [configuration, fetchConfig, , fetchConfigError] = useConfiguration();
|
||||||
|
const tracker = useTracking(configuration);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetchConfigError) {
|
||||||
|
console.error(fetchConfigError);
|
||||||
|
}
|
||||||
|
}, [fetchConfigError]);
|
||||||
|
|
||||||
|
useEffect(() => { fetchConfig() }, [fetchConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationsContext.Provider value={{ notification, setNotification }} >
|
<NotificationsContext.Provider value={{ notification, setNotification }} >
|
||||||
<NotificationBar onClose={() => setNotification(null)} />
|
|
||||||
<Router>
|
<Router>
|
||||||
<Switch>
|
<Tracker tracker={tracker}>
|
||||||
<Route path={ResetPasswordStep1Route} exact>
|
<NotificationBar onClose={() => setNotification(null)} />
|
||||||
<ResetPasswordStep1 />
|
<Switch>
|
||||||
</Route>
|
<Route path={ResetPasswordStep1Route} exact>
|
||||||
<Route path={ResetPasswordStep2Route} exact>
|
<ResetPasswordStep1 />
|
||||||
<ResetPasswordStep2 />
|
</Route>
|
||||||
</Route>
|
<Route path={ResetPasswordStep2Route} exact>
|
||||||
<Route path={RegisterSecurityKeyRoute} exact>
|
<ResetPasswordStep2 />
|
||||||
<RegisterSecurityKey />
|
</Route>
|
||||||
</Route>
|
<Route path={RegisterSecurityKeyRoute} exact>
|
||||||
<Route path={RegisterOneTimePasswordRoute} exact>
|
<RegisterSecurityKey />
|
||||||
<RegisterOneTimePassword />
|
</Route>
|
||||||
</Route>
|
<Route path={RegisterOneTimePasswordRoute} exact>
|
||||||
<Route path={LogoutRoute} exact>
|
<RegisterOneTimePassword />
|
||||||
<SignOut />
|
</Route>
|
||||||
</Route>
|
<Route path={LogoutRoute} exact>
|
||||||
<Route path={FirstFactorRoute}>
|
<SignOut />
|
||||||
<LoginPortal />
|
</Route>
|
||||||
</Route>
|
<Route path={FirstFactorRoute}>
|
||||||
<Route path="/">
|
<LoginPortal />
|
||||||
<Redirect to={FirstFactorRoute}></Redirect>
|
</Route>
|
||||||
</Route>
|
<Route path="/">
|
||||||
</Switch>
|
<Redirect to={FirstFactorRoute}></Redirect>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Tracker>
|
||||||
</Router>
|
</Router>
|
||||||
</NotificationsContext.Provider>
|
</NotificationsContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { mount } from "enzyme";
|
||||||
|
import Tracker from "./Tracker";
|
||||||
|
|
||||||
|
import { MemoryRouter as Router } from 'react-router-dom';
|
||||||
|
|
||||||
|
const mountWithRouter = node => mount(<Router>{node}</Router>);
|
||||||
|
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
mountWithRouter(<Tracker trackingIDs={[]} />);
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { useEffect, useCallback, Fragment, ReactNode } from "react";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import ReactGA, { Tracker } from "react-ga";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
tracker: Tracker | undefined;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (props: Props) {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const trackPage = useCallback((page: string) => {
|
||||||
|
if (props.tracker) {
|
||||||
|
ReactGA.set({ page });
|
||||||
|
ReactGA.pageview(page);
|
||||||
|
}
|
||||||
|
}, [props.tracker]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.tracker) {
|
||||||
|
ReactGA.initialize([props.tracker]);
|
||||||
|
}
|
||||||
|
}, [props.tracker]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
trackPage(location.pathname + location.search);
|
||||||
|
}, [trackPage, location.pathname, location.search]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{props.children}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,6 +1,10 @@
|
||||||
import { useRemoteCall } from "./RemoteCall";
|
import { useRemoteCall } from "./RemoteCall";
|
||||||
import { getAvailable2FAMethods } from "../services/Configuration";
|
import { getConfiguration, getExtendedConfiguration } from "../services/Configuration";
|
||||||
|
|
||||||
export function useAutheliaConfiguration() {
|
export function useConfiguration() {
|
||||||
return useRemoteCall(getAvailable2FAMethods, []);
|
return useRemoteCall(getConfiguration, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useExtendedConfiguration() {
|
||||||
|
return useRemoteCall(getExtendedConfiguration, []);
|
||||||
}
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Configuration } from "../models/Configuration";
|
||||||
|
import { Tracker } from "react-ga";
|
||||||
|
|
||||||
|
export function useTracking(configuration: Configuration | undefined) {
|
||||||
|
const [trackingIds, setTrackingIds] = useState(undefined as Tracker | undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (configuration && configuration.ga_tracking_id) {
|
||||||
|
setTrackingIds({ trackingId: configuration.ga_tracking_id });
|
||||||
|
}
|
||||||
|
}, [configuration]);
|
||||||
|
|
||||||
|
return trackingIds;
|
||||||
|
}
|
|
@ -1,3 +1,9 @@
|
||||||
import { SecondFactorMethod } from "./Methods";
|
import { SecondFactorMethod } from "./Methods";
|
||||||
|
|
||||||
export type Configuration = Set<SecondFactorMethod>
|
export interface Configuration {
|
||||||
|
ga_tracking_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtendedConfiguration {
|
||||||
|
available_methods: Set<SecondFactorMethod>;
|
||||||
|
}
|
|
@ -25,6 +25,9 @@ export const UserInfoPath = "/api/user/info";
|
||||||
export const UserInfo2FAMethodPath = "/api/user/info/2fa_method";
|
export const UserInfo2FAMethodPath = "/api/user/info/2fa_method";
|
||||||
export const Available2FAMethodsPath = "/api/secondfactor/available";
|
export const Available2FAMethodsPath = "/api/secondfactor/available";
|
||||||
|
|
||||||
|
export const ConfigurationPath = "/api/configuration";
|
||||||
|
export const ExtendedConfigurationPath = "/api/configuration/extended";
|
||||||
|
|
||||||
export interface ErrorResponse {
|
export interface ErrorResponse {
|
||||||
status: "KO";
|
status: "KO";
|
||||||
message: string;
|
message: string;
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import { Get } from "./Client";
|
import { Get } from "./Client";
|
||||||
import { Available2FAMethodsPath } from "./Api";
|
import { ExtendedConfigurationPath, ConfigurationPath } from "./Api";
|
||||||
import { Method2FA, toEnum } from "./UserPreferences";
|
import { toEnum, Method2FA } from "./UserPreferences";
|
||||||
import { Configuration } from "../models/Configuration";
|
import { Configuration, ExtendedConfiguration } from "../models/Configuration";
|
||||||
|
|
||||||
export async function getAvailable2FAMethods(): Promise<Configuration> {
|
export async function getConfiguration(): Promise<Configuration> {
|
||||||
const methods = await Get<Method2FA[]>(Available2FAMethodsPath);
|
return Get<Configuration>(ConfigurationPath);
|
||||||
return new Set(methods.map(toEnum));
|
}
|
||||||
|
|
||||||
|
interface ExtendedConfigurationPayload {
|
||||||
|
available_methods: Method2FA[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getExtendedConfiguration(): Promise<ExtendedConfiguration> {
|
||||||
|
const config = await Get<ExtendedConfigurationPayload>(ExtendedConfigurationPath);
|
||||||
|
return { ...config, available_methods: new Set(config.available_methods.map(toEnum)) };
|
||||||
}
|
}
|
|
@ -13,7 +13,7 @@ import { useNotifications } from "../../hooks/NotificationsContext";
|
||||||
import { useRedirectionURL } from "../../hooks/RedirectionURL";
|
import { useRedirectionURL } from "../../hooks/RedirectionURL";
|
||||||
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
|
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
|
||||||
import { SecondFactorMethod } from "../../models/Methods";
|
import { SecondFactorMethod } from "../../models/Methods";
|
||||||
import { useAutheliaConfiguration } from "../../hooks/Configuration";
|
import { useExtendedConfiguration } from "../../hooks/Configuration";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -24,7 +24,7 @@ export default function () {
|
||||||
|
|
||||||
const [state, fetchState, , fetchStateError] = useAutheliaState();
|
const [state, fetchState, , fetchStateError] = useAutheliaState();
|
||||||
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo();
|
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo();
|
||||||
const [configuration, fetchConfiguration, , fetchConfigurationError] = useAutheliaConfiguration();
|
const [configuration, fetchConfiguration, , fetchConfigurationError] = useExtendedConfiguration();
|
||||||
|
|
||||||
const redirect = useCallback((url: string) => history.push(url), [history]);
|
const redirect = useCallback((url: string) => history.push(url), [history]);
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from "../../../Routes";
|
} from "../../../Routes";
|
||||||
import { setPrefered2FAMethod } from "../../../services/UserPreferences";
|
import { setPrefered2FAMethod } from "../../../services/UserPreferences";
|
||||||
import { UserInfo } from "../../../models/UserInfo";
|
import { UserInfo } from "../../../models/UserInfo";
|
||||||
import { Configuration } from "../../../models/Configuration";
|
import { ExtendedConfiguration } from "../../../models/Configuration";
|
||||||
import u2fApi from "u2f-api";
|
import u2fApi from "u2f-api";
|
||||||
import { AuthenticationLevel } from "../../../services/State";
|
import { AuthenticationLevel } from "../../../services/State";
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ export interface Props {
|
||||||
authenticationLevel: AuthenticationLevel;
|
authenticationLevel: AuthenticationLevel;
|
||||||
|
|
||||||
userInfo: UserInfo;
|
userInfo: UserInfo;
|
||||||
configuration: Configuration;
|
configuration: ExtendedConfiguration;
|
||||||
|
|
||||||
onMethodChanged: (method: SecondFactorMethod) => void;
|
onMethodChanged: (method: SecondFactorMethod) => void;
|
||||||
onAuthenticationSuccess: (redirectURL: string | undefined) => void;
|
onAuthenticationSuccess: (redirectURL: string | undefined) => void;
|
||||||
|
@ -89,7 +89,7 @@ export default function (props: Props) {
|
||||||
showBrand>
|
showBrand>
|
||||||
<MethodSelectionDialog
|
<MethodSelectionDialog
|
||||||
open={methodSelectionOpen}
|
open={methodSelectionOpen}
|
||||||
methods={props.configuration}
|
methods={props.configuration.available_methods}
|
||||||
u2fSupported={u2fSupported}
|
u2fSupported={u2fSupported}
|
||||||
onClose={() => setMethodSelectionOpen(false)}
|
onClose={() => setMethodSelectionOpen(false)}
|
||||||
onClick={handleMethodSelected} />
|
onClick={handleMethodSelected} />
|
||||||
|
|
Loading…
Reference in New Issue