mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 06:21:11 +00:00 
			
		
		
		
	- The current architecture is inherently insecure, because you can
construct the 'secret' cookie value with values that are available in
the database. Thus provides zero protection when a database is
dumped/leaked.
- This patch implements a new architecture that's inspired from: [Paragonie Initiative](https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#secure-remember-me-cookies).
- Integration testing is added to ensure the new mechanism works.
- Removes a setting, because it's not used anymore.
(cherry-pick from eff097448b)
Conflicts:
	modules/context/context_cookie.go
	trivial context conflicts
	routers/web/web.go
	ctx.GetSiteCookie(setting.CookieRememberName) moved from services/auth/middleware.go
		
	
			
		
			
				
	
	
		
			163 lines
		
	
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			163 lines
		
	
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2023 The Forgejo Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package integration
 | |
| 
 | |
| import (
 | |
| 	"encoding/hex"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 
 | |
| 	"code.gitea.io/gitea/models/auth"
 | |
| 	"code.gitea.io/gitea/models/db"
 | |
| 	"code.gitea.io/gitea/models/unittest"
 | |
| 	user_model "code.gitea.io/gitea/models/user"
 | |
| 	"code.gitea.io/gitea/modules/setting"
 | |
| 	"code.gitea.io/gitea/modules/timeutil"
 | |
| 	"code.gitea.io/gitea/tests"
 | |
| 
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| )
 | |
| 
 | |
| // GetSessionForLTACookie returns a new session with only the LTA cookie being set.
 | |
| func GetSessionForLTACookie(t *testing.T, ltaCookie *http.Cookie) *TestSession {
 | |
| 	t.Helper()
 | |
| 
 | |
| 	ch := http.Header{}
 | |
| 	ch.Add("Cookie", ltaCookie.String())
 | |
| 	cr := http.Request{Header: ch}
 | |
| 
 | |
| 	session := emptyTestSession(t)
 | |
| 	baseURL, err := url.Parse(setting.AppURL)
 | |
| 	assert.NoError(t, err)
 | |
| 	session.jar.SetCookies(baseURL, cr.Cookies())
 | |
| 
 | |
| 	return session
 | |
| }
 | |
| 
 | |
| // GetLTACookieValue returns the value of the LTA cookie.
 | |
| func GetLTACookieValue(t *testing.T, sess *TestSession) string {
 | |
| 	t.Helper()
 | |
| 
 | |
| 	rememberCookie := sess.GetCookie(setting.CookieRememberName)
 | |
| 	assert.NotNil(t, rememberCookie)
 | |
| 
 | |
| 	cookieValue, err := url.QueryUnescape(rememberCookie.Value)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	return cookieValue
 | |
| }
 | |
| 
 | |
| // TestSessionCookie checks if the session cookie provides authentication.
 | |
| func TestSessionCookie(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	sess := loginUser(t, "user1")
 | |
| 	assert.NotNil(t, sess.GetCookie(setting.SessionConfig.CookieName))
 | |
| 
 | |
| 	req := NewRequest(t, "GET", "/user/settings")
 | |
| 	sess.MakeRequest(t, req, http.StatusOK)
 | |
| }
 | |
| 
 | |
| // TestLTACookie checks if the LTA cookie that's returned is valid, exists in the database
 | |
| // and provides authentication of no session cookie is present.
 | |
| func TestLTACookie(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | |
| 	sess := emptyTestSession(t)
 | |
| 
 | |
| 	req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
 | |
| 		"_csrf":     GetCSRF(t, sess, "/user/login"),
 | |
| 		"user_name": user.Name,
 | |
| 		"password":  userPassword,
 | |
| 		"remember":  "true",
 | |
| 	})
 | |
| 	sess.MakeRequest(t, req, http.StatusSeeOther)
 | |
| 
 | |
| 	// Checks if the database entry exist for the user.
 | |
| 	ltaCookieValue := GetLTACookieValue(t, sess)
 | |
| 	lookupKey, validator, found := strings.Cut(ltaCookieValue, ":")
 | |
| 	assert.True(t, found)
 | |
| 	rawValidator, err := hex.DecodeString(validator)
 | |
| 	assert.NoError(t, err)
 | |
| 	unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{LookupKey: lookupKey, HashedValidator: auth.HashValidator(rawValidator), UID: user.ID})
 | |
| 
 | |
| 	// Check if the LTA cookie it provides authentication.
 | |
| 	// If LTA cookie provides authentication /user/login shouldn't return status 200.
 | |
| 	session := GetSessionForLTACookie(t, sess.GetCookie(setting.CookieRememberName))
 | |
| 	req = NewRequest(t, "GET", "/user/login")
 | |
| 	session.MakeRequest(t, req, http.StatusSeeOther)
 | |
| }
 | |
| 
 | |
| // TestLTAPasswordChange checks that LTA doesn't provide authentication when a
 | |
| // password change has happened and that the new LTA does provide authentication.
 | |
| func TestLTAPasswordChange(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | |
| 
 | |
| 	sess := loginUserWithPasswordRemember(t, user.Name, userPassword, true)
 | |
| 	oldRememberCookie := sess.GetCookie(setting.CookieRememberName)
 | |
| 	assert.NotNil(t, oldRememberCookie)
 | |
| 
 | |
| 	// Make a simple password change.
 | |
| 	req := NewRequestWithValues(t, "POST", "/user/settings/account", map[string]string{
 | |
| 		"_csrf":        GetCSRF(t, sess, "/user/settings/account"),
 | |
| 		"old_password": userPassword,
 | |
| 		"password":     "password2",
 | |
| 		"retype":       "password2",
 | |
| 	})
 | |
| 	sess.MakeRequest(t, req, http.StatusSeeOther)
 | |
| 	rememberCookie := sess.GetCookie(setting.CookieRememberName)
 | |
| 	assert.NotNil(t, rememberCookie)
 | |
| 
 | |
| 	// Check if the password really changed.
 | |
| 	assert.NotEqualValues(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).Passwd, user.Passwd)
 | |
| 
 | |
| 	// /user/settings/account should provide with a new LTA cookie, so check for that.
 | |
| 	// If LTA cookie provides authentication /user/login shouldn't return status 200.
 | |
| 	session := GetSessionForLTACookie(t, rememberCookie)
 | |
| 	req = NewRequest(t, "GET", "/user/login")
 | |
| 	session.MakeRequest(t, req, http.StatusSeeOther)
 | |
| 
 | |
| 	// Check if the old LTA token is invalidated.
 | |
| 	session = GetSessionForLTACookie(t, oldRememberCookie)
 | |
| 	req = NewRequest(t, "GET", "/user/login")
 | |
| 	session.MakeRequest(t, req, http.StatusOK)
 | |
| }
 | |
| 
 | |
| // TestLTAExpiry tests that the LTA expiry works.
 | |
| func TestLTAExpiry(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | |
| 
 | |
| 	sess := loginUserWithPasswordRemember(t, user.Name, userPassword, true)
 | |
| 
 | |
| 	ltaCookieValie := GetLTACookieValue(t, sess)
 | |
| 	lookupKey, _, found := strings.Cut(ltaCookieValie, ":")
 | |
| 	assert.True(t, found)
 | |
| 
 | |
| 	// Ensure it's not expired.
 | |
| 	lta := unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
 | |
| 	assert.False(t, lta.IsExpired())
 | |
| 
 | |
| 	// Manually stub LTA's expiry.
 | |
| 	_, err := db.GetEngine(db.DefaultContext).ID(lta.ID).Table("forgejo_auth_token").Cols("expiry").Update(&auth.AuthorizationToken{Expiry: timeutil.TimeStampNow()})
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	// Ensure it's expired.
 | |
| 	lta = unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
 | |
| 	assert.True(t, lta.IsExpired())
 | |
| 
 | |
| 	// Should return 200 OK, because LTA doesn't provide authorization anymore.
 | |
| 	session := GetSessionForLTACookie(t, sess.GetCookie(setting.CookieRememberName))
 | |
| 	req := NewRequest(t, "GET", "/user/login")
 | |
| 	session.MakeRequest(t, req, http.StatusOK)
 | |
| 
 | |
| 	// Ensure it's deleted.
 | |
| 	unittest.AssertNotExistsBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
 | |
| }
 |