From 68e934ab5de6a4e8bec6f3faa8bf1e05899b8fb8 Mon Sep 17 00:00:00 2001 From: Xinyu Zhou Date: Wed, 23 Nov 2022 05:13:18 +0800 Subject: [PATCH] Add option to enable CAPTCHA validation for login (#21638) Enable this to require captcha validation for user login. You also must enable `ENABLE_CAPTCHA`. Summary: - Consolidate CAPTCHA template - add CAPTCHA handle and context - add `REQUIRE_CAPTCHA_FOR_LOGIN` config and docs - Consolidate CAPTCHA set-up and verification code Partially resolved #6049 Signed-off-by: Xinyu Zhou Signed-off-by: Andrew Thornton Co-authored-by: Andrew Thornton --- custom/conf/app.example.ini | 3 + .../doc/advanced/config-cheat-sheet.en-us.md | 1 + .../doc/advanced/config-cheat-sheet.zh-cn.md | 3 +- modules/context/captcha.go | 59 +++++++++++++++++ modules/setting/service.go | 2 + routers/web/auth/auth.go | 63 ++++++------------- routers/web/auth/linkaccount.go | 28 +-------- routers/web/auth/openid.go | 49 ++------------- services/forms/user_form.go | 11 ++-- services/forms/user_form_auth_openid.go | 7 +-- templates/user/auth/captcha.tmpl | 24 +++++++ templates/user/auth/signin_inner.tmpl | 2 + templates/user/auth/signup_inner.tmpl | 28 +-------- .../user/auth/signup_openid_register.tmpl | 28 +-------- 14 files changed, 128 insertions(+), 180 deletions(-) create mode 100644 templates/user/auth/captcha.tmpl diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 76482bf607af..e7ddda4b8a3c 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -759,6 +759,9 @@ ROUTER = console ;; Enable captcha validation for registration ;ENABLE_CAPTCHA = false ;; +;; Enable this to require captcha validation for login +;REQUIRE_CAPTCHA_FOR_LOGIN = false +;; ;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha. ;CAPTCHA_TYPE = image ;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 4e7ef492f90b..468c6d5edcac 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -634,6 +634,7 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o - `ENABLE_REVERSE_PROXY_FULL_NAME`: **false**: Enable this to allow to auto-registration with a provided full name for the user. - `ENABLE_CAPTCHA`: **false**: Enable this to use captcha validation for registration. +- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: Enable this to require captcha validation for login. You also must enable `ENABLE_CAPTCHA`. - `REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA`: **false**: Enable this to force captcha validation even for External Accounts (i.e. GitHub, OpenID Connect, etc). You also must enable `ENABLE_CAPTCHA`. - `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha\] diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index 576007f75b25..f10b6258c87a 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -145,7 +145,8 @@ menu: - `ENABLE_NOTIFY_MAIL`: 是否发送工单创建等提醒邮件,需要 `Mailer` 被激活。 - `ENABLE_REVERSE_PROXY_AUTHENTICATION`: 允许反向代理认证,更多细节见:https://github.com/gogits/gogs/issues/165 - `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: 允许通过反向认证做自动注册。 -- `ENABLE_CAPTCHA`: 注册时使用图片验证码。 +- `ENABLE_CAPTCHA`: **false**: 注册时使用图片验证码。 +- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: 登录时需要图片验证码。需要同时开启 `ENABLE_CAPTCHA`。 ### Service - Expore (`service.explore`) diff --git a/modules/context/captcha.go b/modules/context/captcha.go index 6117d3071335..0bd003da6cb3 100644 --- a/modules/context/captcha.go +++ b/modules/context/captcha.go @@ -5,9 +5,15 @@ package context import ( + "fmt" "sync" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/hcaptcha" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/mcaptcha" + "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/setting" "gitea.com/go-chi/captcha" @@ -28,3 +34,56 @@ func GetImageCaptcha() *captcha.Captcha { }) return cpt } + +// SetCaptchaData sets common captcha data +func SetCaptchaData(ctx *Context) { + if !setting.Service.EnableCaptcha { + return + } + ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha + ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL + ctx.Data["Captcha"] = 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 +} + +const ( + gRecaptchaResponseField = "g-recaptcha-response" + hCaptchaResponseField = "h-captcha-response" + mCaptchaResponseField = "m-captcha-response" +) + +// VerifyCaptcha verifies Captcha data +// No-op if captchas are not enabled +func VerifyCaptcha(ctx *Context, tpl base.TplName, form interface{}) { + if !setting.Service.EnableCaptcha { + return + } + + var valid bool + var err error + switch setting.Service.CaptchaType { + case setting.ImageCaptcha: + valid = GetImageCaptcha().VerifyReq(ctx.Req) + case setting.ReCaptcha: + valid, err = recaptcha.Verify(ctx, ctx.Req.Form.Get(gRecaptchaResponseField)) + case setting.HCaptcha: + valid, err = hcaptcha.Verify(ctx, ctx.Req.Form.Get(hCaptchaResponseField)) + case setting.MCaptcha: + valid, err = mcaptcha.Verify(ctx, ctx.Req.Form.Get(mCaptchaResponseField)) + default: + ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType)) + return + } + if err != nil { + log.Debug("%v", err) + } + + if !valid { + ctx.Data["Err_Captcha"] = true + ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tpl, form) + } +} diff --git a/modules/setting/service.go b/modules/setting/service.go index 10e389995032..d2eb6ebcd7ef 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -40,6 +40,7 @@ var Service = struct { EnableReverseProxyEmail bool EnableReverseProxyFullName bool EnableCaptcha bool + RequireCaptchaForLogin bool RequireExternalRegistrationCaptcha bool RequireExternalRegistrationPassword bool CaptchaType string @@ -130,6 +131,7 @@ func newService() { Service.EnableReverseProxyEmail = sec.Key("ENABLE_REVERSE_PROXY_EMAIL").MustBool() Service.EnableReverseProxyFullName = sec.Key("ENABLE_REVERSE_PROXY_FULL_NAME").MustBool() Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool(false) + Service.RequireCaptchaForLogin = sec.Key("REQUIRE_CAPTCHA_FOR_LOGIN").MustBool(false) Service.RequireExternalRegistrationCaptcha = sec.Key("REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA").MustBool(Service.EnableCaptcha) Service.RequireExternalRegistrationPassword = sec.Key("REQUIRE_EXTERNAL_REGISTRATION_PASSWORD").MustBool() Service.CaptchaType = sec.Key("CAPTCHA_TYPE").MustString(ImageCaptcha) diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 2919fd351366..133a7cced169 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -17,11 +17,8 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/eventsource" - "code.gitea.io/gitea/modules/hcaptcha" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/mcaptcha" "code.gitea.io/gitea/modules/password" - "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -163,6 +160,10 @@ func SignIn(ctx *context.Context) { ctx.Data["PageIsLogin"] = true ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled() + if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { + context.SetCaptchaData(ctx) + } + ctx.HTML(http.StatusOK, tplSignIn) } @@ -189,6 +190,16 @@ func SignInPost(ctx *context.Context) { } form := web.GetForm(ctx).(*forms.SignInForm) + + if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { + context.SetCaptchaData(ctx) + + context.VerifyCaptcha(ctx, tplSignIn, form) + if ctx.Written() { + return + } + } + u, source, err := auth_service.UserSignIn(form.UserName, form.Password) if err != nil { if user_model.IsErrUserNotExist(err) || user_model.IsErrEmailAddressNotExist(err) { @@ -383,14 +394,7 @@ func SignUp(ctx *context.Context) { ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" - ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha - 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 + context.SetCaptchaData(ctx) ctx.Data["PageIsSignUp"] = true // Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true @@ -406,14 +410,7 @@ func SignUpPost(ctx *context.Context) { ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" - ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha - 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 + context.SetCaptchaData(ctx) ctx.Data["PageIsSignUp"] = true // Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true @@ -427,31 +424,9 @@ func SignUpPost(ctx *context.Context) { return } - if setting.Service.EnableCaptcha { - var valid bool - var err error - switch setting.Service.CaptchaType { - case setting.ImageCaptcha: - valid = context.GetImageCaptcha().VerifyReq(ctx.Req) - case setting.ReCaptcha: - valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse) - case setting.HCaptcha: - valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse) - case setting.MCaptcha: - valid, err = mcaptcha.Verify(ctx, form.McaptchaResponse) - default: - ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType)) - return - } - if err != nil { - log.Debug("%s", err.Error()) - } - - if !valid { - ctx.Data["Err_Captcha"] = true - ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUp, &form) - return - } + context.VerifyCaptcha(ctx, tplSignUp, form) + if ctx.Written() { + return } if !form.IsEmailDomainAllowed() { diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index d3211eaa5c70..c36eaee07825 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -14,10 +14,6 @@ import ( 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/hcaptcha" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/mcaptcha" - "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" auth_service "code.gitea.io/gitea/services/auth" @@ -221,28 +217,8 @@ func LinkAccountPostRegister(ctx *context.Context) { } if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha { - var valid bool - var err error - switch setting.Service.CaptchaType { - case setting.ImageCaptcha: - valid = context.GetImageCaptcha().VerifyReq(ctx.Req) - case setting.ReCaptcha: - valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse) - case setting.HCaptcha: - valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse) - case setting.MCaptcha: - valid, err = mcaptcha.Verify(ctx, form.McaptchaResponse) - default: - ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType)) - return - } - if err != nil { - log.Debug("%s", err.Error()) - } - - if !valid { - ctx.Data["Err_Captcha"] = true - ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplLinkAccount, &form) + context.VerifyCaptcha(ctx, tplLinkAccount, form) + if ctx.Written() { return } } diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index d34f4db7c014..eedf3f5c1ba0 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -13,10 +13,7 @@ import ( "code.gitea.io/gitea/modules/auth/openid" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/hcaptcha" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/mcaptcha" - "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -357,14 +354,7 @@ func RegisterOpenIDPost(ctx *context.Context) { ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsOpenIDRegister"] = true ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp - ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha - 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 + context.SetCaptchaData(ctx) ctx.Data["OpenID"] = oid if setting.Service.AllowOnlyInternalRegistration { @@ -373,42 +363,11 @@ func RegisterOpenIDPost(ctx *context.Context) { } if setting.Service.EnableCaptcha { - var valid bool - var err error - switch setting.Service.CaptchaType { - case setting.ImageCaptcha: - valid = context.GetImageCaptcha().VerifyReq(ctx.Req) - case setting.ReCaptcha: - if err := ctx.Req.ParseForm(); err != nil { - ctx.ServerError("", err) - return - } - valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse) - case setting.HCaptcha: - if err := ctx.Req.ParseForm(); err != nil { - ctx.ServerError("", err) - return - } - valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse) - case setting.MCaptcha: - if err := ctx.Req.ParseForm(); err != nil { - ctx.ServerError("", err) - return - } - valid, err = mcaptcha.Verify(ctx, form.McaptchaResponse) - default: - ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType)) - return - } - if err != nil { - log.Debug("%s", err.Error()) - } - - if !valid { - ctx.Data["Err_Captcha"] = true - ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUpOID, &form) + if err := ctx.Req.ParseForm(); err != nil { + ctx.ServerError("", err) return } + context.VerifyCaptcha(ctx, tplSignUpOID, form) } length := setting.MinPasswordLength diff --git a/services/forms/user_form.go b/services/forms/user_form.go index ed8ccf12ea7e..da30ae94d6e5 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -91,13 +91,10 @@ func (f *InstallForm) Validate(req *http.Request, errs binding.Errors) binding.E // RegisterForm form for registering type RegisterForm struct { - UserName string `binding:"Required;Username;MaxSize(40)"` - Email string `binding:"Required;MaxSize(254)"` - Password string `binding:"MaxSize(255)"` - Retype string - GRecaptchaResponse string `form:"g-recaptcha-response"` - HcaptchaResponse string `form:"h-captcha-response"` - McaptchaResponse string `form:"m-captcha-response"` + UserName string `binding:"Required;Username;MaxSize(40)"` + Email string `binding:"Required;MaxSize(254)"` + Password string `binding:"MaxSize(255)"` + Retype string } // Validate validates the fields diff --git a/services/forms/user_form_auth_openid.go b/services/forms/user_form_auth_openid.go index d1ed0a23c78a..459c938f082a 100644 --- a/services/forms/user_form_auth_openid.go +++ b/services/forms/user_form_auth_openid.go @@ -27,11 +27,8 @@ func (f *SignInOpenIDForm) Validate(req *http.Request, errs binding.Errors) bind // SignUpOpenIDForm form for signin up with OpenID type SignUpOpenIDForm struct { - UserName string `binding:"Required;Username;MaxSize(40)"` - Email string `binding:"Required;Email;MaxSize(254)"` - GRecaptchaResponse string `form:"g-recaptcha-response"` - HcaptchaResponse string `form:"h-captcha-response"` - McaptchaResponse string `form:"m-captcha-response"` + UserName string `binding:"Required;Username;MaxSize(40)"` + Email string `binding:"Required;Email;MaxSize(254)"` } // Validate validates the fields diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl new file mode 100644 index 000000000000..87b22a0720ed --- /dev/null +++ b/templates/user/auth/captcha.tmpl @@ -0,0 +1,24 @@ +{{if .EnableCaptcha}}{{if eq .CaptchaType "image"}} +
+ + {{.Captcha.CreateHTML}} +
+
+ + +
+{{else if eq .CaptchaType "recaptcha"}} +
+
+
+{{else if eq .CaptchaType "hcaptcha"}} +
+
+
+{{else if eq .CaptchaType "mcaptcha"}} +
+ {{.locale.Tr "captcha"}} +
+
+
+{{end}}{{end}} diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 18875f45a391..f14bac10eee7 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -31,6 +31,8 @@ {{end}} + {{template "user/auth/captcha" .}} +
{{end}} - {{if and .EnableCaptcha (eq .CaptchaType "image")}} -
- - {{.Captcha.CreateHTML}} -
-
- - -
- {{end}} - {{if and .EnableCaptcha (eq .CaptchaType "recaptcha")}} -
-
-
- {{end}} - {{if and .EnableCaptcha (eq .CaptchaType "hcaptcha")}} -
-
-
- {{end}} - {{if and .EnableCaptcha (eq .CaptchaType "mcaptcha")}} -
- {{.locale.Tr "captcha"}} -
-
-
- {{end}} + {{template "user/auth/captcha" .}}
diff --git a/templates/user/auth/signup_openid_register.tmpl b/templates/user/auth/signup_openid_register.tmpl index 9c0558311f01..e54600ec8220 100644 --- a/templates/user/auth/signup_openid_register.tmpl +++ b/templates/user/auth/signup_openid_register.tmpl @@ -20,31 +20,9 @@
- {{if and .EnableCaptcha (eq .CaptchaType "image")}} -
- - {{.Captcha.CreateHTML}} -
-
- - -
- {{end}} - {{if and .EnableCaptcha (eq .CaptchaType "recaptcha")}} -
-
-
- {{end}} - {{if and .EnableCaptcha (eq .CaptchaType "hcaptcha")}} -
-
-
- {{end}} - {{if and .EnableCaptcha (eq .CaptchaType "mcaptcha")}} -
-
-
- {{end}} + + {{template "user/auth/captcha" .}} +