1
0
Fork 0
forgejo/routers/user/auth.go
Damien Goutte-Gattat fc4a8c2980
Allow blocking some email domains from registering an account (#14667)
Gitea allows to whitelist email domains so that only email addresses from certain domains are allowed to register an account, but does not currently allows to do the opposite: blacklisting email domains so that addresses from certain domains are *forbidden* to register an account.

The idea has been briefly mentioned in the discussion about issue #6350, but never implemented. This PR does that.

The rationale is that, in my experience of running a Gitea instance, *a single email domain* is responsible for *most* of the spam accounts, and for *all* of the spam accounts that manage to get past the email confirmation step. So on top of the other spam mitigation measures already available (email confirmation, CAPTCHA, etc.), having the option to block a particularly annoying domain would be helpful.

close #13628
2021-02-15 00:31:29 +01:00

1629 lines
51 KiB
Go

// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package user
import (
"errors"
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth/oauth2"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
auth "code.gitea.io/gitea/modules/forms"
"code.gitea.io/gitea/modules/hcaptcha"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/recaptcha"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/mailer"
"github.com/markbates/goth"
"github.com/tstranex/u2f"
)
const (
// tplMustChangePassword template for updating a user's password
tplMustChangePassword = "user/auth/change_passwd"
// 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"
tplForgotPassword base.TplName = "user/auth/forgot_passwd"
tplResetPassword base.TplName = "user/auth/reset_passwd"
tplTwofa base.TplName = "user/auth/twofa"
tplTwofaScratch base.TplName = "user/auth/twofa_scratch"
tplLinkAccount base.TplName = "user/auth/link_account"
tplU2F base.TplName = "user/auth/u2f"
)
// AutoSignIn reads cookie and try to auto-login.
func AutoSignIn(ctx *context.Context) (bool, error) {
if !models.HasEngine {
return false, nil
}
uname := ctx.GetCookie(setting.CookieUserName)
if len(uname) == 0 {
return false, nil
}
isSucceed := false
defer func() {
if !isSucceed {
log.Trace("auto-login cookie cleared: %s", uname)
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
}
}()
u, err := models.GetUserByName(uname)
if err != nil {
if !models.IsErrUserNotExist(err) {
return false, fmt.Errorf("GetUserByName: %v", err)
}
return false, nil
}
if val, ok := ctx.GetSuperSecureCookie(
base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); !ok || val != u.Name {
return false, nil
}
isSucceed = true
// Set session IDs
if err := ctx.Session.Set("uid", u.ID); err != nil {
return false, err
}
if err := ctx.Session.Set("uname", u.Name); err != nil {
return false, err
}
if err := ctx.Session.Release(); err != nil {
return false, err
}
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
return true, nil
}
func checkAutoLogin(ctx *context.Context) bool {
// Check auto-login.
isSucceed, err := AutoSignIn(ctx)
if err != nil {
ctx.ServerError("AutoSignIn", err)
return true
}
redirectTo := ctx.Query("redirect_to")
if len(redirectTo) > 0 {
ctx.SetCookie("redirect_to", redirectTo, 0, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
} else {
redirectTo = ctx.GetCookie("redirect_to")
}
if isSucceed {
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
ctx.RedirectToFirst(redirectTo, setting.AppSubURL+string(setting.LandingPageURL))
return true
}
return false
}
// SignIn render sign in page
func SignIn(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_in")
// Check auto-login.
if checkAutoLogin(ctx) {
return
}
orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
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
ctx.Data["EnableSSPI"] = models.IsSSPIEnabled()
ctx.HTML(200, tplSignIn)
}
// SignInPost response for sign in request
func SignInPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_in")
orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
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
ctx.Data["EnableSSPI"] = models.IsSSPIEnabled()
if ctx.HasError() {
ctx.HTML(200, tplSignIn)
return
}
form := web.GetForm(ctx).(*auth.SignInForm)
u, err := models.UserSignIn(form.UserName, form.Password)
if err != nil {
if models.IsErrUserNotExist(err) {
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 models.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 models.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(200, "user/auth/prohibit_login")
} else if models.IsErrUserInactive(err) {
if setting.Service.RegisterEmailConfirm {
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
ctx.HTML(200, 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(200, "user/auth/prohibit_login")
}
} else {
ctx.ServerError("UserSignIn", err)
}
return
}
// If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page.
_, err = models.GetTwoFactorByUID(u.ID)
if err != nil {
if models.IsErrTwoFactorNotEnrolled(err) {
handleSignIn(ctx, u, form.Remember)
} else {
ctx.ServerError("UserSignIn", err)
}
return
}
// User needs to use 2FA, save data and redirect to 2FA page.
if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
ctx.ServerError("UserSignIn: Unable to set twofaUid in session", err)
return
}
if err := ctx.Session.Set("twofaRemember", form.Remember); err != nil {
ctx.ServerError("UserSignIn: Unable to set twofaRemember in session", err)
return
}
if err := ctx.Session.Release(); err != nil {
ctx.ServerError("UserSignIn: Unable to save session", err)
return
}
regs, err := models.GetU2FRegistrationsByUID(u.ID)
if err == nil && len(regs) > 0 {
ctx.Redirect(setting.AppSubURL + "/user/u2f")
return
}
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
}
// TwoFactor shows the user a two-factor authentication page.
func TwoFactor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("twofa")
// Check auto-login.
if checkAutoLogin(ctx) {
return
}
// Ensure user is in a 2FA session.
if ctx.Session.Get("twofaUid") == nil {
ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
return
}
ctx.HTML(200, tplTwofa)
}
// TwoFactorPost validates a user's two-factor authentication token.
func TwoFactorPost(ctx *context.Context) {
form := web.GetForm(ctx).(*auth.TwoFactorAuthForm)
ctx.Data["Title"] = ctx.Tr("twofa")
// Ensure user is in a 2FA session.
idSess := ctx.Session.Get("twofaUid")
if idSess == nil {
ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
return
}
id := idSess.(int64)
twofa, err := models.GetTwoFactorByUID(id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
// Validate the passcode with the stored TOTP secret.
ok, err := twofa.ValidateTOTP(form.Passcode)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if ok && twofa.LastUsedPasscode != form.Passcode {
remember := ctx.Session.Get("twofaRemember").(bool)
u, err := models.GetUserByID(id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if ctx.Session.Get("linkAccount") != nil {
gothUser := ctx.Session.Get("linkAccountGothUser")
if gothUser == nil {
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
return
}
err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User))
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
}
twofa.LastUsedPasscode = form.Passcode
if err = models.UpdateTwoFactor(twofa); err != nil {
ctx.ServerError("UserSignIn", err)
return
}
handleSignIn(ctx, u, remember)
return
}
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, auth.TwoFactorAuthForm{})
}
// TwoFactorScratch shows the scratch code form for two-factor authentication.
func TwoFactorScratch(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("twofa_scratch")
// Check auto-login.
if checkAutoLogin(ctx) {
return
}
// Ensure user is in a 2FA session.
if ctx.Session.Get("twofaUid") == nil {
ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
return
}
ctx.HTML(200, tplTwofaScratch)
}
// TwoFactorScratchPost validates and invalidates a user's two-factor scratch token.
func TwoFactorScratchPost(ctx *context.Context) {
form := web.GetForm(ctx).(*auth.TwoFactorScratchAuthForm)
ctx.Data["Title"] = ctx.Tr("twofa_scratch")
// Ensure user is in a 2FA session.
idSess := ctx.Session.Get("twofaUid")
if idSess == nil {
ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
return
}
id := idSess.(int64)
twofa, err := models.GetTwoFactorByUID(id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
// Validate the passcode with the stored TOTP secret.
if twofa.VerifyScratchToken(form.Token) {
// Invalidate the scratch token.
_, err = twofa.GenerateScratchToken()
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if err = models.UpdateTwoFactor(twofa); err != nil {
ctx.ServerError("UserSignIn", err)
return
}
remember := ctx.Session.Get("twofaRemember").(bool)
u, err := models.GetUserByID(id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
handleSignInFull(ctx, u, remember, false)
ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
return
}
ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, auth.TwoFactorScratchAuthForm{})
}
// U2F shows the U2F login page
func U2F(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("twofa")
ctx.Data["RequireU2F"] = true
// Check auto-login.
if checkAutoLogin(ctx) {
return
}
// Ensure user is in a 2FA session.
if ctx.Session.Get("twofaUid") == nil {
ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
return
}
ctx.HTML(200, tplU2F)
}
// U2FChallenge submits a sign challenge to the browser
func U2FChallenge(ctx *context.Context) {
// Ensure user is in a U2F session.
idSess := ctx.Session.Get("twofaUid")
if idSess == nil {
ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
return
}
id := idSess.(int64)
regs, err := models.GetU2FRegistrationsByUID(id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if len(regs) == 0 {
ctx.ServerError("UserSignIn", errors.New("no device registered"))
return
}
challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
if err != nil {
ctx.ServerError("u2f.NewChallenge", err)
return
}
if err := ctx.Session.Set("u2fChallenge", challenge); err != nil {
ctx.ServerError("UserSignIn: unable to set u2fChallenge in session", err)
return
}
if err := ctx.Session.Release(); err != nil {
ctx.ServerError("UserSignIn: unable to store session", err)
}
ctx.JSON(200, challenge.SignRequest(regs.ToRegistrations()))
}
// U2FSign authenticates the user by signResp
func U2FSign(ctx *context.Context) {
signResp := web.GetForm(ctx).(*u2f.SignResponse)
challSess := ctx.Session.Get("u2fChallenge")
idSess := ctx.Session.Get("twofaUid")
if challSess == nil || idSess == nil {
ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
return
}
challenge := challSess.(*u2f.Challenge)
id := idSess.(int64)
regs, err := models.GetU2FRegistrationsByUID(id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
for _, reg := range regs {
r, err := reg.Parse()
if err != nil {
log.Fatal("parsing u2f registration: %v", err)
continue
}
newCounter, authErr := r.Authenticate(*signResp, *challenge, reg.Counter)
if authErr == nil {
reg.Counter = newCounter
user, err := models.GetUserByID(id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
remember := ctx.Session.Get("twofaRemember").(bool)
if err := reg.UpdateCounter(); err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if ctx.Session.Get("linkAccount") != nil {
gothUser := ctx.Session.Get("linkAccountGothUser")
if gothUser == nil {
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
return
}
err = externalaccount.LinkAccountToUser(user, gothUser.(goth.User))
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
}
redirect := handleSignInFull(ctx, user, remember, false)
if redirect == "" {
redirect = setting.AppSubURL + "/"
}
ctx.PlainText(200, []byte(redirect))
return
}
}
ctx.Error(401)
}
// This handles the final part of the sign-in process of the user.
func handleSignIn(ctx *context.Context, u *models.User, remember bool) {
handleSignInFull(ctx, u, remember, true)
}
func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) string {
if remember {
days := 86400 * setting.LogInRememberDays
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
setting.CookieRememberName, u.Name, days, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
}
_ = ctx.Session.Delete("openid_verified_uri")
_ = ctx.Session.Delete("openid_signin_remember")
_ = ctx.Session.Delete("openid_determined_email")
_ = ctx.Session.Delete("openid_determined_username")
_ = ctx.Session.Delete("twofaUid")
_ = ctx.Session.Delete("twofaRemember")
_ = ctx.Session.Delete("u2fChallenge")
_ = ctx.Session.Delete("linkAccount")
if err := ctx.Session.Set("uid", u.ID); err != nil {
log.Error("Error setting uid %d in session: %v", u.ID, err)
}
if err := ctx.Session.Set("uname", u.Name); err != nil {
log.Error("Error setting uname %s session: %v", u.Name, err)
}
if err := ctx.Session.Release(); err != nil {
log.Error("Unable to store session: %v", err)
}
// Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one.
if len(u.Language) == 0 {
u.Language = ctx.Locale.Language()
if err := models.UpdateUserCols(u, "language"); err != nil {
log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
return setting.AppSubURL + "/"
}
}
ctx.SetCookie("lang", u.Language, nil, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
// Clear whatever CSRF has right now, force to generate a new one
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
// Register last login
u.SetLastLogin()
if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
ctx.ServerError("UpdateUserCols", err)
return setting.AppSubURL + "/"
}
if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
if obeyRedirect {
ctx.RedirectToFirst(redirectTo)
}
return redirectTo
}
if obeyRedirect {
ctx.Redirect(setting.AppSubURL + "/")
}
return setting.AppSubURL + "/"
}
// SignInOAuth handles the OAuth2 login buttons
func SignInOAuth(ctx *context.Context) {
provider := ctx.Params(":provider")
loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
if err != nil {
ctx.ServerError("SignIn", err)
return
}
// try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
user, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
if err == nil && user != nil {
// we got the user without going through the whole OAuth2 authentication flow again
handleOAuth2SignIn(user, gothUser, ctx, err)
return
}
if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
if strings.Contains(err.Error(), "no provider for ") {
if err = models.ResetOAuth2(); err != nil {
ctx.ServerError("SignIn", err)
return
}
if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
ctx.ServerError("SignIn", err)
}
return
}
ctx.ServerError("SignIn", err)
}
// redirect is done in oauth2.Auth
}
// SignInOAuthCallback handles the callback from the given provider
func SignInOAuthCallback(ctx *context.Context) {
provider := ctx.Params(":provider")
// first look if the provider is still active
loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
if err != nil {
ctx.ServerError("SignIn", err)
return
}
if loginSource == nil {
ctx.ServerError("SignIn", errors.New("No valid provider found, check configured callback url in provider"))
return
}
u, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
handleOAuth2SignIn(u, gothUser, ctx, err)
}
func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context, err error) {
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if u == nil {
// no existing user is found, request attach or new account
if err := ctx.Session.Set("linkAccountGothUser", gothUser); err != nil {
log.Error("Error setting linkAccountGothUser in session: %v", err)
}
if err := ctx.Session.Release(); err != nil {
log.Error("Error storing session: %v", err)
}
ctx.Redirect(setting.AppSubURL + "/user/link_account")
return
}
// If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page.
_, err = models.GetTwoFactorByUID(u.ID)
if err != nil {
if !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("UserSignIn", err)
return
}
if err := ctx.Session.Set("uid", u.ID); err != nil {
log.Error("Error setting uid in session: %v", err)
}
if err := ctx.Session.Set("uname", u.Name); err != nil {
log.Error("Error setting uname in session: %v", err)
}
if err := ctx.Session.Release(); err != nil {
log.Error("Error storing session: %v", err)
}
// Clear whatever CSRF has right now, force to generate a new one
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
// Register last login
u.SetLastLogin()
if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
ctx.ServerError("UpdateUserCols", err)
return
}
// update external user information
if err := models.UpdateExternalUser(u, gothUser); err != nil {
log.Error("UpdateExternalUser failed: %v", err)
}
if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 {
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
ctx.RedirectToFirst(redirectTo)
return
}
ctx.Redirect(setting.AppSubURL + "/")
return
}
// User needs to use 2FA, save data and redirect to 2FA page.
if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
log.Error("Error setting twofaUid in session: %v", err)
}
if err := ctx.Session.Set("twofaRemember", false); err != nil {
log.Error("Error setting twofaRemember in session: %v", err)
}
if err := ctx.Session.Release(); err != nil {
log.Error("Error storing session: %v", err)
}
// If U2F is enrolled -> Redirect to U2F instead
regs, err := models.GetU2FRegistrationsByUID(u.ID)
if err == nil && len(regs) > 0 {
ctx.Redirect(setting.AppSubURL + "/user/u2f")
return
}
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
}
// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
// login the user
func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response)
if err != nil {
if err.Error() == "securecookie: the value is too long" {
log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
}
return nil, goth.User{}, err
}
user := &models.User{
LoginName: gothUser.UserID,
LoginType: models.LoginOAuth2,
LoginSource: loginSource.ID,
}
hasUser, err := models.GetUser(user)
if err != nil {
return nil, goth.User{}, err
}
if hasUser {
return user, gothUser, nil
}
// search in external linked users
externalLoginUser := &models.ExternalLoginUser{
ExternalID: gothUser.UserID,
LoginSourceID: loginSource.ID,
}
hasUser, err = models.GetExternalLogin(externalLoginUser)
if err != nil {
return nil, goth.User{}, err
}
if hasUser {
user, err = models.GetUserByID(externalLoginUser.UserID)
return user, gothUser, err
}
// no user found to login
return nil, gothUser, nil
}
// LinkAccount shows the page where the user can decide to login or create a new account
func LinkAccount(ctx *context.Context) {
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
ctx.Data["Title"] = ctx.Tr("link_account")
ctx.Data["LinkAccountMode"] = true
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["ShowRegistrationButton"] = false
// use this to set the right link into the signIn and signUp templates in the link_account template
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser := ctx.Session.Get("linkAccountGothUser")
if gothUser == nil {
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
return
}
uname := gothUser.(goth.User).NickName
email := gothUser.(goth.User).Email
ctx.Data["user_name"] = uname
ctx.Data["email"] = email
if len(email) != 0 {
u, err := models.GetUserByEmail(email)
if err != nil && !models.IsErrUserNotExist(err) {
ctx.ServerError("UserSignIn", err)
return
}
if u != nil {
ctx.Data["user_exists"] = true
}
} else if len(uname) != 0 {
u, err := models.GetUserByName(uname)
if err != nil && !models.IsErrUserNotExist(err) {
ctx.ServerError("UserSignIn", err)
return
}
if u != nil {
ctx.Data["user_exists"] = true
}
}
ctx.HTML(200, tplLinkAccount)
}
// LinkAccountPostSignIn handle the coupling of external account with another account using signIn
func LinkAccountPostSignIn(ctx *context.Context) {
signInForm := web.GetForm(ctx).(*auth.SignInForm)
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
ctx.Data["Title"] = ctx.Tr("link_account")
ctx.Data["LinkAccountMode"] = true
ctx.Data["LinkAccountModeSignIn"] = true
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["ShowRegistrationButton"] = false
// use this to set the right link into the signIn and signUp templates in the link_account template
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser := ctx.Session.Get("linkAccountGothUser")
if gothUser == nil {
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
return
}
if ctx.HasError() {
ctx.HTML(200, tplLinkAccount)
return
}
u, err := models.UserSignIn(signInForm.UserName, signInForm.Password)
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.Data["user_exists"] = true
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplLinkAccount, &signInForm)
} else {
ctx.ServerError("UserLinkAccount", err)
}
return
}
// If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page.
_, err = models.GetTwoFactorByUID(u.ID)
if err != nil {
if !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("UserLinkAccount", err)
return
}
err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User))
if err != nil {
ctx.ServerError("UserLinkAccount", err)
return
}
handleSignIn(ctx, u, signInForm.Remember)
return
}
// User needs to use 2FA, save data and redirect to 2FA page.
if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
log.Error("Error setting twofaUid in session: %v", err)
}
if err := ctx.Session.Set("twofaRemember", signInForm.Remember); err != nil {
log.Error("Error setting twofaRemember in session: %v", err)
}
if err := ctx.Session.Set("linkAccount", true); err != nil {
log.Error("Error setting linkAccount in session: %v", err)
}
if err := ctx.Session.Release(); err != nil {
log.Error("Error storing session: %v", err)
}
// If U2F is enrolled -> Redirect to U2F instead
regs, err := models.GetU2FRegistrationsByUID(u.ID)
if err == nil && len(regs) > 0 {
ctx.Redirect(setting.AppSubURL + "/user/u2f")
return
}
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
}
// LinkAccountPostRegister handle the creation of a new account for an external account using signUp
func LinkAccountPostRegister(ctx *context.Context) {
form := web.GetForm(ctx).(*auth.RegisterForm)
// TODO Make insecure passwords optional for local accounts also,
// once email-based Second-Factor Auth is available
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
ctx.Data["Title"] = ctx.Tr("link_account")
ctx.Data["LinkAccountMode"] = true
ctx.Data["LinkAccountModeRegister"] = true
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["ShowRegistrationButton"] = false
// use this to set the right link into the signIn and signUp templates in the link_account template
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser := ctx.Session.Get("linkAccountGothUser")
if gothUser == nil {
ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
return
}
if ctx.HasError() {
ctx.HTML(200, tplLinkAccount)
return
}
if setting.Service.DisableRegistration {
ctx.Error(403)
return
}
if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha {
var valid bool
var err error
switch setting.Service.CaptchaType {
case setting.ImageCaptcha:
valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
case setting.ReCaptcha:
valid, err = recaptcha.Verify(ctx.Req.Context(), form.GRecaptchaResponse)
case setting.HCaptcha:
valid, err = hcaptcha.Verify(ctx.Req.Context(), form.HcaptchaResponse)
default:
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
return
}
if err != nil {
log.Debug("%s", err.Error())
}
if !valid {
ctx.Data["Err_Captcha"] = true
ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplLinkAccount, &form)
return
}
}
if setting.Service.AllowOnlyExternalRegistration || !setting.Service.RequireExternalRegistrationPassword {
// In models.User an empty password is classed as not set, so we set form.Password to empty.
// Eventually the database should be changed to indicate "Second Factor"-enabled accounts
// (accounts that do not introduce the security vulnerabilities of a password).
// If a user decides to circumvent second-factor security, and purposefully create a password,
// they can still do so using the "Recover Account" option.
form.Password = ""
} else {
if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
return
}
if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
return
}
}
loginSource, err := models.GetActiveOAuth2LoginSourceByName(gothUser.(goth.User).Provider)
if err != nil {
ctx.ServerError("CreateUser", err)
}
u := &models.User{
Name: form.UserName,
Email: form.Email,
Passwd: form.Password,
IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
LoginType: models.LoginOAuth2,
LoginSource: loginSource.ID,
LoginName: gothUser.(goth.User).UserID,
}
//nolint: dupl
if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplLinkAccount, &form)
case models.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplLinkAccount, &form)
case models.IsErrEmailInvalid(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSignUp, &form)
case models.IsErrNameReserved(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplLinkAccount, &form)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplLinkAccount, &form)
case models.IsErrNameCharsNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplLinkAccount, &form)
default:
ctx.ServerError("CreateUser", err)
}
return
}
log.Trace("Account created: %s", u.Name)
// Auto-set admin for the only user.
if models.CountUsers() == 1 {
u.IsAdmin = true
u.IsActive = true
u.SetLastLogin()
if err := models.UpdateUserCols(u, "is_admin", "is_active", "last_login_unix"); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
}
// update external user information
if err := models.UpdateExternalUser(u, gothUser.(goth.User)); err != nil {
log.Error("UpdateExternalUser failed: %v", err)
}
// Send confirmation email
if setting.Service.RegisterEmailConfirm && u.ID > 1 {
mailer.SendActivateAccountMail(ctx.Locale, u)
ctx.Data["IsSendRegisterMail"] = true
ctx.Data["Email"] = u.Email
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
ctx.HTML(200, TplActivate)
if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
return
}
ctx.Redirect(setting.AppSubURL + "/user/login")
}
// HandleSignOut resets the session and sets the cookies
func HandleSignOut(ctx *context.Context) {
_ = ctx.Session.Flush()
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
ctx.SetCookie("lang", "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true) // Setting the lang cookie will trigger the middleware to reset the language ot previous state.
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) // logout default should set redirect to to default
}
// SignOut sign out from login status
func SignOut(ctx *context.Context) {
if ctx.User != nil {
eventsource.GetManager().SendMessageBlocking(ctx.User.ID, &eventsource.Event{
Name: "logout",
Data: ctx.Session.ID(),
})
}
HandleSignOut(ctx)
ctx.Redirect(setting.AppSubURL + "/")
}
// 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"
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["PageIsSignUp"] = true
//Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration
ctx.HTML(200, tplSignUp)
}
// SignUpPost response for sign up information submission
func SignUpPost(ctx *context.Context) {
form := web.GetForm(ctx).(*auth.RegisterForm)
ctx.Data["Title"] = ctx.Tr("sign_up")
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["PageIsSignUp"] = true
//Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration {
ctx.Error(403)
return
}
if ctx.HasError() {
ctx.HTML(200, tplSignUp)
return
}
if setting.Service.EnableCaptcha {
var valid bool
var err error
switch setting.Service.CaptchaType {
case setting.ImageCaptcha:
valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
case setting.ReCaptcha:
valid, err = recaptcha.Verify(ctx.Req.Context(), form.GRecaptchaResponse)
case setting.HCaptcha:
valid, err = hcaptcha.Verify(ctx.Req.Context(), form.HcaptchaResponse)
default:
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
return
}
if err != nil {
log.Debug("%s", err.Error())
}
if !valid {
ctx.Data["Err_Captcha"] = true
ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUp, &form)
return
}
}
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
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplSignUp, &form)
return
}
pwned, err := password.IsPwned(ctx.Req.Context(), form.Password)
if pwned {
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(errMsg, tplSignUp, &form)
return
}
u := &models.User{
Name: form.UserName,
Email: form.Email,
Passwd: form.Password,
IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
}
if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSignUp, &form)
case models.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignUp, &form)
case models.IsErrEmailInvalid(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSignUp, &form)
case models.IsErrNameReserved(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplSignUp, &form)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSignUp, &form)
default:
ctx.ServerError("CreateUser", err)
}
return
}
log.Trace("Account created: %s", u.Name)
// Auto-set admin for the only user.
if models.CountUsers() == 1 {
u.IsAdmin = true
u.IsActive = true
u.SetLastLogin()
if err := models.UpdateUserCols(u, "is_admin", "is_active", "last_login_unix"); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
}
// Send confirmation email, no need for social account.
if setting.Service.RegisterEmailConfirm && u.ID > 1 {
mailer.SendActivateAccountMail(ctx.Locale, u)
ctx.Data["IsSendRegisterMail"] = true
ctx.Data["Email"] = u.Email
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
ctx.HTML(200, TplActivate)
if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
return
}
ctx.Flash.Success(ctx.Tr("auth.sign_up_successful"))
handleSignInFull(ctx, u, false, true)
}
// Activate render activate user page
func Activate(ctx *context.Context) {
code := ctx.Query("code")
password := ctx.Query("password")
if len(code) == 0 {
ctx.Data["IsActivatePage"] = true
if ctx.User.IsActive {
ctx.Error(404)
return
}
// Resend confirmation email.
if setting.Service.RegisterEmailConfirm {
if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
ctx.Data["ResendLimited"] = true
} else {
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
}
} else {
ctx.Data["ServiceNotEnabled"] = true
}
ctx.HTML(200, TplActivate)
return
}
user := models.VerifyUserActiveCode(code)
// if code is wrong
if user == nil {
ctx.Data["IsActivateFailed"] = true
ctx.HTML(200, TplActivate)
return
}
// if account is local account, verify password
if user.LoginSource == 0 {
if len(password) == 0 {
ctx.Data["Code"] = code
ctx.Data["NeedsPassword"] = true
ctx.HTML(200, TplActivate)
return
}
if !user.ValidatePassword(password) {
ctx.Data["IsActivateFailed"] = true
ctx.HTML(200, TplActivate)
return
}
}
user.IsActive = true
var err error
if user.Rands, err = models.GetUserSalt(); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
if err := models.UpdateUserCols(user, "is_active", "rands"); err != nil {
if models.IsErrUserNotExist(err) {
ctx.Error(404)
} else {
ctx.ServerError("UpdateUser", err)
}
return
}
log.Trace("User activated: %s", user.Name)
if err := ctx.Session.Set("uid", user.ID); err != nil {
log.Error(fmt.Sprintf("Error setting uid in session: %v", err))
}
if err := ctx.Session.Set("uname", user.Name); err != nil {
log.Error(fmt.Sprintf("Error setting uname in session: %v", err))
}
if err := ctx.Session.Release(); err != nil {
log.Error("Error storing session: %v", err)
}
ctx.Flash.Success(ctx.Tr("auth.account_activated"))
ctx.Redirect(setting.AppSubURL + "/")
}
// ActivateEmail render the activate email page
func ActivateEmail(ctx *context.Context) {
code := ctx.Query("code")
emailStr := ctx.Query("email")
// Verify code.
if email := models.VerifyActiveEmailCode(code, emailStr); email != nil {
if err := email.Activate(); err != nil {
ctx.ServerError("ActivateEmail", err)
}
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
if u, err := models.GetUserByID(email.UID); err != nil {
log.Warn("GetUserByID: %d", email.UID)
} else {
// 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")
}
// ForgotPasswd render the forget pasword page
func ForgotPasswd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
if setting.MailService == nil {
ctx.Data["IsResetDisable"] = true
ctx.HTML(200, tplForgotPassword)
return
}
email := ctx.Query("email")
ctx.Data["Email"] = email
ctx.Data["IsResetRequest"] = true
ctx.HTML(200, tplForgotPassword)
}
// ForgotPasswdPost response for forget password request
func ForgotPasswdPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
if setting.MailService == nil {
ctx.NotFound("ForgotPasswdPost", nil)
return
}
ctx.Data["IsResetRequest"] = true
email := ctx.Query("email")
ctx.Data["Email"] = email
u, err := models.GetUserByEmail(email)
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
ctx.Data["IsResetSent"] = true
ctx.HTML(200, tplForgotPassword)
return
}
ctx.ServerError("user.ResetPasswd(check existence)", err)
return
}
if !u.IsLocal() && !u.IsOAuth2() {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
return
}
if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
ctx.Data["ResendLimited"] = true
ctx.HTML(200, tplForgotPassword)
return
}
mailer.SendResetPasswordMail(ctx.Locale, u)
if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
ctx.Data["IsResetSent"] = true
ctx.HTML(200, tplForgotPassword)
}
func commonResetPassword(ctx *context.Context) (*models.User, *models.TwoFactor) {
code := ctx.Query("code")
ctx.Data["Title"] = ctx.Tr("auth.reset_password")
ctx.Data["Code"] = code
if nil != ctx.User {
ctx.Data["user_signed_in"] = true
}
if len(code) == 0 {
ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
return nil, nil
}
// Fail early, don't frustrate the user
u := models.VerifyUserActiveCode(code)
if u == nil {
ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
return nil, nil
}
twofa, err := models.GetTwoFactorByUID(u.ID)
if err != nil {
if !models.IsErrTwoFactorNotEnrolled(err) {
ctx.Error(http.StatusInternalServerError, "CommonResetPassword", err.Error())
return nil, nil
}
} else {
ctx.Data["has_two_factor"] = true
ctx.Data["scratch_code"] = ctx.QueryBool("scratch_code")
}
// Show the user that they are affecting the account that they intended to
ctx.Data["user_email"] = u.Email
if nil != ctx.User && u.ID != ctx.User.ID {
ctx.Flash.Error(ctx.Tr("auth.reset_password_wrong_user", ctx.User.Email, u.Email))
return nil, nil
}
return u, twofa
}
// ResetPasswd render the account recovery page
func ResetPasswd(ctx *context.Context) {
ctx.Data["IsResetForm"] = true
commonResetPassword(ctx)
if ctx.Written() {
return
}
ctx.HTML(200, tplResetPassword)
}
// ResetPasswdPost response from account recovery request
func ResetPasswdPost(ctx *context.Context) {
u, twofa := commonResetPassword(ctx)
if ctx.Written() {
return
}
if u == nil {
// Flash error has been set
ctx.HTML(200, tplResetPassword)
return
}
// Validate password length.
passwd := ctx.Query("password")
if len(passwd) < setting.MinPasswordLength {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
return
} else if !password.IsComplexEnough(passwd) {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplResetPassword, nil)
return
} else if pwned, err := password.IsPwned(ctx.Req.Context(), passwd); pwned || err != nil {
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(errMsg, tplResetPassword, nil)
return
}
// Handle two-factor
regenerateScratchToken := false
if twofa != nil {
if ctx.QueryBool("scratch_code") {
if !twofa.VerifyScratchToken(ctx.Query("token")) {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Token"] = true
ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
return
}
regenerateScratchToken = true
} else {
passcode := ctx.Query("passcode")
ok, err := twofa.ValidateTOTP(passcode)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err.Error())
return
}
if !ok || twofa.LastUsedPasscode == passcode {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Passcode"] = true
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
return
}
twofa.LastUsedPasscode = passcode
if err = models.UpdateTwoFactor(twofa); err != nil {
ctx.ServerError("ResetPasswdPost: UpdateTwoFactor", err)
return
}
}
}
var err error
if u.Rands, err = models.GetUserSalt(); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
if err = u.SetPassword(passwd); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
u.MustChangePassword = false
if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
log.Trace("User password reset: %s", u.Name)
ctx.Data["IsResetFailed"] = true
remember := len(ctx.Query("remember")) != 0
if regenerateScratchToken {
// Invalidate the scratch token.
_, err = twofa.GenerateScratchToken()
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if err = models.UpdateTwoFactor(twofa); err != nil {
ctx.ServerError("UserSignIn", err)
return
}
handleSignInFull(ctx, u, remember, false)
ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
return
}
handleSignInFull(ctx, u, remember, true)
}
// MustChangePassword renders the page to change a user's password
func MustChangePassword(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
ctx.Data["MustChangePassword"] = true
ctx.HTML(200, tplMustChangePassword)
}
// MustChangePasswordPost response for updating a user's password after his/her
// account was created by an admin
func MustChangePasswordPost(ctx *context.Context) {
form := web.GetForm(ctx).(*auth.MustChangePasswordForm)
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
if ctx.HasError() {
ctx.HTML(200, tplMustChangePassword)
return
}
u := ctx.User
// Make sure only requests for users who are eligible to change their password via
// this method passes through
if !u.MustChangePassword {
ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page"))
return
}
if form.Password != form.Retype {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form)
return
}
if len(form.Password) < setting.MinPasswordLength {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
return
}
var err error
if err = u.SetPassword(form.Password); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
u.MustChangePassword = false
if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
log.Trace("User updated password: %s", u.Name)
if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
ctx.RedirectToFirst(redirectTo)
return
}
ctx.Redirect(setting.AppSubURL + "/")
}