Add Recaptcha functionality to Gitea (#4044)
This commit is contained in:
		
							parent
							
								
									54fedd4070
								
							
						
					
					
						commit
						f035dcd4f2
					
				
					 13 changed files with 163 additions and 15 deletions
				
			
		|  | @ -301,7 +301,13 @@ ENABLE_NOTIFY_MAIL = false | |||
| ENABLE_REVERSE_PROXY_AUTHENTICATION = false | ||||
| ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false | ||||
| ; Enable captcha validation for registration | ||||
| ENABLE_CAPTCHA = true | ||||
| ENABLE_CAPTCHA = false | ||||
| ; Type of captcha you want to use. Options: image, recaptcha | ||||
| CAPTCHA_TYPE = image | ||||
| ; Enable recaptcha to use Google's recaptcha service | ||||
| ; Go to https://www.google.com/recaptcha/admin to sign up for a key | ||||
| RECAPTCHA_SECRET  =  | ||||
| RECAPTCHA_SITEKEY =  | ||||
| ; Default value for KeepEmailPrivate | ||||
| ; Each new user will get the value of this setting copied into their profile | ||||
| DEFAULT_KEEP_EMAIL_PRIVATE = false | ||||
|  |  | |||
|  | @ -177,7 +177,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. | |||
| - `ENABLE_REVERSE_PROXY_AUTHENTICATION`: **false**: Enable this to allow reverse proxy authentication. | ||||
| - `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: **false**: Enable this to allow auto-registration | ||||
|    for reverse authentication. | ||||
| - `ENABLE_CAPTCHA`: **true**: Enable this to use captcha validation for registration. | ||||
| - `ENABLE_CAPTCHA`: **false**: Enable this to use captcha validation for registration. | ||||
| - `CAPTCHA_TYPE`: **image**: \[image, recaptcha\] | ||||
| - `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha  | ||||
| - `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha | ||||
| 
 | ||||
| ## Webhook (`webhook`) | ||||
| 
 | ||||
|  |  | |||
|  | @ -76,6 +76,7 @@ type RegisterForm struct { | |||
| 	Email              string `binding:"Required;Email;MaxSize(254)"` | ||||
| 	Password           string `binding:"Required;MaxSize(255)"` | ||||
| 	Retype             string | ||||
| 	GRecaptchaResponse string `form:"g-recaptcha-response"` | ||||
| } | ||||
| 
 | ||||
| // Validate valideates the fields
 | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ func (f *SignInOpenIDForm) Validate(ctx *macaron.Context, errs binding.Errors) b | |||
| type SignUpOpenIDForm struct { | ||||
| 	UserName           string `binding:"Required;AlphaDashDot;MaxSize(35)"` | ||||
| 	Email              string `binding:"Required;Email;MaxSize(254)"` | ||||
| 	GRecaptchaResponse string `form:"g-recaptcha-response"` | ||||
| } | ||||
| 
 | ||||
| // Validate valideates the fields
 | ||||
|  |  | |||
							
								
								
									
										47
									
								
								modules/recaptcha/recaptcha.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								modules/recaptcha/recaptcha.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | |||
| // Copyright 2018 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package recaptcha | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
| 
 | ||||
| // Response is the structure of JSON returned from API
 | ||||
| type Response struct { | ||||
| 	Success     bool      `json:"success"` | ||||
| 	ChallengeTS time.Time `json:"challenge_ts"` | ||||
| 	Hostname    string    `json:"hostname"` | ||||
| 	ErrorCodes  []string  `json:"error-codes"` | ||||
| } | ||||
| 
 | ||||
| const apiURL = "https://www.google.com/recaptcha/api/siteverify" | ||||
| 
 | ||||
| // Verify calls Google Recaptcha API to verify token
 | ||||
| func Verify(response string) (bool, error) { | ||||
| 	resp, err := http.PostForm(apiURL, | ||||
| 		url.Values{"secret": {setting.Service.RecaptchaSecret}, "response": {response}}) | ||||
| 	if err != nil { | ||||
| 		return false, fmt.Errorf("Failed to send CAPTCHA response: %s", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return false, fmt.Errorf("Failed to read CAPTCHA response: %s", err) | ||||
| 	} | ||||
| 	var jsonResponse Response | ||||
| 	err = json.Unmarshal(body, &jsonResponse) | ||||
| 	if err != nil { | ||||
| 		return false, fmt.Errorf("Failed to parse CAPTCHA response: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return jsonResponse.Success, nil | ||||
| } | ||||
|  | @ -75,6 +75,12 @@ const ( | |||
| 	RepoCreatingPublic             = "public" | ||||
| ) | ||||
| 
 | ||||
| // enumerates all the types of captchas
 | ||||
| const ( | ||||
| 	ImageCaptcha = "image" | ||||
| 	ReCaptcha    = "recaptcha" | ||||
| ) | ||||
| 
 | ||||
| // settings
 | ||||
| var ( | ||||
| 	// AppVer settings
 | ||||
|  | @ -1165,6 +1171,9 @@ var Service struct { | |||
| 	EnableReverseProxyAuth                  bool | ||||
| 	EnableReverseProxyAutoRegister          bool | ||||
| 	EnableCaptcha                           bool | ||||
| 	CaptchaType                             string | ||||
| 	RecaptchaSecret                         string | ||||
| 	RecaptchaSitekey                        string | ||||
| 	DefaultKeepEmailPrivate                 bool | ||||
| 	DefaultAllowCreateOrganization          bool | ||||
| 	EnableTimetracking                      bool | ||||
|  | @ -1189,7 +1198,10 @@ func newService() { | |||
| 	Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() | ||||
| 	Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool() | ||||
| 	Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() | ||||
| 	Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool() | ||||
| 	Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool(false) | ||||
| 	Service.CaptchaType = sec.Key("CAPTCHA_TYPE").MustString(ImageCaptcha) | ||||
| 	Service.RecaptchaSecret = sec.Key("RECAPTCHA_SECRET").MustString("") | ||||
| 	Service.RecaptchaSitekey = sec.Key("RECAPTCHA_SITEKEY").MustString("") | ||||
| 	Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool() | ||||
| 	Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true) | ||||
| 	Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true) | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -80,6 +80,23 @@ | |||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @media only screen and (min-width: 768px) { | ||||
|     .g-recaptcha { | ||||
|         margin: 0 auto !important; | ||||
|         width: 304px; | ||||
|         padding-left: 30px; | ||||
|     } | ||||
| } | ||||
| @media screen and (max-height: 575px){ | ||||
|     #rc-imageselect, .g-recaptcha { | ||||
|         transform:scale(0.77); | ||||
|         -webkit-transform:scale(0.77); | ||||
|         transform-origin:0 0; | ||||
|         -webkit-transform-origin:0 0; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .user.activate, | ||||
| .user.forgot.password, | ||||
| .user.reset.password, | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/recaptcha" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
|  | @ -641,6 +642,8 @@ func LinkAccount(ctx *context.Context) { | |||
| 	ctx.Data["Title"] = ctx.Tr("link_account") | ||||
| 	ctx.Data["LinkAccountMode"] = true | ||||
| 	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha | ||||
| 	ctx.Data["CaptchaType"] = setting.Service.CaptchaType | ||||
| 	ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey | ||||
| 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration | ||||
| 	ctx.Data["ShowRegistrationButton"] = false | ||||
| 
 | ||||
|  | @ -666,6 +669,8 @@ func LinkAccountPostSignIn(ctx *context.Context, signInForm auth.SignInForm) { | |||
| 	ctx.Data["LinkAccountMode"] = true | ||||
| 	ctx.Data["LinkAccountModeSignIn"] = true | ||||
| 	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha | ||||
| 	ctx.Data["CaptchaType"] = setting.Service.CaptchaType | ||||
| 	ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey | ||||
| 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration | ||||
| 	ctx.Data["ShowRegistrationButton"] = false | ||||
| 
 | ||||
|  | @ -732,6 +737,8 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au | |||
| 	ctx.Data["LinkAccountMode"] = true | ||||
| 	ctx.Data["LinkAccountModeRegister"] = true | ||||
| 	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha | ||||
| 	ctx.Data["CaptchaType"] = setting.Service.CaptchaType | ||||
| 	ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey | ||||
| 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration | ||||
| 	ctx.Data["ShowRegistrationButton"] = false | ||||
| 
 | ||||
|  | @ -755,12 +762,21 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if setting.Service.EnableCaptcha && !cpt.VerifyReq(ctx.Req) { | ||||
| 	if setting.Service.EnableCaptcha && setting.Service.CaptchaType == setting.ImageCaptcha && !cpt.VerifyReq(ctx.Req) { | ||||
| 		ctx.Data["Err_Captcha"] = true | ||||
| 		ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplLinkAccount, &form) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if setting.Service.EnableCaptcha && setting.Service.CaptchaType == setting.ReCaptcha { | ||||
| 		valid, _ := recaptcha.Verify(form.GRecaptchaResponse) | ||||
| 		if !valid { | ||||
| 			ctx.Data["Err_Captcha"] = true | ||||
| 			ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplLinkAccount, &form) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	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) | ||||
|  | @ -858,6 +874,9 @@ func SignUp(ctx *context.Context) { | |||
| 
 | ||||
| 	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha | ||||
| 
 | ||||
| 	ctx.Data["CaptchaType"] = setting.Service.CaptchaType | ||||
| 	ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey | ||||
| 
 | ||||
| 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration | ||||
| 
 | ||||
| 	ctx.HTML(200, tplSignUp) | ||||
|  | @ -871,6 +890,9 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo | |||
| 
 | ||||
| 	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha | ||||
| 
 | ||||
| 	ctx.Data["CaptchaType"] = setting.Service.CaptchaType | ||||
| 	ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey | ||||
| 
 | ||||
| 	//Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
 | ||||
| 	if !setting.Service.ShowRegistrationButton { | ||||
| 		ctx.Error(403) | ||||
|  | @ -882,12 +904,21 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if setting.Service.EnableCaptcha && !cpt.VerifyReq(ctx.Req) { | ||||
| 	if setting.Service.EnableCaptcha && setting.Service.CaptchaType == setting.ImageCaptcha && !cpt.VerifyReq(ctx.Req) { | ||||
| 		ctx.Data["Err_Captcha"] = true | ||||
| 		ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUp, &form) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if setting.Service.EnableCaptcha && setting.Service.CaptchaType == setting.ReCaptcha { | ||||
| 		valid, _ := recaptcha.Verify(form.GRecaptchaResponse) | ||||
| 		if !valid { | ||||
| 			ctx.Data["Err_Captcha"] = true | ||||
| 			ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUp, &form) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if form.Password != form.Retype { | ||||
| 		ctx.Data["Err_Password"] = true | ||||
| 		ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplSignUp, &form) | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/generate" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/recaptcha" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 
 | ||||
| 	"github.com/go-macaron/captcha" | ||||
|  | @ -308,6 +309,8 @@ func RegisterOpenID(ctx *context.Context) { | |||
| 	ctx.Data["PageIsOpenIDRegister"] = true | ||||
| 	ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp | ||||
| 	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha | ||||
| 	ctx.Data["CaptchaType"] = setting.Service.CaptchaType | ||||
| 	ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey | ||||
| 	ctx.Data["OpenID"] = oid | ||||
| 	userName, _ := ctx.Session.Get("openid_determined_username").(string) | ||||
| 	if userName != "" { | ||||
|  | @ -333,14 +336,26 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si | |||
| 	ctx.Data["PageIsOpenIDRegister"] = true | ||||
| 	ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp | ||||
| 	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha | ||||
| 	ctx.Data["CaptchaType"] = setting.Service.CaptchaType | ||||
| 	ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey | ||||
| 	ctx.Data["OpenID"] = oid | ||||
| 
 | ||||
| 	if setting.Service.EnableCaptcha && !cpt.VerifyReq(ctx.Req) { | ||||
| 	if setting.Service.EnableCaptcha && setting.Service.CaptchaType == setting.ImageCaptcha && !cpt.VerifyReq(ctx.Req) { | ||||
| 		ctx.Data["Err_Captcha"] = true | ||||
| 		ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUpOID, &form) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if setting.Service.EnableCaptcha && setting.Service.CaptchaType == setting.ReCaptcha { | ||||
| 		ctx.Req.ParseForm() | ||||
| 		valid, _ := recaptcha.Verify(form.GRecaptchaResponse) | ||||
| 		if !valid { | ||||
| 			ctx.Data["Err_Captcha"] = true | ||||
| 			ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUpOID, &form) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	len := setting.MinPasswordLength | ||||
| 	if len < 256 { | ||||
| 		len = 256 | ||||
|  |  | |||
|  | @ -67,6 +67,11 @@ | |||
| {{if .RequireU2F}} | ||||
| 	<script src="{{AppSubUrl}}/vendor/plugins/u2f/index.js"></script> | ||||
| {{end}} | ||||
| {{if .EnableCaptcha}} | ||||
| 	{{if eq .CaptchaType "recaptcha"}} | ||||
| 		<script src="https://www.google.com/recaptcha/api.js" async></script> | ||||
| 	{{end}} | ||||
| {{end}} | ||||
| {{if .RequireTribute}} | ||||
| 	<script src="{{AppSubUrl}}/vendor/plugins/tribute/tribute.min.js"></script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ | |||
| 							<label for="retype">{{.i18n.Tr "re_type"}}</label> | ||||
| 							<input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="off" required> | ||||
| 						</div> | ||||
| 						{{if .EnableCaptcha}} | ||||
| 						{{if and .EnableCaptcha (eq .CaptchaType "image")}} | ||||
| 							<div class="inline field"> | ||||
| 								<label></label> | ||||
| 								{{.Captcha.CreateHtml}} | ||||
|  | @ -39,6 +39,11 @@ | |||
| 								<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off"> | ||||
| 							</div> | ||||
| 						{{end}} | ||||
| 						{{if and .EnableCaptcha (eq .CaptchaType "recaptcha")}} | ||||
| 							<div class="inline field required"> | ||||
| 								<div class="g-recaptcha" data-sitekey="{{ .RecaptchaSitekey }}"></div> | ||||
| 							</div> | ||||
| 						{{end}} | ||||
| 
 | ||||
| 						<div class="inline field"> | ||||
| 							<label></label> | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ | |||
| 						<label for="email">{{.i18n.Tr "email"}}</label> | ||||
| 						<input id="email" name="email" type="email" value="{{.email}}" required> | ||||
| 					</div> | ||||
| 					{{if .EnableCaptcha}} | ||||
| 					{{if and .EnableCaptcha (eq .CaptchaType "image")}} | ||||
| 						<div class="inline field"> | ||||
| 							<label></label> | ||||
| 							{{.Captcha.CreateHtml}} | ||||
|  | @ -30,6 +30,11 @@ | |||
| 							<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off"> | ||||
| 						</div> | ||||
| 					{{end}} | ||||
| 					{{if and .EnableCaptcha (eq .CaptchaType "recaptcha")}} | ||||
| 						<div class="inline field required"> | ||||
| 							<div class="g-recaptcha" data-sitekey="{{ .RecaptchaSitekey }}"></div> | ||||
| 						</div> | ||||
| 					{{end}} | ||||
| 					<div class="inline field"> | ||||
| 						<label for="openid">OpenID URI</label> | ||||
| 						<input id="openid" value="{{ .OpenID }}" readonly> | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue