Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Score image captcha #1611

Open
wants to merge 34 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5e61cb8
Pass sessions for image captcha. Attach scores
forgetso Jan 12, 2025
ccd2157
Merge branch 'staging' into score-image-captcha
forgetso Jan 12, 2025
908bdbe
lint-fix
forgetso Jan 13, 2025
6fc3bdc
Merge branch 'staging' into score-image-captcha
forgetso Jan 17, 2025
bde9742
Merge staging
forgetso Jan 17, 2025
b70037d
Extend session storage period
forgetso Jan 17, 2025
1ce1f6b
Pass score through to verify methods
forgetso Jan 17, 2025
a3d45e4
Set references to frictionlesss tokens. Breakdown scores into compone…
forgetso Jan 19, 2025
f4b6b0c
remove zod ObjectId refine
forgetso Jan 19, 2025
c3536d5
Fix auth. Add some tests
forgetso Jan 19, 2025
941c757
Add a util function for getting frictionless token IDs
forgetso Jan 19, 2025
c8d86de
lint-fix
forgetso Jan 19, 2025
94588ee
Fix tests
forgetso Jan 20, 2025
996b7b2
Update packate dep
forgetso Jan 20, 2025
5595cce
Add tier to client record
forgetso Jan 20, 2025
3995efc
Remove score reporting for free tier
forgetso Jan 20, 2025
44c50fb
Add tier to ClientRecord
forgetso Jan 20, 2025
a6b8c7b
wip - admin routes
forgetso Jan 20, 2025
40ab3d3
Fix admin API auth
forgetso Jan 20, 2025
81d850c
Set captcha type
forgetso Jan 20, 2025
acd5340
Remove debug and restrict score on verify API endpoint
forgetso Jan 20, 2025
2ea8aec
Make frictionless penalties env configurable
forgetso Jan 20, 2025
e9bd05c
Add test command to api-express-router
forgetso Jan 20, 2025
7e877f6
lint-fix
forgetso Jan 20, 2025
667910f
Fix authMiddleware tests
forgetso Jan 20, 2025
1695069
lint-fix
forgetso Jan 20, 2025
8dec690
Allow score to be stored on user access policy
forgetso Jan 20, 2025
3226064
Allow passing score via API
forgetso Jan 20, 2025
a3e84b3
Add missing score
forgetso Jan 20, 2025
e705992
Fix user access policy tests
forgetso Jan 20, 2025
a55e563
Remove debug and ensure correct score is used
forgetso Jan 20, 2025
74fd0f2
Merge branch 'staging' into score-image-captcha
forgetso Jan 20, 2025
f2b70aa
Merge branch 'staging' into score-image-captcha
forgetso Jan 20, 2025
239551e
Merge branch 'staging' into score-image-captcha
forgetso Jan 22, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
</div>
<div class="row">
<div class="procaptcha w-300 m-auto"
data-sitekey="5FWCbfR7pH9QiZqLgmm5Rw4QbFwyU5EaMqUV4G6xrvrTZDtC"
data-sitekey="5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw"
data-captcha-type="image"></div>
</div>
<div class="row text-center">
Expand Down
7 changes: 4 additions & 3 deletions demos/client-example-server/src/controllers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ const verify = async (
}),
});

const verified = (await response.json()).verified;
console.log("Verified", verified);
return verified;
const verifiedResponse = await response.json();
console.log(verifiedResponse);
return verifiedResponse.verified;
}
// verify using the TypeScript library
const verified = await prosopoServer.isVerified(token);
console.log(verified);
return verified;
};

Expand Down
3 changes: 2 additions & 1 deletion demos/cypress-shared/cypress/e2e/captcha.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { getPairAsync } from "@prosopo/keyring";
import {
AdminApiPaths,
type Captcha,
CaptchaType,
type IUserSettings,
} from "@prosopo/types";
import { at } from "@prosopo/util";
Expand All @@ -32,7 +33,7 @@ describe("Captchas", () => {
const signature = u8aToHex(pair.sign(timestamp.toString()));
const adminSiteKeyURL = `http://localhost:9229${AdminApiPaths.SiteKeyRegister}`;
const settings: IUserSettings = {
captchaType: "pow",
captchaType: CaptchaType.frictionless,
domains: ["0.0.0.0"],
frictionlessThreshold: 0.5,
powDifficulty: 2,
Expand Down
3 changes: 2 additions & 1 deletion demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { getPairAsync } from "@prosopo/keyring";
import {
AdminApiPaths,
type Captcha,
CaptchaType,
type IUserSettings,
} from "@prosopo/types";
import { checkboxClass, getWidgetElement } from "../support/commands.js";
Expand All @@ -31,7 +32,7 @@ describe("Captchas", () => {
const signature = u8aToHex(pair.sign(timestamp.toString()));
const adminSiteKeyURL = `http://localhost:9229${AdminApiPaths.SiteKeyRegister}`;
const settings: IUserSettings = {
captchaType: "pow",
captchaType: CaptchaType.frictionless,
domains: ["0.0.0.0"],
frictionlessThreshold: 0.5,
powDifficulty: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { getPairAsync } from "@prosopo/keyring";
import {
AdminApiPaths,
type Captcha,
CaptchaType,
type IUserSettings,
} from "@prosopo/types";
import { checkboxClass, getWidgetElement } from "../support/commands.js";
Expand All @@ -31,7 +32,7 @@ describe("Captchas", () => {
const signature = u8aToHex(pair.sign(timestamp.toString()));
const adminSiteKeyURL = `http://localhost:9229${AdminApiPaths.SiteKeyRegister}`;
const settings: IUserSettings = {
captchaType: "pow",
captchaType: CaptchaType.frictionless,
domains: ["0.0.0.0"],
frictionlessThreshold: 0.5,
powDifficulty: 2,
Expand Down
2 changes: 1 addition & 1 deletion demos/ios-webview/procaptcha/procaptcha.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<form action="" method="POST">
<input type="text" name="email" placeholder="Email" />
<input type="password" name="password" placeholder="Password" />
<div class="procaptcha" data-sitekey="5FWCbfR7pH9QiZqLgmm5Rw4QbFwyU5EaMqUV4G6xrvrTZDtC" data-captcha-type="image"></div>
<div class="procaptcha" data-sitekey="5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" data-captcha-type="image"></div>
<br />
<input type="submit" value="Submit" />
</form>
Expand Down
1 change: 1 addition & 0 deletions demos/provider-mock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"build:cjs": "echo 'no cjs build'"
},
"dependencies": {
"@prosopo/api-express-router": "2.3.1",
"@prosopo/common": "2.3.1",
"@prosopo/provider": "2.3.1",
"@prosopo/types": "2.3.1",
Expand Down
3 changes: 2 additions & 1 deletion demos/provider-mock/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ProsopoApiError } from "@prosopo/common";
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -12,6 +11,8 @@ import { ProsopoApiError } from "@prosopo/common";
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { ProsopoApiError } from "@prosopo/common";
import {
ApiPaths,
VerifySolutionBody,
Expand Down
3 changes: 2 additions & 1 deletion demos/provider-mock/src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { handleErrors } from "@prosopo/api-express-router";
import { LogLevel, getLogger } from "@prosopo/common";
import { i18nMiddleware } from "@prosopo/locale";
import { handleErrors } from "@prosopo/provider";
import cors from "cors";
import express from "express";
import { prosopoRouter } from "./api.js";
Expand Down
3 changes: 3 additions & 0 deletions demos/provider-mock/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
},
"include": ["src", "src/**/*.json"],
"references": [
{
"path": "../../packages/api-express-router"
},
{
"path": "../../packages/common"
},
Expand Down
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/api-express-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
}
},
"scripts": {
"test": "echo \"No test specified\"",
"test": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --config ./vite.test.config.ts",
"clean": "tsc --build --clean",
"build": "tsc --build --verbose",
"build:cjs": "npx vite --config vite.cjs.config.ts build"
Expand Down
46 changes: 27 additions & 19 deletions packages/api-express-router/src/apiExpressRouterFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@
// limitations under the License.

import type { ApiRoute, ApiRoutesProvider } from "@prosopo/api-route";
import { type Request, type Response, Router } from "express";
import {
type NextFunction,
type Request,
type Response,
Router,
} from "express";
import type { ApiExpressEndpointAdapter } from "./endpointAdapter/apiExpressEndpointAdapter.js";
import { handleErrors } from "./errorHandler.js";

class ApiExpressRouterFactory {
public createRouter(
Expand All @@ -26,6 +32,11 @@ class ApiExpressRouterFactory {

this.registerRoutes(router, apiRoutes, apiEndpointAdapter);

// Your error handler should always be at the end of your application stack. Apparently it means not only after all
// app.use() but also after all your app.get() and app.post() calls.
// https://stackoverflow.com/a/62358794/1178971
router.use(handleErrors);

return router;
}

Expand All @@ -35,26 +46,23 @@ class ApiExpressRouterFactory {
apiEndpointAdapter: ApiExpressEndpointAdapter,
): void {
for (const route of routes) {
this.registerRoute(router, route, apiEndpointAdapter);
router.post(
route.path,
async (
request: Request,
response: Response,
next: NextFunction,
): Promise<void> => {
await apiEndpointAdapter.handleRequest(
route.endpoint,
request,
response,
next,
);
},
);
}
}

protected registerRoute(
router: Router,
route: ApiRoute,
apiEndpointAdapter: ApiExpressEndpointAdapter,
): void {
router.post(
route.path,
async (request: Request, response: Response): Promise<void> => {
await apiEndpointAdapter.handleRequest(
route.endpoint,
request,
response,
);
},
);
}
}

export { ApiExpressRouterFactory };
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
// limitations under the License.

import type { ApiEndpoint } from "@prosopo/api-route";
import type { Logger } from "@prosopo/common";
import type { Request, Response } from "express";
import { type Logger, ProsopoApiError } from "@prosopo/common";
import type { NextFunction, Request, Response } from "express";
import type { ZodType } from "zod";
import type { ApiExpressEndpointAdapter } from "./apiExpressEndpointAdapter.js";

Expand All @@ -28,10 +28,20 @@ class ApiExpressDefaultEndpointAdapter implements ApiExpressEndpointAdapter {
endpoint: ApiEndpoint<ZodType | undefined>,
request: Request,
response: Response,
next: NextFunction,
): Promise<void> {
let args: unknown;
try {
const args = endpoint.getRequestArgsSchema()?.parse(request.body);
args = endpoint.getRequestArgsSchema()?.parse(request.body);
} catch (error) {
return next(
new ProsopoApiError("API.PARSE_ERROR", {
context: { code: 400, error: error },
}),
);
}

try {
const apiEndpointResponse = await endpoint.processRequest(args);

response.json(apiEndpointResponse);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
// limitations under the License.

import type { ApiEndpoint } from "@prosopo/api-route";
import type { Request, Response } from "express";
import type { NextFunction, Request, Response } from "express";
import type { ZodType } from "zod";

interface ApiExpressEndpointAdapter {
handleRequest(
endpoint: ApiEndpoint<ZodType | undefined>,
request: Request,
response: Response,
next: NextFunction,
): Promise<void>;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/api-express-router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ export {
createApiExpressDefaultEndpointAdapter,
type ApiExpressEndpointAdapter,
};

export * from "./errorHandler.js";
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { NextFunction, Request, Response } from "express";
// limitations under the License.
import { describe, expect, it, vi } from "vitest";
import { ZodError } from "zod";
import { handleErrors } from "../../../api/errorHandler.js";
import { handleErrors } from "../../errorHandler.js";

describe("handleErrors", () => {
it("should handle ProsopoApiError", () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/api-express-router/vite.test.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import fs from "node:fs";
import path from "node:path";
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { ViteTestConfig } from "@prosopo/config";
import dotenv from "dotenv";
process.env.NODE_ENV = "test";
// if .env.test exists at this level, use it, otherwise use the one at the root
const envFile = `.env.${process.env.NODE_ENV || "development"}`;
let envPath = envFile;
if (fs.existsSync(envFile)) {
envPath = path.resolve(envFile);
} else if (fs.existsSync(`../../${envFile}`)) {
envPath = path.resolve(`../../${envFile}`);
} else {
throw new Error(`No ${envFile} file found`);
}

dotenv.config({ path: envPath });

export default ViteTestConfig;
4 changes: 4 additions & 0 deletions packages/api/src/api/ProviderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default class ProviderApi
public getCaptchaChallenge(
userAccount: string,
randomProvider: RandomProvider,
sessionId?: string,
): Promise<CaptchaResponseBody> {
const { provider } = randomProvider;
const dappAccount = this.account;
Expand All @@ -67,6 +68,9 @@ export default class ProviderApi
[ApiParams.user]: userAccount,
[ApiParams.datasetId]: provider.datasetId,
};
if (sessionId) {
body[ApiParams.sessionId] = sessionId;
}
return this.post(ApiPaths.GetImageCaptchaChallenge, body, {
headers: {
"Prosopo-Site-Key": this.account,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/siteKeyRegister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { KeyringPair } from "@polkadot/keyring/types";
import { LogLevel, type Logger, getLogger } from "@prosopo/common";
import { ProviderEnvironment } from "@prosopo/env";
import { Tasks } from "@prosopo/provider";
import type { ProsopoConfigOutput } from "@prosopo/types";
import { CaptchaTypeSpec, type ProsopoConfigOutput } from "@prosopo/types";
import type { ArgumentsCamelCase, Argv } from "yargs";
import { validateSiteKey } from "./validators.js";

Expand Down Expand Up @@ -70,7 +70,7 @@ export default (
} = argv;
const tasks = new Tasks(env);
await tasks.clientTaskManager.registerSiteKey(sitekey as string, {
captchaType: captcha_type as "image" | "pow" | "frictionless",
captchaType: CaptchaTypeSpec.parse(captcha_type),
frictionlessThreshold: frictionless_threshold as number,
domains: url as string[],
powDifficulty: pow_difficulty as number,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/siteKeyRegisterApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ProviderApi } from "@prosopo/api";
import { LogLevel, type Logger, getLogger } from "@prosopo/common";
import { ProviderEnvironment } from "@prosopo/env";
import { Tasks } from "@prosopo/provider";
import type { ProsopoConfigOutput } from "@prosopo/types";
import { CaptchaTypeSpec, type ProsopoConfigOutput } from "@prosopo/types";
import { u8aToHex } from "@prosopo/util";
import type { ArgumentsCamelCase, Argv } from "yargs";
import { validateSiteKey } from "./validators.js";
Expand Down Expand Up @@ -84,7 +84,7 @@ export default (
await api.registerSiteKey(
sitekey as string,
{
captchaType: captcha_type as "image" | "pow" | "frictionless",
captchaType: CaptchaTypeSpec.parse(captcha_type),
frictionlessThreshold: frictionless_threshold as number,
domains: domains as string[],
powDifficulty: pow_difficulty as number,
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/prosopo.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getLogLevel } from "@prosopo/common";
import {
DatabaseTypes,
EnvironmentTypesSchema,
FrictionlessPenalties,
type ProsopoCaptchaCountConfigSchemaInput,
type ProsopoCaptchaSolutionConfigSchema,
type ProsopoConfigInput,
Expand Down Expand Up @@ -92,7 +93,10 @@ export default function getConfig(
},
captchaSolutions: captchaSolutionsConfig,
captchas: captchaServeConfig,
devOnlyWatchEvents: process.env._DEV_ONLY_WATCH_EVENTS === "true",
penalties: FrictionlessPenalties.parse({
PENALTY_OLD_TIMESTAMP: process.env.PENALTY_OLD_TIMESTAMP,
PENALTY_ACCESS_RULE: process.env.PENALTY_ACCESS_RULE,
}),
mongoEventsUri: process.env.PROSOPO_MONGO_EVENTS_URI || "",
mongoCaptchaUri: process.env.PROSOPO_MONGO_CAPTCHA_URI || "",
mongoClientUri: process.env.PROSOPO_MONGO_CLIENT_URI || "",
Expand Down
Loading
Loading