Skip to content

Commit

Permalink
Set references to frictionlesss tokens. Breakdown scores into compone…
Browse files Browse the repository at this point in the history
…nts. Add to score when user access policy set.
  • Loading branch information
forgetso committed Jan 19, 2025
1 parent 1ce1f6b commit a3d45e4
Show file tree
Hide file tree
Showing 29 changed files with 329 additions and 92 deletions.
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
20 changes: 18 additions & 2 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 @@ -46,11 +57,16 @@ class ApiExpressRouterFactory {
): void {
router.post(
route.path,
async (request: Request, response: Response): Promise<void> => {
async (
request: Request,
response: Response,
next: NextFunction,
): Promise<void> => {
await apiEndpointAdapter.handleRequest(
route.endpoint,
request,
response,
next,
);
},
);
Expand Down
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
File renamed without changes.
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
24 changes: 17 additions & 7 deletions packages/database/src/databases/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
type DatasetWithIdsAndTree,
DatasetWithIdsAndTreeSchema,
type Hash,
type PendingCaptchaRequest,
type PoWChallengeComponents,
type PoWChallengeId,
type RequestHeaders,
Expand All @@ -41,11 +40,13 @@ import {
ClientRecordSchema,
DatasetRecordSchema,
type FrictionlessToken,
type FrictionlessTokenId,
FrictionlessTokenRecordSchema,
type IPBlockRuleRecord,
IPBlockRuleRecordSchema,
type IProviderDatabase,
type IUserDataSlim,
type PendingCaptchaRequest,
type PendingCaptchaRequestMongoose,
PendingRecordSchema,
type PoWCaptchaRecord,
Expand Down Expand Up @@ -566,7 +567,7 @@ export class ProviderDatabase
* @param providerSignature
* @param ipAddress
* @param headers
* @param score
* @param frictionlessTokenId
* @param serverChecked
* @param userSubmitted
* @param storedStatus
Expand All @@ -580,7 +581,7 @@ export class ProviderDatabase
providerSignature: string,
ipAddress: bigint,
headers: RequestHeaders,
score?: number,
frictionlessTokenId?: FrictionlessTokenId,
serverChecked = false,
userSubmitted = false,
storedStatus: StoredStatus = StoredStatusNames.notStored,
Expand All @@ -600,7 +601,7 @@ export class ProviderDatabase
providerSignature,
userSignature,
lastUpdatedTimestamp: Date.now(),
score,
frictionlessTokenId,
};

try {
Expand Down Expand Up @@ -941,9 +942,18 @@ export class ProviderDatabase
return doc._id;
}

/** Update a frictionless token record */
async updateFrictionlessTokenRecord(
tokenId: FrictionlessTokenId,
updates: Partial<FrictionlessTokenRecord>,
): Promise<void> {
const filter: Pick<FrictionlessTokenRecord, "_id"> = { _id: tokenId };
await this.tables.frictionlessToken.updateOne(filter, updates);
}

/** Get a frictionless token record */
async getFrictionlessTokenRecordByTokenId(
tokenId: ObjectId,
tokenId: FrictionlessTokenId,
): Promise<FrictionlessTokenRecord | undefined> {
const filter: Pick<FrictionlessTokenRecord, "_id"> = { _id: tokenId };
const doc =
Expand Down Expand Up @@ -1014,7 +1024,7 @@ export class ProviderDatabase
requestedAtTimestamp: number,
ipAddress: bigint,
headers: RequestHeaders,
score?: number,
frictionlessTokenId?: FrictionlessTokenId,
): Promise<void> {
if (!isHex(requestHash)) {
throw new ProsopoDBError("DATABASE.INVALID_HASH", {
Expand All @@ -1033,7 +1043,7 @@ export class ProviderDatabase
requestedAtTimestamp: new Date(requestedAtTimestamp),
headers,
ipAddress,
score,
frictionlessTokenId,
};
await this.tables?.pending.updateOne(
{ requestHash: requestHash },
Expand Down
1 change: 1 addition & 0 deletions packages/provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@polkadot/util": "12.6.2",
"@polkadot/util-crypto": "12.6.2",
"@prosopo/api-route": "2.3.1",
"@prosopo/api-express-router": "2.3.1",
"@prosopo/common": "2.3.1",
"@prosopo/config": "2.3.1",
"@prosopo/keyring": "2.3.1",
Expand Down
57 changes: 41 additions & 16 deletions packages/provider/src/api/captcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { validateAddress } from "@polkadot/util-crypto/address";
import { handleErrors } from "@prosopo/api-express-router";
import { ProsopoApiError } from "@prosopo/common";
import { parseCaptchaAssets } from "@prosopo/datasets";
import {
Expand All @@ -34,15 +35,19 @@ import {
SubmitPowCaptchaSolutionBody,
type SubmitPowCaptchaSolutionBodyTypeOutput,
} from "@prosopo/types";
import type { FrictionlessTokenId } from "@prosopo/types-database";
import type { ProviderEnvironment } from "@prosopo/types-env";
import { createImageCaptchaConfigResolver } from "@prosopo/user-access-policy";
import { flatten } from "@prosopo/util";
import express, { type Router } from "express";
import type { ObjectId } from "mongoose";
import { getBotScore } from "../tasks/detection/getBotScore.js";
import {
PENALTY_ACCESS_RULE,
PENALTY_OLD_TIMESTAMP,
} from "../tasks/frictionless/frictionlessPenalties.js";
import { Tasks } from "../tasks/tasks.js";
import { getIPAddress } from "../util.js";
import { handleErrors } from "./errorHandler.js";

const DEFAULT_FRICTIONLESS_THRESHOLD = 0.5;
const TEN_MINUTES = 60 * 10 * 1000;
Expand Down Expand Up @@ -125,7 +130,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
);
const clientSettings = await tasks.db.getClientRecord(dapp);

let score: number | undefined;
let frictionlessTokenId: FrictionlessTokenId | undefined;

if (sessionId) {
const sessionRecord = await tasks.db.checkAndRemoveSession(sessionId);
Expand All @@ -134,10 +139,9 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
await tasks.db.getFrictionlessTokenRecordByTokenId(
sessionRecord.tokenId,
);
// there should be a token record. Remove any lScore before storing downstream
score = tokenRecord
? tokenRecord.score - (tokenRecord.lScore || 0)
: 1;
frictionlessTokenId = tokenRecord
? (tokenRecord._id as FrictionlessTokenId)
: undefined;
}
} else if (!(clientSettings?.settings?.captchaType === "image")) {
// Throw an error if an image captcha has been requested without a session and the client is not configured for image captchas
Expand All @@ -155,7 +159,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
ipAddress,
flatten(req.headers),
captchaConfig,
score,
frictionlessTokenId,
);
const captchaResponse: CaptchaResponseBody = {
[ApiParams.status]: "ok",
Expand Down Expand Up @@ -308,7 +312,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
);
}

let score: number | undefined;
let frictionlessTokenId: FrictionlessTokenId | undefined;

if (sessionId) {
const sessionRecord = await tasks.db.checkAndRemoveSession(sessionId);
Expand All @@ -330,8 +334,9 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
const tokenRecord = await tasks.db.getFrictionlessTokenRecordByTokenId(
sessionRecord.tokenId,
);
// there should be a token record. Remove any lScore before storing downstream
score = tokenRecord ? tokenRecord.score - (tokenRecord.lScore || 0) : 1;
frictionlessTokenId = tokenRecord
? (tokenRecord._id as FrictionlessTokenId)
: undefined;
} else if (!(clientSettings?.settings?.captchaType === "pow")) {
// Throw an error if a pow captcha has been requested without a session and the client is not configured for pow captchas
return next(
Expand Down Expand Up @@ -374,7 +379,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
challenge.providerSignature,
getIPAddress(req.ip || "").bigInt(),
flatten(req.headers),
score,
frictionlessTokenId,
);

const getPowCaptchaResponse: GetPowCaptchaResponse = {
Expand Down Expand Up @@ -449,8 +454,6 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
try {
validateAddress(user, false, 42);

validateAddress(dapp, false, 42);

const clientRecord = await tasks.db.getClientRecord(dapp);

if (!clientRecord) {
Expand Down Expand Up @@ -515,7 +518,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router {

const { baseBotScore, timestamp } = await getBotScore(token);

const botScore = baseBotScore + lScore;
let botScore = baseBotScore + lScore;

const clientConfig = await tasks.db.getClientRecord(dapp);
const botThreshold =
Expand All @@ -527,7 +530,10 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
token,
score: botScore,
threshold: botThreshold,
...(lScore && { lScore }),
scoreComponents: {
baseScore: baseBotScore,
...(lScore && { lScore }),
},
});

// If the timestamp is older than 10 minutes, send an image captcha
Expand All @@ -536,6 +542,14 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
"Timestamp is older than 10 minutes",
new Date(timestamp),
);
botScore += PENALTY_OLD_TIMESTAMP;
await tasks.db.updateFrictionlessTokenRecord(tokenId, {
score: botScore,
scoreComponents: {
baseScore: baseBotScore,
timeout: PENALTY_OLD_TIMESTAMP,
},
});
return res.json(
await tasks.frictionlessManager.sendImageCaptcha(tokenId),
);
Expand All @@ -544,15 +558,25 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
// Check if the IP address is blocked
const ipAddress = getIPAddress(req.ip || "");

// If the user or IP address has an image captcha config defined, send an image captcha
const imageCaptchaConfigDefined =
await imageCaptchaConfigResolver.isConfigDefined(
dapp,
ipAddress,
user,
);

if (imageCaptchaConfigDefined)
if (imageCaptchaConfigDefined) {
botScore += PENALTY_ACCESS_RULE;
await tasks.db.updateFrictionlessTokenRecord(tokenId, {
score: botScore,
scoreComponents: {
baseScore: baseBotScore,
accessPolicy: PENALTY_ACCESS_RULE,
},
});
return res.json(tasks.frictionlessManager.sendImageCaptcha(tokenId));
}

// If the bot score is greater than the threshold, send an image captcha
if (Number(botScore) > botThreshold) {
Expand All @@ -561,6 +585,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
);
}

// Otherwise, send a PoW captcha
const response =
await tasks.frictionlessManager.sendPowCaptcha(tokenId);

Expand Down
Loading

0 comments on commit a3d45e4

Please sign in to comment.