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 }