Skip to content

Commit

Permalink
Implement support for hCaptcha (#6)
Browse files Browse the repository at this point in the history
* Use ENUM instead of rc.replace. Add hCaptcha

* New initReCaptcha using ENUM. Deprecated old.

* Update render() to support hCaptcha

* Use rc.provider instead of rc.replace

* checkVerification() support for hCaptcha

* Update example in main()

* Update README

* Missed committing a value in render()
  • Loading branch information
ThomasTJdev authored Apr 15, 2020
1 parent 3d2650f commit 0c471d2
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 30 deletions.
56 changes: 49 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# reCAPTCHA

reCAPTCHA support for Nim, supporting rendering a capctcha and verifying a user's response.
reCAPTCHA support for Nim, supporting rendering a capctcha and verifying a user's response. This library supports reCaptcha from:
* https://www.google.com/recaptcha/admin
* https://recaptcha.net
* https://hcaptcha.com

## Installation

Expand All @@ -10,22 +13,26 @@ nimble install recaptcha

## Usage

Before using this modul, be sure to [register your site with Google](https://www.google.com/recaptcha/admin) in order to get your client secret and site key. These are required to use this module.
Before using this modul, be sure to register at the reCaptcha providers website in order to get your client secret and site key. These are required to use this module. Register at:
* [Google reCaptcha](https://www.google.com/recaptcha/admin)
* [hCaptcha](https://hcaptcha.com)

Once you have your client secret and site key, you should create an instance of `ReCaptcha`:

```nim
import recaptcha
let captcha = initReCaptcha(MY_SECRET_KEY, MY_SITE_KEY)
let captcha = initReCaptcha("MY_SECRET_KEY", "MY_SITE_KEY", Google)
#let captcha = initReCaptcha("MY_SECRET_KEY", "MY_SITE_KEY", RecaptchaNet) <-- using recaptcha.net
#let captcha = initReCaptcha("MY_SECRET_KEY", "MY_SITE_KEY", Hcaptcha) <-- using hcaptcha.com
```

You can print out the required HTML code to show the reCAPTCHA element on the page using the `render` method:

```nim
import recaptcha
let captcha = initReCaptcha(MY_SECRET_KEY, MY_SITE_KEY)
let captcha = initReCaptcha("MY_SECRET_KEY", "MY_SITE_KEY", Google)
echo captcha.render()
```
Expand All @@ -37,7 +44,7 @@ Once the user has submitted the captcha back to you, you should verify their res
```nim
import recaptcha, asyncdispatch
let captcha = initReCaptcha(MY_SECRET_KEY, MY_SITE_KEY)
let captcha = initReCaptcha("MY_SECRET_KEY", "MY_SITE_KEY", Google)
let response = await captcha.verify(THEIR_RESPONSE)
```
Expand All @@ -46,14 +53,49 @@ If the user is a valid user, `response` will be `true`. Otherwise, it will be `f

Should there be an issue with the data sent to the reCAPTCHA service, an exception of type `CaptchaVerificationError` will be thrown.

In most cases, captchas are used within a HTML `<form>` element. In this case, the user's response will be available within the `g-recaptcha-response` form parameter.
In most cases, captchas are used within a HTML `<form>` element. In this case, the user's response will be available within form parameter named `g-recaptcha-response` for `Google` and `reCaptha.net` and within `h-recaptcha-repsonse` for `hCaptcha`.

You may also optionally pass the user's IP address along to reCAPTCHA during verification as an extra check:

```nim
import recaptcha, asyncdispatch
let captcha = initReCaptcha(MY_SECRET_KEY, MY_SITE_KEY)
let captcha = initReCaptcha("MY_SECRET_KEY", "MY_SITE_KEY", Google)
let response = await captcha.verify(THEIR_RESPONSE, USERS_IP_ADDRESS)
```

# Example

The example below uses `Google` as validator.

```nim
import recaptcha, asyncdispatch, jester
from strutils import format
const htmlForm = """
<form method="POST" action="/verify">
<input type="text" name="email">
<input type="password" name="password">
<div class="g-recaptcha" data-sitekey="$1"></div>
<button type="submit">Verify</button>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</form>
"""
let
secretKey = "123456789"
siteKey = "987654321"
captcha = initReCaptcha(secretKey, siteKey, Google)
routes:
get "/#":
resp(htmlForm.format(siteKey))
post "/verify":
let checkCap = await captcha.verify(@"g-recaptcha-response")
if checkCap:
resp("Welcome")
else:
resp("You failed the captcha")
```
81 changes: 58 additions & 23 deletions src/recaptcha.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@

import asyncdispatch, httpclient, json

type
Provider* = enum
Google
RecaptchaNet
Hcaptcha

const
VerifyUrl: string = "https://www.google.com/recaptcha/api/siteverify"
VerifyUrlReplace: string = "https://recaptcha.net/recaptcha/api/siteverify"
CaptchaScript: string = r"""<script src="https://www.google.com/recaptcha/api.js" async defer></script>"""
CaptchaScriptReplace: string = r"""<script src="https://recaptcha.net/recaptcha/api.js" async defer></script>"""
CaptchaElementStart: string = r"""<div class="g-recaptcha" data-sitekey=""""
CaptchaElementEnd: string = r""""></div>"""
NoScriptElementStart: string = r"""<noscript>
VerifyUrlRecaptchaNet: string = "https://recaptcha.net/recaptcha/api/siteverify"
VerifyUrlhCaptcha: string = "https://hcaptcha.com/siteverify"
CaptchaScript*: string = """<script src="https://www.google.com/recaptcha/api.js" async defer></script>"""
CaptchaScriptRecaptchaNet*: string = """<script src="https://recaptcha.net/recaptcha/api.js" async defer></script>"""
CaptchaScriptHcaptcha*: string = """<script src="https://hcaptcha.com/1/api.js" async defer></script>"""
CaptchaElementStart: string = """<div class="g-recaptcha" data-sitekey=""""
CaptchaElementStartHcaptcha: string = """<div class="h-captcha" data-sitekey=""""
CaptchaElementEnd: string = """"></div>"""
NoScriptElementStart: string = """<noscript>
<div>
<div style="width: 302px; height: 422px; position: relative;">
<div style="width: 302px; height: 422px; position: absolute;">
Expand Down Expand Up @@ -43,41 +52,60 @@ type
## The reCAPTCHA secret key.
siteKey: string
## The reCAPTCHA site key.
replace: bool
provider: Provider
## The catpcha provider: Google, reCaptchaNet, hCaptcha
replace {.deprecated.}: bool
## Default use www.google.com.If true, use "www.recaptcha.net".
## Docs in https://developers.google.com/recaptcha/docs/faq#can-i-use-recaptcha-globally.

CaptchaVerificationError* = object of Exception
## Error thrown if something goes wrong whilst attempting to verify a captcha response.

proc initReCaptcha*(secret, siteKey: string, replace = false): ReCaptcha =
proc initReCaptcha*(secret, siteKey: string, provider: Provider = Google): ReCaptcha =
## Initialise a ReCaptcha instance with the given secret key and site key.
##
## The secret key and site key can be generated at https://www.google.com/recaptcha/admin
result = ReCaptcha(
secret: secret,
siteKey: siteKey,
replace: replace
provider: provider
)

proc initReCaptcha*(secret, siteKey: string, replace = false): ReCaptcha {.deprecated.} =
## Initialise a ReCaptcha instance with the given secret key and site key.
##
## The secret key and site key can be generated at https://www.google.com/recaptcha/admin
let provider = if replace: Google else: RecaptchaNet
result = ReCaptcha(
secret: secret,
siteKey: siteKey,
provider: provider
)


proc render*(rc: ReCaptcha, includeNoScript: bool = false): string =
## Render the required code to display the captcha.
##
## If you set `includeNoScript` to `true`, then the `<noscript>` element required to support browsers without JS will be included in the output.
## By default, this is disabled as you have to modify the settings for your reCAPTCHA domain to set the security level to the minimum level to support this.
## For more information, see the reCAPTCHA support page: https://developers.google.com/recaptcha/docs/faq#does-recaptcha-support-users-that-dont-have-javascript-enabled
result = CaptchaElementStart
if rc.provider == Hcaptcha:
result = CaptchaElementStartHcaptcha
else:
result = CaptchaElementStart
result.add(rc.siteKey)
result.add(CaptchaElementEnd)
result.add("\n")
if not rc.replace:
if rc.provider == Google:
result.add(CaptchaScript)
else:
result.add(CaptchaScriptReplace)
elif rc.provider == RecaptchaNet:
result.add(CaptchaScriptRecaptchaNet)
elif rc.provider == Hcaptcha:
result.add(CaptchaScriptHcaptcha)

if includeNoScript:
if includeNoScript and rc.provider != Hcaptcha:
result.add("\n")
if not rc.replace:
if rc.provider == Google:
result.add(NoScriptElementStart)
else:
result.add(NoScriptElementStartReplace)
Expand All @@ -88,14 +116,19 @@ proc `$`*(rc: ReCaptcha): string =
## Render the required code to display the captcha.
result = rc.render()

proc checkVerification(mpd: MultipartData, replace: bool): Future[bool] {.async.} =
proc checkVerification(mpd: MultipartData, provider: Provider): Future[bool] {.async.} =
let
client = newAsyncHttpClient()
var response: AsyncResponse
if not replace:
var
response: AsyncResponse

case provider
of Google:
response = await client.post(VerifyUrl, multipart=mpd)
else:
response = await client.post(VerifyUrlReplace, multipart=mpd)
of RecaptchaNet:
response = await client.post(VerifyUrlRecaptchaNet, multipart=mpd)
of Hcaptcha:
response = await client.post(VerifyUrlhCaptcha, multipart=mpd)

let
jsonContent = parseJson(await response.body)
Expand All @@ -113,6 +146,8 @@ proc checkVerification(mpd: MultipartData, replace: bool): Future[bool] {.async.
raise newException(CaptchaVerificationError, "The response parameter is missing.")
of "invalid-input-response":
raise newException(CaptchaVerificationError, "The response parameter is invalid or malformed.")
of "bad-request":
raise newException(CaptchaVerificationError, "The request is invalid or malformed.")
else: discard

result = if success != nil: success.getBool() else: false
Expand All @@ -124,15 +159,15 @@ proc verify*(rc: ReCaptcha, reCaptchaResponse, remoteIp: string): Future[bool] {
"response": reCaptchaResponse,
"remoteip": remoteIp
})
result = await checkVerification(multiPart, rc.replace)
result = await checkVerification(multiPart, rc.provider)

proc verify*(rc: ReCaptcha, reCaptchaResponse: string): Future[bool] {.async.} =
## Verify the given reCAPTCHA response.
let multiPart = newMultipartData({
"secret": rc.secret,
"response": reCaptchaResponse,
})
result = await checkVerification(multiPart, rc.replace)
result = await checkVerification(multiPart, rc.provider)

when not defined(nimdoc) and isMainModule:
import os, jester
Expand Down Expand Up @@ -164,7 +199,7 @@ when not defined(nimdoc) and isMainModule:
secretKey = getEnv("RECAPTCHA_SECRET")
siteKey = getEnv("RECAPTCHA_SITEKEY")

captcha = initReCaptcha(secretKey, siteKey)
captcha = initReCaptcha(secretKey, siteKey, Google)

runForever()

Expand Down

0 comments on commit 0c471d2

Please # to comment.