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