4 |
5 | Friendly Captcha Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🛡️ Friendly Challenge
2 |
3 | [](https://www.npmjs.com/package/friendly-challenge) [](https://docs.friendlycaptcha.com)
4 |
5 |  
6 |
7 | This repository contains the widget code and documentation for Friendly Captcha, here is a [demo](https://friendlycaptcha.com/demo) of the widget in a real form.
8 |
9 | Friendly Captcha is a proof-of-work based CAPTCHA alternative that respects the user's privacy, see the [**Friendly Captcha website**](https://friendlycaptcha.com).
10 |
11 | ## Getting started
12 | Read the [**documentation**](https://docs.friendlycaptcha.com).
13 |
14 | ## Changelog
15 | Check the [**changelog**](https://docs.friendlycaptcha.com/#/changelog).
16 |
17 | ## License
18 | [**MIT**](./LICENSE.md)
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { findCaptchaElements } from "./dom";
2 | import { WidgetInstance } from "./captcha";
3 |
4 | declare global {
5 | interface Window {
6 | friendlyChallenge: any;
7 | }
8 | }
9 |
10 | window.friendlyChallenge = {
11 | WidgetInstance: WidgetInstance,
12 | };
13 |
14 | function setup() {
15 | let autoWidget = window.friendlyChallenge.autoWidget;
16 |
17 | const elements = findCaptchaElements();
18 | for (let index = 0; index < elements.length; index++) {
19 | const hElement = elements[index] as HTMLElement;
20 | if (hElement && !hElement.dataset["attached"]) {
21 | autoWidget = new WidgetInstance(hElement);
22 | // We set the "data-attached" attribute so we don't attach to the same element twice.
23 | hElement.dataset["attached"] = "1";
24 | }
25 | }
26 | window.friendlyChallenge.autoWidget = autoWidget;
27 | }
28 |
29 | if (document.readyState !== "loading") {
30 | setup();
31 | } else {
32 | document.addEventListener("DOMContentLoaded", setup);
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2020-2023 Friendly Captcha GmbH
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Friendly Captcha Documentation
2 |
3 | > This is documentation for Friendly Captcha **v1**.
4 | >
5 | > You can [learn more](https://developer.friendlycaptcha.com/docs/v2/versions) about v1 and v2, or you can switch to the [Friendly Captcha v2 Developer Hub](https://developer.friendlycaptcha.com).
6 |
7 | Friendly Captcha is a system for preventing spam on your website. You can add the **Friendly Captcha widget** to your web app to fight spam, with little impact to the user experience.
8 |
9 | Friendly Captcha sends the user a cryptographic puzzle that takes the user's device a few seconds to solve, the user doesn't have to do anything.
10 |
11 | This documentation will show you how to integrate Friendly Captcha into your website. If you wish to learn more about Friendly Captcha, visit the [**Friendly Captcha website**](https://friendlycaptcha.com).
12 |
13 | **Screenshots of the widget in action**
14 |  
15 |
16 | ## Next steps
17 | * The [**installation guide**](/installation) shows you how to add Friendly Captcha to your website in 3 steps.
18 | * The [**Javascript API**](/widget_api) allows you to customize the integration with your website.
19 |
--------------------------------------------------------------------------------
/src/headless.ts:
--------------------------------------------------------------------------------
1 | // Defensive init to make it easier to integrate with Gatsby, NextJS, and friends.
2 | let nav: Navigator;
3 | let ua: string;
4 | if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") {
5 | nav = navigator;
6 | ua = nav.userAgent.toLowerCase();
7 | }
8 |
9 | /**
10 | * Headless browser detection on the clientside is imperfect. One can modify any clientside code to disable or change this check,
11 | * and one can spoof whatever is checked here. However, that doesn't make it worthless: it's yet another hurdle for spammers and
12 | * it stops unsophisticated scripters from making any request whatsoever.
13 | */
14 | export function isHeadless() {
15 | return (
16 | //tell-tale bot signs
17 | ua.indexOf("headless") !== -1 ||
18 | nav.appVersion.indexOf("Headless") !== -1 ||
19 | ua.indexOf("bot") !== -1 || // http://www.useragentstring.com/pages/useragentstring.php?typ=Browser
20 | ua.indexOf("crawl") !== -1 || // Only IE5 has two distributions that has this on windows NT.. so yeah.
21 | nav.webdriver === true ||
22 | !nav.language ||
23 | (nav.languages !== undefined && !nav.languages.length) // IE 11 does not support NavigatorLanguage.languages https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/languages
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/docs/theme.md:
--------------------------------------------------------------------------------
1 | # Customizing the look
2 |
3 | The styling of Friendly Captcha is done in plain CSS, and you can change it however you want. A stylesheet gets injected into the `head` of your HTML document when the first widget is loaded.
4 |
5 | ## Dark mode
6 |
7 |  
8 |
9 | Friendly Captcha ships with two built-in themes, by adding the `dark` class to your `frc-captcha` element you can enable dark mode:
10 |
11 | ```html
12 |
13 |
14 |
15 |
16 |
17 | ```
18 |
19 |
20 | ## Using your own stylesheet
21 | You can create your own stylesheet for the Friendly Captcha widget. The [existing css file](https://github.com/FriendlyCaptcha/friendly-challenge/blob/master/src/styles.css) is probably a good start.
22 |
23 | If any HTML element with id `frc-style` is present on the HTML document, the original styles will not be injected. So to use your own custom theme you could add the following:
24 |
25 | ```html
26 |
27 | ```
28 |
29 | Alternatively, if you are using the library approach, you can use the `skipStyleInjection` option to prevent a stylesheet from getting injected.
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "parser": "@typescript-eslint/parser",
8 | "parserOptions": {
9 | "project": "tsconfig.json",
10 | "sourceType": "module"
11 | },
12 | "plugins": [
13 | "@typescript-eslint"
14 | ],
15 | "rules": {
16 | "@typescript-eslint/member-delimiter-style": [
17 | "error",
18 | {
19 | "multiline": {
20 | "delimiter": "semi",
21 | "requireLast": true
22 | },
23 | "singleline": {
24 | "delimiter": "semi",
25 | "requireLast": false
26 | }
27 | }
28 | ],
29 | "@typescript-eslint/semi": [
30 | "error",
31 | "always"
32 | ],
33 | "@typescript-eslint/no-use-before-define": "off",
34 | "@typescript-eslint/explicit-function-return-type": "off",
35 | "@typescript-eslint/no-explicit-any": "off",
36 | "@typescript-eslint/no-unused-vars": [
37 | "warn",
38 | {
39 | "argsIgnorePattern": "^_",
40 | "varsIgnorePattern": "^_"
41 | }
42 | ],
43 | },
44 | "extends": [
45 | "eslint:recommended",
46 | "plugin:@typescript-eslint/eslint-recommended",
47 | "plugin:@typescript-eslint/recommended"
48 | ]
49 | };
--------------------------------------------------------------------------------
/docs/csp.md:
--------------------------------------------------------------------------------
1 | # Content Security Policy (CSP)
2 |
3 | Content Security Policy is a way to secure your website from cross-site scripting (XSS). The HTTP Content-Security-Policy response header allows website administrators to control resources the user agent is allowed to load for a given page.
4 |
5 | ## Configuring your CSP for Friendly Captcha
6 |
7 | If you are using a CSP for your website you will have to configure it to allow Friendly Captcha's script to be loaded. There are three ways this can be achieved:
8 |
9 | ### Source-based approach (Easiest)
10 |
11 | You will only need to add the following directives:
12 |
13 | - If you're loading the widget from the CDN:
14 |
15 | `script-src 'wasm-unsafe-eval' https://cdn.jsdelivr.net/npm/; worker-src blob:; child-src blob:`
16 |
17 | > note: as an extra precaution you can use the full path to our CDN hosted widget by replacing `https://cdn.jsdelivr.net/npm/` above with `https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.19/widget.module.min.js https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.19/widget.min.js` (making sure the widget versions are correct)
18 |
19 | - If you're loading the widget from your own bundle:
20 |
21 | `script-src 'wasm-unsafe-eval' 'self'; worker-src blob:; child-src blob:`
22 |
23 | ### Nonce-based approach (Recommended)
24 |
25 | See [this guide](https://content-security-policy.com/nonce/) and include your nonce in the `widget.min.js` and `widget.module.min.js` script tag.
26 |
27 | ### Hash-based approach
28 |
29 | See [this guide](https://content-security-policy.com/hash/)
30 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export type Solver = (puzzleBuffer: Uint8Array, threshold: number, n?: number) => Uint8Array[];
2 |
3 | export type MessageFromWorker = ReadyMessage | StartedMessage | DonePartMessage | ErrorMessage;
4 | export type MessageToWorker = StartMessage | SolverMessage;
5 |
6 | export interface StartMessage {
7 | type: "start";
8 | puzzleSolverInput: Uint8Array;
9 | threshold: number;
10 | puzzleIndex: number;
11 | puzzleNumber: number;
12 | }
13 |
14 | export interface SolverMessage {
15 | type: "solver";
16 | forceJS: boolean;
17 | }
18 |
19 | export interface ReadyMessage {
20 | type: "ready";
21 | solver: 1 | 2;
22 | }
23 |
24 | export interface StartedMessage {
25 | type: "started";
26 | }
27 |
28 | export interface ErrorMessage {
29 | type: "error";
30 | message: string;
31 | }
32 |
33 | export interface ProgressMessage {
34 | /**
35 | * Number of solutions to be found in total
36 | */
37 | n: number;
38 | /**
39 | * Number of all hashes calculated
40 | */
41 | h: number;
42 | /**
43 | * Time this solution took in seconds
44 | */
45 | t: number;
46 | /**
47 | * This is the i'th solution.
48 | */
49 | i: number;
50 | }
51 |
52 | export interface DonePartMessage {
53 | type: "done";
54 | solution: Uint8Array;
55 | puzzleIndex: number;
56 | puzzleNumber: number;
57 | /**
58 | * Hashes attempted for this solution
59 | */
60 | h: number;
61 | }
62 |
63 | export interface DoneMessage {
64 | solution: Uint8Array;
65 | /**
66 | * Total number of hashes that were required
67 | */
68 | h: number;
69 | /**
70 | * Total time it took in seconds
71 | */
72 | t: number;
73 | diagnostics: Uint8Array;
74 | solver: 1 | 2;
75 | }
76 |
--------------------------------------------------------------------------------
/src/html/polyfill.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FriendlyCaptcha
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/rollup.library.config.ts:
--------------------------------------------------------------------------------
1 | import resolve from '@rollup/plugin-node-resolve';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import typescript from 'rollup-plugin-typescript2';
4 | import {string} from 'rollup-plugin-string';
5 | import replace from '@rollup/plugin-replace';
6 |
7 | import dts from "rollup-plugin-dts";
8 |
9 | const CleanCSS = require('clean-css');
10 |
11 | // Inline plugin to load css as minified string
12 | const css = () => {return {
13 | name: "css",
14 | transform(code, id) {
15 | if (id.endsWith(".css")) {
16 | const minified = new CleanCSS({level: 2}).minify(code);
17 | return `export default '${minified.styles}'`;
18 | }
19 | }
20 | }}
21 |
22 | const ts = () => {
23 | return typescript({
24 | useTsconfigDeclarationDir: true,
25 | include: [
26 | './src/**/*.ts',
27 | ],
28 | })
29 | }
30 |
31 | export default [
32 | {
33 | input: `src/index.ts`,
34 | output: {
35 | file: 'dist/index.js',
36 | format: 'es',
37 | sourcemap: true,
38 | },
39 | plugins: [
40 | ts(),
41 | commonjs(),
42 | resolve(),
43 | string({
44 | include: ["dist/worker.min.js"],
45 | }),
46 | css(),
47 | ],
48 | },
49 | {
50 | input: `src/index.ts`,
51 | output: [
52 | {
53 | file: 'dist/compat/index.js',
54 | format: 'es',
55 | sourcemap: true,
56 | },
57 | ],
58 | plugins: [
59 | ts(),
60 | replace({"dist/worker.min.js": "dist/worker.compat.min.js"}),
61 | commonjs(),
62 | resolve(),
63 | string({
64 | include: ["dist/worker.compat.min.js"],
65 | }),
66 | css(),
67 | ],
68 | },
69 | {
70 | input: `src/index.ts`,
71 | output: [{file: "dist/friendly-challenge.d.ts", format: "es"}],
72 | plugins: [dts()],
73 | }
74 | ];
--------------------------------------------------------------------------------
/rollup.widget.config.ts:
--------------------------------------------------------------------------------
1 | import resolve from '@rollup/plugin-node-resolve';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import typescript from 'rollup-plugin-typescript2';
4 | import {string} from 'rollup-plugin-string';
5 | import replace from '@rollup/plugin-replace';
6 |
7 | const CleanCSS = require('clean-css');
8 |
9 | // Inline plugin to load css as minified string
10 | const css = () => {return {
11 | name: "css",
12 | transform(code, id) {
13 | if (id.endsWith(".css")) {
14 | const minified = new CleanCSS({level: 2}).minify(code);
15 | return `export default '${minified.styles}'`;
16 | }
17 | }
18 | }}
19 |
20 | const ts = () => {
21 | return typescript({
22 | useTsconfigDeclarationDir: true,
23 | include: [
24 | './src/**/*.ts',
25 | ],
26 | })
27 | }
28 |
29 | export default [
30 | {
31 | input: `src/main.ts`,
32 | output: {
33 | file: 'dist/widget.module.js',
34 | format: 'es',
35 | sourcemap: false,
36 | name: "friendlyChallenge",
37 | },
38 | watch: {
39 | include: 'src/**',
40 | },
41 | plugins: [
42 | ts(),
43 | commonjs(),
44 | resolve(),
45 | string({
46 | include: ["dist/worker.min.js"],
47 | }),
48 | css(),
49 | ],
50 | },
51 | {
52 | input: `src/main.ts`,
53 | output: [
54 | {
55 | file: 'dist/widget-pre-babel.js',
56 | format: 'iife',
57 | sourcemap: false,
58 | name: "friendlyChallenge",
59 | },
60 | ],
61 | watch: {
62 | include: 'src/**',
63 | },
64 | plugins: [
65 | ts(),
66 | replace({"dist/worker.min.js": "dist/worker.compat.min.js"}),
67 | commonjs(),
68 | resolve(),
69 | string({
70 | include: ["dist/worker.compat.min.js"],
71 | }),
72 | css(),
73 | ],
74 | }
75 | ];
--------------------------------------------------------------------------------
/docs/automated_testing.md:
--------------------------------------------------------------------------------
1 | # Automated Testing
2 |
3 | Testing can be at odds with a captcha you add to your website or app. Perhaps you use automated testing tools like [Cypress](https://www.cypress.io/), [Selenium](https://www.selenium.dev/), or [Puppeteer](https://github.com/puppeteer/puppeteer). On this page you will find some tips and approaches that will help work out a testing story for pages that include a captcha widget.
4 |
5 | ## Testing tips
6 |
7 | ### Mocking out the API
8 | The easiest and most recommended approach is to mock out the API in the backend. Instead of calling our API and getting the JSON response, you instead always use `{success: true}` when running in test mode. With this approach you can keep all other code in your application the same.
9 |
10 | ### IP or password-based gating
11 | If you perform automated end-to-end tests in production you may want to conditionally disable the captcha check for your test-runner.
12 | * If you know the IPs of the machines that run these tests you could whitelist those and skip the captcha check for those (or use the mocking approach above).
13 | * If you do not know the IP beforehand, you could have your test-runner put a secret in the form submission that you check for. In other words, you can specify a password that bypasses the captcha.
14 |
15 | ### Dynamic button enabling
16 | Many websites that have a form protected using Friendly Captcha will only enable the submit button after the captcha widget is finished to prevent users from submitting without a valid captcha solution. When you are using a browser automation tool you may want to enable the button despite the captcha not being completed, we advice you achieve this by executing a snippet of Javascript (which is something you can do in all browser testing automation tools).
17 |
18 |
19 | Here's an example such snippet:
20 | ```javascript
21 | const button = document.querySelector("#my-button");
22 | button.disabled = false;
23 | ```
24 |
--------------------------------------------------------------------------------
/docs/legacy_endpoint.md:
--------------------------------------------------------------------------------
1 | # Updating from the legacy endpoint
2 |
3 | Since April 2021 we offer our global API on `api.friendlycaptcha.com/api/`.
4 |
5 | Before then our documentation and widget distributions would default to `friendlycaptcha.com/api/`.
6 |
7 | Only a small subset of our users are still using the old domain. We are starting to actively reach out to those that have not updated since, asking them to make the switch to the `api.friendlycaptcha.com` subdomain.
8 |
9 | We are planning to sunset the API endpoints on the old domain entirely. Therefore, action on your part is needed.
10 |
11 | ## Why?
12 |
13 | Serving our API from a separate subdomain allows for better reliability and better privacy preservation.
14 |
15 | This change also offers us more flexibility in how we serve our website and apps. It simplifies moving away from a non-EU CDN provider.
16 |
17 | ---
18 |
19 | ## Upgrade instructions
20 |
21 | Please perform the following updates. Upgrading should only take a few minutes, there have been no breaking changes for either the widget or the siteverify endpoints.
22 |
23 | ### Widget and the `friendly-challenge` library
24 |
25 | If you are using version `0.8.3` or below, please update to the latest version as soon as possible. If you are using a more recent version still we advise to upgrade to the latest version (`0.9.19`).
26 |
27 | The changelog can be found [here](https://github.com/FriendlyCaptcha/friendly-challenge/blob/master/docs/changelog.md).
28 |
29 | ### If you are using the WordPress plugin
30 |
31 | Please upgrade to the latest version. Versions of the plugin before `1.2.0` are affected (released April 2021).
32 |
33 | ### Backend code
34 |
35 | Please replace the following in your backend code:
36 |
37 | `https://friendlycaptcha.com/api/v1/siteverify`
38 |
39 | Replace it with
40 |
41 | `https://api.friendlycaptcha.com/api/v1/siteverify`
42 |
43 | > Note: If you are using the 🇪🇺 EU endpoint for siteverify, no changes are required in the backend code.
44 |
--------------------------------------------------------------------------------
/src/html/manual-polyfill.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FriendlyCaptcha
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/puzzle.ts:
--------------------------------------------------------------------------------
1 | import { decode } from "friendly-pow/base64";
2 | import { difficultyToThreshold } from "friendly-pow/encoding";
3 | import { NUMBER_OF_PUZZLES_OFFSET, PUZZLE_DIFFICULTY_OFFSET, PUZZLE_EXPIRY_OFFSET } from "friendly-pow/puzzle";
4 | import { Localization } from "./localization";
5 |
6 | export interface Puzzle {
7 | signature: string;
8 | base64: string;
9 | buffer: Uint8Array; // input puzzle
10 | threshold: number; // Related to difficulty
11 | n: number; // Amount of puzzles to solve
12 | expiry: number; // Expiry in milliseconds from now
13 | }
14 |
15 | export function decodeBase64Puzzle(base64Puzzle: string): Puzzle {
16 | const parts = base64Puzzle.split(".");
17 | const puzzle = parts[1];
18 | const arr = decode(puzzle);
19 | return {
20 | signature: parts[0],
21 | base64: puzzle,
22 | buffer: arr,
23 | n: arr[NUMBER_OF_PUZZLES_OFFSET],
24 | threshold: difficultyToThreshold(arr[PUZZLE_DIFFICULTY_OFFSET]),
25 | expiry: arr[PUZZLE_EXPIRY_OFFSET] * 300000,
26 | };
27 | }
28 |
29 | export async function getPuzzle(urlsSeparatedByComma: string, siteKey: string, lang: Localization): Promise {
30 | const urls = urlsSeparatedByComma.split(",");
31 | for (let i = 0; i < urls.length; i++) {
32 | try {
33 | const response = await fetchAndRetryWithBackoff(
34 | urls[i] + "?sitekey=" + siteKey,
35 | { headers: [["x-frc-client", "js-0.9.19"]], mode: "cors" },
36 | 2
37 | );
38 | if (response.ok) {
39 | const json = await response.json();
40 | return json.data.puzzle;
41 | } else {
42 | let json;
43 | try {
44 | json = await response.json();
45 | } catch (e) {
46 | /* Do nothing, the error is not valid JSON */
47 | }
48 |
49 | if (json && json.errors && json.errors[0] === "endpoint_not_enabled") {
50 | throw Error(`Endpoint not allowed (${response.status})`);
51 | }
52 |
53 | if (i === urls.length - 1) {
54 | throw Error(`Response status ${response.status} ${response.statusText} ${json ? json.errors : ""}`);
55 | }
56 | }
57 | } catch (e) {
58 | console.error("[FRC Fetch]:", e);
59 | const err = new Error(`${lang.text_fetch_error} ${urls[i]}`);
60 | (err as any).rawError = e;
61 | throw err;
62 | }
63 | }
64 | // This code should never be reached.
65 | throw Error(`Internal error`);
66 | }
67 |
68 | /**
69 | * Retries given request with exponential backoff (starting with 1000ms delay, multiplying by 4 every time)
70 | * @param url Request (can be string url) to fetch
71 | * @param opts Options for fetch
72 | * @param n Number of times to attempt before giving up.
73 | */
74 | export async function fetchAndRetryWithBackoff(url: RequestInfo, opts: RequestInit, n: number): Promise {
75 | let time = 1000;
76 | return fetch(url, opts).catch(async (error) => {
77 | if (n === 0) throw error;
78 | await new Promise((r) => setTimeout(r, time));
79 | time *= 4;
80 | return fetchAndRetryWithBackoff(url, opts, n - 1);
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/docs/verification_api.md:
--------------------------------------------------------------------------------
1 | # Verification API
2 |
3 | You will need an API key to prove it's you, you can create one on the [**API Keys page**](https://app.friendlycaptcha.eu/dashboard) in the dashboard.
4 |
5 | To verify the CAPTCHA solution, make a POST request to `https://api.friendlycaptcha.com/api/v1/siteverify` with the following parameters:
6 |
7 | | POST Parameter | Description |
8 | |----------------|-----------------------------------------------------|
9 | | `solution` | The solution value that the user submitted in the `frc-captcha-solution` field |
10 | | `secret` | An API key that proves it's you, create one on the Friendly Captcha website |
11 | | `sitekey` | **Optional:** the sitekey that you want to make sure the puzzle was generated from. |
12 |
13 | You can pass these parameters in a JSON body, or as formdata.
14 |
15 | > If your account is on the **Advanced** or **Enterprise** plan your server can also make a request [to our EU endpoint](./eu_endpoint).
16 |
17 | ### The verification response
18 |
19 | The response will tell you whether the CAPTCHA solution is valid and hasn't been used before. The response body is a JSON object:
20 |
21 | ```JSON
22 | {
23 | "success": true|false,
24 | "errors": [...] // optional
25 | }
26 | ```
27 |
28 | If `success` is false, `errors` will be a list containing at least one of the following error codes below. **If you are seeing status code 400 or 401 your server code is probably not configured correctly.**
29 |
30 |
31 | | Error code | Status |Description |
32 | |----------------|----------|-------------------------------------------|
33 | | `secret_missing` | 400 | You forgot to add the secret (=API key) parameter. |
34 | | `secret_invalid` | 401 | The API key you provided was invalid. |
35 | | `solution_missing` | 400 | You forgot to add the solution parameter. |
36 | | `bad_request` | 400 | Something else is wrong with your request, e.g. your request body is empty. |
37 | | `solution_invalid` | 200 | The solution you provided was invalid (perhaps the user tried to tamper with the puzzle). |
38 | | `solution_timeout_or_duplicate` | 200 | The puzzle that the solution was for has expired or has already been used. |
39 |
40 |
41 | > ⚠️ Status code 200 does not mean the solution was valid, it just means the verification was performed succesfully. Use the `success` field.
42 |
43 | A solution can be invalid for a number of reasons, perhaps the user submitted before the CAPTCHA was completed or they tried to change the puzzle to make it easier. The first case can be prevented by disabling the submit button until the CAPTCHA has been completed succesfully.
44 |
45 | ### Verification Best practices
46 | If you receive a response code other than 200 in production, you should probably accept the user's form despite not having been able to verify the CAPTCHA solution.
47 |
48 | Maybe your server is misconfigured or the Friendly Captcha servers are down. While we try to make sure that never happens, it is a good idea to assume one day disaster will strike.
49 |
50 | An example: you are using Friendly Captcha for a sign up form and you can't verify the solution. It is better to trust the user and let them sign up anyway, because otherwise no signup will be possible at all. Do send an alert to yourself!
51 |
52 |
--------------------------------------------------------------------------------
/docs/browser_support.md:
--------------------------------------------------------------------------------
1 | # Browser Support
2 |
3 | All modern browsers are supported, on both mobile and desktop, all releases up to at least 8 years old. That includes Safari, Edge, Chrome, Firefox, and Opera. Internet Explorer 11 also works, with some sidenotes (see the section below). See the targeted [**browserlist compatible browsers**](https://browserslist.dev/?q=c2luY2UgMjAxMywgbm90IGRlYWQsIG5vdCBpZSA8PTEwLCBub3QgaWVfbW9iIDw9IDEx).
4 |
5 | ## Polyfills
6 |
7 | If you want to support browsers over 5 years old, you will need some polyfills (`fetch`, `Promise`, `URL` and `Object.assign`).
8 |
9 | You can use the build that includes the polyfills:
10 |
11 | ```html
12 |
18 |
24 | ```
25 |
26 | Or you can include the polyfills manually:
27 |
28 | ```html
29 |
30 |
31 |
32 |
33 | ```
34 |
35 | If you find any compatibility issues please create a [**Github issue**](https://github.com/FriendlyCaptcha/friendly-challenge/issues).
36 |
37 | ## Compatibility mode for the library
38 |
39 | If you are importing _friendly-challenge_ into your own bundle and want to support old browsers (those that don't support ES2017) you should change your imports to be from `friendly-challenge/compat`. For example:
40 |
41 | ```
42 | import {WidgetInstance} from 'friendly-challenge'
43 | // change to
44 | import {WidgetInstance} from 'friendly-challenge/compat'
45 | ```
46 |
47 | Both imports are ES2017, use a tool like Babel to transpile it to ES5 or lower. The difference between these two imports is the webworker script which is included as a string. In the _compat_ build it is ES5 compatible and includes necessary polyfills (at the cost of slighlty worse performance and an extra 3KB bundle size).
48 |
49 | ### Old browser speed
50 |
51 | The Javascript engine in old browsers is generally slower than modern ones, the CAPTCHA may take a minute to solve on very old browsers (>5 years old).
52 |
53 | ## NoScript
54 |
55 | Users need to have Javascript enabled to solve the CAPTCHA. We recommend you add a note for users that have Javascript disabled by default:
56 |
57 | ```html
58 |
59 | ```
60 |
61 | This will only be visible to users without Javascript enabled.
62 |
63 | ## Internet Explorer
64 |
65 | Internet Explorer 11 is supported out of the box, but take note that **the Javascript engine is very slow in Internet Explorer leading to a poor user experience**: the CAPTCHA will likely take more than a minute to solve.
66 |
67 | Consider displaying a message to IE users that they should use a different browser. You can use this Javascript snippet to display a note after the widget in Internet Explorer only:
68 |
69 | ```javascript
70 | if (!!document.documentMode) {
71 | // Only true in Internet Explorer
72 | Array.prototype.slice.call(document.querySelectorAll(".frc-captcha")).forEach(function (element) {
73 | var messageElement = document.createElement("p");
74 | messageElement.innerHTML =
75 | "The anti-robot check works better and faster in modern browsers such as Edge, Firefox, or Chrome. Please consider updating your browser";
76 | element.parentNode.insertBefore(messageElement, element.nextSibling);
77 | });
78 | }
79 | ```
80 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | .frc-captcha * {
2 | /* Mostly a CSS reset so existing website styles don't clash */
3 | margin: 0;
4 | padding: 0;
5 | border: 0;
6 | text-align: initial;
7 | border-radius: 0;
8 | filter: none !important;
9 | transition: none !important;
10 | font-weight: normal;
11 | font-size: 14px;
12 | line-height: 1.2;
13 | text-decoration: none;
14 | background-color: initial;
15 | color: #222;
16 | }
17 |
18 | .frc-captcha {
19 | position: relative;
20 | min-width: 250px;
21 | max-width: 312px;
22 | border: 1px solid #f4f4f4;
23 | padding-bottom: 12px;
24 | background-color: #fff;
25 | }
26 |
27 | .frc-captcha b {
28 | font-weight: bold;
29 | }
30 |
31 | .frc-container {
32 | display: flex;
33 | align-items: center;
34 | min-height: 52px;
35 | }
36 |
37 | .frc-icon {
38 | fill: #222;
39 | stroke: #222;
40 | flex-shrink: 0;
41 | margin: 8px 8px 0 8px;
42 | }
43 |
44 | .frc-icon.frc-warning {
45 | fill: #C00;
46 | }
47 |
48 | .frc-success .frc-icon {
49 | animation: frc-fade-in 1s both ease-in;
50 | }
51 |
52 | .frc-content {
53 | white-space: nowrap;
54 | display: flex;
55 | flex-direction: column;
56 | margin: 4px 6px 0 0;
57 | overflow-x: auto;
58 | flex-grow: 1;
59 | }
60 |
61 | .frc-banner {
62 | position: absolute;
63 | bottom: 0px;
64 | right: 6px;
65 | line-height: 1;
66 | }
67 |
68 | .frc-banner * {
69 | font-size: 10px;
70 | opacity: 0.8;
71 | text-decoration: none;
72 | }
73 |
74 | .frc-progress {
75 | -webkit-appearance: none;
76 | -moz-appearance: none;
77 | appearance: none;
78 | margin: 3px 0;
79 | height: 4px;
80 | border: none;
81 | background-color: #eee;
82 | color: #222;
83 | width: 100%;
84 | transition: all 0.5s linear;
85 | }
86 |
87 | .frc-progress::-webkit-progress-bar {
88 | background: #eee;
89 | }
90 |
91 | .frc-progress::-webkit-progress-value {
92 | background: #222;
93 | }
94 |
95 | .frc-progress::-moz-progress-bar {
96 | background: #222;
97 | }
98 |
99 | .frc-button {
100 | cursor: pointer;
101 | padding: 2px 6px;
102 | background-color: #f1f1f1;
103 | border: 1px solid transparent;
104 | text-align: center;
105 | font-weight: 600;
106 | text-transform: none;
107 | }
108 |
109 | .frc-button:focus {
110 | border: 1px solid #333;
111 | }
112 |
113 | .frc-button:hover {
114 | background-color: #ddd;
115 | }
116 |
117 | .frc-captcha-solution {
118 | display: none;
119 | }
120 |
121 | .frc-err-url {
122 | text-decoration: underline;
123 | font-size: 0.9em;
124 | }
125 |
126 | /* RTL support */
127 |
128 | .frc-rtl {
129 | direction: rtl;
130 | }
131 |
132 | .frc-rtl .frc-content {
133 | margin: 4px 0 0 6px;
134 | }
135 |
136 | .frc-banner.frc-rtl {
137 | left: 6px;
138 | right: auto;
139 | }
140 |
141 | /* Dark theme */
142 |
143 | .dark.frc-captcha {
144 | color: #fff;
145 | background-color: #222;
146 | border-color: #333;
147 | }
148 |
149 | .dark.frc-captcha * {
150 | color: #fff;
151 | }
152 |
153 | .dark.frc-captcha button {
154 | background-color: #444;
155 | }
156 |
157 | .dark .frc-icon {
158 | fill: #fff;
159 | stroke: #fff;
160 | }
161 |
162 | .dark .frc-progress {
163 | background-color: #444;
164 | }
165 |
166 | .dark .frc-progress::-webkit-progress-bar {
167 | background: #444;
168 | }
169 |
170 | .dark .frc-progress::-webkit-progress-value {
171 | background: #ddd;
172 | }
173 |
174 | .dark .frc-progress::-moz-progress-bar {
175 | background: #ddd;
176 | }
177 |
178 | @keyframes frc-fade-in {
179 | from {
180 | opacity: 0;
181 | }
182 | to {
183 | opacity: 1;
184 | }
185 | }
--------------------------------------------------------------------------------
/docs/eu_endpoint.md:
--------------------------------------------------------------------------------
1 | # 🇪🇺 Dedicated EU Endpoint
2 |
3 | By default the FriendlyCaptcha widget talks to our global service served from all over the world to retrieve CAPTCHA puzzles. Depending on your user's geography this request may be served from outside the EU.
4 |
5 | As a premium feature we offer a dedicated forwarding endpoint hosted in Germany as an additional guarantee that the personal information (i.e. visitor IP addresses) never leave the EU.
6 |
7 | > Note: Using this service requires a **Friendly Captcha Advanced** or **Enterprise** plan.
8 |
9 | ## Enabling the EU endpoint
10 | Open your [account page](https://app.friendlycaptcha.eu/dashboard/accounts") and click **Manage** on the app you want to enable the EU endpoint for.
11 |
12 | In the **Puzzle Endpoints** section you are able to enable and disable the endpoints you allow your visitors to fetch puzzles from.
13 |
14 | > We advise you to enable both for now. Later when you confirm everything is working you can disable the global endpoint.
15 |
16 | ## Configuring the widget
17 | The widget talks to the global endpoint by default, you need to tell it to use the EU endpoint instead.
18 |
19 | You can use the `data-puzzle-endpoint` HTML attribute:
20 |
21 | ```html
22 |
23 |
24 | ```
25 |
26 | Or if you are using the [Javascript widget API](http://docs.friendlycaptcha.com/#/widget_api?id=javascript-api) you can specify it in the options passed in the constructor:
27 | ```javascript
28 | import { WidgetInstance } from "friendly-challenge";
29 |
30 | const element = document.querySelector("#my-widget");
31 | const options = {
32 | puzzleEndpoint: "https://eu-api.friendlycaptcha.eu/api/v1/puzzle",
33 | /* ... other options */
34 | }
35 | const widget = new WidgetInstance(element, options);
36 | ```
37 |
38 | And with this the widget will only ever make requests to our EU endpoint. No changes are required for the verification of submitted solutions.
39 |
40 | ## Fallback to global service
41 | Although we work hard to make sure it never happens, disaster may one day strike (e.g. a meteor strike to our German data center). In case our EU endpoint service goes down you can instruct your widget to use the global service as a fallback.
42 |
43 | You can do this by specifying both endpoints separated with a comma (`,`) in order of preference:
44 | ```html
45 |
46 |
47 | ```
48 |
49 | ## EU Verification endpoint
50 | Your servers can also use our EU endpoint for the verification of submitted puzzles.
51 |
52 | Instead of the [usual verification endpoint](./verification_api) your server makes the POST request to `https://eu-api.friendlycaptcha.eu/api/v1/siteverify`.
53 |
54 | ## Reference
55 | For reference, these are the puzzle and siteverify endpoints.
56 |
57 | ### Puzzle (used in the widget configuration)
58 |
59 | | Endpoint Name | URL |
60 | |----------------|----------|
61 | | 🌍 Global | https://api.friendlycaptcha.com/api/v1/puzzle
62 | | 🇪🇺 EU | https://eu-api.friendlycaptcha.eu/api/v1/puzzle
63 |
64 | ### Siteverify (used in your backend server)
65 |
66 | | Endpoint Name | URL |
67 | |----------------|----------|
68 | | 🌍 Global | https://api.friendlycaptcha.com/api/v1/siteverify
69 | | 🇪🇺 EU | https://eu-api.friendlycaptcha.eu/api/v1/siteverify
70 |
71 |
72 | ## Troubleshooting
73 | If your widget or the browser console shows **Endpoint not allowed** or **403 Forbidden**, double-check that the you enabled the configured endpoint for the given sitekey.
74 |
75 | If you run into any other issues you can of course always reach out.
76 |
--------------------------------------------------------------------------------
/src/worker.ts:
--------------------------------------------------------------------------------
1 | import { decode } from "friendly-pow/base64";
2 | import { base64 } from "friendly-pow/wasm/optimized.wrap";
3 | import { getWasmSolver } from "friendly-pow/api/wasm";
4 | import { getJSSolver } from "friendly-pow/api/js";
5 | import { SOLVER_TYPE_JS, SOLVER_TYPE_WASM } from "friendly-pow/constants";
6 | import { Solver, DonePartMessage, MessageToWorker } from "./types";
7 |
8 | if (!Uint8Array.prototype.slice) {
9 | Object.defineProperty(Uint8Array.prototype, "slice", {
10 | value: function (begin: number, end: number) {
11 | return new Uint8Array(Array.prototype.slice.call(this, begin, end));
12 | },
13 | });
14 | }
15 |
16 | // Not technically correct, but it makes TS happy..
17 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
18 | // @ts-ignore
19 | declare let self: Worker;
20 |
21 | (self as any).ASC_TARGET = 0;
22 |
23 | // 1 for JS, 2 for WASM
24 | let solverType: 1 | 2;
25 |
26 | // Puzzle consisting of zeroes
27 | let setSolver: (s: Solver) => void;
28 | const solver: Promise = new Promise((resolve) => (setSolver = resolve));
29 |
30 | self.onerror = (evt: any) => {
31 | self.postMessage({
32 | type: "error",
33 | message: JSON.stringify(evt),
34 | });
35 | };
36 |
37 | self.onmessage = async (evt: any) => {
38 | const data: MessageToWorker = evt.data;
39 | try {
40 | /**
41 | * Compile the WASM and setup the solver.
42 | * If WASM support is not present, it uses the JS version instead.
43 | */
44 | if (data.type === "solver") {
45 | if (data.forceJS) {
46 | solverType = SOLVER_TYPE_JS;
47 | const s = await getJSSolver();
48 | setSolver(s);
49 | } else {
50 | try {
51 | solverType = SOLVER_TYPE_WASM;
52 | const module = WebAssembly.compile(decode(base64));
53 | const s = await getWasmSolver(await module);
54 | setSolver(s);
55 | } catch (e: any) {
56 | console.log(
57 | "FriendlyCaptcha failed to initialize WebAssembly, falling back to Javascript solver: " + e.toString()
58 | );
59 | solverType = SOLVER_TYPE_JS;
60 | const s = await getJSSolver();
61 | setSolver(s);
62 | }
63 | }
64 |
65 | self.postMessage({
66 | type: "ready",
67 | solver: solverType,
68 | });
69 | } else if (data.type === "start") {
70 | const solve = await solver;
71 |
72 | self.postMessage({
73 | type: "started",
74 | });
75 |
76 | let totalH = 0;
77 | let solution!: Uint8Array;
78 |
79 | // We loop over a uint32 to find as solution, it is technically possible (but extremely unlikely - only possible with very high difficulty) that
80 | // there is no solution, here we loop over one byte further up too in case that happens.
81 | for (let b = 0; b < 256; b++) {
82 | data.puzzleSolverInput[123] = b;
83 | const [s, hash] = solve(data.puzzleSolverInput, data.threshold);
84 | if (hash.length === 0) {
85 | // This means 2^32 puzzles were evaluated, which takes a while in a browser!
86 | // As we try 256 times, this is not fatal
87 | console.warn("FC: Internal error or no solution found");
88 | totalH += Math.pow(2, 32) - 1;
89 | continue;
90 | }
91 | solution = s;
92 | break;
93 | }
94 |
95 | const view = new DataView(solution.slice(-4).buffer);
96 | totalH += view.getUint32(0, true);
97 |
98 | self.postMessage({
99 | type: "done",
100 | solution: solution.slice(-8), // The last 8 bytes are the solution nonce
101 | h: totalH,
102 | puzzleIndex: data.puzzleIndex,
103 | puzzleNumber: data.puzzleNumber,
104 | } as DonePartMessage);
105 | }
106 | } catch (e) {
107 | setTimeout(() => {
108 | throw e;
109 | });
110 | }
111 | };
112 |
--------------------------------------------------------------------------------
/src/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FriendlyCaptcha
6 |
7 |
8 |
9 |
10 |
11 |
12 |
21 |
22 |
23 |
24 |
Localization tests
25 |
26 |
27 |
28 |
29 |
30 |
This one has an invalid data-lang, it should fall back to English.
FriendlyCaptcha ⇗
36 | ${fieldName === "-" ? "":``}`;
37 | }
38 |
39 | /**
40 | * Used when the widget is ready to start solving.
41 | */
42 | export function getReadyHTML(fieldName: string, l: Localization) {
43 | return getTemplate(
44 | fieldName,
45 | l.rtl,
46 | ``,
47 | true,
48 | l.text_ready,
49 | ".UNSTARTED",
50 | l.button_start,
51 | false
52 | );
53 | }
54 |
55 | /**
56 | * Used when the widget is retrieving a puzzle
57 | */
58 | export function getFetchingHTML(fieldName: string, l: Localization) {
59 | return getTemplate(fieldName, l.rtl, loaderSVG, true, l.text_fetching, ".FETCHING", undefined, true);
60 | }
61 |
62 | /**
63 | * Used when the solver is running, displays a progress bar.
64 | */
65 | export function getRunningHTML(fieldName: string, l: Localization) {
66 | return getTemplate(fieldName, l.rtl, loaderSVG, true, l.text_solving, ".UNFINISHED", undefined, true);
67 | }
68 |
69 | export function getDoneHTML(fieldName: string, l: Localization, solution: string, data: DoneMessage) {
70 | const timeData = `${data.t.toFixed(0)}s (${((data.h / data.t) * 0.001).toFixed(0)}K/s)${
71 | data.solver === SOLVER_TYPE_JS ? " JS Fallback" : ""
72 | }`;
73 | return getTemplate(
74 | fieldName,
75 | l.rtl,
76 | `${l.text_completed_sr}`,
77 | false,
78 | l.text_completed,
79 | solution,
80 | undefined,
81 | false,
82 | timeData,
83 | "frc-success"
84 | );
85 | }
86 |
87 | export function getExpiredHTML(fieldName: string, l: Localization) {
88 | return getTemplate(fieldName, l.rtl, errorSVG, true, l.text_expired, ".EXPIRED", l.button_restart);
89 | }
90 |
91 | export function getErrorHTML(
92 | fieldName: string,
93 | l: Localization,
94 | errorDescription: string,
95 | recoverable = true,
96 | headless = false
97 | ) {
98 | return getTemplate(
99 | fieldName,
100 | l.rtl,
101 | errorSVG,
102 | true,
103 | `${l.text_error} ${errorDescription}`,
104 | headless ? ".HEADLESS_ERROR" : ".ERROR",
105 | recoverable ? l.button_retry : undefined
106 | );
107 | }
108 |
109 | export function findCaptchaElements() {
110 | const elements = document.querySelectorAll(".frc-captcha");
111 | if (elements.length === 0) {
112 | console.warn("FriendlyCaptcha: No div was found with .frc-captcha class");
113 | }
114 | return elements;
115 | }
116 |
117 | /**
118 | * Injects the style if no #frc-style element is already present
119 | * (to support custom stylesheets)
120 | */
121 | export function injectStyle(styleNonce: string | null = null) {
122 | if (!document.querySelector("#frc-style")) {
123 | const styleSheet = document.createElement("style");
124 | styleSheet.id = "frc-style";
125 | styleSheet.innerHTML = css;
126 |
127 | if (styleNonce) {
128 | styleSheet.setAttribute('nonce', styleNonce);
129 | }
130 |
131 | document.head.appendChild(styleSheet);
132 | }
133 | }
134 |
135 | /**
136 | * @param element parent element of friendlycaptcha
137 | * @param progress value between 0 and 1
138 | */
139 | export function updateProgressBar(element: HTMLElement, data: ProgressMessage) {
140 | const p = element.querySelector(".frc-progress") as HTMLProgressElement;
141 | const perc = (data.i + 1) / data.n;
142 | if (p) {
143 | p.value = perc;
144 | p.innerText = (perc*100).toFixed(1) + "%";
145 | p.title = data.i + 1 + "/" + data.n + " (" + ((data.h / data.t) * 0.001).toFixed(0) + "K/s)";
146 | }
147 | }
148 |
149 | /**
150 | * Traverses parent nodes until a
60 |
70 |
71 |
72 | """;
73 | }
74 | class FriendlyCaptcha extends StatefulWidget {
75 | Function(String solution) callback;
76 |
77 | String sitekey;
78 | String theme;
79 | String start;
80 | String lang;
81 | String puzzleEndpoint;
82 |
83 | FriendlyCaptcha({
84 | @required this.sitekey,
85 | @required this.callback,
86 | this.theme = "",
87 | this.start = "auto",
88 | this.lang = "",
89 | this.puzzleEndpoint = ""}
90 | ) {}
91 |
92 | @override
93 | State createState() {
94 | return CaptchaState();
95 | }
96 | }
97 |
98 | class CaptchaState extends State {
99 | InAppWebViewController webViewController;
100 |
101 | @override
102 | initState() {
103 | super.initState();
104 | }
105 |
106 | @override
107 | Widget build(BuildContext context) {
108 | var htmlSource = buildPageContent(
109 | sitekey: widget.sitekey,
110 | lang: widget.lang,
111 | puzzleEndpoint: widget.puzzleEndpoint,
112 | theme: widget.theme,
113 | start: widget.start,
114 | );
115 |
116 | return ConstrainedBox(
117 | constraints: BoxConstraints(
118 | maxHeight: 100, // Empirically determined to fit the widget.. to be improved
119 | ),
120 | child: Container(
121 | child: InAppWebView(
122 | initialData: InAppWebViewInitialData(
123 | data: htmlSource
124 | ),
125 | initialOptions: InAppWebViewGroupOptions(
126 | crossPlatform: InAppWebViewOptions(
127 | useShouldOverrideUrlLoading: true,
128 | disableContextMenu: true,
129 | clearCache: true,
130 | incognito: true,
131 | applicationNameForUserAgent: "FriendlyCaptchaFlutter"
132 | ),
133 | android: AndroidInAppWebViewOptions(
134 | useHybridComposition: true,
135 | )
136 | ),
137 | onConsoleMessage: (controller, consoleMessage) {
138 | print(consoleMessage); // Useful for debugging, this prints (error) messages from the webview.
139 | },
140 | shouldOverrideUrlLoading: (controller, navigationAction) async {
141 | // We deny any navigation away (which could be caused by the user clicking a link)
142 | return NavigationActionPolicy.CANCEL;
143 | },
144 | onWebViewCreated: (InAppWebViewController w) {
145 | webViewController = w;
146 | w.addJavaScriptHandler(handlerName: 'solutionCallback', callback: (args) {
147 | widget.callback(args[0]["solution"]);
148 | });
149 | },
150 | )
151 | )
152 | );
153 | }
154 | }
155 | ```
156 |
157 | ### 3. Use the widget anywhere in your app
158 |
159 | The widget takes two required arguments: a `callback` that is called when the widget is completed, and your `sitekey`.
160 |
161 | Optionally you can also pass `lang`, `puzzleEndpoint`, `start` (defaults to `"auto"`), `theme` (`"dark"` is the only theme built-in). See the [`data-attributes` documentation](#/widget_api?id=data-start-attribute).
162 |
163 | ```dart
164 | // Creates a German widget
165 | FriendlyCaptcha(
166 | callback: mySolutionCallback,
167 | sitekey: "",
168 | lang: "de",
169 | )
170 | ```
171 |
172 | **Notes:**
173 |
174 | - Usually you would store the `solution` that gets passed to the callback in your app's state.
175 | - When the user performs the action you want to require the captcha for (e.g. user signup), you would send along this solution to your server.
176 | - In your backend server you then talk to our [verification API](#/verification_api) to check whether the captcha was valid.
177 |
178 | **Possible improvements (contributions welcome!):**
179 |
180 | - If the user has no network connection we currently don't display an error.
181 | - Widget reset functionality (a captcha solution can only be used once, currently you would recreate the widget entirely if your user can submit multiple times).
182 | - Publish the above code as a Dart package and embed the widget's Javascript code so that we don't make any request to a CDN.
183 | - Currently there is some space around the widget. You can edit the body's CSS to have it match your app's background color.
184 | In the future the embedded webpage communicate its size so that the embedded WebView's size can exactly match the widget.
185 |
186 | ## Example app
187 |
188 | We created an example app which you can view [here](https://github.com/FriendlyCaptcha/friendly-captcha-flutter-example). The relevant source file is [here](https://github.com/FriendlyCaptcha/friendly-captcha-flutter-example/blob/main/friendly_captcha_flutter_app/lib/main.dart).
189 |
190 | #### Example App screenshot
191 |
192 | 
193 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | **There are three steps to adding Friendly Captcha to your website:**
4 |
5 | 1. Create an account on the [**Friendly Captcha website**](https://friendlycaptcha.com/signup) (it's free) and generate a `sitekey`.
6 | 2. Add the Friendly Captcha widget to your website
7 | 3. Change your server code to verify the CAPTCHA solutions
8 |
9 | Let's go!
10 |
11 | ## 1. Generating a sitekey
12 |
13 | Log in to your Friendly Captcha account and head to the [Applications page](https://app.friendlycaptcha.eu/dashboard).
14 |
15 | Click `Create New Application` and enter the necessary details. Once you have completed this, take note of the `sitekey` value under the application name, we will need it in the next step.
16 |
17 | A sitekey always starts with the characters `FC`.
18 |
19 | > If you don't have an account yet, you can create one [here](https://friendlycaptcha.com/signup).
20 |
21 | ## 2. Adding the widget
22 |
23 | ### Adding the widget script
24 |
25 | The **friendly-challenge** library contains the code for CAPTCHA widget. You have two options on how to add this to your website, either you can use a script tag to load the widget from any CDN that hosts NPM packages, or you can import the code into your own Javascript bundle.
26 |
27 | #### Option A: Using a script tag
28 |
29 | ```html
30 |
36 |
37 | ```
38 |
39 | > Make sure to always import a specific version (e.g. `friendly-challenge@0.9.19`), then you can be sure that the script you import and integrate with your website doesn't change unexpectedly.
40 |
41 | It is recommended that you include the `async` and `defer` attributes like in the examples above, they make sure that the browser does not wait to load these scripts to show your website. The size of the scripts is 18KB (8.5KB compressed) for modern browsers, and 24KB (10KB compressed) for old browsers.
42 |
43 | > If you want to support old browsers, you can instead use a polyfill build, see the [**browser support**](browser_support?id=polyfills) page.
44 |
45 | ##### Download and self-host the widget library (releases, recommended by GDPR)
46 |
47 | Instead of using a CDN (e.g. for GDPR reasons) you can of course also download the library .js files and host them on your server.
48 | Simply download the latest release from one of the CDN's mentioned above, like: https://cdn.jsdelivr.net/npm/friendly-challenge/
49 |
50 | - The module widget.module.min.js: https://cdn.jsdelivr.net/npm/friendly-challenge/widget.module.min.js
51 | - The nomodule widget.min.js: https://cdn.jsdelivr.net/npm/friendly-challenge/widget.min.js
52 |
53 | **Please remember to update to the latest release from time to time.**
54 |
55 | #### Option B: Import the library into your Javascript code
56 |
57 | Alternatively, you can install the **friendly-challenge** library using a package manager such as npm:
58 |
59 | ```bash
60 | npm install --save friendly-challenge
61 | ```
62 |
63 | You can then import it into your app:
64 |
65 | ```javascript
66 | import "friendly-challenge/widget";
67 | ```
68 |
69 | > It is also possible to create and interact with the widget using the Javascript API. In this tutorial we will consider the simple case in which you want to secure a simple HTML form. If you are making a single page application (using e.g. React) you will probably want to use the API instead. See the [API documentation page](/widget_api).
70 |
71 | ### Adding the widget itself
72 |
73 | The friendly-challenge code you added won't do anything unless there is a special HTML element present that tells it where to create the widget. It will check for this widget once when it gets loaded, you can programmatically make it check for the element again.
74 |
75 | Where you want to add a Friendly Captcha widget, add
76 |
77 | ```html
78 |
79 | ```
80 |
81 | Replace `` with the sitekey that you created in step 1. The widget will be rendered where you include this element, this should be inside the `