forked from forgejo/forgejo
[GITEA] rework long-term authentication
- 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
)
This commit is contained in:
parent
d1e5d9d664
commit
c26ac31816
17 changed files with 365 additions and 154 deletions
163
tests/integration/auth_token_test.go
Normal file
163
tests/integration/auth_token_test.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
// 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})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue