[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 runningSuiteFile = ".suite"
|
||||||
|
|
||||||
var headless bool
|
var headless bool
|
||||||
|
var testPattern string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
SuitesTestCmd.Flags().BoolVar(&headless, "headless", false, "Run tests in headless mode")
|
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.
|
// 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 suite(s) are provided as argument
|
||||||
if len(args) == 1 {
|
if len(args) >= 1 {
|
||||||
suiteArg := args[0]
|
suiteArg := args[0]
|
||||||
|
|
||||||
if runningSuite != "" && suiteArg != runningSuite {
|
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"))
|
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(strings.Split(suiteArg, ","), runningSuite == ""); err != nil {
|
||||||
if err := runMultipleSuitesTests(suiteNames, runningSuite == ""); err != nil {
|
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -239,7 +240,13 @@ func runSuiteTests(suiteName string, withEnv bool) error {
|
||||||
if suite.TestTimeout > 0 {
|
if suite.TestTimeout > 0 {
|
||||||
timeout = fmt.Sprintf("%ds", int64(suite.TestTimeout/time.Second))
|
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.Infof("Running tests of suite %s...", suiteName)
|
||||||
log.Debugf("Running tests with command: %s", testCmdLine)
|
log.Debugf("Running tests with command: %s", testCmdLine)
|
||||||
|
|
|
@ -87,7 +87,7 @@ func startServer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
clock := utils.RealClock{}
|
clock := utils.RealClock{}
|
||||||
authorizer := authorization.NewAuthorizer(*config.AccessControl)
|
authorizer := authorization.NewAuthorizer(config.AccessControl)
|
||||||
sessionProvider := session.NewProvider(config.Session)
|
sessionProvider := session.NewProvider(config.Session)
|
||||||
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
|
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
|
||||||
|
|
||||||
|
|
|
@ -160,7 +160,7 @@ func selectMatchingRules(rules []schema.ACLRule, subject Subject, object Object)
|
||||||
return selectMatchingObjectRules(matchingRules, object)
|
return selectMatchingObjectRules(matchingRules, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
func policyToLevel(policy string) Level {
|
func PolicyToLevel(policy string) Level {
|
||||||
switch policy {
|
switch policy {
|
||||||
case "bypass":
|
case "bypass":
|
||||||
return Bypass
|
return Bypass
|
||||||
|
@ -183,7 +183,7 @@ func (p *Authorizer) GetRequiredLevel(subject Subject, requestURL url.URL) Level
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(matchingRules) > 0 {
|
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)
|
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) {
|
func TestRunSuite(t *testing.T) {
|
||||||
s := AuthorizerSuite{}
|
s := AuthorizerSuite{}
|
||||||
suite.Run(t, &s)
|
suite.Run(t, &s)
|
||||||
|
|
|
@ -15,10 +15,10 @@ type Configuration struct {
|
||||||
AuthenticationBackend AuthenticationBackendConfiguration `mapstructure:"authentication_backend"`
|
AuthenticationBackend AuthenticationBackendConfiguration `mapstructure:"authentication_backend"`
|
||||||
Session SessionConfiguration `mapstructure:"session"`
|
Session SessionConfiguration `mapstructure:"session"`
|
||||||
|
|
||||||
TOTP *TOTPConfiguration `mapstructure:"totp"`
|
TOTP *TOTPConfiguration `mapstructure:"totp"`
|
||||||
DuoAPI *DuoAPIConfiguration `mapstructure:"duo_api"`
|
DuoAPI *DuoAPIConfiguration `mapstructure:"duo_api"`
|
||||||
AccessControl *AccessControlConfiguration `mapstructure:"access_control"`
|
AccessControl AccessControlConfiguration `mapstructure:"access_control"`
|
||||||
Regulation *RegulationConfiguration `mapstructure:"regulation"`
|
Regulation *RegulationConfiguration `mapstructure:"regulation"`
|
||||||
Storage *StorageConfiguration `mapstructure:"storage"`
|
Storage *StorageConfiguration `mapstructure:"storage"`
|
||||||
Notifier *NotifierConfiguration `mapstructure:"notifier"`
|
Notifier *NotifierConfiguration `mapstructure:"notifier"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,5 +49,9 @@ func Validate(configuration *schema.Configuration, validator *schema.StructValid
|
||||||
ValidateNotifier(configuration.Notifier, validator)
|
ValidateNotifier(configuration.Notifier, validator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if configuration.AccessControl.DefaultPolicy == "" {
|
||||||
|
configuration.AccessControl.DefaultPolicy = "deny"
|
||||||
|
}
|
||||||
|
|
||||||
ValidateSQLStorage(configuration.Storage, validator)
|
ValidateSQLStorage(configuration.Storage, validator)
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,3 +91,13 @@ func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) {
|
||||||
require.Len(t, validator.Errors(), 1)
|
require.Len(t, validator.Errors(), 1)
|
||||||
assert.EqualError(t, validator.Errors()[0], "A notifier configuration must be provided")
|
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 (
|
import (
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
|
"github.com/authelia/authelia/internal/authorization"
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExtendedConfigurationBody struct {
|
type ExtendedConfigurationBody struct {
|
||||||
AvailableMethods MethodList `json:"available_methods"`
|
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.
|
// 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)
|
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)
|
ctx.SetJSONBody(body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,12 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
"github.com/authelia/authelia/internal/authorization"
|
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
"github.com/authelia/authelia/internal/regulation"
|
"github.com/authelia/authelia/internal/regulation"
|
||||||
"github.com/authelia/authelia/internal/session"
|
"github.com/authelia/authelia/internal/session"
|
||||||
"github.com/authelia/authelia/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FirstFactorPost is the handler performing the first factory.
|
// FirstFactorPost is the handler performing the first factory.
|
||||||
|
@ -111,30 +108,5 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if bodyJSON.TargetURL != "" {
|
Handle1FAResponse(ctx, bodyJSON.TargetURL, userSession.Username, userSession.Groups)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/authorization"
|
||||||
"github.com/authelia/authelia/internal/mocks"
|
"github.com/authelia/authelia/internal/mocks"
|
||||||
"github.com/authelia/authelia/internal/models"
|
"github.com/authelia/authelia/internal/models"
|
||||||
|
|
||||||
|
@ -229,7 +230,76 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() {
|
||||||
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
|
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFirstFactorSuite(t *testing.T) {
|
type FirstFactorRedirectionSuite struct {
|
||||||
firstFactorSuite := new(FirstFactorSuite)
|
suite.Suite
|
||||||
suite.Run(t, firstFactorSuite)
|
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleAuthResponse(ctx, requestBody.TargetURL)
|
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,6 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleAuthResponse(ctx, bodyJSON.TargetURL)
|
Handle2FAResponse(ctx, bodyJSON.TargetURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,6 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleAuthResponse(ctx, requestBody.TargetURL)
|
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,29 +4,79 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/authorization"
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
"github.com/authelia/authelia/internal/utils"
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleAuthResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
|
func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI string, username string, groups []string) {
|
||||||
if targetURI != "" {
|
if targetURI == "" {
|
||||||
targetURL, err := url.ParseRequestURI(targetURI)
|
if authorization.PolicyToLevel(ctx.Configuration.AccessControl.DefaultPolicy) == authorization.OneFactor &&
|
||||||
|
ctx.Configuration.DefaultRedirectionURL != "" {
|
||||||
if err != nil {
|
ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
||||||
ctx.Error(fmt.Errorf("Unable to parse target URL: %s", err), mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetURL != nil && utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
|
|
||||||
ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
|
|
||||||
} else {
|
} else {
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
}
|
}
|
||||||
} else {
|
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 != "" {
|
if ctx.Configuration.DefaultRedirectionURL != "" {
|
||||||
ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
||||||
} else {
|
} else {
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetURL, err := url.ParseRequestURI(targetURI)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(fmt.Errorf("Unable to parse target URL: %s", err), mfaValidationFailedMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetURL != nil && utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
|
||||||
|
ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
|
||||||
|
} else {
|
||||||
|
ctx.ReplyOK()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,9 +66,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
||||||
datetime, _ := time.Parse("2006-Jan-02", "2013-Feb-03")
|
datetime, _ := time.Parse("2006-Jan-02", "2013-Feb-03")
|
||||||
mockAuthelia.Clock.Set(datetime)
|
mockAuthelia.Clock.Set(datetime)
|
||||||
|
|
||||||
configuration := schema.Configuration{
|
configuration := schema.Configuration{}
|
||||||
AccessControl: new(schema.AccessControlConfiguration),
|
|
||||||
}
|
|
||||||
configuration.Session.Name = "authelia_session"
|
configuration.Session.Name = "authelia_session"
|
||||||
configuration.AccessControl.DefaultPolicy = "deny"
|
configuration.AccessControl.DefaultPolicy = "deny"
|
||||||
configuration.AccessControl.Rules = []schema.ACLRule{schema.ACLRule{
|
configuration.AccessControl.Rules = []schema.ACLRule{schema.ACLRule{
|
||||||
|
@ -98,7 +96,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
||||||
providers.Notifier = mockAuthelia.NotifierMock
|
providers.Notifier = mockAuthelia.NotifierMock
|
||||||
|
|
||||||
providers.Authorizer = authorization.NewAuthorizer(
|
providers.Authorizer = authorization.NewAuthorizer(
|
||||||
*configuration.AccessControl)
|
configuration.AccessControl)
|
||||||
|
|
||||||
providers.SessionProvider = session.NewProvider(
|
providers.SessionProvider = session.NewProvider(
|
||||||
configuration.Session)
|
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
|
|
@ -6,4 +6,6 @@ services:
|
||||||
- AUTHELIA_SESSION_SECRET=unsecure_session_secret
|
- AUTHELIA_SESSION_SECRET=unsecure_session_secret
|
||||||
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:/tmp'
|
||||||
|
user: ${USER_ID}:${GROUP_ID}
|
|
@ -1,19 +1,19 @@
|
||||||
users:
|
users:
|
||||||
bob:
|
bob:
|
||||||
password: '$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
|
password: $6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||||
email: bob.dylan@authelia.com
|
email: bob.dylan@authelia.com
|
||||||
groups:
|
groups:
|
||||||
- dev
|
- dev
|
||||||
harry:
|
harry:
|
||||||
password: '$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
|
password: $6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||||
email: harry.potter@authelia.com
|
email: harry.potter@authelia.com
|
||||||
groups: []
|
groups: []
|
||||||
james:
|
james:
|
||||||
password: '$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
|
password: $6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||||
email: james.dean@authelia.com
|
email: james.dean@authelia.com
|
||||||
groups: []
|
groups: []
|
||||||
john:
|
john:
|
||||||
password: '$6$rounds=50000$LnfgDsc2WD8F2qNf$0gcCt8jlqAGZRv2ee3mCFsfAr1P4N7kESWEf36Xtw6OjkhAcQuGVOBHXp0lFuZbppa7YlgHk3VD28aSQu9U9S1'
|
password: $6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||||
email: john.doe@authelia.com
|
email: john.doe@authelia.com
|
||||||
groups:
|
groups:
|
||||||
- admins
|
- admins
|
||||||
|
|
|
@ -70,7 +70,7 @@ func (s *RedirectionCheckScenario) TestShouldRedirectOnlyWhenDomainIsHandledByAu
|
||||||
if redirected {
|
if redirected {
|
||||||
s.verifySecretAuthorized(ctx, t)
|
s.verifySecretAuthorized(ctx, t)
|
||||||
} else {
|
} else {
|
||||||
s.WaitElementLocatedByClassName(ctx, t, "success-icon")
|
s.verifyIsAuthenticatedPage(ctx, t)
|
||||||
}
|
}
|
||||||
s.doLogout(ctx, t)
|
s.doLogout(ctx, t)
|
||||||
})
|
})
|
||||||
|
|
|
@ -65,17 +65,17 @@ func (s *UserPreferencesScenario) TestShouldRememberLastUsed2FAMethod() {
|
||||||
// Then go back to portal.
|
// Then go back to portal.
|
||||||
s.doVisit(s.T(), LoginBaseURL)
|
s.doVisit(s.T(), LoginBaseURL)
|
||||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||||
|
|
||||||
// And check the latest method is still used.
|
// And check the latest method is still used.
|
||||||
s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method")
|
s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method")
|
||||||
// Meaning the authentication is successful
|
// 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.
|
// Logout the user and see what user 'harry' sees.
|
||||||
s.doLogout(ctx, s.T())
|
s.doLogout(ctx, s.T())
|
||||||
s.doLoginOneFactor(ctx, s.T(), "harry", "password", false, "")
|
s.doLoginOneFactor(ctx, s.T(), "harry", "password", false, "")
|
||||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||||
s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method")
|
s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method")
|
||||||
|
|
||||||
s.doLogout(ctx, s.T())
|
s.doLogout(ctx, s.T())
|
||||||
s.verifyIsFirstFactorPage(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.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||||
s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method")
|
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
|
// Eventually restore the default method
|
||||||
s.doChangeMethod(ctx, s.T(), "one-time-password")
|
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)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
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.doChangeMethod(ctx, s.T(), "one-time-password")
|
||||||
s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method")
|
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.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||||
s.doChangeMethod(ctx, s.T(), "push-notification")
|
s.doChangeMethod(ctx, s.T(), "push-notification")
|
||||||
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
|
s.verifyIsHome(ctx, s.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DuoPushWebDriverSuite) TestShouldFailAuthentication() {
|
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
|
// Visit the login page and wait for redirection to 2FA page with success icon displayed
|
||||||
s.doVisit(s.T(), LoginBaseURL)
|
s.doVisit(s.T(), LoginBaseURL)
|
||||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
s.verifyIsAuthenticatedPage(ctx, s.T())
|
||||||
|
|
||||||
// Check whether the success icon is displayed
|
|
||||||
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice() {
|
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/node": "12.12.12",
|
||||||
"@types/qrcode.react": "^1.0.0",
|
"@types/qrcode.react": "^1.0.0",
|
||||||
"@types/query-string": "^6.3.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-dom": "16.9.4",
|
||||||
"@types/react-ga": "^2.3.0",
|
"@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",
|
||||||
|
"babel-preset-react-app": "^9.1.1",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"enzyme": "^3.10.0",
|
"enzyme": "^3.10.0",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
export const FirstFactorRoute = "/";
|
export const FirstFactorRoute = "/";
|
||||||
|
export const AuthenticatedRoute = "/authenticated";
|
||||||
|
|
||||||
export const SecondFactorRoute = "/2fa";
|
export const SecondFactorRoute = "/2fa";
|
||||||
export const SecondFactorU2FRoute = "/2fa/security-key";
|
export const SecondFactorU2FRoute = "/2fa/security-key";
|
||||||
|
|
|
@ -2,9 +2,7 @@ import React from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
|
||||||
export interface Props { }
|
export default function () {
|
||||||
|
|
||||||
export default function (props: Props) {
|
|
||||||
return (
|
return (
|
||||||
<FontAwesomeIcon icon={faCheckCircle} size="4x" color="green" className="success-icon" />
|
<FontAwesomeIcon icon={faCheckCircle} size="4x" color="green" className="success-icon" />
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,4 +6,5 @@ export interface Configuration {
|
||||||
|
|
||||||
export interface ExtendedConfiguration {
|
export interface ExtendedConfiguration {
|
||||||
available_methods: Set<SecondFactorMethod>;
|
available_methods: Set<SecondFactorMethod>;
|
||||||
|
one_factor_default_policy: boolean;
|
||||||
}
|
}
|
|
@ -9,6 +9,7 @@ export async function getConfiguration(): Promise<Configuration> {
|
||||||
|
|
||||||
interface ExtendedConfigurationPayload {
|
interface ExtendedConfigurationPayload {
|
||||||
available_methods: Method2FA[];
|
available_methods: Method2FA[];
|
||||||
|
one_factor_default_policy: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExtendedConfiguration(): Promise<ExtendedConfiguration> {
|
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 SecondFactorForm from "./SecondFactor/SecondFactorForm";
|
||||||
import {
|
import {
|
||||||
FirstFactorRoute, SecondFactorRoute, SecondFactorTOTPRoute,
|
FirstFactorRoute, SecondFactorRoute, SecondFactorTOTPRoute,
|
||||||
SecondFactorPushRoute, SecondFactorU2FRoute
|
SecondFactorPushRoute, SecondFactorU2FRoute, AuthenticatedRoute
|
||||||
} from "../../Routes";
|
} from "../../Routes";
|
||||||
import { useAutheliaState } from "../../hooks/State";
|
import { useAutheliaState } from "../../hooks/State";
|
||||||
import LoadingPage from "../LoadingPage/LoadingPage";
|
import LoadingPage from "../LoadingPage/LoadingPage";
|
||||||
|
@ -14,6 +14,7 @@ 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 { useExtendedConfiguration } from "../../hooks/Configuration";
|
import { useExtendedConfiguration } from "../../hooks/Configuration";
|
||||||
|
import AuthenticatedView from "./AuthenticatedView/AuthenticatedView";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -77,19 +78,23 @@ export default function () {
|
||||||
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
||||||
setFirstFactorDisabled(false);
|
setFirstFactorDisabled(false);
|
||||||
redirect(`${FirstFactorRoute}${redirectionSuffix}`);
|
redirect(`${FirstFactorRoute}${redirectionSuffix}`);
|
||||||
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo) {
|
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo && configuration) {
|
||||||
if (userInfo.method === SecondFactorMethod.U2F) {
|
if (configuration.one_factor_default_policy) {
|
||||||
redirect(`${SecondFactorU2FRoute}${redirectionSuffix}`);
|
redirect(AuthenticatedRoute);
|
||||||
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
|
|
||||||
redirect(`${SecondFactorPushRoute}${redirectionSuffix}`);
|
|
||||||
} else {
|
} else {
|
||||||
redirect(`${SecondFactorTOTPRoute}${redirectionSuffix}`);
|
if (userInfo.method === SecondFactorMethod.U2F) {
|
||||||
|
redirect(`${SecondFactorU2FRoute}${redirectionSuffix}`);
|
||||||
|
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
|
||||||
|
redirect(`${SecondFactorPushRoute}${redirectionSuffix}`);
|
||||||
|
} else {
|
||||||
|
redirect(`${SecondFactorTOTPRoute}${redirectionSuffix}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [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) {
|
if (redirectionURL) {
|
||||||
// Do an external redirection pushed by the server.
|
// Do an external redirection pushed by the server.
|
||||||
window.location.href = redirectionURL;
|
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 &&
|
const firstFactorReady = state !== undefined &&
|
||||||
state.authentication_level === AuthenticationLevel.Unauthenticated &&
|
state.authentication_level === AuthenticationLevel.Unauthenticated &&
|
||||||
location.pathname === FirstFactorRoute;
|
location.pathname === FirstFactorRoute;
|
||||||
|
@ -120,7 +116,7 @@ export default function () {
|
||||||
disabled={firstFactorDisabled}
|
disabled={firstFactorDisabled}
|
||||||
onAuthenticationStart={() => setFirstFactorDisabled(true)}
|
onAuthenticationStart={() => setFirstFactorDisabled(true)}
|
||||||
onAuthenticationFailure={() => setFirstFactorDisabled(false)}
|
onAuthenticationFailure={() => setFirstFactorDisabled(false)}
|
||||||
onAuthenticationSuccess={handleFirstFactorSuccess} />
|
onAuthenticationSuccess={handleAuthSuccess} />
|
||||||
</ComponentOrLoading>
|
</ComponentOrLoading>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={SecondFactorRoute}>
|
<Route path={SecondFactorRoute}>
|
||||||
|
@ -130,7 +126,10 @@ export default function () {
|
||||||
userInfo={userInfo}
|
userInfo={userInfo}
|
||||||
configuration={configuration}
|
configuration={configuration}
|
||||||
onMethodChanged={() => fetchUserInfo()}
|
onMethodChanged={() => fetchUserInfo()}
|
||||||
onAuthenticationSuccess={handleSecondFactorSuccess} /> : null}
|
onAuthenticationSuccess={handleAuthSuccess} /> : null}
|
||||||
|
</Route>
|
||||||
|
<Route path={AuthenticatedRoute} exact>
|
||||||
|
{state ? <AuthenticatedView username={state.username} /> : null}
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Redirect to={FirstFactorRoute} />
|
<Redirect to={FirstFactorRoute} />
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { ReactNode, Fragment } from "react";
|
import React, { ReactNode, Fragment } from "react";
|
||||||
import { makeStyles, Typography, Link, useTheme } from "@material-ui/core";
|
import { makeStyles, Typography, Link, useTheme } from "@material-ui/core";
|
||||||
import SuccessIcon from "../../../components/SuccessIcon";
|
|
||||||
import InformationIcon from "../../../components/InformationIcon";
|
import InformationIcon from "../../../components/InformationIcon";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
import Authenticated from "../Authenticated";
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
ALREADY_AUTHENTICATED = 1,
|
ALREADY_AUTHENTICATED = 1,
|
||||||
|
@ -27,7 +27,7 @@ export default function (props: Props) {
|
||||||
let stateClass: string = '';
|
let stateClass: string = '';
|
||||||
switch (props.state) {
|
switch (props.state) {
|
||||||
case State.ALREADY_AUTHENTICATED:
|
case State.ALREADY_AUTHENTICATED:
|
||||||
container = <AlreadyAuthenticatedContainer />
|
container = <Authenticated />
|
||||||
stateClass = "state-already-authenticated";
|
stateClass = "state-already-authenticated";
|
||||||
break;
|
break;
|
||||||
case State.NOT_REGISTERED:
|
case State.NOT_REGISTERED:
|
||||||
|
@ -42,6 +42,7 @@ export default function (props: Props) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={props.id}>
|
<div id={props.id}>
|
||||||
<Typography variant="h6">{props.title}</Typography>
|
<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() {
|
function NotRegisteredContainer() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
|
|
958
web/yarn.lock
958
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue