2022-01-02 13:12:35 +00:00
|
|
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
|
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
2022-11-27 18:20:29 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
2022-01-02 13:12:35 +00:00
|
|
|
|
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
2023-11-22 16:26:21 +00:00
|
|
|
"crypto/subtle"
|
|
|
|
"encoding/hex"
|
2022-10-19 19:07:21 +00:00
|
|
|
"errors"
|
2022-01-02 13:12:35 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"code.gitea.io/gitea/models/auth"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
2023-02-19 07:35:20 +00:00
|
|
|
"code.gitea.io/gitea/modules/auth/password"
|
2022-01-02 13:12:35 +00:00
|
|
|
"code.gitea.io/gitea/modules/base"
|
|
|
|
"code.gitea.io/gitea/modules/eventsource"
|
2024-04-15 13:03:08 +00:00
|
|
|
"code.gitea.io/gitea/modules/httplib"
|
2022-01-02 13:12:35 +00:00
|
|
|
"code.gitea.io/gitea/modules/log"
|
2024-02-04 13:29:09 +00:00
|
|
|
"code.gitea.io/gitea/modules/optional"
|
2022-01-02 13:12:35 +00:00
|
|
|
"code.gitea.io/gitea/modules/session"
|
|
|
|
"code.gitea.io/gitea/modules/setting"
|
|
|
|
"code.gitea.io/gitea/modules/timeutil"
|
2022-10-19 19:07:21 +00:00
|
|
|
"code.gitea.io/gitea/modules/util"
|
2022-01-02 13:12:35 +00:00
|
|
|
"code.gitea.io/gitea/modules/web"
|
|
|
|
"code.gitea.io/gitea/modules/web/middleware"
|
|
|
|
auth_service "code.gitea.io/gitea/services/auth"
|
|
|
|
"code.gitea.io/gitea/services/auth/source/oauth2"
|
2024-02-27 07:12:22 +00:00
|
|
|
"code.gitea.io/gitea/services/context"
|
2022-01-02 13:12:35 +00:00
|
|
|
"code.gitea.io/gitea/services/externalaccount"
|
|
|
|
"code.gitea.io/gitea/services/forms"
|
|
|
|
"code.gitea.io/gitea/services/mailer"
|
2023-09-07 07:11:29 +00:00
|
|
|
notify_service "code.gitea.io/gitea/services/notify"
|
2024-02-04 13:29:09 +00:00
|
|
|
user_service "code.gitea.io/gitea/services/user"
|
2022-01-02 13:12:35 +00:00
|
|
|
|
|
|
|
"github.com/markbates/goth"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// tplSignIn template for sign in page
|
|
|
|
tplSignIn base.TplName = "user/auth/signin"
|
|
|
|
// tplSignUp template path for sign up page
|
|
|
|
tplSignUp base.TplName = "user/auth/signup"
|
|
|
|
// TplActivate template path for activate user
|
|
|
|
TplActivate base.TplName = "user/auth/activate"
|
|
|
|
)
|
|
|
|
|
2023-10-14 00:56:41 +00:00
|
|
|
// autoSignIn reads cookie and try to auto-login.
|
|
|
|
func autoSignIn(ctx *context.Context) (bool, error) {
|
2022-01-02 13:12:35 +00:00
|
|
|
isSucceed := false
|
|
|
|
defer func() {
|
|
|
|
if !isSucceed {
|
2023-04-13 19:45:33 +00:00
|
|
|
ctx.DeleteSiteCookie(setting.CookieRememberName)
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2023-11-22 16:26:21 +00:00
|
|
|
authCookie := ctx.GetSiteCookie(setting.CookieRememberName)
|
|
|
|
if len(authCookie) == 0 {
|
|
|
|
return false, nil
|
2023-10-14 00:56:41 +00:00
|
|
|
}
|
|
|
|
|
2023-11-22 16:26:21 +00:00
|
|
|
lookupKey, validator, found := strings.Cut(authCookie, ":")
|
|
|
|
if !found {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
authToken, err := auth.FindAuthToken(ctx, lookupKey)
|
2022-01-02 13:12:35 +00:00
|
|
|
if err != nil {
|
2023-11-22 16:26:21 +00:00
|
|
|
if errors.Is(err, util.ErrNotExist) {
|
2023-10-14 00:56:41 +00:00
|
|
|
return false, nil
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
2023-10-14 00:56:41 +00:00
|
|
|
return false, err
|
|
|
|
}
|
2023-11-22 16:26:21 +00:00
|
|
|
|
|
|
|
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 {
|
2022-01-02 13:12:35 +00:00
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
2023-11-22 16:26:21 +00:00
|
|
|
u, err := user_model.GetUserByID(ctx, authToken.UID)
|
2023-10-14 00:56:41 +00:00
|
|
|
if err != nil {
|
|
|
|
if !user_model.IsErrUserNotExist(err) {
|
|
|
|
return false, fmt.Errorf("GetUserByID: %w", err)
|
|
|
|
}
|
2022-01-02 13:12:35 +00:00
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
isSucceed = true
|
|
|
|
|
2023-07-04 18:36:08 +00:00
|
|
|
if err := updateSession(ctx, nil, map[string]any{
|
2022-11-10 11:43:06 +00:00
|
|
|
// Set session IDs
|
2023-11-22 16:26:21 +00:00
|
|
|
"uid": u.ID,
|
2022-11-10 11:43:06 +00:00
|
|
|
}); err != nil {
|
|
|
|
return false, fmt.Errorf("unable to updateSession: %w", err)
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := resetLocale(ctx, u); err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2023-04-13 19:45:33 +00:00
|
|
|
ctx.Csrf.DeleteCookie(ctx)
|
2022-01-02 13:12:35 +00:00
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func resetLocale(ctx *context.Context, u *user_model.User) error {
|
|
|
|
// Language setting of the user overwrites the one previously set
|
|
|
|
// If the user does not have a locale set, we save the current one.
|
2024-02-04 13:29:09 +00:00
|
|
|
if u.Language == "" {
|
|
|
|
opts := &user_service.UpdateOptions{
|
|
|
|
Language: optional.Some(ctx.Locale.Language()),
|
|
|
|
}
|
|
|
|
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
2022-01-02 13:12:35 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
middleware.SetLocaleCookie(ctx.Resp, u.Language, 0)
|
|
|
|
|
|
|
|
if ctx.Locale.Language() != u.Language {
|
|
|
|
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-05 02:12:03 +00:00
|
|
|
func RedirectAfterLogin(ctx *context.Context) {
|
|
|
|
redirectTo := ctx.FormString("redirect_to")
|
|
|
|
if redirectTo == "" {
|
|
|
|
redirectTo = ctx.GetSiteCookie("redirect_to")
|
|
|
|
}
|
|
|
|
middleware.DeleteRedirectToCookie(ctx.Resp)
|
|
|
|
nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL)
|
|
|
|
if setting.LandingPageURL == setting.LandingPageLogin {
|
|
|
|
nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
|
|
|
|
}
|
|
|
|
ctx.RedirectToFirst(redirectTo, nextRedirectTo)
|
|
|
|
}
|
|
|
|
|
2023-10-14 00:56:41 +00:00
|
|
|
func CheckAutoLogin(ctx *context.Context) bool {
|
2024-03-05 02:12:03 +00:00
|
|
|
isSucceed, err := autoSignIn(ctx) // try to auto-login
|
2022-01-02 13:12:35 +00:00
|
|
|
if err != nil {
|
2023-10-14 00:56:41 +00:00
|
|
|
ctx.ServerError("autoSignIn", err)
|
2022-01-02 13:12:35 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
redirectTo := ctx.FormString("redirect_to")
|
|
|
|
if len(redirectTo) > 0 {
|
|
|
|
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
|
|
|
|
}
|
|
|
|
|
|
|
|
if isSucceed {
|
2024-03-05 02:12:03 +00:00
|
|
|
RedirectAfterLogin(ctx)
|
2022-01-02 13:12:35 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// SignIn render sign in page
|
|
|
|
func SignIn(ctx *context.Context) {
|
|
|
|
ctx.Data["Title"] = ctx.Tr("sign_in")
|
|
|
|
|
2023-10-14 00:56:41 +00:00
|
|
|
if CheckAutoLogin(ctx) {
|
2022-01-02 13:12:35 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-03-05 02:12:03 +00:00
|
|
|
if ctx.IsSigned {
|
|
|
|
RedirectAfterLogin(ctx)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-03-02 15:42:31 +00:00
|
|
|
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
2022-01-02 13:12:35 +00:00
|
|
|
if err != nil {
|
|
|
|
ctx.ServerError("UserSignIn", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.Data["OAuth2Providers"] = oauth2Providers
|
|
|
|
ctx.Data["Title"] = ctx.Tr("sign_in")
|
|
|
|
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
|
|
|
|
ctx.Data["PageIsSignIn"] = true
|
|
|
|
ctx.Data["PageIsLogin"] = true
|
2023-10-11 04:24:07 +00:00
|
|
|
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
|
2022-01-02 13:12:35 +00:00
|
|
|
|
2022-11-22 21:13:18 +00:00
|
|
|
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
|
|
|
|
context.SetCaptchaData(ctx)
|
|
|
|
}
|
|
|
|
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.HTML(http.StatusOK, tplSignIn)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SignInPost response for sign in request
|
|
|
|
func SignInPost(ctx *context.Context) {
|
|
|
|
ctx.Data["Title"] = ctx.Tr("sign_in")
|
|
|
|
|
2024-03-02 15:42:31 +00:00
|
|
|
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
2022-01-02 13:12:35 +00:00
|
|
|
if err != nil {
|
|
|
|
ctx.ServerError("UserSignIn", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.Data["OAuth2Providers"] = oauth2Providers
|
|
|
|
ctx.Data["Title"] = ctx.Tr("sign_in")
|
|
|
|
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
|
|
|
|
ctx.Data["PageIsSignIn"] = true
|
|
|
|
ctx.Data["PageIsLogin"] = true
|
2023-10-11 04:24:07 +00:00
|
|
|
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
|
2022-01-02 13:12:35 +00:00
|
|
|
|
|
|
|
if ctx.HasError() {
|
|
|
|
ctx.HTML(http.StatusOK, tplSignIn)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
form := web.GetForm(ctx).(*forms.SignInForm)
|
2022-11-22 21:13:18 +00:00
|
|
|
|
|
|
|
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
|
|
|
|
context.SetCaptchaData(ctx)
|
|
|
|
|
|
|
|
context.VerifyCaptcha(ctx, tplSignIn, form)
|
|
|
|
if ctx.Written() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-14 17:09:32 +00:00
|
|
|
u, source, err := auth_service.UserSignIn(ctx, form.UserName, form.Password)
|
2022-01-02 13:12:35 +00:00
|
|
|
if err != nil {
|
2023-07-03 22:39:38 +00:00
|
|
|
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
|
|
|
|
log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
|
|
|
|
} else if user_model.IsErrEmailAlreadyUsed(err) {
|
|
|
|
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignIn, &form)
|
|
|
|
log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
|
|
|
|
} else if user_model.IsErrUserProhibitLogin(err) {
|
|
|
|
log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
|
|
|
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
|
|
|
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
|
|
|
} else if user_model.IsErrUserInactive(err) {
|
|
|
|
if setting.Service.RegisterEmailConfirm {
|
|
|
|
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
|
|
|
ctx.HTML(http.StatusOK, TplActivate)
|
|
|
|
} else {
|
|
|
|
log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
|
|
|
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
|
|
|
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ctx.ServerError("UserSignIn", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now handle 2FA:
|
|
|
|
|
|
|
|
// First of all if the source can skip local two fa we're done
|
|
|
|
if skipper, ok := source.Cfg.(auth_service.LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
|
|
|
|
handleSignIn(ctx, u, form.Remember)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this user is enrolled in 2FA TOTP, we can't sign the user in just yet.
|
|
|
|
// Instead, redirect them to the 2FA authentication page.
|
2023-09-15 06:13:19 +00:00
|
|
|
hasTOTPtwofa, err := auth.HasTwoFactorByUID(ctx, u.ID)
|
2022-01-02 13:12:35 +00:00
|
|
|
if err != nil {
|
|
|
|
ctx.ServerError("UserSignIn", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-14 15:03:31 +00:00
|
|
|
// Check if the user has webauthn registration
|
2023-09-16 14:39:12 +00:00
|
|
|
hasWebAuthnTwofa, err := auth.HasWebAuthnRegistrationsByUID(ctx, u.ID)
|
2022-01-02 13:12:35 +00:00
|
|
|
if err != nil {
|
|
|
|
ctx.ServerError("UserSignIn", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-14 15:03:31 +00:00
|
|
|
if !hasTOTPtwofa && !hasWebAuthnTwofa {
|
2022-01-02 13:12:35 +00:00
|
|
|
// No two factor auth configured we can sign in the user
|
|
|
|
handleSignIn(ctx, u, form.Remember)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-07-04 18:36:08 +00:00
|
|
|
updates := map[string]any{
|
2022-11-10 11:43:06 +00:00
|
|
|
// User will need to use 2FA TOTP or WebAuthn, save data
|
|
|
|
"twofaUid": u.ID,
|
|
|
|
"twofaRemember": form.Remember,
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
if hasTOTPtwofa {
|
2022-06-27 02:20:58 +00:00
|
|
|
// User will need to use WebAuthn, save data
|
2022-11-10 11:43:06 +00:00
|
|
|
updates["totpEnrolled"] = u.ID
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
2022-11-10 11:43:06 +00:00
|
|
|
if err := updateSession(ctx, nil, updates); err != nil {
|
|
|
|
ctx.ServerError("UserSignIn: Unable to update session", err)
|
2022-01-02 13:12:35 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-27 02:20:58 +00:00
|
|
|
// If we have WebAuthn redirect there first
|
2022-01-14 15:03:31 +00:00
|
|
|
if hasWebAuthnTwofa {
|
|
|
|
ctx.Redirect(setting.AppSubURL + "/user/webauthn")
|
2022-01-02 13:12:35 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fallback to 2FA
|
|
|
|
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
|
|
|
|
}
|
|
|
|
|
|
|
|
// This handles the final part of the sign-in process of the user.
|
|
|
|
func handleSignIn(ctx *context.Context, u *user_model.User, remember bool) {
|
|
|
|
redirect := handleSignInFull(ctx, u, remember, true)
|
|
|
|
if ctx.Written() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.Redirect(redirect)
|
|
|
|
}
|
|
|
|
|
2022-01-20 17:46:10 +00:00
|
|
|
func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRedirect bool) string {
|
2022-01-02 13:12:35 +00:00
|
|
|
if remember {
|
2023-11-22 16:26:21 +00:00
|
|
|
if err := ctx.SetLTACookie(u); err != nil {
|
|
|
|
ctx.ServerError("GenerateAuthToken", err)
|
2023-10-14 00:56:41 +00:00
|
|
|
return setting.AppSubURL + "/"
|
|
|
|
}
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
|
2022-11-10 11:43:06 +00:00
|
|
|
if err := updateSession(ctx, []string{
|
|
|
|
// Delete the openid, 2fa and linkaccount data
|
|
|
|
"openid_verified_uri",
|
|
|
|
"openid_signin_remember",
|
|
|
|
"openid_determined_email",
|
|
|
|
"openid_determined_username",
|
|
|
|
"twofaUid",
|
|
|
|
"twofaRemember",
|
|
|
|
"linkAccount",
|
2023-07-04 18:36:08 +00:00
|
|
|
}, map[string]any{
|
2023-11-22 16:26:21 +00:00
|
|
|
"uid": u.ID,
|
2022-11-10 11:43:06 +00:00
|
|
|
}); err != nil {
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.ServerError("RegenerateSession", err)
|
|
|
|
return setting.AppSubURL + "/"
|
|
|
|
}
|
|
|
|
|
|
|
|
// Language setting of the user overwrites the one previously set
|
|
|
|
// If the user does not have a locale set, we save the current one.
|
2024-02-04 13:29:09 +00:00
|
|
|
if u.Language == "" {
|
|
|
|
opts := &user_service.UpdateOptions{
|
|
|
|
Language: optional.Some(ctx.Locale.Language()),
|
|
|
|
}
|
|
|
|
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
|
|
|
ctx.ServerError("UpdateUser Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, ctx.Locale.Language()))
|
2022-01-02 13:12:35 +00:00
|
|
|
return setting.AppSubURL + "/"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
middleware.SetLocaleCookie(ctx.Resp, u.Language, 0)
|
|
|
|
|
|
|
|
if ctx.Locale.Language() != u.Language {
|
|
|
|
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
|
|
|
|
}
|
|
|
|
|
2022-04-08 05:21:05 +00:00
|
|
|
// Clear whatever CSRF cookie has right now, force to generate a new one
|
2023-04-13 19:45:33 +00:00
|
|
|
ctx.Csrf.DeleteCookie(ctx)
|
2022-01-02 13:12:35 +00:00
|
|
|
|
|
|
|
// Register last login
|
2024-02-04 13:29:09 +00:00
|
|
|
if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
|
|
|
|
ctx.ServerError("UpdateUser", err)
|
2022-01-02 13:12:35 +00:00
|
|
|
return setting.AppSubURL + "/"
|
|
|
|
}
|
|
|
|
|
2024-04-15 13:03:08 +00:00
|
|
|
redirectTo := ctx.GetSiteCookie("redirect_to")
|
|
|
|
if redirectTo != "" {
|
2022-01-02 13:12:35 +00:00
|
|
|
middleware.DeleteRedirectToCookie(ctx.Resp)
|
|
|
|
}
|
|
|
|
if obeyRedirect {
|
2024-04-15 13:03:08 +00:00
|
|
|
return ctx.RedirectToFirst(redirectTo)
|
|
|
|
}
|
|
|
|
if !httplib.IsRiskyRedirectURL(redirectTo) {
|
|
|
|
return redirectTo
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
return setting.AppSubURL + "/"
|
|
|
|
}
|
|
|
|
|
2024-01-04 00:48:20 +00:00
|
|
|
func getUserName(gothUser *goth.User) (string, error) {
|
2022-01-02 13:12:35 +00:00
|
|
|
switch setting.OAuth2Client.Username {
|
|
|
|
case setting.OAuth2UsernameEmail:
|
2024-01-04 00:48:20 +00:00
|
|
|
return user_model.NormalizeUserName(strings.Split(gothUser.Email, "@")[0])
|
2022-01-02 13:12:35 +00:00
|
|
|
case setting.OAuth2UsernameNickname:
|
2024-01-04 00:48:20 +00:00
|
|
|
return user_model.NormalizeUserName(gothUser.NickName)
|
2022-01-02 13:12:35 +00:00
|
|
|
default: // OAuth2UsernameUserid
|
2024-01-04 00:48:20 +00:00
|
|
|
return gothUser.UserID, nil
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// HandleSignOut resets the session and sets the cookies
|
|
|
|
func HandleSignOut(ctx *context.Context) {
|
|
|
|
_ = ctx.Session.Flush()
|
|
|
|
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
|
2023-04-13 19:45:33 +00:00
|
|
|
ctx.DeleteSiteCookie(setting.CookieRememberName)
|
|
|
|
ctx.Csrf.DeleteCookie(ctx)
|
2022-01-02 13:12:35 +00:00
|
|
|
middleware.DeleteRedirectToCookie(ctx.Resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SignOut sign out from login status
|
|
|
|
func SignOut(ctx *context.Context) {
|
2022-03-22 07:03:22 +00:00
|
|
|
if ctx.Doer != nil {
|
2024-08-12 13:31:43 +00:00
|
|
|
eventsource.GetManager().SendMessage(ctx.Doer.ID, &eventsource.Event{
|
2022-01-02 13:12:35 +00:00
|
|
|
Name: "logout",
|
|
|
|
Data: ctx.Session.ID(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
HandleSignOut(ctx)
|
2023-06-26 19:36:10 +00:00
|
|
|
ctx.JSONRedirect(setting.AppSubURL + "/")
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SignUp render the register page
|
|
|
|
func SignUp(ctx *context.Context) {
|
|
|
|
ctx.Data["Title"] = ctx.Tr("sign_up")
|
|
|
|
|
|
|
|
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
|
|
|
|
|
2024-03-02 15:42:31 +00:00
|
|
|
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
2023-09-13 05:14:21 +00:00
|
|
|
if err != nil {
|
|
|
|
ctx.ServerError("UserSignUp", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.Data["OAuth2Providers"] = oauth2Providers
|
2022-11-22 21:13:18 +00:00
|
|
|
context.SetCaptchaData(ctx)
|
2023-09-13 05:14:21 +00:00
|
|
|
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.Data["PageIsSignUp"] = true
|
|
|
|
|
2022-01-20 17:46:10 +00:00
|
|
|
// Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration
|
|
|
|
|
2023-08-31 16:26:13 +00:00
|
|
|
redirectTo := ctx.FormString("redirect_to")
|
|
|
|
if len(redirectTo) > 0 {
|
|
|
|
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
|
|
|
|
}
|
|
|
|
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.HTML(http.StatusOK, tplSignUp)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SignUpPost response for sign up information submission
|
|
|
|
func SignUpPost(ctx *context.Context) {
|
|
|
|
form := web.GetForm(ctx).(*forms.RegisterForm)
|
|
|
|
ctx.Data["Title"] = ctx.Tr("sign_up")
|
|
|
|
|
|
|
|
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
|
|
|
|
|
2024-03-02 15:42:31 +00:00
|
|
|
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
2023-09-13 05:14:21 +00:00
|
|
|
if err != nil {
|
|
|
|
ctx.ServerError("UserSignUp", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.Data["OAuth2Providers"] = oauth2Providers
|
2022-11-22 21:13:18 +00:00
|
|
|
context.SetCaptchaData(ctx)
|
2023-09-13 05:14:21 +00:00
|
|
|
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.Data["PageIsSignUp"] = true
|
|
|
|
|
2022-01-20 17:46:10 +00:00
|
|
|
// Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
|
2022-01-02 13:12:35 +00:00
|
|
|
if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration {
|
|
|
|
ctx.Error(http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if ctx.HasError() {
|
|
|
|
ctx.HTML(http.StatusOK, tplSignUp)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-22 21:13:18 +00:00
|
|
|
context.VerifyCaptcha(ctx, tplSignUp, form)
|
|
|
|
if ctx.Written() {
|
|
|
|
return
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if !form.IsEmailDomainAllowed() {
|
|
|
|
ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if form.Password != form.Retype {
|
|
|
|
ctx.Data["Err_Password"] = true
|
|
|
|
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplSignUp, &form)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(form.Password) < setting.MinPasswordLength {
|
|
|
|
ctx.Data["Err_Password"] = true
|
|
|
|
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !password.IsComplexEnough(form.Password) {
|
|
|
|
ctx.Data["Err_Password"] = true
|
2023-05-08 09:36:54 +00:00
|
|
|
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
|
2022-01-02 13:12:35 +00:00
|
|
|
return
|
|
|
|
}
|
2024-02-04 13:29:09 +00:00
|
|
|
if err := password.IsPwned(ctx, form.Password); err != nil {
|
2022-01-02 13:12:35 +00:00
|
|
|
errMsg := ctx.Tr("auth.password_pwned")
|
2024-02-04 13:29:09 +00:00
|
|
|
if password.IsErrIsPwnedRequest(err) {
|
2022-01-02 13:12:35 +00:00
|
|
|
log.Error(err.Error())
|
|
|
|
errMsg = ctx.Tr("auth.password_pwned_err")
|
|
|
|
}
|
|
|
|
ctx.Data["Err_Password"] = true
|
|
|
|
ctx.RenderWithErr(errMsg, tplSignUp, &form)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
u := &user_model.User{
|
2022-04-29 19:38:11 +00:00
|
|
|
Name: form.UserName,
|
|
|
|
Email: form.Email,
|
|
|
|
Passwd: form.Password,
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
|
2022-04-29 19:38:11 +00:00
|
|
|
if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil, false) {
|
2022-01-02 13:12:35 +00:00
|
|
|
// error already handled
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.Flash.Success(ctx.Tr("auth.sign_up_successful"))
|
|
|
|
handleSignIn(ctx, u, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
// createAndHandleCreatedUser calls createUserInContext and
|
|
|
|
// then handleUserCreated.
|
2023-07-04 18:36:08 +00:00
|
|
|
func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) bool {
|
2022-04-29 19:38:11 +00:00
|
|
|
if !createUserInContext(ctx, tpl, form, u, overwrites, gothUser, allowLink) {
|
2022-01-02 13:12:35 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
return handleUserCreated(ctx, u, gothUser)
|
|
|
|
}
|
|
|
|
|
|
|
|
// createUserInContext creates a user and handles errors within a given context.
|
|
|
|
// Optionally a template can be specified.
|
2023-07-04 18:36:08 +00:00
|
|
|
func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) {
|
2023-09-14 17:09:32 +00:00
|
|
|
if err := user_model.CreateUser(ctx, u, overwrites); err != nil {
|
2022-01-02 13:12:35 +00:00
|
|
|
if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
|
|
|
|
if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
|
|
|
|
var user *user_model.User
|
|
|
|
user = &user_model.User{Name: u.Name}
|
2023-09-14 17:09:32 +00:00
|
|
|
hasUser, err := user_model.GetUser(ctx, user)
|
2022-01-02 13:12:35 +00:00
|
|
|
if !hasUser || err != nil {
|
|
|
|
user = &user_model.User{Email: u.Email}
|
2023-09-14 17:09:32 +00:00
|
|
|
hasUser, err = user_model.GetUser(ctx, user)
|
2022-01-02 13:12:35 +00:00
|
|
|
if !hasUser || err != nil {
|
|
|
|
ctx.ServerError("UserLinkAccount", err)
|
2023-07-07 05:31:56 +00:00
|
|
|
return false
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: probably we should respect 'remember' user's choice...
|
|
|
|
linkAccount(ctx, user, *gothUser, true)
|
2023-07-07 05:31:56 +00:00
|
|
|
return false // user is already created here, all redirects are handled
|
2022-01-02 13:12:35 +00:00
|
|
|
} else if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingLogin {
|
|
|
|
showLinkingLogin(ctx, *gothUser)
|
2023-07-07 05:31:56 +00:00
|
|
|
return false // user will be created only after linking login
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// handle error without template
|
|
|
|
if len(tpl) == 0 {
|
|
|
|
ctx.ServerError("CreateUser", err)
|
2023-07-07 05:31:56 +00:00
|
|
|
return false
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// handle error with template
|
|
|
|
switch {
|
|
|
|
case user_model.IsErrUserAlreadyExist(err):
|
|
|
|
ctx.Data["Err_UserName"] = true
|
|
|
|
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, form)
|
|
|
|
case user_model.IsErrEmailAlreadyUsed(err):
|
|
|
|
ctx.Data["Err_Email"] = true
|
|
|
|
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
|
2022-03-14 17:39:54 +00:00
|
|
|
case user_model.IsErrEmailCharIsNotSupported(err):
|
|
|
|
ctx.Data["Err_Email"] = true
|
|
|
|
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
|
2022-01-02 13:12:35 +00:00
|
|
|
case user_model.IsErrEmailInvalid(err):
|
|
|
|
ctx.Data["Err_Email"] = true
|
|
|
|
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
|
|
|
|
case db.IsErrNameReserved(err):
|
|
|
|
ctx.Data["Err_UserName"] = true
|
|
|
|
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
|
|
|
|
case db.IsErrNamePatternNotAllowed(err):
|
|
|
|
ctx.Data["Err_UserName"] = true
|
|
|
|
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
|
|
|
|
case db.IsErrNameCharsNotAllowed(err):
|
|
|
|
ctx.Data["Err_UserName"] = true
|
|
|
|
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tpl, form)
|
|
|
|
default:
|
|
|
|
ctx.ServerError("CreateUser", err)
|
|
|
|
}
|
2023-07-07 05:31:56 +00:00
|
|
|
return false
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
log.Trace("Account created: %s", u.Name)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleUserCreated does additional steps after a new user is created.
|
|
|
|
// It auto-sets admin for the only user, updates the optional external user and
|
|
|
|
// sends a confirmation email if required.
|
|
|
|
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
|
|
|
|
// Auto-set admin for the only user.
|
2023-09-14 17:09:32 +00:00
|
|
|
if user_model.CountUsers(ctx, nil) == 1 {
|
2024-02-04 13:29:09 +00:00
|
|
|
opts := &user_service.UpdateOptions{
|
|
|
|
IsActive: optional.Some(true),
|
|
|
|
IsAdmin: optional.Some(true),
|
|
|
|
SetLastLogin: true,
|
|
|
|
}
|
|
|
|
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.ServerError("UpdateUser", err)
|
2023-07-07 05:31:56 +00:00
|
|
|
return false
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-07 07:11:29 +00:00
|
|
|
notify_service.NewUserSignUp(ctx, u)
|
2022-01-02 13:12:35 +00:00
|
|
|
// update external user information
|
|
|
|
if gothUser != nil {
|
allow synchronizing user status from OAuth2 login providers (#31572)
This leverages the existing `sync_external_users` cron job to
synchronize the `IsActive` flag on users who use an OAuth2 provider set
to synchronize. This synchronization is done by checking for expired
access tokens, and using the stored refresh token to request a new
access token. If the response back from the OAuth2 provider is the
`invalid_grant` error code, the user is marked as inactive. However, the
user is able to reactivate their account by logging in the web browser
through their OAuth2 flow.
Also changed to support this is that a linked `ExternalLoginUser` is
always created upon a login or signup via OAuth2.
Ideally, we would also refresh permissions from the configured OAuth
provider (e.g., admin, restricted and group mappings) to match the
implementation of LDAP. However, the OAuth library used for this `goth`,
doesn't seem to support issuing a session via refresh tokens. The
interface provides a [`RefreshToken`
method](https://github.com/markbates/goth/blob/master/provider.go#L20),
but the returned `oauth.Token` doesn't implement the `goth.Session` we
would need to call `FetchUser`. Due to specific implementations, we
would need to build a compatibility function for every provider, since
they cast to concrete types (e.g.
[Azure](https://github.com/markbates/goth/blob/master/providers/azureadv2/azureadv2.go#L132))
---------
Co-authored-by: Kyle D <kdumontnu@gmail.com>
(cherry picked from commit 416c36f3034e228a27258b5a8a15eec4e5e426ba)
Conflicts:
- tests/integration/auth_ldap_test.go
Trivial conflict resolved by manually applying the change.
- routers/web/auth/oauth.go
Technically not a conflict, but the original PR removed the
modules/util import, which in our version, is still in use. Added it
back.
2024-07-16 18:33:16 +00:00
|
|
|
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, *gothUser); err != nil {
|
|
|
|
log.Error("EnsureLinkExternalToUser failed: %v", err)
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send confirmation email
|
|
|
|
if !u.IsActive && u.ID > 1 {
|
2022-03-18 09:57:07 +00:00
|
|
|
if setting.Service.RegisterManualConfirm {
|
|
|
|
ctx.Data["ManualActivationOnly"] = true
|
|
|
|
ctx.HTML(http.StatusOK, TplActivate)
|
2023-07-07 05:31:56 +00:00
|
|
|
return false
|
2022-03-18 09:57:07 +00:00
|
|
|
}
|
|
|
|
|
2022-01-02 13:12:35 +00:00
|
|
|
mailer.SendActivateAccountMail(ctx.Locale, u)
|
|
|
|
|
|
|
|
ctx.Data["IsSendRegisterMail"] = true
|
|
|
|
ctx.Data["Email"] = u.Email
|
2022-06-26 14:19:22 +00:00
|
|
|
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.HTML(http.StatusOK, TplActivate)
|
|
|
|
|
2023-12-19 09:29:05 +00:00
|
|
|
if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
|
|
|
|
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
2023-07-07 05:31:56 +00:00
|
|
|
return false
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Activate render activate user page
|
|
|
|
func Activate(ctx *context.Context) {
|
|
|
|
code := ctx.FormString("code")
|
|
|
|
|
|
|
|
if len(code) == 0 {
|
|
|
|
ctx.Data["IsActivatePage"] = true
|
2022-03-22 07:03:22 +00:00
|
|
|
if ctx.Doer == nil || ctx.Doer.IsActive {
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.NotFound("invalid user", nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// Resend confirmation email.
|
|
|
|
if setting.Service.RegisterEmailConfirm {
|
[GITEA] Allow changing the email address before activation
During registration, one may be required to give their email address, to
be verified and activated later. However, if one makes a mistake, a
typo, they may end up with an account that cannot be activated due to
having a wrong email address.
They can still log in, but not change the email address, thus, no way to
activate it without help from an administrator.
To remedy this issue, lets allow changing the email address for logged
in, but not activated users.
This fixes gitea#17785.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit aaaece28e4c6a8980cef932e224e84933d7c9262)
(cherry picked from commit 639dafabec0a5c1f943b44ca02f72c5ba2fc5e10)
(cherry picked from commit d699c12cebea7dbbda950ae257a46d53c39f22ea)
[GITEA] Allow changing the email address before activation (squash) cache is always active
This needs to be revisited because the MailResendLimit is not enforced
and turns out to not be tested.
See e7cb8da2a8 * Always enable caches (#28527)
(cherry picked from commit 43ded8ee30ab5c7a40a456600cdaa8a0fbdccec2)
Rate limit pre-activation email change separately
Changing the email address before any email address is activated should
be subject to a different rate limit than the normal activation email
resending. If there's only one rate limit for both, then if a newly
signed up quickly discovers they gave a wrong email address, they'd have
to wait three minutes to change it.
With the two separate limits, they don't - but they'll have to wait
three minutes before they can change the email address again.
The downside of this setup is that a malicious actor can alternate
between resending and changing the email address (to something like
`user+$idx@domain`, delivered to the same inbox) to effectively halving
the rate limit. I do not think there's a better solution, and this feels
like such a small attack surface that I'd deem it acceptable.
The way the code works after this change is that `ActivatePost` will now
check the `MailChangeLimit_user` key rather than `MailResendLimit_user`,
and if we're within the limit, it will set `MailChangedJustNow_user`. The
`Activate` method - which sends the activation email, whether it is a
normal resend, or one following an email change - will check
`MailChangedJustNow_user`, and if it is set, it will check the rate
limit against `MailChangedLimit_user`, otherwise against
`MailResendLimit_user`, and then will delete the
`MailChangedJustNow_user` key from the cache.
Fixes #2040.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit e35d2af2e56f4ecb3a4f6d1109d02c8aa1a6d182)
(cherry picked from commit 03989418a70d3445e0edada7fbe5a4151d7836b1)
(cherry picked from commit f50e0dfe5e90d6a31c5b59e687580e8b2725c22b)
(cherry picked from commit cad9184a3653e6c80de2e006a0d699b816980987)
(cherry picked from commit e2da5d7fe13a685606913a131687a94f9f5fcfeb)
(cherry picked from commit 3a80534d4db523efe56b368489f81dc1cb2c99f7)
2023-12-07 11:57:16 +00:00
|
|
|
var cacheKey string
|
|
|
|
if ctx.Cache.IsExist("MailChangedJustNow_" + ctx.Doer.LowerName) {
|
|
|
|
cacheKey = "MailChangedLimit_"
|
|
|
|
if err := ctx.Cache.Delete("MailChangedJustNow_" + ctx.Doer.LowerName); err != nil {
|
|
|
|
log.Error("Delete cache(MailChangedJustNow) fail: %v", err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
cacheKey = "MailResendLimit_"
|
|
|
|
}
|
|
|
|
if ctx.Cache.IsExist(cacheKey + ctx.Doer.LowerName) {
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.Data["ResendLimited"] = true
|
|
|
|
} else {
|
2022-06-26 14:19:22 +00:00
|
|
|
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
|
2022-03-22 07:03:22 +00:00
|
|
|
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
|
2022-01-02 13:12:35 +00:00
|
|
|
|
[GITEA] Allow changing the email address before activation
During registration, one may be required to give their email address, to
be verified and activated later. However, if one makes a mistake, a
typo, they may end up with an account that cannot be activated due to
having a wrong email address.
They can still log in, but not change the email address, thus, no way to
activate it without help from an administrator.
To remedy this issue, lets allow changing the email address for logged
in, but not activated users.
This fixes gitea#17785.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit aaaece28e4c6a8980cef932e224e84933d7c9262)
(cherry picked from commit 639dafabec0a5c1f943b44ca02f72c5ba2fc5e10)
(cherry picked from commit d699c12cebea7dbbda950ae257a46d53c39f22ea)
[GITEA] Allow changing the email address before activation (squash) cache is always active
This needs to be revisited because the MailResendLimit is not enforced
and turns out to not be tested.
See e7cb8da2a8 * Always enable caches (#28527)
(cherry picked from commit 43ded8ee30ab5c7a40a456600cdaa8a0fbdccec2)
Rate limit pre-activation email change separately
Changing the email address before any email address is activated should
be subject to a different rate limit than the normal activation email
resending. If there's only one rate limit for both, then if a newly
signed up quickly discovers they gave a wrong email address, they'd have
to wait three minutes to change it.
With the two separate limits, they don't - but they'll have to wait
three minutes before they can change the email address again.
The downside of this setup is that a malicious actor can alternate
between resending and changing the email address (to something like
`user+$idx@domain`, delivered to the same inbox) to effectively halving
the rate limit. I do not think there's a better solution, and this feels
like such a small attack surface that I'd deem it acceptable.
The way the code works after this change is that `ActivatePost` will now
check the `MailChangeLimit_user` key rather than `MailResendLimit_user`,
and if we're within the limit, it will set `MailChangedJustNow_user`. The
`Activate` method - which sends the activation email, whether it is a
normal resend, or one following an email change - will check
`MailChangedJustNow_user`, and if it is set, it will check the rate
limit against `MailChangedLimit_user`, otherwise against
`MailResendLimit_user`, and then will delete the
`MailChangedJustNow_user` key from the cache.
Fixes #2040.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit e35d2af2e56f4ecb3a4f6d1109d02c8aa1a6d182)
(cherry picked from commit 03989418a70d3445e0edada7fbe5a4151d7836b1)
(cherry picked from commit f50e0dfe5e90d6a31c5b59e687580e8b2725c22b)
(cherry picked from commit cad9184a3653e6c80de2e006a0d699b816980987)
(cherry picked from commit e2da5d7fe13a685606913a131687a94f9f5fcfeb)
(cherry picked from commit 3a80534d4db523efe56b368489f81dc1cb2c99f7)
2023-12-07 11:57:16 +00:00
|
|
|
if err := ctx.Cache.Put(cacheKey+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
2023-12-19 09:29:05 +00:00
|
|
|
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ctx.Data["ServiceNotEnabled"] = true
|
|
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, TplActivate)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-14 17:09:32 +00:00
|
|
|
user := user_model.VerifyUserActiveCode(ctx, code)
|
2022-01-02 13:12:35 +00:00
|
|
|
// if code is wrong
|
|
|
|
if user == nil {
|
2023-01-28 07:59:46 +00:00
|
|
|
ctx.Data["IsCodeInvalid"] = true
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.HTML(http.StatusOK, TplActivate)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// if account is local account, verify password
|
|
|
|
if user.LoginSource == 0 {
|
|
|
|
ctx.Data["Code"] = code
|
|
|
|
ctx.Data["NeedsPassword"] = true
|
|
|
|
ctx.HTML(http.StatusOK, TplActivate)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
handleAccountActivation(ctx, user)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ActivatePost handles account activation with password check
|
|
|
|
func ActivatePost(ctx *context.Context) {
|
|
|
|
code := ctx.FormString("code")
|
|
|
|
if len(code) == 0 {
|
[GITEA] Allow changing the email address before activation
During registration, one may be required to give their email address, to
be verified and activated later. However, if one makes a mistake, a
typo, they may end up with an account that cannot be activated due to
having a wrong email address.
They can still log in, but not change the email address, thus, no way to
activate it without help from an administrator.
To remedy this issue, lets allow changing the email address for logged
in, but not activated users.
This fixes gitea#17785.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit aaaece28e4c6a8980cef932e224e84933d7c9262)
(cherry picked from commit 639dafabec0a5c1f943b44ca02f72c5ba2fc5e10)
(cherry picked from commit d699c12cebea7dbbda950ae257a46d53c39f22ea)
[GITEA] Allow changing the email address before activation (squash) cache is always active
This needs to be revisited because the MailResendLimit is not enforced
and turns out to not be tested.
See e7cb8da2a8 * Always enable caches (#28527)
(cherry picked from commit 43ded8ee30ab5c7a40a456600cdaa8a0fbdccec2)
Rate limit pre-activation email change separately
Changing the email address before any email address is activated should
be subject to a different rate limit than the normal activation email
resending. If there's only one rate limit for both, then if a newly
signed up quickly discovers they gave a wrong email address, they'd have
to wait three minutes to change it.
With the two separate limits, they don't - but they'll have to wait
three minutes before they can change the email address again.
The downside of this setup is that a malicious actor can alternate
between resending and changing the email address (to something like
`user+$idx@domain`, delivered to the same inbox) to effectively halving
the rate limit. I do not think there's a better solution, and this feels
like such a small attack surface that I'd deem it acceptable.
The way the code works after this change is that `ActivatePost` will now
check the `MailChangeLimit_user` key rather than `MailResendLimit_user`,
and if we're within the limit, it will set `MailChangedJustNow_user`. The
`Activate` method - which sends the activation email, whether it is a
normal resend, or one following an email change - will check
`MailChangedJustNow_user`, and if it is set, it will check the rate
limit against `MailChangedLimit_user`, otherwise against
`MailResendLimit_user`, and then will delete the
`MailChangedJustNow_user` key from the cache.
Fixes #2040.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit e35d2af2e56f4ecb3a4f6d1109d02c8aa1a6d182)
(cherry picked from commit 03989418a70d3445e0edada7fbe5a4151d7836b1)
(cherry picked from commit f50e0dfe5e90d6a31c5b59e687580e8b2725c22b)
(cherry picked from commit cad9184a3653e6c80de2e006a0d699b816980987)
(cherry picked from commit e2da5d7fe13a685606913a131687a94f9f5fcfeb)
(cherry picked from commit 3a80534d4db523efe56b368489f81dc1cb2c99f7)
2023-12-07 11:57:16 +00:00
|
|
|
email := ctx.FormString("email")
|
|
|
|
if len(email) > 0 {
|
|
|
|
ctx.Data["IsActivatePage"] = true
|
|
|
|
if ctx.Doer == nil || ctx.Doer.IsActive {
|
|
|
|
ctx.NotFound("invalid user", nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// Change the primary email
|
|
|
|
if setting.Service.RegisterEmailConfirm {
|
|
|
|
if ctx.Cache.IsExist("MailChangeLimit_" + ctx.Doer.LowerName) {
|
|
|
|
ctx.Data["ResendLimited"] = true
|
|
|
|
} else {
|
|
|
|
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
|
2024-02-05 15:49:19 +00:00
|
|
|
err := user_service.ReplaceInactivePrimaryEmail(ctx, ctx.Doer.Email, &user_model.EmailAddress{
|
[GITEA] Allow changing the email address before activation
During registration, one may be required to give their email address, to
be verified and activated later. However, if one makes a mistake, a
typo, they may end up with an account that cannot be activated due to
having a wrong email address.
They can still log in, but not change the email address, thus, no way to
activate it without help from an administrator.
To remedy this issue, lets allow changing the email address for logged
in, but not activated users.
This fixes gitea#17785.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit aaaece28e4c6a8980cef932e224e84933d7c9262)
(cherry picked from commit 639dafabec0a5c1f943b44ca02f72c5ba2fc5e10)
(cherry picked from commit d699c12cebea7dbbda950ae257a46d53c39f22ea)
[GITEA] Allow changing the email address before activation (squash) cache is always active
This needs to be revisited because the MailResendLimit is not enforced
and turns out to not be tested.
See e7cb8da2a8 * Always enable caches (#28527)
(cherry picked from commit 43ded8ee30ab5c7a40a456600cdaa8a0fbdccec2)
Rate limit pre-activation email change separately
Changing the email address before any email address is activated should
be subject to a different rate limit than the normal activation email
resending. If there's only one rate limit for both, then if a newly
signed up quickly discovers they gave a wrong email address, they'd have
to wait three minutes to change it.
With the two separate limits, they don't - but they'll have to wait
three minutes before they can change the email address again.
The downside of this setup is that a malicious actor can alternate
between resending and changing the email address (to something like
`user+$idx@domain`, delivered to the same inbox) to effectively halving
the rate limit. I do not think there's a better solution, and this feels
like such a small attack surface that I'd deem it acceptable.
The way the code works after this change is that `ActivatePost` will now
check the `MailChangeLimit_user` key rather than `MailResendLimit_user`,
and if we're within the limit, it will set `MailChangedJustNow_user`. The
`Activate` method - which sends the activation email, whether it is a
normal resend, or one following an email change - will check
`MailChangedJustNow_user`, and if it is set, it will check the rate
limit against `MailChangedLimit_user`, otherwise against
`MailResendLimit_user`, and then will delete the
`MailChangedJustNow_user` key from the cache.
Fixes #2040.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit e35d2af2e56f4ecb3a4f6d1109d02c8aa1a6d182)
(cherry picked from commit 03989418a70d3445e0edada7fbe5a4151d7836b1)
(cherry picked from commit f50e0dfe5e90d6a31c5b59e687580e8b2725c22b)
(cherry picked from commit cad9184a3653e6c80de2e006a0d699b816980987)
(cherry picked from commit e2da5d7fe13a685606913a131687a94f9f5fcfeb)
(cherry picked from commit 3a80534d4db523efe56b368489f81dc1cb2c99f7)
2023-12-07 11:57:16 +00:00
|
|
|
UID: ctx.Doer.ID,
|
|
|
|
Email: email,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
ctx.Data["IsActivatePage"] = false
|
|
|
|
log.Error("Couldn't replace inactive primary email of user %d: %v", ctx.Doer.ID, err)
|
|
|
|
ctx.RenderWithErr(ctx.Tr("auth.change_unconfirmed_email_error", err), TplActivate, nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err := ctx.Cache.Put("MailChangeLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
|
|
|
log.Error("Set cache(MailChangeLimit) fail: %v", err)
|
|
|
|
}
|
|
|
|
if err := ctx.Cache.Put("MailChangedJustNow_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
|
|
|
log.Error("Set cache(MailChangedJustNow) fail: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Confirmation mail will be re-sent after the redirect to `/user/activate` below.
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ctx.Data["ServiceNotEnabled"] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.Redirect(setting.AppSubURL + "/user/activate")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-14 17:09:32 +00:00
|
|
|
user := user_model.VerifyUserActiveCode(ctx, code)
|
2022-01-02 13:12:35 +00:00
|
|
|
// if code is wrong
|
|
|
|
if user == nil {
|
2023-01-28 07:59:46 +00:00
|
|
|
ctx.Data["IsCodeInvalid"] = true
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.HTML(http.StatusOK, TplActivate)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// if account is local account, verify password
|
|
|
|
if user.LoginSource == 0 {
|
|
|
|
password := ctx.FormString("password")
|
|
|
|
if len(password) == 0 {
|
|
|
|
ctx.Data["Code"] = code
|
|
|
|
ctx.Data["NeedsPassword"] = true
|
|
|
|
ctx.HTML(http.StatusOK, TplActivate)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !user.ValidatePassword(password) {
|
2023-01-28 07:59:46 +00:00
|
|
|
ctx.Data["IsPasswordInvalid"] = true
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.HTML(http.StatusOK, TplActivate)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleAccountActivation(ctx, user)
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleAccountActivation(ctx *context.Context, user *user_model.User) {
|
|
|
|
user.IsActive = true
|
|
|
|
var err error
|
|
|
|
if user.Rands, err = user_model.GetUserSalt(); err != nil {
|
|
|
|
ctx.ServerError("UpdateUser", err)
|
|
|
|
return
|
|
|
|
}
|
2022-03-22 15:22:54 +00:00
|
|
|
if err := user_model.UpdateUserCols(ctx, user, "is_active", "rands"); err != nil {
|
2022-01-02 13:12:35 +00:00
|
|
|
if user_model.IsErrUserNotExist(err) {
|
|
|
|
ctx.NotFound("UpdateUserCols", err)
|
|
|
|
} else {
|
|
|
|
ctx.ServerError("UpdateUser", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-14 17:09:32 +00:00
|
|
|
if err := user_model.ActivateUserEmail(ctx, user.ID, user.Email, true); err != nil {
|
2022-01-02 13:12:35 +00:00
|
|
|
log.Error("Unable to activate email for user: %-v with email: %s: %v", user, user.Email, err)
|
|
|
|
ctx.ServerError("ActivateUserEmail", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Trace("User activated: %s", user.Name)
|
|
|
|
|
2023-07-04 18:36:08 +00:00
|
|
|
if err := updateSession(ctx, nil, map[string]any{
|
2023-11-22 16:26:21 +00:00
|
|
|
"uid": user.ID,
|
2022-11-10 11:43:06 +00:00
|
|
|
}); err != nil {
|
2022-01-02 13:12:35 +00:00
|
|
|
log.Error("Unable to regenerate session for user: %-v with email: %s: %v", user, user.Email, err)
|
|
|
|
ctx.ServerError("ActivateUserEmail", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := resetLocale(ctx, user); err != nil {
|
|
|
|
ctx.ServerError("resetLocale", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-04 13:29:09 +00:00
|
|
|
if err := user_service.UpdateUser(ctx, user, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
|
|
|
|
ctx.ServerError("UpdateUser", err)
|
2022-11-09 16:42:06 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.Flash.Success(ctx.Tr("auth.account_activated"))
|
2023-08-31 16:26:13 +00:00
|
|
|
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
|
|
|
|
middleware.DeleteRedirectToCookie(ctx.Resp)
|
|
|
|
ctx.RedirectToFirst(redirectTo)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.Redirect(setting.AppSubURL + "/")
|
|
|
|
}
|
|
|
|
|
|
|
|
// ActivateEmail render the activate email page
|
|
|
|
func ActivateEmail(ctx *context.Context) {
|
|
|
|
code := ctx.FormString("code")
|
|
|
|
emailStr := ctx.FormString("email")
|
|
|
|
|
|
|
|
// Verify code.
|
2023-09-14 17:09:32 +00:00
|
|
|
if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil {
|
|
|
|
if err := user_model.ActivateEmail(ctx, email); err != nil {
|
2022-01-02 13:12:35 +00:00
|
|
|
ctx.ServerError("ActivateEmail", err)
|
2024-05-28 09:31:59 +00:00
|
|
|
return
|
2022-01-02 13:12:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
log.Trace("Email activated: %s", email.Email)
|
|
|
|
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
|
|
|
|
|
2022-12-03 02:48:26 +00:00
|
|
|
if u, err := user_model.GetUserByID(ctx, email.UID); err != nil {
|
2022-01-02 13:12:35 +00:00
|
|
|
log.Warn("GetUserByID: %d", email.UID)
|
2023-12-19 09:29:05 +00:00
|
|
|
} else {
|
2022-01-02 13:12:35 +00:00
|
|
|
// 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.
|
|
|
|
// Should users be logged in automatically here? (consider 2FA requirements, etc.)
|
|
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
|
|
|
|
}
|
2022-11-10 11:43:06 +00:00
|
|
|
|
2023-07-04 18:36:08 +00:00
|
|
|
func updateSession(ctx *context.Context, deletes []string, updates map[string]any) error {
|
2022-11-10 11:43:06 +00:00
|
|
|
if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
|
|
|
|
return fmt.Errorf("regenerate session: %w", err)
|
|
|
|
}
|
|
|
|
sess := ctx.Session
|
|
|
|
sessID := sess.ID()
|
|
|
|
for _, k := range deletes {
|
|
|
|
if err := sess.Delete(k); err != nil {
|
|
|
|
return fmt.Errorf("delete %v in session[%s]: %w", k, sessID, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for k, v := range updates {
|
|
|
|
if err := sess.Set(k, v); err != nil {
|
|
|
|
return fmt.Errorf("set %v in session[%s]: %w", k, sessID, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := sess.Release(); err != nil {
|
|
|
|
return fmt.Errorf("store session[%s]: %w", sessID, err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|