[FIX] Redirect to default URL after 1FA when default policy is one_factor. (#611)
* Redirect to default URL after 1FA when default policy is one_factor. User is now redirected to the default redirection URL after 1FA if the default policy is set to one_factor and there is no target URL or if the target URL is unsafe. Also, if the default policy is set to one_factor and the user is already authenticated, if she visits the login portal, the 'already authenticated' view is displayed with a logout button. This fixes #581. * Update users.yml * Fix permissions issue causing suite test failurepull/621/head
parent
9c9d8518eb
commit
d1d02d9eae
|
@ -27,9 +27,11 @@ var ErrNoRunningSuite = errors.New("no running suite")
|
|||
var runningSuiteFile = ".suite"
|
||||
|
||||
var headless bool
|
||||
var testPattern string
|
||||
|
||||
func init() {
|
||||
SuitesTestCmd.Flags().BoolVar(&headless, "headless", false, "Run tests in headless mode")
|
||||
SuitesTestCmd.Flags().StringVar(&testPattern, "test", "", "The single test to run")
|
||||
}
|
||||
|
||||
// SuitesListCmd Command for listing the available suites.
|
||||
|
@ -184,15 +186,14 @@ func testSuite(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// If suite(s) are provided as argument
|
||||
if len(args) == 1 {
|
||||
if len(args) >= 1 {
|
||||
suiteArg := args[0]
|
||||
|
||||
if runningSuite != "" && suiteArg != runningSuite {
|
||||
log.Fatal(errors.New("Running suite (" + runningSuite + ") is different than suite(s) to be tested (" + suiteArg + "). Shutdown running suite and retry"))
|
||||
}
|
||||
|
||||
suiteNames := strings.Split(suiteArg, ",")
|
||||
if err := runMultipleSuitesTests(suiteNames, runningSuite == ""); err != nil {
|
||||
if err := runMultipleSuitesTests(strings.Split(suiteArg, ","), runningSuite == ""); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
|
@ -239,7 +240,13 @@ func runSuiteTests(suiteName string, withEnv bool) error {
|
|||
if suite.TestTimeout > 0 {
|
||||
timeout = fmt.Sprintf("%ds", int64(suite.TestTimeout/time.Second))
|
||||
}
|
||||
testCmdLine := fmt.Sprintf("go test -count=1 -v ./internal/suites -timeout %s -run '^(Test%sSuite)$'", timeout, suiteName)
|
||||
testCmdLine := fmt.Sprintf("go test -count=1 -v ./internal/suites -timeout %s ", timeout)
|
||||
|
||||
if testPattern != "" {
|
||||
testCmdLine += fmt.Sprintf("-run '%s'", testPattern)
|
||||
} else {
|
||||
testCmdLine += fmt.Sprintf("-run '^(Test%sSuite)$'", suiteName)
|
||||
}
|
||||
|
||||
log.Infof("Running tests of suite %s...", suiteName)
|
||||
log.Debugf("Running tests with command: %s", testCmdLine)
|
||||
|
|
|
@ -87,7 +87,7 @@ func startServer() {
|
|||
}
|
||||
|
||||
clock := utils.RealClock{}
|
||||
authorizer := authorization.NewAuthorizer(*config.AccessControl)
|
||||
authorizer := authorization.NewAuthorizer(config.AccessControl)
|
||||
sessionProvider := session.NewProvider(config.Session)
|
||||
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ func selectMatchingRules(rules []schema.ACLRule, subject Subject, object Object)
|
|||
return selectMatchingObjectRules(matchingRules, object)
|
||||
}
|
||||
|
||||
func policyToLevel(policy string) Level {
|
||||
func PolicyToLevel(policy string) Level {
|
||||
switch policy {
|
||||
case "bypass":
|
||||
return Bypass
|
||||
|
@ -183,7 +183,7 @@ func (p *Authorizer) GetRequiredLevel(subject Subject, requestURL url.URL) Level
|
|||
})
|
||||
|
||||
if len(matchingRules) > 0 {
|
||||
return policyToLevel(matchingRules[0].Policy)
|
||||
return PolicyToLevel(matchingRules[0].Policy)
|
||||
}
|
||||
return policyToLevel(p.configuration.DefaultPolicy)
|
||||
return PolicyToLevel(p.configuration.DefaultPolicy)
|
||||
}
|
||||
|
|
|
@ -255,6 +255,15 @@ func (s *AuthorizerSuite) TestShouldCheckResourceMatching() {
|
|||
tester.CheckAuthorizations(s.T(), John, "https://resource.example.com/xyz/embedded/abc", Bypass)
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestPolicyToLevel() {
|
||||
s.Assert().Equal(Bypass, PolicyToLevel("bypass"))
|
||||
s.Assert().Equal(OneFactor, PolicyToLevel("one_factor"))
|
||||
s.Assert().Equal(TwoFactor, PolicyToLevel("two_factor"))
|
||||
s.Assert().Equal(Denied, PolicyToLevel("deny"))
|
||||
|
||||
s.Assert().Equal(Denied, PolicyToLevel("whatever"))
|
||||
}
|
||||
|
||||
func TestRunSuite(t *testing.T) {
|
||||
s := AuthorizerSuite{}
|
||||
suite.Run(t, &s)
|
||||
|
|
|
@ -17,7 +17,7 @@ type Configuration struct {
|
|||
|
||||
TOTP *TOTPConfiguration `mapstructure:"totp"`
|
||||
DuoAPI *DuoAPIConfiguration `mapstructure:"duo_api"`
|
||||
AccessControl *AccessControlConfiguration `mapstructure:"access_control"`
|
||||
AccessControl AccessControlConfiguration `mapstructure:"access_control"`
|
||||
Regulation *RegulationConfiguration `mapstructure:"regulation"`
|
||||
Storage *StorageConfiguration `mapstructure:"storage"`
|
||||
Notifier *NotifierConfiguration `mapstructure:"notifier"`
|
||||
|
|
|
@ -49,5 +49,9 @@ func Validate(configuration *schema.Configuration, validator *schema.StructValid
|
|||
ValidateNotifier(configuration.Notifier, validator)
|
||||
}
|
||||
|
||||
if configuration.AccessControl.DefaultPolicy == "" {
|
||||
configuration.AccessControl.DefaultPolicy = "deny"
|
||||
}
|
||||
|
||||
ValidateSQLStorage(configuration.Storage, validator)
|
||||
}
|
||||
|
|
|
@ -91,3 +91,13 @@ func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) {
|
|||
require.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "A notifier configuration must be provided")
|
||||
}
|
||||
|
||||
func TestShouldAddDefaultAccessControl(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
|
||||
Validate(&config, validator)
|
||||
require.Len(t, validator.Errors(), 0)
|
||||
assert.NotNil(t, config.AccessControl)
|
||||
assert.Equal(t, "deny", config.AccessControl.DefaultPolicy)
|
||||
}
|
||||
|
|
|
@ -2,11 +2,15 @@ package handlers
|
|||
|
||||
import (
|
||||
"github.com/authelia/authelia/internal/authentication"
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
||||
type ExtendedConfigurationBody struct {
|
||||
AvailableMethods MethodList `json:"available_methods"`
|
||||
|
||||
// OneFactorDefaultPolicy is set if default policy is 'one_factor'
|
||||
OneFactorDefaultPolicy bool `json:"one_factor_default_policy"`
|
||||
}
|
||||
|
||||
// ExtendedConfigurationGet get the extended configuration accessible to authenticated users.
|
||||
|
@ -18,6 +22,10 @@ func ExtendedConfigurationGet(ctx *middlewares.AutheliaCtx) {
|
|||
body.AvailableMethods = append(body.AvailableMethods, authentication.Push)
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Available methods are %s", body.AvailableMethods)
|
||||
defaultPolicy := authorization.PolicyToLevel(ctx.Configuration.AccessControl.DefaultPolicy)
|
||||
body.OneFactorDefaultPolicy = defaultPolicy == authorization.OneFactor
|
||||
ctx.Logger.Tracef("Default policy set to one factor: %v", body.OneFactorDefaultPolicy)
|
||||
|
||||
ctx.Logger.Tracef("Available methods are %s", body.AvailableMethods)
|
||||
ctx.SetJSONBody(body)
|
||||
}
|
||||
|
|
|
@ -2,15 +2,12 @@ package handlers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/authelia/authelia/internal/authentication"
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
"github.com/authelia/authelia/internal/regulation"
|
||||
"github.com/authelia/authelia/internal/session"
|
||||
"github.com/authelia/authelia/internal/utils"
|
||||
)
|
||||
|
||||
// FirstFactorPost is the handler performing the first factory.
|
||||
|
@ -111,30 +108,5 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
if bodyJSON.TargetURL != "" {
|
||||
targetURL, err := url.ParseRequestURI(bodyJSON.TargetURL)
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to parse target URL %s: %s", bodyJSON.TargetURL, err), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
requiredLevel := ctx.Providers.Authorizer.GetRequiredLevel(authorization.Subject{
|
||||
Username: userSession.Username,
|
||||
Groups: userSession.Groups,
|
||||
IP: ctx.RemoteIP(),
|
||||
}, *targetURL)
|
||||
|
||||
ctx.Logger.Debugf("Required level for the URL %s is %d", targetURL.String(), requiredLevel)
|
||||
|
||||
safeRedirection := utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain)
|
||||
|
||||
if safeRedirection && requiredLevel <= authorization.OneFactor {
|
||||
ctx.Logger.Debugf("Redirection is safe, redirecting...")
|
||||
response := redirectResponse{bodyJSON.TargetURL}
|
||||
ctx.SetJSONBody(response)
|
||||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
Handle1FAResponse(ctx, bodyJSON.TargetURL, userSession.Username, userSession.Groups)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
"github.com/authelia/authelia/internal/mocks"
|
||||
"github.com/authelia/authelia/internal/models"
|
||||
|
||||
|
@ -229,7 +230,76 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() {
|
|||
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
|
||||
}
|
||||
|
||||
func TestFirstFactorSuite(t *testing.T) {
|
||||
firstFactorSuite := new(FirstFactorSuite)
|
||||
suite.Run(t, firstFactorSuite)
|
||||
type FirstFactorRedirectionSuite struct {
|
||||
suite.Suite
|
||||
|
||||
mock *mocks.MockAutheliaCtx
|
||||
}
|
||||
|
||||
func (s *FirstFactorRedirectionSuite) SetupTest() {
|
||||
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
||||
s.mock.Ctx.Configuration.DefaultRedirectionURL = "https://default.local"
|
||||
s.mock.Ctx.Configuration.AccessControl.DefaultPolicy = "one_factor"
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(
|
||||
s.mock.Ctx.Configuration.AccessControl)
|
||||
|
||||
s.mock.UserProviderMock.
|
||||
EXPECT().
|
||||
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
||||
Return(true, nil)
|
||||
|
||||
s.mock.UserProviderMock.
|
||||
EXPECT().
|
||||
GetDetails(gomock.Eq("test")).
|
||||
Return(&authentication.UserDetails{
|
||||
Emails: []string{"test@example.com"},
|
||||
Groups: []string{"dev", "admins"},
|
||||
}, nil)
|
||||
|
||||
s.mock.StorageProviderMock.
|
||||
EXPECT().
|
||||
AppendAuthenticationLog(gomock.Any()).
|
||||
Return(nil)
|
||||
}
|
||||
|
||||
func (s *FirstFactorRedirectionSuite) TearDownTest() {
|
||||
s.mock.Close()
|
||||
}
|
||||
|
||||
// When the target url is unknown, default policy is to one_factor and default_redirect_url
|
||||
// is provided, the user should be redirected to the default url.
|
||||
func (s *FirstFactorRedirectionSuite) TestShouldRedirectUserToDefaultRedirectionURLWhenNoTargetURLProvided() {
|
||||
s.mock.Ctx.Request.SetBodyString(`{
|
||||
"username": "test",
|
||||
"password": "hello",
|
||||
"keepMeLoggedIn": false
|
||||
}`)
|
||||
FirstFactorPost(s.mock.Ctx)
|
||||
|
||||
// Respond with 200.
|
||||
s.mock.Assert200OK(s.T(), redirectResponse{
|
||||
Redirect: "https://default.local",
|
||||
})
|
||||
}
|
||||
|
||||
// When the target url is unsafe, default policy is set to one_factor and default_redirect_url
|
||||
// is provided, the user should be redirected to the default url.
|
||||
func (s *FirstFactorRedirectionSuite) TestShouldRedirectUserToDefaultRedirectionURLWhenURLIsUnsafe() {
|
||||
s.mock.Ctx.Request.SetBodyString(`{
|
||||
"username": "test",
|
||||
"password": "hello",
|
||||
"keepMeLoggedIn": false,
|
||||
"targetURL": "http://notsafe.local"
|
||||
}`)
|
||||
FirstFactorPost(s.mock.Ctx)
|
||||
|
||||
// Respond with 200.
|
||||
s.mock.Assert200OK(s.T(), redirectResponse{
|
||||
Redirect: "https://default.local",
|
||||
})
|
||||
}
|
||||
|
||||
func TestFirstFactorSuite(t *testing.T) {
|
||||
suite.Run(t, new(FirstFactorSuite))
|
||||
suite.Run(t, new(FirstFactorRedirectionSuite))
|
||||
}
|
||||
|
|
|
@ -51,6 +51,6 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
|
|||
return
|
||||
}
|
||||
|
||||
HandleAuthResponse(ctx, requestBody.TargetURL)
|
||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,6 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
|
|||
return
|
||||
}
|
||||
|
||||
HandleAuthResponse(ctx, bodyJSON.TargetURL)
|
||||
Handle2FAResponse(ctx, bodyJSON.TargetURL)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,6 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler
|
|||
return
|
||||
}
|
||||
|
||||
HandleAuthResponse(ctx, requestBody.TargetURL)
|
||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,69 @@ import (
|
|||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
"github.com/authelia/authelia/internal/utils"
|
||||
)
|
||||
|
||||
func HandleAuthResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
|
||||
if targetURI != "" {
|
||||
func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI string, username string, groups []string) {
|
||||
if targetURI == "" {
|
||||
if authorization.PolicyToLevel(ctx.Configuration.AccessControl.DefaultPolicy) == authorization.OneFactor &&
|
||||
ctx.Configuration.DefaultRedirectionURL != "" {
|
||||
ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
||||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
targetURL, err := url.ParseRequestURI(targetURI)
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to parse target URL %s: %s", targetURI, err), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
requiredLevel := ctx.Providers.Authorizer.GetRequiredLevel(authorization.Subject{
|
||||
Username: username,
|
||||
Groups: groups,
|
||||
IP: ctx.RemoteIP(),
|
||||
}, *targetURL)
|
||||
|
||||
ctx.Logger.Debugf("Required level for the URL %s is %d", targetURI, requiredLevel)
|
||||
|
||||
if requiredLevel > authorization.OneFactor {
|
||||
ctx.Logger.Warnf("%s requires more than 1FA, cannot be redirected to", targetURI)
|
||||
ctx.ReplyOK()
|
||||
return
|
||||
}
|
||||
|
||||
safeRedirection := utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain)
|
||||
|
||||
if !safeRedirection {
|
||||
if authorization.PolicyToLevel(ctx.Configuration.AccessControl.DefaultPolicy) == authorization.OneFactor &&
|
||||
ctx.Configuration.DefaultRedirectionURL != "" {
|
||||
ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
||||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
|
||||
response := redirectResponse{Redirect: targetURI}
|
||||
ctx.SetJSONBody(response)
|
||||
}
|
||||
|
||||
func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
|
||||
if targetURI == "" {
|
||||
if ctx.Configuration.DefaultRedirectionURL != "" {
|
||||
ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
||||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
targetURL, err := url.ParseRequestURI(targetURI)
|
||||
|
||||
if err != nil {
|
||||
|
@ -22,11 +79,4 @@ func HandleAuthResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
|
|||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
} else {
|
||||
if ctx.Configuration.DefaultRedirectionURL != "" {
|
||||
ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
||||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,9 +66,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
|||
datetime, _ := time.Parse("2006-Jan-02", "2013-Feb-03")
|
||||
mockAuthelia.Clock.Set(datetime)
|
||||
|
||||
configuration := schema.Configuration{
|
||||
AccessControl: new(schema.AccessControlConfiguration),
|
||||
}
|
||||
configuration := schema.Configuration{}
|
||||
configuration.Session.Name = "authelia_session"
|
||||
configuration.AccessControl.DefaultPolicy = "deny"
|
||||
configuration.AccessControl.Rules = []schema.ACLRule{schema.ACLRule{
|
||||
|
@ -98,7 +96,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
|||
providers.Notifier = mockAuthelia.NotifierMock
|
||||
|
||||
providers.Authorizer = authorization.NewAuthorizer(
|
||||
*configuration.AccessControl)
|
||||
configuration.AccessControl)
|
||||
|
||||
providers.SessionProvider = session.NewProvider(
|
||||
configuration.Session)
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
###############################################################
|
||||
# Authelia minimal configuration #
|
||||
###############################################################
|
||||
|
||||
port: 9091
|
||||
|
||||
logs_level: debug
|
||||
|
||||
default_redirection_url: https://home.example.com:8080/
|
||||
|
||||
jwt_secret: unsecure_secret
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: /var/lib/authelia/users.yml
|
||||
|
||||
session:
|
||||
secret: unsecure_session_secret
|
||||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
|
||||
storage:
|
||||
local:
|
||||
path: /var/lib/authelia/db.sqlite
|
||||
|
||||
access_control:
|
||||
default_policy: one_factor
|
||||
|
||||
notifier:
|
||||
smtp:
|
||||
host: smtp
|
||||
port: 1025
|
||||
sender: admin@example.com
|
||||
disable_require_tls: true
|
|
@ -0,0 +1,6 @@
|
|||
version: '3'
|
||||
services:
|
||||
authelia-backend:
|
||||
volumes:
|
||||
- './internal/suites/OneFactorDefaultPolicy/configuration.yml:/etc/authelia/configuration.yml:ro'
|
||||
- './internal/suites/OneFactorDefaultPolicy/users.yml:/var/lib/authelia/users.yml'
|
|
@ -0,0 +1,29 @@
|
|||
###############################################################
|
||||
# Users Database #
|
||||
###############################################################
|
||||
|
||||
# This file can be used if you do not have an LDAP set up.
|
||||
|
||||
# List of users
|
||||
users:
|
||||
john:
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
|
||||
harry:
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: harry.potter@authelia.com
|
||||
groups: []
|
||||
|
||||
bob:
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: bob.dylan@authelia.com
|
||||
groups:
|
||||
- dev
|
||||
|
||||
james:
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: james.dean@authelia.com
|
|
@ -7,3 +7,5 @@ services:
|
|||
volumes:
|
||||
- './internal/suites/Standalone/configuration.yml:/etc/authelia/configuration.yml:ro'
|
||||
- './internal/suites/Standalone/users.yml:/var/lib/authelia/users.yml'
|
||||
- '/tmp:/tmp'
|
||||
user: ${USER_ID}:${GROUP_ID}
|
|
@ -1,19 +1,19 @@
|
|||
users:
|
||||
bob:
|
||||
password: '$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
|
||||
password: $6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||
email: bob.dylan@authelia.com
|
||||
groups:
|
||||
- dev
|
||||
harry:
|
||||
password: '$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
|
||||
password: $6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||
email: harry.potter@authelia.com
|
||||
groups: []
|
||||
james:
|
||||
password: '$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
|
||||
password: $6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||
email: james.dean@authelia.com
|
||||
groups: []
|
||||
john:
|
||||
password: '$6$rounds=50000$LnfgDsc2WD8F2qNf$0gcCt8jlqAGZRv2ee3mCFsfAr1P4N7kESWEf36Xtw6OjkhAcQuGVOBHXp0lFuZbppa7YlgHk3VD28aSQu9U9S1'
|
||||
password: $6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
|
|
|
@ -70,7 +70,7 @@ func (s *RedirectionCheckScenario) TestShouldRedirectOnlyWhenDomainIsHandledByAu
|
|||
if redirected {
|
||||
s.verifySecretAuthorized(ctx, t)
|
||||
} else {
|
||||
s.WaitElementLocatedByClassName(ctx, t, "success-icon")
|
||||
s.verifyIsAuthenticatedPage(ctx, t)
|
||||
}
|
||||
s.doLogout(ctx, t)
|
||||
})
|
||||
|
|
|
@ -65,17 +65,17 @@ func (s *UserPreferencesScenario) TestShouldRememberLastUsed2FAMethod() {
|
|||
// Then go back to portal.
|
||||
s.doVisit(s.T(), LoginBaseURL)
|
||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||
|
||||
// And check the latest method is still used.
|
||||
s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method")
|
||||
// Meaning the authentication is successful
|
||||
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
|
||||
s.verifyIsHome(ctx, s.T())
|
||||
|
||||
// Logout the user and see what user 'harry' sees.
|
||||
s.doLogout(ctx, s.T())
|
||||
s.doLoginOneFactor(ctx, s.T(), "harry", "password", false, "")
|
||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||
s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method")
|
||||
|
||||
s.doLogout(ctx, s.T())
|
||||
s.verifyIsFirstFactorPage(ctx, s.T())
|
||||
|
||||
|
@ -83,7 +83,10 @@ func (s *UserPreferencesScenario) TestShouldRememberLastUsed2FAMethod() {
|
|||
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||
s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method")
|
||||
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
|
||||
s.verifyIsHome(ctx, s.T())
|
||||
|
||||
s.doLogout(ctx, s.T())
|
||||
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||
|
||||
// Eventually restore the default method
|
||||
s.doChangeMethod(ctx, s.T(), "one-time-password")
|
||||
|
|
|
@ -46,6 +46,9 @@ func (s *DuoPushWebDriverSuite) TearDownTest() {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s.doLogout(ctx, s.T())
|
||||
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||
s.doChangeMethod(ctx, s.T(), "one-time-password")
|
||||
s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method")
|
||||
}
|
||||
|
@ -58,7 +61,7 @@ func (s *DuoPushWebDriverSuite) TestShouldSucceedAuthentication() {
|
|||
|
||||
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||
s.doChangeMethod(ctx, s.T(), "push-notification")
|
||||
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
|
||||
s.verifyIsHome(ctx, s.T())
|
||||
}
|
||||
|
||||
func (s *DuoPushWebDriverSuite) TestShouldFailAuthentication() {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var oneFactorDefaultPolicySuiteName = "OneFactorDefaultPolicy"
|
||||
|
||||
func init() {
|
||||
dockerEnvironment := NewDockerEnvironment([]string{
|
||||
"docker-compose.yml",
|
||||
"internal/suites/OneFactorDefaultPolicy/docker-compose.yml",
|
||||
"example/compose/authelia/docker-compose.backend.{}.yml",
|
||||
"example/compose/authelia/docker-compose.frontend.{}.yml",
|
||||
"example/compose/nginx/backend/docker-compose.yml",
|
||||
"example/compose/nginx/portal/docker-compose.yml",
|
||||
})
|
||||
|
||||
setup := func(suitePath string) error {
|
||||
if err := dockerEnvironment.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaBackendIsReady(dockerEnvironment)
|
||||
}
|
||||
|
||||
onSetupTimeout := func() error {
|
||||
backendLogs, err := dockerEnvironment.Logs("authelia-backend", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(backendLogs)
|
||||
|
||||
frontendLogs, err := dockerEnvironment.Logs("authelia-frontend", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(frontendLogs)
|
||||
return nil
|
||||
}
|
||||
|
||||
teardown := func(suitePath string) error {
|
||||
return dockerEnvironment.Down()
|
||||
}
|
||||
|
||||
GlobalRegistry.Register(oneFactorDefaultPolicySuiteName, Suite{
|
||||
SetUp: setup,
|
||||
SetUpTimeout: 5 * time.Minute,
|
||||
OnSetupTimeout: onSetupTimeout,
|
||||
TestTimeout: 1 * time.Minute,
|
||||
TearDown: teardown,
|
||||
TearDownTimeout: 2 * time.Minute,
|
||||
Description: "This suite has been created to test Authelia with a one factor default policy on all resources",
|
||||
})
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type OneFactorDefaultPolicySuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
type OneFactorDefaultPolicyWebSuite struct {
|
||||
*SeleniumSuite
|
||||
}
|
||||
|
||||
func NewOneFactorDefaultPolicyWebSuite() *OneFactorDefaultPolicyWebSuite {
|
||||
return &OneFactorDefaultPolicyWebSuite{SeleniumSuite: new(SeleniumSuite)}
|
||||
}
|
||||
|
||||
func (s *OneFactorDefaultPolicyWebSuite) SetupSuite() {
|
||||
wds, err := StartWebDriver()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
s.WebDriverSession = wds
|
||||
}
|
||||
|
||||
func (s *OneFactorDefaultPolicyWebSuite) TearDownSuite() {
|
||||
err := s.WebDriverSession.Stop()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OneFactorDefaultPolicyWebSuite) SetupTest() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s.doLogout(ctx, s.T())
|
||||
}
|
||||
|
||||
// No target url is provided, then the user should be redirect to the default url.
|
||||
func (s *OneFactorDefaultPolicyWebSuite) TestShouldRedirectUserToDefaultURL() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||
s.verifyURLIs(ctx, s.T(), HomeBaseURL+"/")
|
||||
}
|
||||
|
||||
// Unsafe URL is provided, then the user should be redirect to the default url.
|
||||
func (s *OneFactorDefaultPolicyWebSuite) TestShouldRedirectUserToDefaultURLWhenURLIsUnsafe() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "http://unsafe.local")
|
||||
s.verifyURLIs(ctx, s.T(), HomeBaseURL+"/")
|
||||
}
|
||||
|
||||
// When use logged in and visit the portal again, she gets redirect to the authenticated view.
|
||||
func (s *OneFactorDefaultPolicyWebSuite) TestShouldDisplayAuthenticatedView() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "http://unsafe.local")
|
||||
s.verifyURLIs(ctx, s.T(), HomeBaseURL+"/")
|
||||
s.doVisit(s.T(), LoginBaseURL)
|
||||
s.verifyIsAuthenticatedPage(ctx, s.T())
|
||||
}
|
||||
|
||||
func (s *OneFactorDefaultPolicySuite) TestWeb() {
|
||||
suite.Run(s.T(), NewOneFactorDefaultPolicyWebSuite())
|
||||
}
|
||||
|
||||
func TestOneFactorDefaultPolicySuite(t *testing.T) {
|
||||
suite.Run(t, new(OneFactorDefaultPolicySuite))
|
||||
}
|
|
@ -62,10 +62,7 @@ func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated
|
|||
|
||||
// Visit the login page and wait for redirection to 2FA page with success icon displayed
|
||||
s.doVisit(s.T(), LoginBaseURL)
|
||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||
|
||||
// Check whether the success icon is displayed
|
||||
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
|
||||
s.verifyIsAuthenticatedPage(ctx, s.T())
|
||||
}
|
||||
|
||||
func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice() {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (wds *WebDriverSession) verifyIsAuthenticatedPage(ctx context.Context, t *testing.T) {
|
||||
wds.WaitElementLocatedByID(ctx, t, "authenticated-stage")
|
||||
}
|
|
@ -16,11 +16,12 @@
|
|||
"@types/node": "12.12.12",
|
||||
"@types/qrcode.react": "^1.0.0",
|
||||
"@types/query-string": "^6.3.0",
|
||||
"@types/react": "16.9.12",
|
||||
"@types/react": "^16.9.19",
|
||||
"@types/react-dom": "16.9.4",
|
||||
"@types/react-ga": "^2.3.0",
|
||||
"@types/react-router-dom": "^5.1.2",
|
||||
"axios": "^0.19.0",
|
||||
"babel-preset-react-app": "^9.1.1",
|
||||
"chai": "^4.2.0",
|
||||
"classnames": "^2.2.6",
|
||||
"enzyme": "^3.10.0",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
export const FirstFactorRoute = "/";
|
||||
export const AuthenticatedRoute = "/authenticated";
|
||||
|
||||
export const SecondFactorRoute = "/2fa";
|
||||
export const SecondFactorU2FRoute = "/2fa/security-key";
|
||||
|
|
|
@ -2,9 +2,7 @@ import React from "react";
|
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
export interface Props { }
|
||||
|
||||
export default function (props: Props) {
|
||||
export default function () {
|
||||
return (
|
||||
<FontAwesomeIcon icon={faCheckCircle} size="4x" color="green" className="success-icon" />
|
||||
)
|
||||
|
|
|
@ -6,4 +6,5 @@ export interface Configuration {
|
|||
|
||||
export interface ExtendedConfiguration {
|
||||
available_methods: Set<SecondFactorMethod>;
|
||||
one_factor_default_policy: boolean;
|
||||
}
|
|
@ -9,6 +9,7 @@ export async function getConfiguration(): Promise<Configuration> {
|
|||
|
||||
interface ExtendedConfigurationPayload {
|
||||
available_methods: Method2FA[];
|
||||
one_factor_default_policy: boolean;
|
||||
}
|
||||
|
||||
export async function getExtendedConfiguration(): Promise<ExtendedConfiguration> {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import React from "react";
|
||||
import SuccessIcon from "../../components/SuccessIcon";
|
||||
import { Typography, makeStyles } from "@material-ui/core";
|
||||
|
||||
export default function () {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div id="authenticated-stage">
|
||||
<div className={classes.iconContainer}>
|
||||
<SuccessIcon />
|
||||
</div>
|
||||
<Typography>Authenticated</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
iconContainer: {
|
||||
marginBottom: theme.spacing(2),
|
||||
flex: "0 0 100%"
|
||||
}
|
||||
}))
|
|
@ -0,0 +1,47 @@
|
|||
import React from "react";
|
||||
import { Grid, makeStyles, Button } from "@material-ui/core";
|
||||
import { useHistory } from "react-router";
|
||||
import LoginLayout from "../../../layouts/LoginLayout";
|
||||
import { LogoutRoute as SignOutRoute } from "../../../Routes";
|
||||
import Authenticated from "../Authenticated";
|
||||
|
||||
export interface Props {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export default function (props: Props) {
|
||||
const style = useStyles();
|
||||
const history = useHistory();
|
||||
|
||||
const handleLogoutClick = () => {
|
||||
history.push(SignOutRoute);
|
||||
}
|
||||
|
||||
return (
|
||||
<LoginLayout
|
||||
id="authenticated-stage"
|
||||
title={`Hi ${props.username}`}
|
||||
showBrand>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
|
||||
Logout
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={style.mainContainer}>
|
||||
<Authenticated />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</LoginLayout>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
mainContainer: {
|
||||
border: "1px solid #d6d6d6",
|
||||
borderRadius: "10px",
|
||||
padding: theme.spacing(4),
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
}
|
||||
}))
|
|
@ -4,7 +4,7 @@ import FirstFactorForm from "./FirstFactor/FirstFactorForm";
|
|||
import SecondFactorForm from "./SecondFactor/SecondFactorForm";
|
||||
import {
|
||||
FirstFactorRoute, SecondFactorRoute, SecondFactorTOTPRoute,
|
||||
SecondFactorPushRoute, SecondFactorU2FRoute
|
||||
SecondFactorPushRoute, SecondFactorU2FRoute, AuthenticatedRoute
|
||||
} from "../../Routes";
|
||||
import { useAutheliaState } from "../../hooks/State";
|
||||
import LoadingPage from "../LoadingPage/LoadingPage";
|
||||
|
@ -14,6 +14,7 @@ import { useRedirectionURL } from "../../hooks/RedirectionURL";
|
|||
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
|
||||
import { SecondFactorMethod } from "../../models/Methods";
|
||||
import { useExtendedConfiguration } from "../../hooks/Configuration";
|
||||
import AuthenticatedView from "./AuthenticatedView/AuthenticatedView";
|
||||
|
||||
export default function () {
|
||||
const history = useHistory();
|
||||
|
@ -77,7 +78,10 @@ export default function () {
|
|||
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
||||
setFirstFactorDisabled(false);
|
||||
redirect(`${FirstFactorRoute}${redirectionSuffix}`);
|
||||
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo) {
|
||||
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo && configuration) {
|
||||
if (configuration.one_factor_default_policy) {
|
||||
redirect(AuthenticatedRoute);
|
||||
} else {
|
||||
if (userInfo.method === SecondFactorMethod.U2F) {
|
||||
redirect(`${SecondFactorU2FRoute}${redirectionSuffix}`);
|
||||
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
|
||||
|
@ -87,9 +91,10 @@ export default function () {
|
|||
}
|
||||
}
|
||||
}
|
||||
}, [state, redirectionURL, redirect, userInfo, setFirstFactorDisabled]);
|
||||
}
|
||||
}, [state, redirectionURL, redirect, userInfo, setFirstFactorDisabled, configuration]);
|
||||
|
||||
const handleFirstFactorSuccess = async (redirectionURL: string | undefined) => {
|
||||
const handleAuthSuccess = async (redirectionURL: string | undefined) => {
|
||||
if (redirectionURL) {
|
||||
// Do an external redirection pushed by the server.
|
||||
window.location.href = redirectionURL;
|
||||
|
@ -99,15 +104,6 @@ export default function () {
|
|||
}
|
||||
}
|
||||
|
||||
const handleSecondFactorSuccess = async (redirectionURL: string | undefined) => {
|
||||
if (redirectionURL) {
|
||||
// Do an external redirection pushed by the server.
|
||||
window.location.href = redirectionURL;
|
||||
} else {
|
||||
fetchState();
|
||||
}
|
||||
}
|
||||
|
||||
const firstFactorReady = state !== undefined &&
|
||||
state.authentication_level === AuthenticationLevel.Unauthenticated &&
|
||||
location.pathname === FirstFactorRoute;
|
||||
|
@ -120,7 +116,7 @@ export default function () {
|
|||
disabled={firstFactorDisabled}
|
||||
onAuthenticationStart={() => setFirstFactorDisabled(true)}
|
||||
onAuthenticationFailure={() => setFirstFactorDisabled(false)}
|
||||
onAuthenticationSuccess={handleFirstFactorSuccess} />
|
||||
onAuthenticationSuccess={handleAuthSuccess} />
|
||||
</ComponentOrLoading>
|
||||
</Route>
|
||||
<Route path={SecondFactorRoute}>
|
||||
|
@ -130,7 +126,10 @@ export default function () {
|
|||
userInfo={userInfo}
|
||||
configuration={configuration}
|
||||
onMethodChanged={() => fetchUserInfo()}
|
||||
onAuthenticationSuccess={handleSecondFactorSuccess} /> : null}
|
||||
onAuthenticationSuccess={handleAuthSuccess} /> : null}
|
||||
</Route>
|
||||
<Route path={AuthenticatedRoute} exact>
|
||||
{state ? <AuthenticatedView username={state.username} /> : null}
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Redirect to={FirstFactorRoute} />
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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";
|
||||
import Authenticated from "../Authenticated";
|
||||
|
||||
export enum State {
|
||||
ALREADY_AUTHENTICATED = 1,
|
||||
|
@ -27,7 +27,7 @@ export default function (props: Props) {
|
|||
let stateClass: string = '';
|
||||
switch (props.state) {
|
||||
case State.ALREADY_AUTHENTICATED:
|
||||
container = <AlreadyAuthenticatedContainer />
|
||||
container = <Authenticated />
|
||||
stateClass = "state-already-authenticated";
|
||||
break;
|
||||
case State.NOT_REGISTERED:
|
||||
|
@ -42,6 +42,7 @@ export default function (props: Props) {
|
|||
break;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div id={props.id}>
|
||||
<Typography variant="h6">{props.title}</Typography>
|
||||
|
@ -76,16 +77,6 @@ const useStyles = makeStyles(theme => ({
|
|||
}
|
||||
}));
|
||||
|
||||
function AlreadyAuthenticatedContainer() {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Fragment>
|
||||
<div style={{ marginBottom: theme.spacing(2), flex: "0 0 100%" }}><SuccessIcon /></div>
|
||||
<Typography style={{ color: "green" }}>Authenticated!</Typography>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
function NotRegisteredContainer() {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
|
|
958
web/yarn.lock
958
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue