From 3003e4f1bf12a7c549fb9abba96a6069f12a9ace Mon Sep 17 00:00:00 2001 From: Tim Wundenberg Date: Tue, 17 Dec 2024 22:21:46 +0100 Subject: [PATCH] feat(security): #328 delete old sessions [tbs] --- handler/middleware/security_headers.go | 14 ++--- less | 28 --------- main_test.go | 85 +++++++++++++++++++++++--- service/auth.go | 2 + 4 files changed, 87 insertions(+), 42 deletions(-) delete mode 100644 less diff --git a/handler/middleware/security_headers.go b/handler/middleware/security_headers.go index 2dce7f3..474b402 100644 --- a/handler/middleware/security_headers.go +++ b/handler/middleware/security_headers.go @@ -14,13 +14,13 @@ func SecurityHeaders(serverSettings *types.Settings) func(http.Handler) http.Han w.Header().Set("Access-Control-Allow-Origin", serverSettings.BaseUrl) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE") w.Header().Set("Content-Security-Policy", - "default-src 'none';"+ - "script-src 'self' https://umami.me-fit.eu"+ - "connect-src 'self' https://umami.me-fit.eu"+ - "img-src 'self'"+ - "style-src 'self'"+ - "form-action 'self'"+ - "frame-ancestors 'none'", + "default-src 'none'; "+ + "script-src 'self' https://umami.me-fit.eu; "+ + "connect-src 'self' https://umami.me-fit.eu; "+ + "img-src 'self'; "+ + "style-src 'self'; "+ + "form-action 'self'; "+ + "frame-ancestors 'none'; ", ) w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") diff --git a/less b/less deleted file mode 100644 index a08a65b..0000000 --- a/less +++ /dev/null @@ -1,28 +0,0 @@ - - __ _ ___ - / /\ | | | |_) -/_/--\ |_| |_| \_ v1.52.3, built with Go go1.22.5 - -mkdir /home/tiwun/source/me-fit/tmp -watching . -watching db -watching handler -watching handler/middleware -watching log -watching migration -watching mocks -!exclude node_modules -watching service -!exclude static -watching template -watching template/auth -watching template/mail -watching template/workout -!exclude tmp -watching types -watching utils -building... -(✓) Complete [ updates=12 duration=10.258748ms ] -cleaning... -deleting /home/tiwun/source/me-fit/tmp -see you again~ diff --git a/main_test.go b/main_test.go index f99df06..5447ed0 100644 --- a/main_test.go +++ b/main_test.go @@ -81,13 +81,13 @@ func TestIntegrationSecurityHeader(t *testing.T) { assert.Equal(t, "GET, POST, DELETE", value) value = resp.Header.Get("Content-Security-Policy") - assert.Equal(t, "default-src 'none';"+ - "script-src 'self' https://umami.me-fit.eu"+ - "connect-src 'self' https://umami.me-fit.eu"+ - "img-src 'self'"+ - "style-src 'self'"+ - "form-action 'self'"+ - "frame-ancestors 'none'", value) + assert.Equal(t, "default-src 'none'; "+ + "script-src 'self' https://umami.me-fit.eu; "+ + "connect-src 'self' https://umami.me-fit.eu; "+ + "img-src 'self'; "+ + "style-src 'self'; "+ + "form-action 'self'; "+ + "frame-ancestors 'none';", value) value = resp.Header.Get("Cross-Origin-Resource-Policy") assert.Equal(t, "same-origin", value) @@ -159,6 +159,77 @@ func TestIntegrationAuth(t *testing.T) { assert.True(t, cookie.HttpOnly, "Cookie is not secure") assert.True(t, cookie.Secure, "Cookie is not secure") }) + t.Run("should change password and invalidate other sessions from user", func(t *testing.T) { + t.Parallel() + + db, basePath, ctx := setupIntegrationTest(t) + // TODO: take decision, if tests should be fully end to end (e.g. always send a signup request) or halway end to end (e.g. insert user into db) + userId := uuid.New() + userIdOther := uuid.New() + + pass := service.GetHashPassword("password", []byte("salt")) + _, err := db.Exec(` + INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) + VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) + + sessionId := "session-id" + assert.Nil(t, err) + _, err = db.Exec(` + INSERT INTO session (session_id, user_id, created_at, expires_at) + VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) + assert.Nil(t, err) + _, err = db.Exec(` + INSERT INTO session (session_id, user_id, created_at, expires_at) + VALUES ("second", ?, datetime(), datetime("now", "+1 day"))`, userId) + assert.Nil(t, err) + _, err = db.Exec(` + INSERT INTO session (session_id, user_id, created_at, expires_at) + VALUES ("other", ?, datetime(), datetime("now", "+1 day"))`, userIdOther) + assert.Nil(t, err) + + req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/change-password", nil) + assert.Nil(t, err) + req.Header.Set("Cookie", "id="+sessionId) + resp, err := httpClient.Do(req) + assert.Nil(t, err) + + html, err := html.Parse(resp.Body) + assert.Nil(t, err) + + csrfToken := findCsrfToken(html) + assert.NotEqual(t, "", csrfToken) + + formData := url.Values{ + "current-password": {"password"}, + "new-password": {"MyNewSecurePassword1!"}, + "csrf-token": {csrfToken}, + } + + req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) + assert.Nil(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Cookie", "id="+sessionId) + req.Header.Set("HX-Request", "true") + resp, err = httpClient.Do(req) + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var sessionIds []string + sessions, err := db.Query("SELECT session_id FROM session ORDER BY session_id") + assert.Nil(t, err) + for sessions.Next() { + var sessionId string + err = sessions.Scan(&sessionId) + assert.Nil(t, err) + sessionIds = append(sessionIds, sessionId) + } + + t.Fatalf("sessionIds: %v", sessionIds) + assert.Equal(t, 2, len(sessionIds)) + assert.Equal(t, "session-id", sessionIds[0]) + assert.Equal(t, "other", sessionIds[0]) + }) } func findCookie(resp *http.Response, name string) *http.Cookie { diff --git a/service/auth.go b/service/auth.go index 6829614..d5e171a 100644 --- a/service/auth.go +++ b/service/auth.go @@ -95,6 +95,7 @@ func NewAuthImpl(db db.Auth, random Random, clock Clock, mail Mail, serverSettin } func (service AuthImpl) SignIn(email string, password string) (*Session, error) { + log.Info("Sign in %s", email) user, err := service.db.GetUserByEmail(email) if err != nil { if errors.Is(err, db.ErrNotFound) { @@ -148,6 +149,7 @@ func (service AuthImpl) SignInSession(sessionId string) (*Session, error) { } func (service AuthImpl) SignInAnonymous() (*Session, error) { + log.Info("Sign in anonymous") sessionDb, err := service.createSession(uuid.Nil) if err != nil { return nil, types.ErrInternal