fix: extend forgejo_auth_token
table
- Add a `purpose` column, this allows the `forgejo_auth_token` table to be used by other parts of Forgejo, while still enjoying the no-compromise architecture. - Remove the 'roll your own crypto' time limited code functions and migrate them to the `forgejo_auth_token` table. This migration ensures generated codes can only be used for their purpose and ensure they are invalidated after their usage by deleting it from the database, this also should help making auditing of the security code easier, as we're no longer trying to stuff a lot of data into a HMAC construction. -Helper functions are rewritten to ensure a safe-by-design approach to these tokens. - Add the `forgejo_auth_token` to dbconsistency doctor and add it to the `deleteUser` function. - TODO: Add cron job to delete expired authorization tokens. - Unit and integration tests added.
This commit is contained in:
parent
0fa436c373
commit
1ce33aa38d
18 changed files with 448 additions and 256 deletions
|
@ -7,7 +7,9 @@ package user
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
|
@ -318,15 +320,14 @@ func (u *User) OrganisationLink() string {
|
|||
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
||||
}
|
||||
|
||||
// GenerateEmailActivateCode generates an activate code based on user information and given e-mail.
|
||||
func (u *User) GenerateEmailActivateCode(email string) string {
|
||||
code := base.CreateTimeLimitCode(
|
||||
fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands),
|
||||
setting.Service.ActiveCodeLives, time.Now(), nil)
|
||||
|
||||
// Add tail hex username
|
||||
code += hex.EncodeToString([]byte(u.LowerName))
|
||||
return code
|
||||
// GenerateEmailAuthorizationCode generates an activation code based for the user for the specified purpose.
|
||||
// The standard expiry is ActiveCodeLives minutes.
|
||||
func (u *User) GenerateEmailAuthorizationCode(ctx context.Context, purpose auth.AuthorizationPurpose) (string, error) {
|
||||
lookup, validator, err := auth.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(setting.Service.ActiveCodeLives)*60), purpose)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return lookup + ":" + validator, nil
|
||||
}
|
||||
|
||||
// GetUserFollowers returns range of user's followers.
|
||||
|
@ -838,35 +839,50 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
|
|||
return count
|
||||
}
|
||||
|
||||
// GetVerifyUser get user by verify code
|
||||
func GetVerifyUser(ctx context.Context, code string) (user *User) {
|
||||
if len(code) <= base.TimeLimitCodeLength {
|
||||
return nil
|
||||
// VerifyUserActiveCode verifies that the code is valid for the given purpose for this user.
|
||||
// If delete is specified, the token will be deleted.
|
||||
func VerifyUserAuthorizationToken(ctx context.Context, code string, purpose auth.AuthorizationPurpose, delete bool) (*User, error) {
|
||||
lookupKey, validator, found := strings.Cut(code, ":")
|
||||
if !found {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// use tail hex username query user
|
||||
hexStr := code[base.TimeLimitCodeLength:]
|
||||
if b, err := hex.DecodeString(hexStr); err == nil {
|
||||
if user, err = GetUserByName(ctx, string(b)); user != nil {
|
||||
return user
|
||||
authToken, err := auth.FindAuthToken(ctx, lookupKey, purpose)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
log.Error("user.getVerifyUser: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
if authToken.IsExpired() {
|
||||
return nil, auth.DeleteAuthToken(ctx, authToken)
|
||||
}
|
||||
|
||||
// VerifyUserActiveCode verifies active code when active account
|
||||
func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
|
||||
if user = GetVerifyUser(ctx, code); user != nil {
|
||||
// time limit code
|
||||
prefix := code[:base.TimeLimitCodeLength]
|
||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands)
|
||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||
return user
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 {
|
||||
return nil, errors.New("validator doesn't match")
|
||||
}
|
||||
|
||||
u, err := GetUserByID(ctx, authToken.UID)
|
||||
if err != nil {
|
||||
if IsErrUserNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if delete {
|
||||
if err := auth.DeleteAuthToken(ctx, authToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// ValidateUser check if user is valid to insert / update into database
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue