[SECURITY] Rework long-term authentication
- This is a 'front-port' of the already existing patch on v1.21 and v1.20, but applied on top of what Gitea has done to rework the LTA mechanism. Forgejo will stick with the reworked mechanism by the Forgejo Security team for the time being. The removal of legacy code (AES-GCM) has been left out. - 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 commit e3d6622a63da9c33eed1e3d102cf28a92ff653d6) (cherry picked from commit fef1a6dac5e25579e42d40209c4cfc06879948b9) (cherry picked from commit b0c5165145fa52f2f7bbec1f50b308bdf1d20ef3) (cherry picked from commit 7ad51b9f8d0647eecacd258f6ee26155da3872e1) (cherry picked from commit 64f053f3834e764112cde26bb0d16c5e88d6b2af) (cherry picked from commit f5e78e4c204ce50b800645d614218b6b6096eecb) Conflicts: services/auth/auth_token_test.go https://codeberg.org/forgejo/forgejo/pulls/2069 (cherry picked from commit f69fc23d4bbadf388c7857040ee0774b824e418e) (cherry picked from commit d955ab3ab02cbb7f1245a8cddec426d64d3ac500) (cherry picked from commit 9220088f902a25c4690bcabf5a40a8d02e784182) (cherry picked from commit c73ac636962c41c71814c273510146f0533264ab) (cherry picked from commit 747a176048ea93085b406429db0e25bb21912eda) Conflicts: models/user/user.go routers/web/user/setting/account.go https://codeberg.org/forgejo/forgejo/pulls/2295
This commit is contained in:
parent
3916357840
commit
c6a53c3172
15 changed files with 328 additions and 309 deletions
|
@ -1,60 +1,96 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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")
|
||||
// AuthorizationToken represents a authorization token to a user.
|
||||
type AuthorizationToken struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UID int64 `xorm:"INDEX"`
|
||||
LookupKey string `xorm:"INDEX UNIQUE"`
|
||||
HashedValidator string
|
||||
Expiry timeutil.TimeStamp
|
||||
}
|
||||
|
||||
type AuthToken struct { //nolint:revive
|
||||
ID string `xorm:"pk"`
|
||||
TokenHash string
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
ExpiresUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
// TableName provides the real table name.
|
||||
func (AuthorizationToken) TableName() string {
|
||||
return "forgejo_auth_token"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(AuthToken))
|
||||
db.RegisterModel(new(AuthorizationToken))
|
||||
}
|
||||
|
||||
func InsertAuthToken(ctx context.Context, t *AuthToken) error {
|
||||
_, err := db.GetEngine(ctx).Insert(t)
|
||||
return err
|
||||
// IsExpired returns if the authorization token is expired.
|
||||
func (authToken *AuthorizationToken) IsExpired() bool {
|
||||
return authToken.Expiry.AsLocalTime().Before(time.Now())
|
||||
}
|
||||
|
||||
func GetAuthTokenByID(ctx context.Context, id string) (*AuthToken, error) {
|
||||
at := &AuthToken{}
|
||||
// GenerateAuthToken generates a new authentication token for the given user.
|
||||
// It returns the lookup key and validator values that should be passed to the
|
||||
// user via a long-term cookie.
|
||||
func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeStamp) (lookupKey, validator string, err error) {
|
||||
// Request 64 random bytes. The first 32 bytes will be used for the lookupKey
|
||||
// and the other 32 bytes will be used for the validator.
|
||||
rBytes, err := util.CryptoRandomBytes(64)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
hexEncoded := hex.EncodeToString(rBytes)
|
||||
validator, lookupKey = hexEncoded[64:], hexEncoded[:64]
|
||||
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(at)
|
||||
_, err = db.GetEngine(ctx).Insert(&AuthorizationToken{
|
||||
UID: userID,
|
||||
Expiry: expiry,
|
||||
LookupKey: lookupKey,
|
||||
HashedValidator: HashValidator(rBytes[32:]),
|
||||
})
|
||||
return lookupKey, validator, err
|
||||
}
|
||||
|
||||
// FindAuthToken will find a authorization token via the lookup key.
|
||||
func FindAuthToken(ctx context.Context, lookupKey string) (*AuthorizationToken, error) {
|
||||
var authToken AuthorizationToken
|
||||
has, err := db.GetEngine(ctx).Where("lookup_key = ?", lookupKey).Get(&authToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, fmt.Errorf("lookup key %q: %w", lookupKey, util.ErrNotExist)
|
||||
}
|
||||
if !has {
|
||||
return nil, ErrAuthTokenNotExist
|
||||
return &authToken, nil
|
||||
}
|
||||
|
||||
// DeleteAuthToken will delete the authorization token.
|
||||
func DeleteAuthToken(ctx context.Context, authToken *AuthorizationToken) error {
|
||||
_, err := db.DeleteByBean(ctx, authToken)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteAuthTokenByUser will delete all authorization tokens for the user.
|
||||
func DeleteAuthTokenByUser(ctx context.Context, userID int64) error {
|
||||
if userID == 0 {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
_, err := db.DeleteByBean(ctx, &AuthorizationToken{UID: userID})
|
||||
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
|
||||
// HashValidator will return a hexified hashed version of the validator.
|
||||
func HashValidator(validator []byte) string {
|
||||
h := sha256.New()
|
||||
h.Write(validator)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ var migrations = []*Migration{
|
|||
NewMigration("Add Forgejo Blocked Users table", forgejo_v1_20.AddForgejoBlockedUser),
|
||||
// v1 -> v2
|
||||
NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable),
|
||||
// v2 -> v3
|
||||
NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||
|
|
26
models/forgejo_migrations/v1_20/v3.go
Normal file
26
models/forgejo_migrations/v1_20/v3.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgejo_v1_20 //nolint:revive
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type AuthorizationToken struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UID int64 `xorm:"INDEX"`
|
||||
LookupKey string `xorm:"INDEX UNIQUE"`
|
||||
HashedValidator string
|
||||
Expiry timeutil.TimeStamp
|
||||
}
|
||||
|
||||
func (AuthorizationToken) TableName() string {
|
||||
return "forgejo_auth_token"
|
||||
}
|
||||
|
||||
func CreateAuthorizationTokenTable(x *xorm.Engine) error {
|
||||
return x.Sync(new(AuthorizationToken))
|
||||
}
|
|
@ -366,6 +366,11 @@ func (u *User) NewGitSig() *git.Signature {
|
|||
// SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
|
||||
// change passwd, salt and passwd_hash_algo fields
|
||||
func (u *User) SetPassword(passwd string) (err error) {
|
||||
// Invalidate all authentication tokens for this user.
|
||||
if err := auth.DeleteAuthTokenByUser(db.DefaultContext, u.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.Salt, err = GetUserSalt(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue