Skip to content

Commit

Permalink
introducing recaptcha v3 api and adding more options for verification (
Browse files Browse the repository at this point in the history
  • Loading branch information
ezzarghili authored Dec 26, 2018
1 parent 6b2824b commit 980c1d9
Show file tree
Hide file tree
Showing 3 changed files with 638 additions and 65 deletions.
80 changes: 64 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,101 @@

[![Build Status](https://travis-ci.org/ezzarghili/recaptcha-go.svg?branch=master)](https://travis-ci.org/ezzarghili/recaptcha-go)

Google reCAPTCHA v2 form submittion in golang
Google reCAPTCHA v2 & v3 form submittion verification in golang

## Usage

Install the package in your environment by using a stable API version, see latest version in release page
The API has changed form last version hence the new major version change.
Old API is still available using the package `gopkg.in/ezzarghili/recaptcha-go.v2` although it does not provide all options available in this version
As always install the package in your environment by using a stable API version, see latest version in release page.

```bash
go get gopkg.in/ezzarghili/recaptcha-go.v2
go get -u gopkg.in/ezzarghili/recaptcha-go.v3
```
### recaptcha v2 API
```go
import "gopkg.in/ezzarghili/recaptcha-go.v3"
func main(){
captcha := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V2, timeout) // for v2 API get your secret from https://www.google.com/recaptcha/admin
}
```

Now everytime you need to verify a V2 API client with no special options request use

```go
err := captcha.Verify(recaptchaResponse)
if err != nil {
// do something with err (log?)
}
// proceed
```
For specific options use the `VerifyWithOptions` method
Availavle options for the v2 api are:

To use it within your own code
```go
Hostname string
ApkPackageName string
ResponseTime float64
RemoteIP string
```
Other v3 options are ignored and method will return nil when succeeded

```go
err := captcha.VerifyWithOptions(recaptchaResponse, , VerifyOption{RemoteIP: "123.123.123.123"})
if err != nil {
// do something with err (log?)
}
// proceed
```

### recaptcha v3 API
```go
import "github.com/ezzarghili/recaptcha-go"
import "github.com/ezzarghili/recaptcha-go.v3"
func main(){
captcha := recaptcha.NewReCAPTCHA(recaptchaSecret) // get your secret from https://www.google.com/recaptcha/admin
captcha := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V3, timeout) // for v3 API use https://g.co/recaptcha/v3 (apperently the same admin UI at the time of writing)
}
```

Now everytime you need to verify a client request use
Now everytime you need to verify a V2 API client with no special options request use

```go
success, err := captcha.Verify(recaptchaResponse, ClientRemoteIP)
if err !=nil {
err := captcha.Verify(recaptchaResponse)
if err != nil {
// do something with err (log?)
}
// proceed with success (true|false)
// proceed
```
For specific options use the `VerifyWithOptions` method
Availavle options for the v3 api are:

or
```go
Treshold float32
Action string
Hostname string
ApkPackageName string
ResponseTime float64
RemoteIP string
```

```go
success, err := captcha.VerifyNoRemoteIP(recaptchaResponse)
if err !=nil {
err := captcha.VerifyWithOptions(recaptchaResponse, , VerifyOption{Action: "hompage", Treshold: 0.8})
if err != nil {
// do something with err (log?)
}
// proceed with success (true|false)
// proceed
```

while `recaptchaResponse` is the form value with name `g-recaptcha-response` sent back by recaptcha server and set for you in the form when user answers the challenge

Both `recaptcha.Verify` and `recaptcha.VerifyNoRemoteIP` return a `bool` and `error` values `(bool, error)`
Both `recaptcha.Verify` and `recaptcha.VerifyWithOptions` return a `error` or `nil` if successful

Use the `error` to check for issues with the secret, connection with the server, options mismatches and incorrect solution.

Use the `error` to check for issues with the secret and connection in the server, and use the `bool` value to verify if the client answered the challenge correctly
This version made timeout explcit to make sure users have the possiblity to set the underling http client timeout suitable for their implemetation.

### Run Tests
Use the standard go means of running test.
You can also check examples of usable in the tests.

```
go test
Expand Down
133 changes: 109 additions & 24 deletions recaptcha.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,61 +11,106 @@ import (

const reCAPTCHALink = "https://www.google.com/recaptcha/api/siteverify"

// VERSION the recaptcha api version
type VERSION int8

const (
// V2 recaptcha api v2
V2 VERSION = iota
// V3 recaptcha api v3, more details can be found here : https://developers.google.com/recaptcha/docs/v3
V3
DEFAULT_TRESHOLD float32 = 0.5
)

type reCHAPTCHARequest struct {
Secret string `json:"secret"`
Response string `json:"response"`
RemoteIP string `json:"remoteip,omitempty"`
}

type reCHAPTCHAResponse struct {
Success bool `json:"success"`
ChallengeTS time.Time `json:"challenge_ts"`
Hostname string `json:"hostname"`
ErrorCodes []string `json:"error-codes,omitempty"`
Success bool `json:"success"`
ChallengeTS time.Time `json:"challenge_ts"`
Hostname string `json:"hostname,omitempty"`
ApkPackageName string `json:"apk_package_name,omitempty"`
Action string `json:"action,omitempty"`
Score float32 `json:"score,omitempty"`
ErrorCodes []string `json:"error-codes,omitempty"`
}

// custom client so we can mock in tests
type netClient interface {
PostForm(url string, formValues url.Values) (resp *http.Response, err error)
}

type clock interface {
Since(t time.Time) time.Duration
}

type realClock struct {
}

func (realClock) Since(t time.Time) time.Duration {
return time.Since(t)
}

// ReCAPTCHA recpatcha holder struct, make adding mocking code simpler
type ReCAPTCHA struct {
Client netClient
Secret string
ReCAPTCHALink string
Version VERSION
Timeout uint
horloge clock
}

// Create new ReCAPTCHA with the reCAPTCHA secret optained from https://www.google.com/recaptcha/admin
func NewReCAPTCHA(ReCAPTCHASecret string) (ReCAPTCHA, error) {
// NewReCAPTCHA Create new ReCAPTCHA with the v2 reCAPTCHA secret optained from https://www.google.com/recaptcha/admin
// or https://www.google.com/recaptcha/admin
func NewReCAPTCHA(ReCAPTCHASecret string, version VERSION, timeout uint) (ReCAPTCHA, error) {
if ReCAPTCHASecret == "" {
return ReCAPTCHA{}, fmt.Errorf("Recaptcha secret cannot be blank.")
return ReCAPTCHA{}, fmt.Errorf("recaptcha secret cannot be blank")
}
return ReCAPTCHA{
Client: &http.Client{
// Go http client does not set a default timeout for request, so we need
// to set one for worse cases when the server hang, we need to make this available in the API
// to make it possible this library's users to change it, for now a 10s timeout seems reasonable
Timeout: 10 * time.Second,
Timeout: time.Duration(timeout) * time.Second,
},
horloge: &realClock{},
Secret: ReCAPTCHASecret,
ReCAPTCHALink: reCAPTCHALink,
Timeout: timeout,
Version: version,
}, nil
}

// Verify returns (true, nil) if no error the client answered the challenge correctly and have correct remoteIP
func (r *ReCAPTCHA) Verify(challengeResponse string, remoteIP string) (bool, error) {
body := reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse, RemoteIP: remoteIP}
return r.confirm(body)
func (r *ReCAPTCHA) Verify(challengeResponse string) error {
body := reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse}
return r.confirm(body, VerifyOption{})
}

// VerifyNoRemoteIP returns (true, nil) if no error and the client answered the challenge correctly
func (r *ReCAPTCHA) VerifyNoRemoteIP(challengeResponse string) (bool, error) {
body := reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse}
return r.confirm(body)
// VerifyOption verification options expected for the challenge
type VerifyOption struct {
Treshold float32 // ignored in v2 recaptcha
Action string // ignored in v2 recaptcha
Hostname string
ApkPackageName string
ResponseTime float64
RemoteIP string
}

func (r *ReCAPTCHA) confirm(recaptcha reCHAPTCHARequest) (Ok bool, Err error) {
Ok, Err = false, nil
// VerifyWithOptions returns (true, nil) if no error the client answered the challenge correctly and have correct remoteIP
func (r *ReCAPTCHA) VerifyWithOptions(challengeResponse string, options VerifyOption) error {
var body reCHAPTCHARequest
if options.RemoteIP == "" {
body = reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse}
} else {
body = reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse, RemoteIP: options.RemoteIP}
}
return r.confirm(body, options)
}

func (r *ReCAPTCHA) confirm(recaptcha reCHAPTCHARequest, options VerifyOption) (Err error) {
Err = nil
var formValues url.Values
if recaptcha.RemoteIP != "" {
formValues = url.Values{"secret": {recaptcha.Secret}, "remoteip": {recaptcha.RemoteIP}, "response": {recaptcha.Response}}
Expand All @@ -74,21 +119,61 @@ func (r *ReCAPTCHA) confirm(recaptcha reCHAPTCHARequest) (Ok bool, Err error) {
}
response, err := r.Client.PostForm(r.ReCAPTCHALink, formValues)
if err != nil {
Err = fmt.Errorf("error posting to recaptcha endpoint: %s", err)
Err = fmt.Errorf("error posting to recaptcha endpoint: '%s'", err)
return
}
defer response.Body.Close()
resultBody, err := ioutil.ReadAll(response.Body)
if err != nil {
Err = fmt.Errorf("couldn't read response body: %s", err)
Err = fmt.Errorf("couldn't read response body: '%s'", err)
return
}
var result reCHAPTCHAResponse
err = json.Unmarshal(resultBody, &result)
if err != nil {
Err = fmt.Errorf("invalid response body json: %s", err)
Err = fmt.Errorf("invalid response body json: '%s'", err)
return
}

if options.Hostname != "" && options.Hostname != result.Hostname {
Err = fmt.Errorf("invalid response hostname '%s', while expecting '%s'", result.Hostname, options.Hostname)
return
}

if options.ApkPackageName != "" && options.ApkPackageName != result.ApkPackageName {
Err = fmt.Errorf("invalid response ApkPackageName '%s', while expecting '%s'", result.ApkPackageName, options.ApkPackageName)
return
}
Ok = result.Success

if options.ResponseTime != 0 {
duration := r.horloge.Since(result.ChallengeTS).Seconds()
if options.ResponseTime < duration {
Err = fmt.Errorf("time spent in resolving challenge '%f', while expecting maximum '%f'", duration, options.ResponseTime)
return
}
}
if r.Version == V3 {
if options.Action != "" && options.Action != result.Action {
Err = fmt.Errorf("invalid response action '%s', while expecting '%s'", result.Action, options.Action)
return
}
if options.Treshold != 0 && options.Treshold >= result.Score {
Err = fmt.Errorf("received score '%f', while expecting minimum '%f'", result.Score, options.Treshold)
return
}
if options.Treshold == 0 && DEFAULT_TRESHOLD >= result.Score {
Err = fmt.Errorf("received score '%f', while expecting minimum '%f'", result.Score, DEFAULT_TRESHOLD)
return
}
}
if result.ErrorCodes != nil {
Err = fmt.Errorf("remote error codes: %v", result.ErrorCodes)
return
}
if !result.Success && recaptcha.RemoteIP != "" {
Err = fmt.Errorf("invalid challenge solution or remote IP")
} else if !result.Success {
Err = fmt.Errorf("invalid challenge solution")
}
return
}
Loading

0 comments on commit 980c1d9

Please sign in to comment.