package suites
import (
"context"
"fmt"
"io"
"log"
"net/http"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/storage"
"github.com/authelia/authelia/v4/internal/utils"
)
type StandaloneWebDriverSuite struct {
*RodSuite
}
func NewStandaloneWebDriverSuite() *StandaloneWebDriverSuite {
return &StandaloneWebDriverSuite{
RodSuite: NewRodSuite(""),
}
}
func (s *StandaloneWebDriverSuite) SetupSuite() {
s.BaseSuite.SetupSuite()
browser, err := StartRod()
if err != nil {
log.Fatal(err)
}
s.RodSession = browser
}
func (s *StandaloneWebDriverSuite) TearDownSuite() {
err := s.RodSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *StandaloneWebDriverSuite) SetupTest() {
s.Page = s.doCreateTab(s.T(), HomeBaseURL)
s.verifyIsHome(s.T(), s.Page)
}
func (s *StandaloneWebDriverSuite) TearDownTest() {
s.collectCoverage(s.Page)
s.MustClose()
}
func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer func() {
cancel()
s.collectScreenshot(ctx.Err(), s.Page)
}()
_ = s.doRegisterAndLogin2FA(s.T(), s.Context(ctx), "john", "password", false, "")
// Visit home page to change context.
s.doVisit(s.T(), s.Context(ctx), HomeBaseURL)
s.verifyIsHome(s.T(), s.Context(ctx))
// Visit the login page and wait for redirection to 2FA page with success icon displayed.
s.doVisit(s.T(), s.Context(ctx), GetLoginBaseURL(BaseDomain))
s.verifyIsAuthenticatedPage(s.T(), s.Context(ctx))
}
func (s *StandaloneWebDriverSuite) TestShouldRedirectAfterOneFactorOnAnotherTab() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
targetURL := fmt.Sprintf("%s/secret.html", SingleFactorBaseURL)
page2 := s.Browser().MustPage(targetURL)
defer func() {
cancel()
s.collectScreenshot(ctx.Err(), s.Page)
s.collectScreenshot(ctx.Err(), page2)
page2.MustClose()
}()
// Open second tab with secret page.
page2.MustWaitLoad()
// Switch to first, visit the login page and wait for redirection to secret page with secret displayed.
s.Page.MustActivate()
s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, BaseDomain, targetURL)
s.verifySecretAuthorized(s.T(), s.Page)
// Switch to second tab and wait for redirection to secret page with secret displayed.
page2.MustActivate()
s.verifySecretAuthorized(s.T(), page2.Context(ctx))
}
func (s *StandaloneWebDriverSuite) TestShouldRedirectAlreadyAuthenticatedUser() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer func() {
cancel()
s.collectScreenshot(ctx.Err(), s.Page)
}()
_ = s.doRegisterAndLogin2FA(s.T(), s.Context(ctx), "john", "password", false, "")
// Visit home page to change context.
s.doVisit(s.T(), s.Context(ctx), HomeBaseURL)
s.verifyIsHome(s.T(), s.Context(ctx))
// Visit the login page and wait for redirection to 2FA page with success icon displayed.
s.doVisit(s.T(), s.Context(ctx), fmt.Sprintf("%s?rd=https://secure.example.com:8080", GetLoginBaseURL(BaseDomain)))
_, err := s.Page.ElementR("h1", "Public resource")
require.NoError(s.T(), err)
s.verifyURLIs(s.T(), s.Context(ctx), "https://secure.example.com:8080/")
}
func (s *StandaloneWebDriverSuite) TestShouldNotRedirectAlreadyAuthenticatedUserToUnsafeURL() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer func() {
cancel()
s.collectScreenshot(ctx.Err(), s.Page)
}()
_ = s.doRegisterAndLogin2FA(s.T(), s.Context(ctx), "john", "password", false, "")
// Visit home page to change context.
s.doVisit(s.T(), s.Context(ctx), HomeBaseURL)
s.verifyIsHome(s.T(), s.Context(ctx))
// Visit the login page and wait for redirection to 2FA page with success icon displayed.
s.doVisit(s.T(), s.Context(ctx), fmt.Sprintf("%s?rd=https://secure.example.local:8080", GetLoginBaseURL(BaseDomain)))
s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "Redirection was determined to be unsafe and aborted. Ensure the redirection URL is correct.")
}
func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer func() {
cancel()
s.collectScreenshot(ctx.Err(), s.Page)
}()
username := "john"
password := "password"
// Clean up any TOTP secret already in DB.
provider := storage.NewSQLiteProvider(&storageLocalTmpConfig)
require.NoError(s.T(), provider.DeleteTOTPConfiguration(ctx, username))
// Login one factor.
s.doLoginOneFactor(s.T(), s.Context(ctx), username, password, false, BaseDomain, "")
// Check the user is asked to register a new device.
s.WaitElementLocatedByClassName(s.T(), s.Context(ctx), "state-not-registered")
// Then register the TOTP factor.
s.doRegisterTOTP(s.T(), s.Context(ctx))
// And logout.
s.doLogout(s.T(), s.Context(ctx))
// Login one factor again.
s.doLoginOneFactor(s.T(), s.Context(ctx), username, password, false, BaseDomain, "")
// now the user should be asked to perform 2FA.
s.WaitElementLocatedByClassName(s.T(), s.Context(ctx), "state-method")
}
type StandaloneSuite struct {
*BaseSuite
}
func NewStandaloneSuite() *StandaloneSuite {
return &StandaloneSuite{
BaseSuite: &BaseSuite{
Name: standaloneSuiteName,
},
}
}
func (s *StandaloneSuite) TestShouldRespectMethodsACL() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify?rd=%s", AutheliaBaseURL, GetLoginBaseURL(BaseDomain)), nil)
s.Assert().NoError(err)
req.Header.Set("X-Forwarded-Method", "GET")
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", fmt.Sprintf("secure.%s", BaseDomain))
req.Header.Set("X-Forwarded-URI", "/")
req.Header.Set("Accept", "text/html; charset=utf8")
client := NewHTTPClient()
res, err := client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, 302)
body, err := io.ReadAll(res.Body)
s.Assert().NoError(err)
urlEncodedAdminURL := url.QueryEscape(SecureBaseURL + "/")
s.Assert().Equal(fmt.Sprintf("302 Found", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s&rm=GET", GetLoginBaseURL(BaseDomain), urlEncodedAdminURL))), string(body))
req.Header.Set("X-Forwarded-Method", "OPTIONS")
res, err = client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, 200)
}
func (s *StandaloneSuite) TestShouldRespondWithCorrectStatusCode() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify?rd=%s", AutheliaBaseURL, GetLoginBaseURL(BaseDomain)), nil)
s.Assert().NoError(err)
req.Header.Set("X-Forwarded-Method", "GET")
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", fmt.Sprintf("secure.%s", BaseDomain))
req.Header.Set("X-Forwarded-URI", "/")
req.Header.Set("Accept", "text/html; charset=utf8")
client := NewHTTPClient()
res, err := client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, 302)
body, err := io.ReadAll(res.Body)
s.Assert().NoError(err)
urlEncodedAdminURL := url.QueryEscape(SecureBaseURL + "/")
s.Assert().Equal(fmt.Sprintf("302 Found", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s&rm=GET", GetLoginBaseURL(BaseDomain), urlEncodedAdminURL))), string(body))
req.Header.Set("X-Forwarded-Method", "POST")
res, err = client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, 303)
body, err = io.ReadAll(res.Body)
s.Assert().NoError(err)
urlEncodedAdminURL = url.QueryEscape(SecureBaseURL + "/")
s.Assert().Equal(fmt.Sprintf("303 See Other", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s&rm=POST", GetLoginBaseURL(BaseDomain), urlEncodedAdminURL))), string(body))
}
// Standard case using nginx.
func (s *StandaloneSuite) TestShouldVerifyAPIVerifyUnauthorized() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify", AutheliaBaseURL), nil)
s.Assert().NoError(err)
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Original-URL", AdminBaseURL)
req.Header.Set("Accept", "text/html; charset=utf8")
client := NewHTTPClient()
res, err := client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, 401)
body, err := io.ReadAll(res.Body)
s.Assert().NoError(err)
s.Assert().Equal("401 Unauthorized", string(body))
}
// Standard case using Kubernetes.
func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalURL() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify?rd=%s", AutheliaBaseURL, GetLoginBaseURL(BaseDomain)), nil)
s.Assert().NoError(err)
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Original-URL", AdminBaseURL)
req.Header.Set("Accept", "text/html; charset=utf8")
client := NewHTTPClient()
res, err := client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, 302)
body, err := io.ReadAll(res.Body)
s.Assert().NoError(err)
urlEncodedAdminURL := url.QueryEscape(AdminBaseURL)
s.Assert().Equal(fmt.Sprintf("302 Found", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s&rm=GET", GetLoginBaseURL(BaseDomain), urlEncodedAdminURL))), string(body))
}
func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify?rd=%s", AutheliaBaseURL, GetLoginBaseURL(BaseDomain)), nil)
s.Assert().NoError(err)
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "secure.example.com:8080")
req.Header.Set("X-Forwarded-URI", "/")
req.Header.Set("Accept", "text/html; charset=utf8")
client := NewHTTPClient()
res, err := client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, 302)
body, err := io.ReadAll(res.Body)
s.Assert().NoError(err)
urlEncodedAdminURL := url.QueryEscape(SecureBaseURL + "/")
s.Assert().Equal(fmt.Sprintf("302 Found", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s&rm=GET", GetLoginBaseURL(BaseDomain), urlEncodedAdminURL))), string(body))
}
func (s *StandaloneSuite) TestShouldRecordMetrics() {
client := NewHTTPClient()
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/health", LoginBaseURL), nil)
s.Require().NoError(err)
res, err := client.Do(req)
s.Require().NoError(err)
s.Assert().Equal(res.StatusCode, 200)
req, err = http.NewRequest("GET", fmt.Sprintf("%s/metrics", LoginBaseURL), nil)
s.Require().NoError(err)
res, err = client.Do(req)
s.Require().NoError(err)
s.Assert().Equal(res.StatusCode, 200)
body, err := io.ReadAll(res.Body)
s.Require().NoError(err)
metrics := string(body)
s.Assert().Contains(metrics, "authelia_request_duration_bucket{")
s.Assert().Contains(metrics, "authelia_request_duration_sum{")
}
func (s *StandaloneSuite) TestStandaloneWebDriverScenario() {
suite.Run(s.T(), NewStandaloneWebDriverSuite())
}
func (s *StandaloneSuite) Test1FAScenario() {
suite.Run(s.T(), New1FAScenario())
}
func (s *StandaloneSuite) Test2FAScenario() {
suite.Run(s.T(), New2FAScenario())
}
func (s *StandaloneSuite) TestBypassPolicyScenario() {
suite.Run(s.T(), NewBypassPolicyScenario())
}
func (s *StandaloneSuite) TestBackendProtectionScenario() {
suite.Run(s.T(), NewBackendProtectionScenario())
}
func (s *StandaloneSuite) TestResetPasswordScenario() {
suite.Run(s.T(), NewResetPasswordScenario())
}
func (s *StandaloneSuite) TestAvailableMethodsScenario() {
suite.Run(s.T(), NewAvailableMethodsScenario([]string{"TIME-BASED ONE-TIME PASSWORD", "SECURITY KEY - WEBAUTHN"}))
}
func (s *StandaloneSuite) TestRedirectionURLScenario() {
suite.Run(s.T(), NewRedirectionURLScenario())
}
func (s *StandaloneSuite) TestRedirectionCheckScenario() {
suite.Run(s.T(), NewRedirectionCheckScenario())
}
func TestStandaloneSuite(t *testing.T) {
if testing.Short() {
t.Skip("skipping suite test in short mode")
}
suite.Run(t, NewStandaloneSuite())
}