1
0
Fork 0
forked from forgejo/forgejo

[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 e3d6622a63)
(cherry picked from commit fef1a6dac5)
(cherry picked from commit b0c5165145)
(cherry picked from commit 7ad51b9f8d)
(cherry picked from commit 64f053f383)
(cherry picked from commit f5e78e4c20)

Conflicts:
	services/auth/auth_token_test.go
	https://codeberg.org/forgejo/forgejo/pulls/2069
(cherry picked from commit f69fc23d4b)
This commit is contained in:
Gusted 2023-11-22 17:26:21 +01:00 committed by Earl Warren
parent 7b254f84cf
commit d955ab3ab0
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
15 changed files with 328 additions and 309 deletions

View file

@ -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))
}