code.gitea.io/gitea@v1.21.7/routers/web/auth/linkaccount.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package auth
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  	"strings"
    11  
    12  	"code.gitea.io/gitea/models/auth"
    13  	user_model "code.gitea.io/gitea/models/user"
    14  	"code.gitea.io/gitea/modules/base"
    15  	"code.gitea.io/gitea/modules/context"
    16  	"code.gitea.io/gitea/modules/log"
    17  	"code.gitea.io/gitea/modules/setting"
    18  	"code.gitea.io/gitea/modules/util"
    19  	"code.gitea.io/gitea/modules/web"
    20  	auth_service "code.gitea.io/gitea/services/auth"
    21  	"code.gitea.io/gitea/services/auth/source/oauth2"
    22  	"code.gitea.io/gitea/services/externalaccount"
    23  	"code.gitea.io/gitea/services/forms"
    24  
    25  	"github.com/markbates/goth"
    26  )
    27  
    28  var tplLinkAccount base.TplName = "user/auth/link_account"
    29  
    30  // LinkAccount shows the page where the user can decide to login or create a new account
    31  func LinkAccount(ctx *context.Context) {
    32  	ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
    33  	ctx.Data["Title"] = ctx.Tr("link_account")
    34  	ctx.Data["LinkAccountMode"] = true
    35  	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
    36  	ctx.Data["Captcha"] = context.GetImageCaptcha()
    37  	ctx.Data["CaptchaType"] = setting.Service.CaptchaType
    38  	ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
    39  	ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
    40  	ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
    41  	ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
    42  	ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
    43  	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
    44  	ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
    45  	ctx.Data["ShowRegistrationButton"] = false
    46  
    47  	// use this to set the right link into the signIn and signUp templates in the link_account template
    48  	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
    49  	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
    50  
    51  	gothUser := ctx.Session.Get("linkAccountGothUser")
    52  	if gothUser == nil {
    53  		ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
    54  		return
    55  	}
    56  
    57  	gu, _ := gothUser.(goth.User)
    58  	uname := getUserName(&gu)
    59  	email := gu.Email
    60  	ctx.Data["user_name"] = uname
    61  	ctx.Data["email"] = email
    62  
    63  	if len(email) != 0 {
    64  		u, err := user_model.GetUserByEmail(ctx, email)
    65  		if err != nil && !user_model.IsErrUserNotExist(err) {
    66  			ctx.ServerError("UserSignIn", err)
    67  			return
    68  		}
    69  		if u != nil {
    70  			ctx.Data["user_exists"] = true
    71  		}
    72  	} else if len(uname) != 0 {
    73  		u, err := user_model.GetUserByName(ctx, uname)
    74  		if err != nil && !user_model.IsErrUserNotExist(err) {
    75  			ctx.ServerError("UserSignIn", err)
    76  			return
    77  		}
    78  		if u != nil {
    79  			ctx.Data["user_exists"] = true
    80  		}
    81  	}
    82  
    83  	ctx.HTML(http.StatusOK, tplLinkAccount)
    84  }
    85  
    86  func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl base.TplName, invoker string, err error) {
    87  	if errors.Is(err, util.ErrNotExist) {
    88  		ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
    89  	} else if errors.Is(err, util.ErrInvalidArgument) {
    90  		ctx.Data["user_exists"] = true
    91  		ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
    92  	} else if user_model.IsErrUserProhibitLogin(err) {
    93  		ctx.Data["user_exists"] = true
    94  		log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err)
    95  		ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
    96  		ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
    97  	} else if user_model.IsErrUserInactive(err) {
    98  		ctx.Data["user_exists"] = true
    99  		if setting.Service.RegisterEmailConfirm {
   100  			ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
   101  			ctx.HTML(http.StatusOK, TplActivate)
   102  		} else {
   103  			log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err)
   104  			ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
   105  			ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
   106  		}
   107  	} else {
   108  		ctx.ServerError(invoker, err)
   109  	}
   110  }
   111  
   112  // LinkAccountPostSignIn handle the coupling of external account with another account using signIn
   113  func LinkAccountPostSignIn(ctx *context.Context) {
   114  	signInForm := web.GetForm(ctx).(*forms.SignInForm)
   115  	ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
   116  	ctx.Data["Title"] = ctx.Tr("link_account")
   117  	ctx.Data["LinkAccountMode"] = true
   118  	ctx.Data["LinkAccountModeSignIn"] = true
   119  	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
   120  	ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
   121  	ctx.Data["Captcha"] = context.GetImageCaptcha()
   122  	ctx.Data["CaptchaType"] = setting.Service.CaptchaType
   123  	ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
   124  	ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
   125  	ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
   126  	ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
   127  	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
   128  	ctx.Data["ShowRegistrationButton"] = false
   129  
   130  	// use this to set the right link into the signIn and signUp templates in the link_account template
   131  	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
   132  	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
   133  
   134  	gothUser := ctx.Session.Get("linkAccountGothUser")
   135  	if gothUser == nil {
   136  		ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
   137  		return
   138  	}
   139  
   140  	if ctx.HasError() {
   141  		ctx.HTML(http.StatusOK, tplLinkAccount)
   142  		return
   143  	}
   144  
   145  	u, _, err := auth_service.UserSignIn(ctx, signInForm.UserName, signInForm.Password)
   146  	if err != nil {
   147  		handleSignInError(ctx, signInForm.UserName, &signInForm, tplLinkAccount, "UserLinkAccount", err)
   148  		return
   149  	}
   150  
   151  	linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember)
   152  }
   153  
   154  func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, remember bool) {
   155  	updateAvatarIfNeed(gothUser.AvatarURL, u)
   156  
   157  	// If this user is enrolled in 2FA, we can't sign the user in just yet.
   158  	// Instead, redirect them to the 2FA authentication page.
   159  	// We deliberately ignore the skip local 2fa setting here because we are linking to a previous user here
   160  	_, err := auth.GetTwoFactorByUID(ctx, u.ID)
   161  	if err != nil {
   162  		if !auth.IsErrTwoFactorNotEnrolled(err) {
   163  			ctx.ServerError("UserLinkAccount", err)
   164  			return
   165  		}
   166  
   167  		err = externalaccount.LinkAccountToUser(ctx, u, gothUser)
   168  		if err != nil {
   169  			ctx.ServerError("UserLinkAccount", err)
   170  			return
   171  		}
   172  
   173  		handleSignIn(ctx, u, remember)
   174  		return
   175  	}
   176  
   177  	if err := updateSession(ctx, nil, map[string]any{
   178  		// User needs to use 2FA, save data and redirect to 2FA page.
   179  		"twofaUid":      u.ID,
   180  		"twofaRemember": remember,
   181  		"linkAccount":   true,
   182  	}); err != nil {
   183  		ctx.ServerError("RegenerateSession", err)
   184  		return
   185  	}
   186  
   187  	// If WebAuthn is enrolled -> Redirect to WebAuthn instead
   188  	regs, err := auth.GetWebAuthnCredentialsByUID(ctx, u.ID)
   189  	if err == nil && len(regs) > 0 {
   190  		ctx.Redirect(setting.AppSubURL + "/user/webauthn")
   191  		return
   192  	}
   193  
   194  	ctx.Redirect(setting.AppSubURL + "/user/two_factor")
   195  }
   196  
   197  // LinkAccountPostRegister handle the creation of a new account for an external account using signUp
   198  func LinkAccountPostRegister(ctx *context.Context) {
   199  	form := web.GetForm(ctx).(*forms.RegisterForm)
   200  	// TODO Make insecure passwords optional for local accounts also,
   201  	//      once email-based Second-Factor Auth is available
   202  	ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
   203  	ctx.Data["Title"] = ctx.Tr("link_account")
   204  	ctx.Data["LinkAccountMode"] = true
   205  	ctx.Data["LinkAccountModeRegister"] = true
   206  	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
   207  	ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
   208  	ctx.Data["Captcha"] = context.GetImageCaptcha()
   209  	ctx.Data["CaptchaType"] = setting.Service.CaptchaType
   210  	ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
   211  	ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
   212  	ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
   213  	ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
   214  	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
   215  	ctx.Data["ShowRegistrationButton"] = false
   216  
   217  	// use this to set the right link into the signIn and signUp templates in the link_account template
   218  	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
   219  	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
   220  
   221  	gothUserInterface := ctx.Session.Get("linkAccountGothUser")
   222  	if gothUserInterface == nil {
   223  		ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
   224  		return
   225  	}
   226  	gothUser, ok := gothUserInterface.(goth.User)
   227  	if !ok {
   228  		ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface))
   229  		return
   230  	}
   231  
   232  	if ctx.HasError() {
   233  		ctx.HTML(http.StatusOK, tplLinkAccount)
   234  		return
   235  	}
   236  
   237  	if setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration {
   238  		ctx.Error(http.StatusForbidden)
   239  		return
   240  	}
   241  
   242  	if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha {
   243  		context.VerifyCaptcha(ctx, tplLinkAccount, form)
   244  		if ctx.Written() {
   245  			return
   246  		}
   247  	}
   248  
   249  	if !form.IsEmailDomainAllowed() {
   250  		ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
   251  		return
   252  	}
   253  
   254  	if setting.Service.AllowOnlyExternalRegistration || !setting.Service.RequireExternalRegistrationPassword {
   255  		// In user_model.User an empty password is classed as not set, so we set form.Password to empty.
   256  		// Eventually the database should be changed to indicate "Second Factor"-enabled accounts
   257  		// (accounts that do not introduce the security vulnerabilities of a password).
   258  		// If a user decides to circumvent second-factor security, and purposefully create a password,
   259  		// they can still do so using the "Recover Account" option.
   260  		form.Password = ""
   261  	} else {
   262  		if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype {
   263  			ctx.Data["Err_Password"] = true
   264  			ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
   265  			return
   266  		}
   267  		if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength {
   268  			ctx.Data["Err_Password"] = true
   269  			ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
   270  			return
   271  		}
   272  	}
   273  
   274  	authSource, err := auth.GetActiveOAuth2SourceByName(gothUser.Provider)
   275  	if err != nil {
   276  		ctx.ServerError("CreateUser", err)
   277  		return
   278  	}
   279  
   280  	u := &user_model.User{
   281  		Name:        form.UserName,
   282  		Email:       form.Email,
   283  		Passwd:      form.Password,
   284  		LoginType:   auth.OAuth2,
   285  		LoginSource: authSource.ID,
   286  		LoginName:   gothUser.UserID,
   287  	}
   288  
   289  	if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &gothUser, false) {
   290  		// error already handled
   291  		return
   292  	}
   293  
   294  	source := authSource.Cfg.(*oauth2.Source)
   295  	if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
   296  		ctx.ServerError("SyncGroupsToTeams", err)
   297  		return
   298  	}
   299  
   300  	handleSignIn(ctx, u, false)
   301  }