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:
Gusted 2024-08-11 05:13:01 +02:00 committed by Earl Warren
parent 0fa436c373
commit 1ce33aa38d
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
18 changed files with 448 additions and 256 deletions

View file

@ -5,8 +5,6 @@
package auth
import (
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"net/http"
@ -63,38 +61,11 @@ func autoSignIn(ctx *context.Context) (bool, error) {
return false, nil
}
lookupKey, validator, found := strings.Cut(authCookie, ":")
if !found {
return false, nil
}
authToken, err := auth.FindAuthToken(ctx, lookupKey)
u, err := user_model.VerifyUserAuthorizationToken(ctx, authCookie, auth.LongTermAuthorization, false)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
return false, nil
}
return false, err
return false, fmt.Errorf("VerifyUserAuthorizationToken: %w", err)
}
if authToken.IsExpired() {
err = auth.DeleteAuthToken(ctx, authToken)
return false, err
}
rawValidator, err := hex.DecodeString(validator)
if err != nil {
return false, err
}
if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 {
return false, nil
}
u, err := user_model.GetUserByID(ctx, authToken.UID)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
return false, fmt.Errorf("GetUserByID: %w", err)
}
if u == nil {
return false, nil
}
@ -633,7 +604,10 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
return false
}
mailer.SendActivateAccountMail(ctx.Locale, u)
if err := mailer.SendActivateAccountMail(ctx, u); err != nil {
ctx.ServerError("SendActivateAccountMail", err)
return false
}
ctx.Data["IsSendRegisterMail"] = true
ctx.Data["Email"] = u.Email
@ -674,7 +648,10 @@ func Activate(ctx *context.Context) {
ctx.Data["ResendLimited"] = true
} else {
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
if err := mailer.SendActivateAccountMail(ctx, ctx.Doer); err != nil {
ctx.ServerError("SendActivateAccountMail", err)
return
}
if err := ctx.Cache.Put(cacheKey+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
@ -687,7 +664,12 @@ func Activate(ctx *context.Context) {
return
}
user := user_model.VerifyUserActiveCode(ctx, code)
user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, false)
if err != nil {
ctx.ServerError("VerifyUserAuthorizationToken", err)
return
}
// if code is wrong
if user == nil {
ctx.Data["IsCodeInvalid"] = true
@ -751,7 +733,12 @@ func ActivatePost(ctx *context.Context) {
return
}
user := user_model.VerifyUserActiveCode(ctx, code)
user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, true)
if err != nil {
ctx.ServerError("VerifyUserAuthorizationToken", err)
return
}
// if code is wrong
if user == nil {
ctx.Data["IsCodeInvalid"] = true
@ -835,23 +822,32 @@ func ActivateEmail(ctx *context.Context) {
code := ctx.FormString("code")
emailStr := ctx.FormString("email")
// Verify code.
if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil {
if err := user_model.ActivateEmail(ctx, email); err != nil {
ctx.ServerError("ActivateEmail", err)
return
}
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
if u, err := user_model.GetUserByID(ctx, email.UID); err != nil {
log.Warn("GetUserByID: %d", email.UID)
} else {
// Allow user to validate more emails
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
}
u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.EmailActivation(emailStr), true)
if err != nil {
ctx.ServerError("VerifyUserAuthorizationToken", err)
return
}
if u == nil {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
email, err := user_model.GetEmailAddressOfUser(ctx, emailStr, u.ID)
if err != nil {
ctx.ServerError("GetEmailAddressOfUser", err)
return
}
if err := user_model.ActivateEmail(ctx, email); err != nil {
ctx.ServerError("ActivateEmail", err)
return
}
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
// Allow user to validate more emails
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
// FIXME: e-mail verification does not require the user to be logged in,
// so this could be redirecting to the login page.