Implement owner setup and browser authentication sessions
This commit is contained in:
parent
077cf7601a
commit
715423ab8e
21 changed files with 2185 additions and 14 deletions
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"drive.local/drivev2/internal/application"
|
||||
"drive.local/drivev2/internal/config"
|
||||
|
|
@ -28,6 +29,32 @@ func (s systemServiceStub) SetupStatus(context.Context) (application.SetupStatus
|
|||
return s.setupStatus, s.setupErr
|
||||
}
|
||||
|
||||
type authenticationServiceStub struct {
|
||||
setupResult application.SetupOwnerResult
|
||||
setupErr error
|
||||
loginResult application.AuthenticatedSession
|
||||
loginErr error
|
||||
session application.BrowserSession
|
||||
sessionErr error
|
||||
logoutErr error
|
||||
}
|
||||
|
||||
func (s authenticationServiceStub) SetupOwner(context.Context, application.SetupOwnerInput) (application.SetupOwnerResult, error) {
|
||||
return s.setupResult, s.setupErr
|
||||
}
|
||||
|
||||
func (s authenticationServiceStub) Login(context.Context, application.LoginInput) (application.AuthenticatedSession, error) {
|
||||
return s.loginResult, s.loginErr
|
||||
}
|
||||
|
||||
func (s authenticationServiceStub) CurrentSession(context.Context, string) (application.BrowserSession, error) {
|
||||
return s.session, s.sessionErr
|
||||
}
|
||||
|
||||
func (s authenticationServiceStub) Logout(context.Context, string, string, string) error {
|
||||
return s.logoutErr
|
||||
}
|
||||
|
||||
func TestLiveHealth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
@ -35,6 +62,7 @@ func TestLiveHealth(t *testing.T) {
|
|||
config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()},
|
||||
slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
systemServiceStub{},
|
||||
authenticationServiceStub{},
|
||||
http.NotFoundHandler(),
|
||||
)
|
||||
request := httptest.NewRequest(http.MethodGet, "/health/live", nil)
|
||||
|
|
@ -57,6 +85,7 @@ func TestReadinessReportsDependencyFailure(t *testing.T) {
|
|||
config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()},
|
||||
slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
systemServiceStub{readinessErr: errors.New("database unavailable")},
|
||||
authenticationServiceStub{},
|
||||
http.NotFoundHandler(),
|
||||
)
|
||||
request := httptest.NewRequest(http.MethodGet, "/health/ready", nil)
|
||||
|
|
@ -82,6 +111,7 @@ func TestSetupStatusUsesApplicationService(t *testing.T) {
|
|||
config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()},
|
||||
slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
systemServiceStub{setupStatus: application.SetupStatus{Initialized: true, Phase: "phase-1", Version: "test"}},
|
||||
authenticationServiceStub{},
|
||||
http.NotFoundHandler(),
|
||||
)
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/setup/status", nil)
|
||||
|
|
@ -96,3 +126,67 @@ func TestSetupStatusUsesApplicationService(t *testing.T) {
|
|||
t.Fatalf("unexpected body: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerSetupSetsHardenedAuthenticationCookies(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expiresAt := time.Now().Add(time.Hour).UTC()
|
||||
authenticated := application.AuthenticatedSession{
|
||||
Session: application.BrowserSession{ID: "session", OwnerID: "owner", ExpiresAt: expiresAt},
|
||||
SessionToken: "session-secret", CSRFToken: "csrf-secret",
|
||||
}
|
||||
server := NewServer(
|
||||
config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()},
|
||||
slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
systemServiceStub{},
|
||||
authenticationServiceStub{setupResult: application.SetupOwnerResult{
|
||||
OwnerID: "owner", RootNodeID: "root", Username: "owner", DisplayName: "Owner",
|
||||
RecoveryCodes: []string{"AAAAA-BBBBB-CCCCC-DDDDD"}, Authentication: authenticated,
|
||||
}},
|
||||
http.NotFoundHandler(),
|
||||
)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/setup", strings.NewReader(`{"username":"owner","displayName":"Owner","password":"correct horse battery staple"}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
server.httpServer.Handler.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusCreated {
|
||||
t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusCreated, recorder.Body.String())
|
||||
}
|
||||
cookies := recorder.Result().Cookies()
|
||||
if len(cookies) != 2 {
|
||||
t.Fatalf("cookie count = %d, want 2", len(cookies))
|
||||
}
|
||||
for _, cookie := range cookies {
|
||||
if !cookie.Secure || cookie.SameSite != http.SameSiteStrictMode || cookie.Path != "/" {
|
||||
t.Fatalf("cookie is not hardened: %#v", cookie)
|
||||
}
|
||||
}
|
||||
if !cookies[0].HttpOnly || cookies[1].HttpOnly {
|
||||
t.Fatalf("unexpected HttpOnly policy: %#v", cookies)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogoutRejectsInvalidCSRF(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := NewServer(
|
||||
config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()},
|
||||
slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
systemServiceStub{},
|
||||
authenticationServiceStub{logoutErr: application.ErrCSRFValidation},
|
||||
http.NotFoundHandler(),
|
||||
)
|
||||
request := httptest.NewRequest(http.MethodDelete, "/api/v1/sessions/current", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
server.httpServer.Handler.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusForbidden {
|
||||
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusForbidden)
|
||||
}
|
||||
if !strings.Contains(recorder.Body.String(), `"code":"csrf_validation_failed"`) {
|
||||
t.Fatalf("unexpected body: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue