// Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package auth import ( "errors" "fmt" "net/http" "strings" "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" auth_service "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/forms" "github.com/markbates/goth" ) var tplLinkAccount base.TplName = "user/auth/link_account" // 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["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration 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 } gu, _ := gothUser.(goth.User) uname := getUserName(&gu) email := gu.Email ctx.Data["user_name"] = uname ctx.Data["email"] = email if len(email) != 0 { u, err := user_model.GetUserByEmail(email) if err != nil && !user_model.IsErrUserNotExist(err) { ctx.ServerError("UserSignIn", err) return } if u != nil { ctx.Data["user_exists"] = true } } else if len(uname) != 0 { u, err := user_model.GetUserByName(ctx, uname) if err != nil && !user_model.IsErrUserNotExist(err) { ctx.ServerError("UserSignIn", err) return } if u != nil { ctx.Data["user_exists"] = true } } ctx.HTML(http.StatusOK, tplLinkAccount) } // LinkAccountPostSignIn handle the coupling of external account with another account using signIn func LinkAccountPostSignIn(ctx *context.Context) { signInForm := web.GetForm(ctx).(*forms.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["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL 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(http.StatusOK, tplLinkAccount) return } u, _, err := auth_service.UserSignIn(signInForm.UserName, signInForm.Password) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Data["user_exists"] = true ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplLinkAccount, &signInForm) } else { ctx.ServerError("UserLinkAccount", err) } return } linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember) } func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, remember bool) { updateAvatarIfNeed(gothUser.AvatarURL, u) // If this user is enrolled in 2FA, we can't sign the user in just yet. // Instead, redirect them to the 2FA authentication page. // We deliberately ignore the skip local 2fa setting here because we are linking to a previous user here _, err := auth.GetTwoFactorByUID(u.ID) if err != nil { if !auth.IsErrTwoFactorNotEnrolled(err) { ctx.ServerError("UserLinkAccount", err) return } err = externalaccount.LinkAccountToUser(u, gothUser) if err != nil { ctx.ServerError("UserLinkAccount", err) return } handleSignIn(ctx, u, remember) return } if err := updateSession(ctx, nil, map[string]interface{}{ // User needs to use 2FA, save data and redirect to 2FA page. "twofaUid": u.ID, "twofaRemember": remember, "linkAccount": true, }); err != nil { ctx.ServerError("RegenerateSession", err) return } // If WebAuthn is enrolled -> Redirect to WebAuthn instead regs, err := auth.GetWebAuthnCredentialsByUID(u.ID) if err == nil && len(regs) > 0 { ctx.Redirect(setting.AppSubURL + "/user/webauthn") 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).(*forms.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["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL 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" gothUserInterface := ctx.Session.Get("linkAccountGothUser") if gothUserInterface == nil { ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session")) return } gothUser, ok := gothUserInterface.(goth.User) if !ok { ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface)) return } if ctx.HasError() { ctx.HTML(http.StatusOK, tplLinkAccount) return } if setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration { ctx.Error(http.StatusForbidden) return } if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha { context.VerifyCaptcha(ctx, tplLinkAccount, form) if ctx.Written() { return } } if !form.IsEmailDomainAllowed() { ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form) return } if setting.Service.AllowOnlyExternalRegistration || !setting.Service.RequireExternalRegistrationPassword { // In user_model.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 } } authSource, err := auth.GetActiveOAuth2SourceByName(gothUser.Provider) if err != nil { ctx.ServerError("CreateUser", err) return } u := &user_model.User{ Name: form.UserName, Email: form.Email, Passwd: form.Password, LoginType: auth.OAuth2, LoginSource: authSource.ID, LoginName: gothUser.UserID, } if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &gothUser, false) { // error already handled return } source := authSource.Cfg.(*oauth2.Source) if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil { ctx.ServerError("SyncGroupsToTeams", err) return } handleSignIn(ctx, u, false) }