Skip to content

Commit

Permalink
Backport enhanced auth token / remember me from v1.22
Browse files Browse the repository at this point in the history
Without increasing database version.

From upstream go-gitea#27606

The mechanism responsible for long-term authentication (the 'remember
me' cookie) uses a weak construction technique. It will hash the user's
hashed password and the rands value; it will then call the secure cookie
code, which will encrypt the user's name with the computed hash. If one
were able to dump the database, they could extract those two values to
rebuild that cookie and impersonate a user. That vulnerability exists
from the date the dump was obtained until a user changed their password.

To fix this security issue, the cookie could be created and verified
using a different technique such as the one explained at
https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#secure-remember-me-cookies.

The PR removes the now obsolete setting `COOKIE_USERNAME`.
  • Loading branch information
brechtvl committed Nov 13, 2023
1 parent c10a323 commit be9d45b
Show file tree
Hide file tree
Showing 22 changed files with 409 additions and 103 deletions.
1 change: 0 additions & 1 deletion docs/content/administration/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,6 @@ And the following unique queues:
- `SECRET_KEY`: **\<random at every install\>**: 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
Expand Down
1 change: 0 additions & 1 deletion docs/content/administration/config-cheat-sheet.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 头部名称,用于提供邮箱信息。
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
60 changes: 60 additions & 0 deletions models/auth/auth_token.go
Original file line number Diff line number Diff line change
@@ -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
}
14 changes: 14 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/xorm"
"xorm.io/xorm/names"
Expand Down Expand Up @@ -649,5 +650,18 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
return err
}
}

// BLENDER: extra migration for backport, without bumping version.
// Remove when upgrading to Gitea 1.22.
type AuthToken struct {
ID string `xorm:"pk"`
TokenHash string
UserID int64 `xorm:"INDEX"`
ExpiresUnix timeutil.TimeStamp `xorm:"INDEX"`
}
if err = x.Sync(new(AuthToken)); err != nil {
return fmt.Errorf("migration blender auth_token failed: %w", err)
}

return nil
}
44 changes: 0 additions & 44 deletions modules/context/context_cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
2 changes: 0 additions & 2 deletions modules/setting/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ var (
SecretKey string
InternalToken string // internal access token
LogInRememberDays int
CookieUserName string
CookieRememberName string
ReverseProxyAuthUser string
ReverseProxyAuthEmail string
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,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.
Expand Down
12 changes: 8 additions & 4 deletions routers/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 2 additions & 4 deletions routers/web/auth/2fa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
64 changes: 40 additions & 24 deletions routers/web/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,41 +43,52 @@ 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 {
if !user_model.IsErrUserNotExist(err) {
return false, fmt.Errorf("GetUserByName: %w", err)
switch err {
case auth_service.ErrAuthTokenInvalidFormat, auth_service.ErrAuthTokenExpired:
return false, nil
}
return false, err
}
if t == nil {
return false, nil
}

if val, ok := ctx.GetSuperSecureCookie(
base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); !ok || val != u.Name {
u, err := user_model.GetUserByID(ctx, t.UserID)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
return false, fmt.Errorf("GetUserByID: %w", err)
}
return false, nil
}

isSucceed = true

nt, token, err := auth_service.RegenerateAuthToken(ctx, t)
if err != nil {
return false, err
}

ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)

if err := updateSession(ctx, nil, map[string]any{
// Set session IDs
"uid": u.ID,
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -161,8 +176,7 @@ func checkForceOAuth(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
}

Expand Down Expand Up @@ -315,10 +329,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{
Expand Down Expand Up @@ -393,7 +410,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)
Expand Down
19 changes: 1 addition & 18 deletions routers/web/auth/openid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
}

Expand Down
Loading

0 comments on commit be9d45b

Please sign in to comment.