The client side of OpenWAF Verify is open source under the Apache License 2.0: github.com/openwaf-dev/verify (external link). That is the code that runs in your visitors' browsers, the protocol it speaks with the edge, and the libraries your server uses to check tokens.
You can read what the Verify widget and the browser challenge do in a visitor's browser, check it against what your site serves, and use the libraries instead of writing your own siteverify calls.
What is open
- Browser widget (
challengejs/). Thewidget.jsscript: the box, its states, theowafVerifyAPI and the hiddenowaf-verify-responsefield. - Proof-of-work worker (
challengejs/). The Web Worker that solves the SHA-256 puzzles, and the fallback that runs on the page without a worker. - Interstitial page (
challengejs/). The HTML template and CSS of the browser challenge page. - Protocol (
spec/). How the browser gets a challenge, sends its solution and receives a token, and the siteverify API. - Siteverify libraries. Go (
siteverify/, modulegithub.com/openwaf-dev/verify), Node.js and TypeScript (node/,@openwaf/verify) and PHP (php/, Composer packageopenwaf/verify). - Components. React (
react/,@openwaf/verify-react) and Vue 3 (vue/,@openwaf/verify-vue). - Examples (
examples/). Small working apps that render the widget and check the token.
What is not open, and why
Some parts stay closed:
- Risk scoring. How the edge turns server-side signals into a score.
- The browser-consistency rule. How the edge checks that a browser behaves like the browser it says it is.
- The in-page automation checks. The module of yes/no checks about automation tools, and how its answer is encoded for each challenge.
- Reputation and threat feeds. The lists and sources behind threat feeds and reputation.
- Edge internals. The proxy and how its nodes work.
These parts decide how hard the edge looks at a request. If their exact rules were public, automation could be tuned to pass them, and each one would stop working on the day it was published.
What they read is not secret. The data each part processes is listed in Data processed and in risk scoring.
The served widget is not byte-identical
The widget your edge serves is this open code plus one closed extension module: the in-page automation checks.
- The module runs a few yes/no checks in the page.
- Its only output is one opaque integer, added to the solve request.
- It reads no fingerprinting surface: no canvas, WebGL, audio, fonts or high-entropy client hints.
Because of that module, a build from the repository is not byte-identical to the bundle at /__waf/challenge/v1/widget.js. Everything else in that bundle is the code in challengejs/.
Check it yourself
You don't have to take our word for it. Open a page that uses Verify, such as the live demo, with your browser's developer tools:
- Network. Every request the widget makes goes to the page's own host, under
/__waf/. Nothing goes to another domain. - Cookies. The widget sets no cookie. The browser challenge page sets only
__waf_pass, after a successful solve. - Content Security Policy. The widget works with
Content-Security-Policy: default-src 'self', with no violations in the console. The demo runs under that policy. - Source. Read the served
widget.jsandworker.jsand compare them withchallengejs/in the repository.
Use the libraries
The libraries call siteverify for you. They follow the siteverify reference and behave the same in every language. For now they install from the GitHub repository: Go and Composer from the v0.1.0 tag, npm from the tarballs attached to the release (external link). The npm and Packagist packages are coming soon.
go get github.com/openwaf-dev/verify/siteverify@v0.1.0c, err := siteverify.New(os.Getenv("OWAF_VERIFY_SECRET"))
if err != nil {
log.Fatal(err)
}
res, err := c.Verify(r.Context(), r.FormValue("owaf-verify-response"), siteverify.Params{
RemoteIP: clientIP,
ExpectedAction: "signup",
})
if err != nil || !res.Success {
http.Error(w, "verification failed", http.StatusForbidden)
return
}
// create the accountnpm install https://github.com/openwaf-dev/verify/releases/download/v0.1.0/openwaf-verify-0.1.0.tgzimport { SiteverifyClient } from "@openwaf/verify";
const client = new SiteverifyClient({ secret: process.env.OWAF_VERIFY_SECRET! });
const result = await client.verify(token, { remoteIp, expectedAction: "signup" });
if (!result.success) {
// reject the form
}composer config repositories.openwaf-verify vcs https://github.com/openwaf-dev/verify
composer require openwaf/verify:^0.1<?php
$client = new OpenWAF\Verify\Siteverify\Client((string) getenv('OWAF_VERIFY_SECRET'));
$result = $client->verify(
$_POST['owaf-verify-response'] ?? '',
remoteIp: $clientIp,
expectedAction: 'signup',
);
if (!$result->success) {
http_response_code(403);
exit('Verification failed');
}
// create the accountnpm install https://github.com/openwaf-dev/verify/releases/download/v0.1.0/openwaf-verify-react-0.1.0.tgzimport { Verify } from "@openwaf/verify-react";
<form action="/signup" method="post">
<input name="email" type="email" required />
<Verify siteKey="owk_YOUR_SITE_KEY" action="signup" onSuccess={(token) => setToken(token)} />
<button type="submit">Sign up</button>
</form>npm install https://github.com/openwaf-dev/verify/releases/download/v0.1.0/openwaf-verify-vue-0.1.0.tgz<script setup>
import { Verify } from "@openwaf/verify-vue";
</script>
<template>
<form action="/signup" method="post">
<input name="email" type="email" required />
<Verify site-key="owk_YOUR_SITE_KEY" action="signup" @success="onToken" />
<button type="submit">Sign up</button>
</form>
</template>The React and Vue components render the same widget, so the page must be served through OpenWAF.
What the server libraries do:
- Defaults. They call
https://dash.openwaf.dev/v1/challenge/siteverifywith a 10-second timeout. You can point them athttps://<your host>/__waf/challenge/siteverifyinstead. - No request when it can't succeed. An empty token returns
missing-input-responseand a token longer than 2048 bytes returnsinvalid-input-response, without calling siteverify. - Host name and action. When you pass an expected host name or action and the token doesn't match, the answer is a failure with
hostname-mismatchoraction-mismatch. These two codes come from the library, not from siteverify. - Answers and errors. Every siteverify answer, including
400,405and429(with itsRetry-After), comes back as a result. A timeout, a network failure, another HTTP status or a body that isn't JSON is an error: sentinel errors in Go,SiteverifyErrorin Node andSiteverifyExceptionin PHP, with the codestimeout,network,httpandbad-response. - No automatic retries. To retry after a timeout, send the same idempotency key again: the second call gets the first answer instead of
timeout-or-duplicate. - The secret stays out of errors. It never appears in error messages, logs or printed objects.
Try them with the test keys: ows_test_pass, ows_test_fail and ows_test_spent work without a widget.
Licence
Everything in the repository is under the Apache License 2.0 (external link). You can use, change and redistribute it, including in commercial products.
Contribute
Open issues and pull requests at github.com/openwaf-dev/verify (external link). We review every change. Changes to the browser code are released with the edge, so a merged change reaches sites with the next edge release.
Report a vulnerability
Don't open a public issue. Write to support@openwaf.dev (security@openwaf.dev reaches the same team) and follow the repository's SECURITY.md (external link).