diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 3fd78d483e..16f8caf22f 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -517,7 +517,6 @@ And the following unique queues: - `SECRET_KEY`: **\**: Global secret key. This key is VERY IMPORTANT, if you lost it, the data encrypted by it (like 2FA secret) can't be decrypted anymore. - `SECRET_KEY_URI`: **_empty_**: Instead of defining SECRET_KEY, this option can be used to use the key stored in a file (example value: `file:/etc/gitea/secret_key`). It shouldn't be lost like SECRET_KEY. - `LOGIN_REMEMBER_DAYS`: **7**: Cookie lifetime, in days. -- `COOKIE_USERNAME`: **gitea\_awesome**: Name of the cookie used to store the current username. - `COOKIE_REMEMBER_NAME`: **gitea\_incredible**: Name of cookie used to store authentication information. - `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**: Header name for reverse proxy diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md index 27f46cd8be..d541013159 100644 --- a/docs/content/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/administration/config-cheat-sheet.zh-cn.md @@ -506,7 +506,6 @@ Gitea 创建以下非唯一队列: - `SECRET_KEY`: **\<每次安装时随机生成\>**:全局服务器安全密钥。这个密钥非常重要,如果丢失将无法解密加密的数据(例如 2FA)。 - `SECRET_KEY_URI`: **_empty_**:与定义 `SECRET_KEY` 不同,此选项可用于使用存储在文件中的密钥(示例值:`file:/etc/gitea/secret_key`)。它不应该像 `SECRET_KEY` 一样容易丢失。 - `LOGIN_REMEMBER_DAYS`: **7**:Cookie 保存时间,单位为天。 -- `COOKIE_USERNAME`: **gitea\_awesome**:保存用户名的 Cookie 名称。 - `COOKIE_REMEMBER_NAME`: **gitea\_incredible**:保存自动登录信息的 Cookie 名称。 - `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**:反向代理认证的 HTTP 头部名称,用于提供用户信息。 - `REVERSE_PROXY_AUTHENTICATION_EMAIL`: **X-WEBAUTH-EMAIL**:反向代理认证的 HTTP 头部名称,用于提供邮箱信息。 diff --git a/models/auth/token.go b/models/auth/access_token.go similarity index 100% rename from models/auth/token.go rename to models/auth/access_token.go diff --git a/models/auth/token_scope.go b/models/auth/access_token_scope.go similarity index 100% rename from models/auth/token_scope.go rename to models/auth/access_token_scope.go diff --git a/models/auth/token_scope_test.go b/models/auth/access_token_scope_test.go similarity index 100% rename from models/auth/token_scope_test.go rename to models/auth/access_token_scope_test.go diff --git a/models/auth/token_test.go b/models/auth/access_token_test.go similarity index 100% rename from models/auth/token_test.go rename to models/auth/access_token_test.go diff --git a/models/auth/auth_token.go b/models/auth/auth_token.go new file mode 100644 index 0000000000..65f1b169eb --- /dev/null +++ b/models/auth/auth_token.go @@ -0,0 +1,60 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "context" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +var ErrAuthTokenNotExist = util.NewNotExistErrorf("auth token does not exist") + +type AuthToken struct { //nolint:revive + ID string `xorm:"pk"` + TokenHash string + UserID int64 `xorm:"INDEX"` + ExpiresUnix timeutil.TimeStamp `xorm:"INDEX"` +} + +func init() { + db.RegisterModel(new(AuthToken)) +} + +func InsertAuthToken(ctx context.Context, t *AuthToken) error { + _, err := db.GetEngine(ctx).Insert(t) + return err +} + +func GetAuthTokenByID(ctx context.Context, id string) (*AuthToken, error) { + at := &AuthToken{} + + has, err := db.GetEngine(ctx).ID(id).Get(at) + if err != nil { + return nil, err + } + if !has { + return nil, ErrAuthTokenNotExist + } + return at, nil +} + +func UpdateAuthTokenByID(ctx context.Context, t *AuthToken) error { + _, err := db.GetEngine(ctx).ID(t.ID).Cols("token_hash", "expires_unix").Update(t) + return err +} + +func DeleteAuthTokenByID(ctx context.Context, id string) error { + _, err := db.GetEngine(ctx).ID(id).Delete(&AuthToken{}) + return err +} + +func DeleteExpiredAuthTokens(ctx context.Context) error { + _, err := db.GetEngine(ctx).Where(builder.Lt{"expires_unix": timeutil.TimeStampNow()}).Delete(&AuthToken{}) + return err +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index a8037fa67e..4a06cdc73a 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -546,6 +546,8 @@ var migrations = []Migration{ // v280 -> v281 NewMigration("Rename user themes", v1_22.RenameUserThemes), + // v281 -> v282 + NewMigration("Add auth_token table", v1_22.CreateAuthTokenTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_22/v281.go b/models/migrations/v1_22/v281.go new file mode 100644 index 0000000000..fc1866aa83 --- /dev/null +++ b/models/migrations/v1_22/v281.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func CreateAuthTokenTable(x *xorm.Engine) error { + type AuthToken struct { + ID string `xorm:"pk"` + TokenHash string + UserID int64 `xorm:"INDEX"` + ExpiresUnix timeutil.TimeStamp `xorm:"INDEX"` + } + + return x.Sync(new(AuthToken)) +} diff --git a/modules/context/context_cookie.go b/modules/context/context_cookie.go index 9ce67a5298..b6f8dadb56 100644 --- a/modules/context/context_cookie.go +++ b/modules/context/context_cookie.go @@ -4,16 +4,11 @@ package context import ( - "encoding/hex" "net/http" "strings" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" - - "github.com/minio/sha256-simd" - "golang.org/x/crypto/pbkdf2" ) const CookieNameFlash = "gitea_flash" @@ -45,42 +40,3 @@ func (ctx *Context) DeleteSiteCookie(name string) { func (ctx *Context) GetSiteCookie(name string) string { return middleware.GetSiteCookie(ctx.Req, name) } - -// GetSuperSecureCookie returns given cookie value from request header with secret string. -func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) { - val := ctx.GetSiteCookie(name) - return ctx.CookieDecrypt(secret, val) -} - -// CookieDecrypt returns given value from with secret string. -func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) { - if val == "" { - return "", false - } - - text, err := hex.DecodeString(val) - if err != nil { - return "", false - } - - key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New) - text, err = util.AESGCMDecrypt(key, text) - return string(text), err == nil -} - -// SetSuperSecureCookie sets given cookie value to response header with secret string. -func (ctx *Context) SetSuperSecureCookie(secret, name, value string, maxAge int) { - text := ctx.CookieEncrypt(secret, value) - ctx.SetSiteCookie(name, text, maxAge) -} - -// CookieEncrypt encrypts a given value using the provided secret -func (ctx *Context) CookieEncrypt(secret, value string) string { - key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New) - text, err := util.AESGCMEncrypt(key, []byte(value)) - if err != nil { - panic("error encrypting cookie: " + err.Error()) - } - - return hex.EncodeToString(text) -} diff --git a/modules/setting/security.go b/modules/setting/security.go index 90f614d4cd..92caa05fad 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -19,7 +19,6 @@ var ( SecretKey string InternalToken string // internal access token LogInRememberDays int - CookieUserName string CookieRememberName string ReverseProxyAuthUser string ReverseProxyAuthEmail string @@ -104,7 +103,6 @@ func loadSecurityFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("security") InstallLock = HasInstallLock(rootCfg) LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7) - CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome") SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY") if SecretKey == "" { // FIXME: https://github.com/go-gitea/gitea/issues/16832 diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b7b99bd7a5..8677461322 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -363,6 +363,7 @@ disable_register_prompt = Registration is disabled. Please contact your site adm disable_register_mail = Email confirmation for registration is disabled. manual_activation_only = Contact your site administrator to complete activation. remember_me = Remember This Device +remember_me.compromised = The login token is not valid anymore which may indicate a compromised account. Please check your account for unusual activities. forgot_password_title= Forgot Password forgot_password = Forgot password? sign_up_now = Need an account? Register now. diff --git a/routers/install/install.go b/routers/install/install.go index 185e4bf6bf..5c0290d2cc 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -27,12 +27,14 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/user" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/common" + auth_service "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/forms" "gitea.com/go-chi/session" @@ -547,11 +549,13 @@ func SubmitInstall(ctx *context.Context) { u, _ = user_model.GetUserByName(ctx, u.Name) } - days := 86400 * setting.LogInRememberDays - ctx.SetSiteCookie(setting.CookieUserName, u.Name, days) + nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID) + if err != nil { + ctx.ServerError("CreateAuthTokenForUserID", err) + return + } - ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd), - setting.CookieRememberName, u.Name, days) + ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day) // Auto-login for admin if err = ctx.Session.Set("uid", u.ID); err != nil { diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go index bc3cb4907c..dc0062ebaa 100644 --- a/routers/web/auth/2fa.go +++ b/routers/web/auth/2fa.go @@ -26,8 +26,7 @@ var ( func TwoFactor(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("twofa") - // Check auto-login. - if checkAutoLogin(ctx) { + if CheckAutoLogin(ctx) { return } @@ -99,8 +98,7 @@ func TwoFactorPost(ctx *context.Context) { func TwoFactorScratch(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("twofa_scratch") - // Check auto-login. - if checkAutoLogin(ctx) { + if CheckAutoLogin(ctx) { return } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index df835a2fa1..1238e52755 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -43,40 +43,51 @@ const ( TplActivate base.TplName = "user/auth/activate" ) -// AutoSignIn reads cookie and try to auto-login. -func AutoSignIn(ctx *context.Context) (bool, error) { +// autoSignIn reads cookie and try to auto-login. +func autoSignIn(ctx *context.Context) (bool, error) { if !db.HasEngine { return false, nil } - uname := ctx.GetSiteCookie(setting.CookieUserName) - if len(uname) == 0 { - return false, nil - } - isSucceed := false defer func() { if !isSucceed { - log.Trace("auto-login cookie cleared: %s", uname) - ctx.DeleteSiteCookie(setting.CookieUserName) ctx.DeleteSiteCookie(setting.CookieRememberName) } }() - u, err := user_model.GetUserByName(ctx, uname) + if err := auth.DeleteExpiredAuthTokens(ctx); err != nil { + log.Error("Failed to delete expired auth tokens: %v", err) + } + + t, err := auth_service.CheckAuthToken(ctx, ctx.GetSiteCookie(setting.CookieRememberName)) + if err != nil { + switch err { + case auth_service.ErrAuthTokenInvalidFormat, auth_service.ErrAuthTokenExpired: + return false, nil + } + return false, err + } + if t == nil { + return false, nil + } + + u, err := user_model.GetUserByID(ctx, t.UserID) if err != nil { if !user_model.IsErrUserNotExist(err) { - return false, fmt.Errorf("GetUserByName: %w", err) + return false, fmt.Errorf("GetUserByID: %w", err) } return false, nil } - if val, ok := ctx.GetSuperSecureCookie( - base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); !ok || val != u.Name { - return false, nil + isSucceed = true + + nt, token, err := auth_service.RegenerateAuthToken(ctx, t) + if err != nil { + return false, err } - isSucceed = true + ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day) if err := updateSession(ctx, nil, map[string]any{ // Set session IDs @@ -113,11 +124,15 @@ func resetLocale(ctx *context.Context, u *user_model.User) error { return nil } -func checkAutoLogin(ctx *context.Context) bool { +func CheckAutoLogin(ctx *context.Context) bool { // Check auto-login - isSucceed, err := AutoSignIn(ctx) + isSucceed, err := autoSignIn(ctx) if err != nil { - ctx.ServerError("AutoSignIn", err) + if errors.Is(err, auth_service.ErrAuthTokenInvalidHash) { + ctx.Flash.Error(ctx.Tr("auth.remember_me.compromised"), true) + return false + } + ctx.ServerError("autoSignIn", err) return true } @@ -141,8 +156,7 @@ func checkAutoLogin(ctx *context.Context) bool { func SignIn(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("sign_in") - // Check auto-login - if checkAutoLogin(ctx) { + if CheckAutoLogin(ctx) { return } @@ -290,10 +304,13 @@ func handleSignIn(ctx *context.Context, u *user_model.User, remember bool) { func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRedirect bool) string { if remember { - days := 86400 * setting.LogInRememberDays - ctx.SetSiteCookie(setting.CookieUserName, u.Name, days) - ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd), - setting.CookieRememberName, u.Name, days) + nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID) + if err != nil { + ctx.ServerError("CreateAuthTokenForUserID", err) + return setting.AppSubURL + "/" + } + + ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day) } if err := updateSession(ctx, []string{ @@ -368,7 +385,6 @@ func getUserName(gothUser *goth.User) string { func HandleSignOut(ctx *context.Context) { _ = ctx.Session.Flush() _ = ctx.Session.Destroy(ctx.Resp, ctx.Req) - ctx.DeleteSiteCookie(setting.CookieUserName) ctx.DeleteSiteCookie(setting.CookieRememberName) ctx.Csrf.DeleteCookie(ctx) middleware.DeleteRedirectToCookie(ctx.Resp) diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index aa07129632..29ef772b1c 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/forms" ) @@ -36,23 +35,7 @@ func SignInOpenID(ctx *context.Context) { return } - // Check auto-login. - isSucceed, err := AutoSignIn(ctx) - if err != nil { - ctx.ServerError("AutoSignIn", err) - return - } - - redirectTo := ctx.FormString("redirect_to") - if len(redirectTo) > 0 { - middleware.SetRedirectToCookie(ctx.Resp, redirectTo) - } else { - redirectTo = ctx.GetSiteCookie("redirect_to") - } - - if isSucceed { - middleware.DeleteRedirectToCookie(ctx.Resp) - ctx.RedirectToFirst(redirectTo) + if CheckAutoLogin(ctx) { return } diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go index 9b516ce396..95c8d262a5 100644 --- a/routers/web/auth/webauthn.go +++ b/routers/web/auth/webauthn.go @@ -26,8 +26,7 @@ var tplWebAuthn base.TplName = "user/auth/webauthn" func WebAuthn(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("twofa") - // Check auto-login. - if checkAutoLogin(ctx) { + if CheckAutoLogin(ctx) { return } diff --git a/routers/web/home.go b/routers/web/home.go index ab3fbde2c9..2321b00efe 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -54,8 +54,7 @@ func Home(ctx *context.Context) { } // Check auto-login. - uname := ctx.GetSiteCookie(setting.CookieUserName) - if len(uname) != 0 { + if ctx.GetSiteCookie(setting.CookieRememberName) != "" { ctx.Redirect(setting.AppSubURL + "/user/login") return } diff --git a/routers/web/web.go b/routers/web/web.go index d2179a0008..6449f7716c 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -187,7 +187,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont // Redirect to log in page if auto-signin info is provided and has not signed in. if !options.SignOutRequired && !ctx.IsSigned && - len(ctx.GetSiteCookie(setting.CookieUserName)) > 0 { + ctx.GetSiteCookie(setting.CookieRememberName) != "" { if ctx.Req.URL.Path != "/user/events" { middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI()) } diff --git a/services/auth/auth_token.go b/services/auth/auth_token.go new file mode 100644 index 0000000000..6b59238c98 --- /dev/null +++ b/services/auth/auth_token.go @@ -0,0 +1,123 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "context" + "crypto/sha256" + "crypto/subtle" + "encoding/hex" + "errors" + "strings" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +// Based on https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#secure-remember-me-cookies + +// The auth token consists of two parts: ID and token hash +// Every device login creates a new auth token with an individual id and hash. +// If a device uses the token to login into the instance, a fresh token gets generated which has the same id but a new hash. + +var ( + ErrAuthTokenInvalidFormat = util.NewInvalidArgumentErrorf("auth token has an invalid format") + ErrAuthTokenExpired = util.NewInvalidArgumentErrorf("auth token has expired") + ErrAuthTokenInvalidHash = util.NewInvalidArgumentErrorf("auth token is invalid") +) + +func CheckAuthToken(ctx context.Context, value string) (*auth_model.AuthToken, error) { + if len(value) == 0 { + return nil, nil + } + + parts := strings.SplitN(value, ":", 2) + if len(parts) != 2 { + return nil, ErrAuthTokenInvalidFormat + } + + t, err := auth_model.GetAuthTokenByID(ctx, parts[0]) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + return nil, ErrAuthTokenExpired + } + return nil, err + } + + if t.ExpiresUnix < timeutil.TimeStampNow() { + return nil, ErrAuthTokenExpired + } + + hashedToken := sha256.Sum256([]byte(parts[1])) + + if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(hex.EncodeToString(hashedToken[:]))) == 0 { + // If an attacker steals a token and uses the token to create a new session the hash gets updated. + // When the victim uses the old token the hashes don't match anymore and the victim should be notified about the compromised token. + return nil, ErrAuthTokenInvalidHash + } + + return t, nil +} + +func RegenerateAuthToken(ctx context.Context, t *auth_model.AuthToken) (*auth_model.AuthToken, string, error) { + token, hash, err := generateTokenAndHash() + if err != nil { + return nil, "", err + } + + newToken := &auth_model.AuthToken{ + ID: t.ID, + TokenHash: hash, + UserID: t.UserID, + ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour), + } + + if err := auth_model.UpdateAuthTokenByID(ctx, newToken); err != nil { + return nil, "", err + } + + return newToken, token, nil +} + +func CreateAuthTokenForUserID(ctx context.Context, userID int64) (*auth_model.AuthToken, string, error) { + t := &auth_model.AuthToken{ + UserID: userID, + ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour), + } + + var err error + t.ID, err = util.CryptoRandomString(10) + if err != nil { + return nil, "", err + } + + token, hash, err := generateTokenAndHash() + if err != nil { + return nil, "", err + } + + t.TokenHash = hash + + if err := auth_model.InsertAuthToken(ctx, t); err != nil { + return nil, "", err + } + + return t, token, nil +} + +func generateTokenAndHash() (string, string, error) { + buf, err := util.CryptoRandomBytes(32) + if err != nil { + return "", "", err + } + + token := hex.EncodeToString(buf) + + hashedToken := sha256.Sum256([]byte(token)) + + return token, hex.EncodeToString(hashedToken[:]), nil +} diff --git a/services/auth/auth_token_test.go b/services/auth/auth_token_test.go new file mode 100644 index 0000000000..654275df17 --- /dev/null +++ b/services/auth/auth_token_test.go @@ -0,0 +1,107 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" +) + +func TestCheckAuthToken(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("Empty", func(t *testing.T) { + token, err := CheckAuthToken(db.DefaultContext, "") + assert.NoError(t, err) + assert.Nil(t, token) + }) + + t.Run("InvalidFormat", func(t *testing.T) { + token, err := CheckAuthToken(db.DefaultContext, "dummy") + assert.ErrorIs(t, err, ErrAuthTokenInvalidFormat) + assert.Nil(t, token) + }) + + t.Run("NotFound", func(t *testing.T) { + token, err := CheckAuthToken(db.DefaultContext, "notexists:dummy") + assert.ErrorIs(t, err, ErrAuthTokenExpired) + assert.Nil(t, token) + }) + + t.Run("Expired", func(t *testing.T) { + timeutil.Set(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) + + at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2) + assert.NoError(t, err) + assert.NotNil(t, at) + assert.NotEmpty(t, token) + + timeutil.Unset() + + at2, err := CheckAuthToken(db.DefaultContext, at.ID+":"+token) + assert.ErrorIs(t, err, ErrAuthTokenExpired) + assert.Nil(t, at2) + + assert.NoError(t, auth_model.DeleteAuthTokenByID(db.DefaultContext, at.ID)) + }) + + t.Run("InvalidHash", func(t *testing.T) { + at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2) + assert.NoError(t, err) + assert.NotNil(t, at) + assert.NotEmpty(t, token) + + at2, err := CheckAuthToken(db.DefaultContext, at.ID+":"+token+"dummy") + assert.ErrorIs(t, err, ErrAuthTokenInvalidHash) + assert.Nil(t, at2) + + assert.NoError(t, auth_model.DeleteAuthTokenByID(db.DefaultContext, at.ID)) + }) + + t.Run("Valid", func(t *testing.T) { + at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2) + assert.NoError(t, err) + assert.NotNil(t, at) + assert.NotEmpty(t, token) + + at2, err := CheckAuthToken(db.DefaultContext, at.ID+":"+token) + assert.NoError(t, err) + assert.NotNil(t, at2) + + assert.NoError(t, auth_model.DeleteAuthTokenByID(db.DefaultContext, at.ID)) + }) +} + +func TestRegenerateAuthToken(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + timeutil.Set(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) + defer timeutil.Unset() + + at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2) + assert.NoError(t, err) + assert.NotNil(t, at) + assert.NotEmpty(t, token) + + timeutil.Set(time.Date(2023, 1, 1, 0, 0, 1, 0, time.UTC)) + + at2, token2, err := RegenerateAuthToken(db.DefaultContext, at) + assert.NoError(t, err) + assert.NotNil(t, at2) + assert.NotEmpty(t, token2) + + assert.Equal(t, at.ID, at2.ID) + assert.Equal(t, at.UserID, at2.UserID) + assert.NotEqual(t, token, token2) + assert.NotEqual(t, at.ExpiresUnix, at2.ExpiresUnix) + + assert.NoError(t, auth_model.DeleteAuthTokenByID(db.DefaultContext, at.ID)) +} diff --git a/services/auth/main_test.go b/services/auth/main_test.go new file mode 100644 index 0000000000..b81c39a1f2 --- /dev/null +++ b/services/auth/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index 9ae45d3242..2584b88f65 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -5,11 +5,13 @@ package integration import ( "net/http" + "net/url" "strings" "testing" "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/translation" "code.gitea.io/gitea/tests" @@ -57,3 +59,37 @@ func TestSignin(t *testing.T) { testLoginFailed(t, s.username, s.password, s.message) } } + +func TestSigninWithRememberMe(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + baseURL, _ := url.Parse(setting.AppURL) + + session := emptyTestSession(t) + req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{ + "_csrf": GetCSRF(t, session, "/user/login"), + "user_name": user.Name, + "password": userPassword, + "remember": "on", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + c := session.GetCookie(setting.CookieRememberName) + assert.NotNil(t, c) + + session = emptyTestSession(t) + + // Without session the settings page should not be reachable + req = NewRequest(t, "GET", "/user/settings") + session.MakeRequest(t, req, http.StatusSeeOther) + + req = NewRequest(t, "GET", "/user/login") + // Set the remember me cookie for the login GET request + session.jar.SetCookies(baseURL, []*http.Cookie{c}) + session.MakeRequest(t, req, http.StatusSeeOther) + + // With session the settings page should be reachable + req = NewRequest(t, "GET", "/user/settings") + session.MakeRequest(t, req, http.StatusOK) +}