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 }