Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git
.gitignore
.env
**/.env
coverage.*
*.out
*.test
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 2

updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
70 changes: 70 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: CI

on:
push:
branches: [main]
pull_request:

concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
minimum-go:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Set up minimum supported Go version
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: "1.24.x"
cache-dependency-path: go.mod

- name: Test package
env:
GOTOOLCHAIN: local
run: go test ./...

quality:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: "1.26.x"
check-latest: true
cache-dependency-path: go.mod

- name: Verify formatting and module metadata
run: |
test -z "$(gofmt -l .)"
go mod tidy -diff
go mod verify

- name: Check workflow syntax
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12

- name: Lint package
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.11.3
args: --config=.golangci.yml --timeout=5m ./...

- name: Test package
run: |
go vet ./...
go test -race -shuffle=on -count=1 ./...
go test -covermode=atomic -coverprofile=coverage.out ./hcaptcha
coverage="$(go tool cover -func=coverage.out | awk '/^total:/ {gsub(/%/, "", $3); print $3}')"
awk -v coverage="$coverage" 'BEGIN { if (coverage + 0 < 80) exit 1 }'

- name: Scan dependency graph
run: go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
55 changes: 55 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Release verification

on:
push:
tags:
- "v*"

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Check out the release tag
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0

- name: Validate release tag
env:
GH_TOKEN: ${{ github.token }}
run: |
semver_pattern='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'
[[ "$GITHUB_REF_NAME" =~ $semver_pattern ]]
test "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" = tag
tag_object_sha="$(git rev-parse "refs/tags/$GITHUB_REF_NAME")"
tag_object="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_object_sha")"
jq -e '.verification.verified == true' >/dev/null <<<"$tag_object"
git fetch origin main
git merge-base --is-ancestor "$(git rev-list -n 1 "$GITHUB_REF_NAME")" origin/main

- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: "1.26.x"
check-latest: true
cache-dependency-path: go.mod

- name: Verify package
run: |
go mod verify
go test -race -count=1 ./...

- name: Run documented live Siteverify test
env:
HCAPTCHA_LIVE_TEST: "1"
run: go test -count=1 -run '^TestLiveSiteverify$' ./hcaptcha

- name: Scan dependency graph
run: go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum

# env file
.env

# Editor/IDE
# .idea/
# .vscode/
.DS_Store
33 changes: 33 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
version: "2"

run:
timeout: 5m
modules-download-mode: readonly

linters:
default: none
enable:
- bodyclose
- errcheck
- errorlint
- gocritic
- gosec
- govet
- ineffassign
- misspell
- noctx
- revive
- staticcheck
- unconvert
- unparam
exclusions:
rules:
# Test-only TLS handlers parse bounded, in-memory request bodies.
- path: _test\.go
text: G120
linters:
- gosec

issues:
max-issues-per-linter: 0
max-same-issues: 0
103 changes: 103 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# hCaptcha Go integration

Small Go client for [hCaptcha Siteverify](https://docs.hcaptcha.com/#verify-the-user-response-server-side). It verifies a response token on your server after the browser completes hCaptcha; it does not render the widget or make authorization decisions for you.

## Install

```sh
go get github.com/hCaptcha/hcaptcha-go/hcaptcha
```

Requires Go 1.24 or later.

## Use

Create a client once with an hCaptcha service secret, then reuse it across handlers. Keep the secret only in server-side configuration.

```go
package main

import (
"net/http"

hcaptcha "github.com/hCaptcha/hcaptcha-go/hcaptcha"
)

var verifier, err = hcaptcha.New("server-side-secret")

func protected(w http.ResponseWriter, r *http.Request) {
if err != nil {
http.Error(w, "verification unavailable", http.StatusInternalServerError)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "invalid form", http.StatusBadRequest)
return
}
result, err := verifier.VerifyRequest(r.Context(), hcaptcha.Request{
Token: r.FormValue("h-captcha-response"),
SiteKey: "expected-sitekey-uuid", // Bind token to this sitekey.
})
if err != nil {
http.Error(w, "verification unavailable", http.StatusBadGateway)
return
}
if result["success"] != true {
http.Error(w, "verification failed", http.StatusForbidden)
return
}

// Perform the protected action.
}
```

Use `Verify(token)` for the minimal case. `VerifyContext(ctx, token)` and `VerifyRequest(ctx, request)` propagate handler cancellation. `Request.Token` is required; `RemoteIP` and `SiteKey` are optional parameters sent only when non-empty.

`Result` is `map[string]any`, retaining all fields returned by Siteverify. Treat a transport or decode `error` separately from an unsuccessful response, and fail closed in both cases. Check `success` before protected work. Inspect `error-codes` for operational logging; do not expose raw verification details to users.

## Browser integration

This package receives the token generated by a browser or mobile integration. Add the hCaptcha widget, submit its `h-captcha-response` field to your backend, then call this package from that backend.

```html
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="your-public-sitekey"></div>
```

Use the [Developer Guide](https://docs.hcaptcha.com/) for the complete browser-to-server flow and the [JavaScript configuration reference](https://docs.hcaptcha.com/configuration) for explicit rendering, callbacks, themes, and client-side errors.

## Security and operations

- Call Siteverify from the backend with a form-encoded POST. Do not expose the secret or accept a client-side success claim.
- Verify tokens promptly and exactly once. hCaptcha tokens are short-lived and single-use.
- Pass the expected `SiteKey` to prevent a token issued for another sitekey from being accepted. `hostname` is informational, not an authentication control.
- Send `RemoteIP` when available, using an IP derived from a trusted-proxy policy. Do not trust forwarding headers from arbitrary clients.
- Configure the hCaptcha domain allowlist and keep the secret in a secret manager or environment variable. [Rotate a leaked secret](https://docs.hcaptcha.com/#rotating-your-siteverify-secret) immediately.

See hCaptcha’s [server-side verification guidance](https://docs.hcaptcha.com/#verify-the-user-response-server-side), [domain allowlist configuration](https://docs.hcaptcha.com/configuration#domain-allowlist), and [Siteverify error-code reference](https://docs.hcaptcha.com/#siteverify-error-codes-table).


## Example and testing

Run the included React/Vite browser and Go backend example:

```sh
cd example
cp .env.local.example .env.local
npm install
./run.sh
```

The sample env uses [hCaptcha's public test keys](https://docs.hcaptcha.com/#integration-testing-test-keys),
which skip visual challenges. To use your own keys locally, follow
[the example's local hostname setup](example/README.md#run). hCaptcha's
[local development guide](https://docs.hcaptcha.com/#local-development)
lists `localhost` and `127.0.0.1` as unsupported hostnames. In this
example, a real-key challenge can still pass there after several checkbox
attempts, but that flow is unreliable.

Run package tests with:

```sh
go test ./...
```
22 changes: 22 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Releasing

This package follows semantic versioning. Releases are Go module tags such as
`v1.0.0`; there are no binary artifacts.

1. Confirm the intended commit is on the default branch and CI is green.
2. Review dependency, vulnerability, documentation, and live-test results.
3. Confirm installation examples and Siteverify documentation match the
release.
4. Create a signed annotated semantic-version tag from the reviewed default
branch commit and push that tag. The release-verification workflow rejects
unsigned, lightweight, invalid semantic-version, or non-default-branch tags
and reruns live and dependency checks.
5. Confirm release verification succeeds, then verify the version through the
Go module proxy and pkg.go.dev.
6. Create the corresponding GitHub release notes.
7. Submit the released package to the public hCaptcha integrations list.

Do not tag from a dirty worktree or move an existing release tag. If a release
is incorrect, publish a new patch version instead. Keep the default branch
protected, require CI and an approving maintainer review, and retain release
provenance with the GitHub release.
21 changes: 21 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Security policy

## Supported versions

Before the first tagged release, security fixes are applied to the latest
commit on the default branch. After release, the latest supported major version
receives security fixes.

## Reporting a vulnerability

Do not open a public GitHub issue for a suspected vulnerability.

Email `support@hcaptcha.com` and copy `security@hcaptcha.com`. Include the
affected version or commit, impact, reproduction steps, and any proposed
mitigation. Do not include live hCaptcha secrets, response tokens, customer
data, or other credentials. Use only accounts and systems you are authorized to
test, avoid privacy violations and service disruption, and allow time for
coordinated remediation before disclosure.

For ordinary integration questions and non-security bugs, use the public issue
tracker after the repository is released.
3 changes: 3 additions & 0 deletions example/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VITE_HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001
HCAPTCHA_SECRET=0x0000000000000000000000000000000000000000
GO_BACKEND_URL=http://127.0.0.1:8080
Loading
Loading