Skip to content

Commit 68e934a

Browse files
Xinyu Zhouzeripath
Xinyu Zhou
andauthored
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 <i@sourcehut.net> Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: Andrew Thornton <art27@cantab.net>
1 parent e77b764 commit 68e934a

File tree

14 files changed

+128
-180
lines changed

14 files changed

+128
-180
lines changed

custom/conf/app.example.ini

+3
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,9 @@ ROUTER = console
759759
;; Enable captcha validation for registration
760760
;ENABLE_CAPTCHA = false
761761
;;
762+
;; Enable this to require captcha validation for login
763+
;REQUIRE_CAPTCHA_FOR_LOGIN = false
764+
;;
762765
;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha.
763766
;CAPTCHA_TYPE = image
764767
;;

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+1
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
634634
- `ENABLE_REVERSE_PROXY_FULL_NAME`: **false**: Enable this to allow to auto-registration with a
635635
provided full name for the user.
636636
- `ENABLE_CAPTCHA`: **false**: Enable this to use captcha validation for registration.
637+
- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: Enable this to require captcha validation for login. You also must enable `ENABLE_CAPTCHA`.
637638
- `REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA`: **false**: Enable this to force captcha validation
638639
even for External Accounts (i.e. GitHub, OpenID Connect, etc). You also must enable `ENABLE_CAPTCHA`.
639640
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha\]

docs/content/doc/advanced/config-cheat-sheet.zh-cn.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ menu:
145145
- `ENABLE_NOTIFY_MAIL`: 是否发送工单创建等提醒邮件,需要 `Mailer` 被激活。
146146
- `ENABLE_REVERSE_PROXY_AUTHENTICATION`: 允许反向代理认证,更多细节见:https://github.com/gogits/gogs/issues/165
147147
- `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: 允许通过反向认证做自动注册。
148-
- `ENABLE_CAPTCHA`: 注册时使用图片验证码。
148+
- `ENABLE_CAPTCHA`: **false**: 注册时使用图片验证码。
149+
- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: 登录时需要图片验证码。需要同时开启 `ENABLE_CAPTCHA`
149150

150151
### Service - Expore (`service.explore`)
151152

modules/context/captcha.go

+59
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@
55
package context
66

77
import (
8+
"fmt"
89
"sync"
910

11+
"code.gitea.io/gitea/modules/base"
1012
"code.gitea.io/gitea/modules/cache"
13+
"code.gitea.io/gitea/modules/hcaptcha"
14+
"code.gitea.io/gitea/modules/log"
15+
"code.gitea.io/gitea/modules/mcaptcha"
16+
"code.gitea.io/gitea/modules/recaptcha"
1117
"code.gitea.io/gitea/modules/setting"
1218

1319
"gitea.com/go-chi/captcha"
@@ -28,3 +34,56 @@ func GetImageCaptcha() *captcha.Captcha {
2834
})
2935
return cpt
3036
}
37+
38+
// SetCaptchaData sets common captcha data
39+
func SetCaptchaData(ctx *Context) {
40+
if !setting.Service.EnableCaptcha {
41+
return
42+
}
43+
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
44+
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
45+
ctx.Data["Captcha"] = GetImageCaptcha()
46+
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
47+
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
48+
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
49+
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
50+
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
51+
}
52+
53+
const (
54+
gRecaptchaResponseField = "g-recaptcha-response"
55+
hCaptchaResponseField = "h-captcha-response"
56+
mCaptchaResponseField = "m-captcha-response"
57+
)
58+
59+
// VerifyCaptcha verifies Captcha data
60+
// No-op if captchas are not enabled
61+
func VerifyCaptcha(ctx *Context, tpl base.TplName, form interface{}) {
62+
if !setting.Service.EnableCaptcha {
63+
return
64+
}
65+
66+
var valid bool
67+
var err error
68+
switch setting.Service.CaptchaType {
69+
case setting.ImageCaptcha:
70+
valid = GetImageCaptcha().VerifyReq(ctx.Req)
71+
case setting.ReCaptcha:
72+
valid, err = recaptcha.Verify(ctx, ctx.Req.Form.Get(gRecaptchaResponseField))
73+
case setting.HCaptcha:
74+
valid, err = hcaptcha.Verify(ctx, ctx.Req.Form.Get(hCaptchaResponseField))
75+
case setting.MCaptcha:
76+
valid, err = mcaptcha.Verify(ctx, ctx.Req.Form.Get(mCaptchaResponseField))
77+
default:
78+
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
79+
return
80+
}
81+
if err != nil {
82+
log.Debug("%v", err)
83+
}
84+
85+
if !valid {
86+
ctx.Data["Err_Captcha"] = true
87+
ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tpl, form)
88+
}
89+
}

modules/setting/service.go

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var Service = struct {
4040
EnableReverseProxyEmail bool
4141
EnableReverseProxyFullName bool
4242
EnableCaptcha bool
43+
RequireCaptchaForLogin bool
4344
RequireExternalRegistrationCaptcha bool
4445
RequireExternalRegistrationPassword bool
4546
CaptchaType string
@@ -130,6 +131,7 @@ func newService() {
130131
Service.EnableReverseProxyEmail = sec.Key("ENABLE_REVERSE_PROXY_EMAIL").MustBool()
131132
Service.EnableReverseProxyFullName = sec.Key("ENABLE_REVERSE_PROXY_FULL_NAME").MustBool()
132133
Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool(false)
134+
Service.RequireCaptchaForLogin = sec.Key("REQUIRE_CAPTCHA_FOR_LOGIN").MustBool(false)
133135
Service.RequireExternalRegistrationCaptcha = sec.Key("REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA").MustBool(Service.EnableCaptcha)
134136
Service.RequireExternalRegistrationPassword = sec.Key("REQUIRE_EXTERNAL_REGISTRATION_PASSWORD").MustBool()
135137
Service.CaptchaType = sec.Key("CAPTCHA_TYPE").MustString(ImageCaptcha)

routers/web/auth/auth.go

+19-44
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ import (
1717
"code.gitea.io/gitea/modules/base"
1818
"code.gitea.io/gitea/modules/context"
1919
"code.gitea.io/gitea/modules/eventsource"
20-
"code.gitea.io/gitea/modules/hcaptcha"
2120
"code.gitea.io/gitea/modules/log"
22-
"code.gitea.io/gitea/modules/mcaptcha"
2321
"code.gitea.io/gitea/modules/password"
24-
"code.gitea.io/gitea/modules/recaptcha"
2522
"code.gitea.io/gitea/modules/session"
2623
"code.gitea.io/gitea/modules/setting"
2724
"code.gitea.io/gitea/modules/timeutil"
@@ -163,6 +160,10 @@ func SignIn(ctx *context.Context) {
163160
ctx.Data["PageIsLogin"] = true
164161
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled()
165162

163+
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
164+
context.SetCaptchaData(ctx)
165+
}
166+
166167
ctx.HTML(http.StatusOK, tplSignIn)
167168
}
168169

@@ -189,6 +190,16 @@ func SignInPost(ctx *context.Context) {
189190
}
190191

191192
form := web.GetForm(ctx).(*forms.SignInForm)
193+
194+
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
195+
context.SetCaptchaData(ctx)
196+
197+
context.VerifyCaptcha(ctx, tplSignIn, form)
198+
if ctx.Written() {
199+
return
200+
}
201+
}
202+
192203
u, source, err := auth_service.UserSignIn(form.UserName, form.Password)
193204
if err != nil {
194205
if user_model.IsErrUserNotExist(err) || user_model.IsErrEmailAddressNotExist(err) {
@@ -383,14 +394,7 @@ func SignUp(ctx *context.Context) {
383394

384395
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
385396

386-
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
387-
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
388-
ctx.Data["Captcha"] = context.GetImageCaptcha()
389-
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
390-
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
391-
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
392-
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
393-
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
397+
context.SetCaptchaData(ctx)
394398
ctx.Data["PageIsSignUp"] = true
395399

396400
// Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
@@ -406,14 +410,7 @@ func SignUpPost(ctx *context.Context) {
406410

407411
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
408412

409-
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
410-
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
411-
ctx.Data["Captcha"] = context.GetImageCaptcha()
412-
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
413-
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
414-
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
415-
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
416-
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
413+
context.SetCaptchaData(ctx)
417414
ctx.Data["PageIsSignUp"] = true
418415

419416
// Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
@@ -427,31 +424,9 @@ func SignUpPost(ctx *context.Context) {
427424
return
428425
}
429426

430-
if setting.Service.EnableCaptcha {
431-
var valid bool
432-
var err error
433-
switch setting.Service.CaptchaType {
434-
case setting.ImageCaptcha:
435-
valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
436-
case setting.ReCaptcha:
437-
valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
438-
case setting.HCaptcha:
439-
valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
440-
case setting.MCaptcha:
441-
valid, err = mcaptcha.Verify(ctx, form.McaptchaResponse)
442-
default:
443-
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
444-
return
445-
}
446-
if err != nil {
447-
log.Debug("%s", err.Error())
448-
}
449-
450-
if !valid {
451-
ctx.Data["Err_Captcha"] = true
452-
ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUp, &form)
453-
return
454-
}
427+
context.VerifyCaptcha(ctx, tplSignUp, form)
428+
if ctx.Written() {
429+
return
455430
}
456431

457432
if !form.IsEmailDomainAllowed() {

routers/web/auth/linkaccount.go

+2-26
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ import (
1414
user_model "code.gitea.io/gitea/models/user"
1515
"code.gitea.io/gitea/modules/base"
1616
"code.gitea.io/gitea/modules/context"
17-
"code.gitea.io/gitea/modules/hcaptcha"
18-
"code.gitea.io/gitea/modules/log"
19-
"code.gitea.io/gitea/modules/mcaptcha"
20-
"code.gitea.io/gitea/modules/recaptcha"
2117
"code.gitea.io/gitea/modules/setting"
2218
"code.gitea.io/gitea/modules/web"
2319
auth_service "code.gitea.io/gitea/services/auth"
@@ -221,28 +217,8 @@ func LinkAccountPostRegister(ctx *context.Context) {
221217
}
222218

223219
if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha {
224-
var valid bool
225-
var err error
226-
switch setting.Service.CaptchaType {
227-
case setting.ImageCaptcha:
228-
valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
229-
case setting.ReCaptcha:
230-
valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
231-
case setting.HCaptcha:
232-
valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
233-
case setting.MCaptcha:
234-
valid, err = mcaptcha.Verify(ctx, form.McaptchaResponse)
235-
default:
236-
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
237-
return
238-
}
239-
if err != nil {
240-
log.Debug("%s", err.Error())
241-
}
242-
243-
if !valid {
244-
ctx.Data["Err_Captcha"] = true
245-
ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplLinkAccount, &form)
220+
context.VerifyCaptcha(ctx, tplLinkAccount, form)
221+
if ctx.Written() {
246222
return
247223
}
248224
}

routers/web/auth/openid.go

+4-45
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ import (
1313
"code.gitea.io/gitea/modules/auth/openid"
1414
"code.gitea.io/gitea/modules/base"
1515
"code.gitea.io/gitea/modules/context"
16-
"code.gitea.io/gitea/modules/hcaptcha"
1716
"code.gitea.io/gitea/modules/log"
18-
"code.gitea.io/gitea/modules/mcaptcha"
19-
"code.gitea.io/gitea/modules/recaptcha"
2017
"code.gitea.io/gitea/modules/setting"
2118
"code.gitea.io/gitea/modules/util"
2219
"code.gitea.io/gitea/modules/web"
@@ -357,14 +354,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
357354
ctx.Data["PageIsSignIn"] = true
358355
ctx.Data["PageIsOpenIDRegister"] = true
359356
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
360-
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
361-
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
362-
ctx.Data["Captcha"] = context.GetImageCaptcha()
363-
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
364-
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
365-
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
366-
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
367-
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
357+
context.SetCaptchaData(ctx)
368358
ctx.Data["OpenID"] = oid
369359

370360
if setting.Service.AllowOnlyInternalRegistration {
@@ -373,42 +363,11 @@ func RegisterOpenIDPost(ctx *context.Context) {
373363
}
374364

375365
if setting.Service.EnableCaptcha {
376-
var valid bool
377-
var err error
378-
switch setting.Service.CaptchaType {
379-
case setting.ImageCaptcha:
380-
valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
381-
case setting.ReCaptcha:
382-
if err := ctx.Req.ParseForm(); err != nil {
383-
ctx.ServerError("", err)
384-
return
385-
}
386-
valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
387-
case setting.HCaptcha:
388-
if err := ctx.Req.ParseForm(); err != nil {
389-
ctx.ServerError("", err)
390-
return
391-
}
392-
valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
393-
case setting.MCaptcha:
394-
if err := ctx.Req.ParseForm(); err != nil {
395-
ctx.ServerError("", err)
396-
return
397-
}
398-
valid, err = mcaptcha.Verify(ctx, form.McaptchaResponse)
399-
default:
400-
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
401-
return
402-
}
403-
if err != nil {
404-
log.Debug("%s", err.Error())
405-
}
406-
407-
if !valid {
408-
ctx.Data["Err_Captcha"] = true
409-
ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUpOID, &form)
366+
if err := ctx.Req.ParseForm(); err != nil {
367+
ctx.ServerError("", err)
410368
return
411369
}
370+
context.VerifyCaptcha(ctx, tplSignUpOID, form)
412371
}
413372

414373
length := setting.MinPasswordLength

services/forms/user_form.go

+4-7
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,10 @@ func (f *InstallForm) Validate(req *http.Request, errs binding.Errors) binding.E
9191

9292
// RegisterForm form for registering
9393
type RegisterForm struct {
94-
UserName string `binding:"Required;Username;MaxSize(40)"`
95-
Email string `binding:"Required;MaxSize(254)"`
96-
Password string `binding:"MaxSize(255)"`
97-
Retype string
98-
GRecaptchaResponse string `form:"g-recaptcha-response"`
99-
HcaptchaResponse string `form:"h-captcha-response"`
100-
McaptchaResponse string `form:"m-captcha-response"`
94+
UserName string `binding:"Required;Username;MaxSize(40)"`
95+
Email string `binding:"Required;MaxSize(254)"`
96+
Password string `binding:"MaxSize(255)"`
97+
Retype string
10198
}
10299

103100
// Validate validates the fields

services/forms/user_form_auth_openid.go

+2-5
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,8 @@ func (f *SignInOpenIDForm) Validate(req *http.Request, errs binding.Errors) bind
2727

2828
// SignUpOpenIDForm form for signin up with OpenID
2929
type SignUpOpenIDForm struct {
30-
UserName string `binding:"Required;Username;MaxSize(40)"`
31-
Email string `binding:"Required;Email;MaxSize(254)"`
32-
GRecaptchaResponse string `form:"g-recaptcha-response"`
33-
HcaptchaResponse string `form:"h-captcha-response"`
34-
McaptchaResponse string `form:"m-captcha-response"`
30+
UserName string `binding:"Required;Username;MaxSize(40)"`
31+
Email string `binding:"Required;Email;MaxSize(254)"`
3532
}
3633

3734
// Validate validates the fields

templates/user/auth/captcha.tmpl

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{{if .EnableCaptcha}}{{if eq .CaptchaType "image"}}
2+
<div class="inline field">
3+
<label>{{/* This is CAPTCHA field */}}</label>
4+
{{.Captcha.CreateHTML}}
5+
</div>
6+
<div class="required inline field {{if .Err_Captcha}}error{{end}}">
7+
<label for="captcha">{{.locale.Tr "captcha"}}</label>
8+
<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off">
9+
</div>
10+
{{else if eq .CaptchaType "recaptcha"}}
11+
<div class="inline field required">
12+
<div class="g-recaptcha" data-sitekey="{{.RecaptchaSitekey}}"></div>
13+
</div>
14+
{{else if eq .CaptchaType "hcaptcha"}}
15+
<div class="inline field required">
16+
<div class="h-captcha" data-sitekey="{{.HcaptchaSitekey}}"></div>
17+
</div>
18+
{{else if eq .CaptchaType "mcaptcha"}}
19+
<div class="inline field df ac db-small captcha-field">
20+
<span>{{.locale.Tr "captcha"}}</span>
21+
<div class="border-secondary w-100-small" id="mcaptcha__widget-container" style="width: 50%; height: 5em"></div>
22+
<div class="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div>
23+
</div>
24+
{{end}}{{end}}

0 commit comments

Comments
 (0)