mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-09-12 01:41:32 +02: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 picked from commiteff097448b
) [GITEA] rework long-term authentication (squash) add migration Reminder: the migration is run via integration tests as explained in the commit "[DB] run all Forgejo migrations in integration tests" (cherry picked from commit4accf7443c
) (cherry picked from commit 99d06e344ebc3b50bafb2ac4473dd95f057d1ddc) (cherry picked from commitd8bc98a8f0
) (cherry picked from commit6404845df9
) (cherry picked from commit72bdd4f3b9
) (cherry picked from commit4b01bb0ce8
) (cherry picked from commitc26ac31816
) (cherry picked from commit8d2dab94a6
) Conflicts: routers/web/auth/auth.go https://codeberg.org/forgejo/forgejo/issues/2158
164 lines
5.7 KiB
Go
164 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})
|
|
}
|