@prosopo/client-example-server 2.7.171 → 2.7.173

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,396 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import { blake2b } from "@noble/hashes/blake2.js";
16
- import { utf8ToBytes } from "@noble/hashes/utils.js";
17
- import { u8aToHex } from "@polkadot/util";
18
- import { randomAsHex } from "@polkadot/util-crypto";
19
- import { ProsopoEnvError } from "@prosopo/common";
20
- import { getPair } from "@prosopo/keyring";
21
- import { getLogger } from "@prosopo/logger";
22
- import { ProsopoServer } from "@prosopo/server";
23
- import {
24
- ApiParams,
25
- CaptchaType,
26
- type KeyringPair,
27
- ProcaptchaResponse,
28
- type ProcaptchaToken,
29
- type ProsopoServerConfigOutput,
30
- } from "@prosopo/types";
31
- import { at } from "@prosopo/util";
32
- import type { NextFunction, Request, Response } from "express";
33
- import jwt, { type JwtPayload } from "jsonwebtoken";
34
- import type { Connection } from "mongoose";
35
- import { z } from "zod";
36
- import type { UserInterface } from "../models/user.js";
37
-
38
- const logger = getLogger("info", "client-example-server:auth");
39
-
40
- const SubscribeBodySpec = ProcaptchaResponse.merge(
41
- z.object({
42
- email: z.string().email(),
43
- password: z.string(),
44
- siteKey: z.string(),
45
- }),
46
- );
47
-
48
- function hashPassword(password: string): string {
49
- return u8aToHex(blake2b(utf8ToBytes(password)));
50
- }
51
-
52
- const NO_IP = "NO_IP";
53
-
54
- /**
55
- * Derives the correct KeyringPair and secret for a given site key by trying different captcha types.
56
- * @param baseSecret - The base secret from config
57
- * @param siteKey - The site key to match against
58
- * @returns Object containing the matched pair and its corresponding secret
59
- * @throws Error if no matching pair is found
60
- */
61
- const getPairAndSecretForSiteKey = (
62
- baseSecret: string,
63
- siteKey: string,
64
- ): { pair: KeyringPair; secret: string } => {
65
- let pair: KeyringPair | undefined;
66
- let secret = baseSecret;
67
-
68
- pair = getPair(secret);
69
-
70
- if (pair.address === siteKey) {
71
- logger.info(() => ({
72
- msg: "Site key matches the server configuration without captcha type suffix",
73
- data: {
74
- siteKeyDerived: pair?.address,
75
- siteKey,
76
- },
77
- }));
78
- return { pair, secret };
79
- }
80
-
81
- for (const captchaType of [
82
- CaptchaType.pow,
83
- CaptchaType.image,
84
- CaptchaType.frictionless,
85
- CaptchaType.puzzle,
86
- ]) {
87
- const newSecret = `${baseSecret}//${captchaType}`;
88
- pair = getPair(newSecret);
89
- logger.info(() => ({
90
- msg: "Checking pair",
91
- data: {
92
- captchaType,
93
- siteKeyDerived: pair?.address,
94
- siteKey,
95
- },
96
- }));
97
- if (pair.address === siteKey) {
98
- secret = newSecret;
99
- break;
100
- }
101
- }
102
-
103
- if (!pair || pair.address !== siteKey) {
104
- throw new Error(
105
- `Site key does not match the server configuration: Provided ${siteKey} vs Server ${pair?.address}`,
106
- );
107
- }
108
-
109
- return { pair, secret };
110
- };
111
-
112
- const getResponse = async (
113
- ip: string,
114
- token: ProcaptchaToken,
115
- secret: string,
116
- verifyEndpoint: string,
117
- email?: string,
118
- ) => {
119
- // Only include ip if environment is production
120
- const body: Record<string, string> = {
121
- [ApiParams.token]: token,
122
- [ApiParams.secret]: secret,
123
- ...(email ? { [ApiParams.email]: email } : {}),
124
- };
125
-
126
- if (process.env.NODE_ENV !== "development") {
127
- body[ApiParams.ip] = ip;
128
- }
129
-
130
- logger.info(() => ({
131
- data: {
132
- body: { ...body, [ApiParams.secret]: "[redacted]" },
133
- },
134
- }));
135
-
136
- const response = await fetch(verifyEndpoint, {
137
- method: "POST",
138
- body: JSON.stringify(body),
139
- });
140
- return response.json();
141
- };
142
-
143
- const verify = async (
144
- prosopoServer: ProsopoServer,
145
- verifyType: string,
146
- verifyEndpoint: string,
147
- token: ProcaptchaToken,
148
- secret: string,
149
- ip: string,
150
- email?: string,
151
- // The session id the widget was rendered with. Forwarded so the provider
152
- // can confirm the token was earned in this same session; omitted, no
153
- // correlation is attempted.
154
- clientSessionId?: string,
155
- ) => {
156
- if (verifyType === "api") {
157
- // verify using the API endpoint
158
- logger.info(() => ({
159
- msg: "Verifying using the API endpoint",
160
- data: {
161
- verifyEndpoint,
162
- },
163
- }));
164
-
165
- const response = await getResponse(
166
- ip,
167
- token,
168
- secret,
169
- verifyEndpoint,
170
- email,
171
- );
172
- logger.info(() => ({
173
- data: {
174
- response,
175
- },
176
- }));
177
- return response.verified;
178
- }
179
- // verify using the TypeScript library
180
- const verified = await prosopoServer.isVerified(
181
- token,
182
- ip,
183
- email,
184
- clientSessionId,
185
- );
186
- logger.info(() => ({
187
- data: {
188
- verified,
189
- },
190
- }));
191
- return verified[ApiParams.verified];
192
- };
193
-
194
- const signup = async (
195
- mongoose: Connection,
196
- config: ProsopoServerConfigOutput,
197
- verifyEndpoint: string,
198
- verifyType: string,
199
- req: Request,
200
- res: Response,
201
- next: NextFunction,
202
- ) => {
203
- try {
204
- if (!config.account.secret) {
205
- throw new ProsopoEnvError("GENERAL.MNEMONIC_UNDEFINED", {
206
- context: { missingParams: ["PROSOPO_SITE_PRIVATE_KEY"] },
207
- });
208
- }
209
- const User = mongoose.model<UserInterface>("User");
210
- // checks if email exists
211
- const dbUser = await User.findOne({
212
- email: { $eq: req.body.email },
213
- });
214
- const payload = SubscribeBodySpec.parse(req.body);
215
- // get the procaptcha-response token
216
- const token = payload[ApiParams.procaptchaResponse];
217
- const siteKey = payload[ApiParams.siteKey];
218
-
219
- const { pair, secret } = getPairAndSecretForSiteKey(
220
- config.account.secret,
221
- siteKey,
222
- );
223
-
224
- const prosopoServer = new ProsopoServer(config, pair);
225
- if (dbUser) {
226
- return res.status(409).json({ message: "email already exists" });
227
- }
228
-
229
- const email = req.body.email;
230
- const verified = await verify(
231
- prosopoServer,
232
- verifyType,
233
- verifyEndpoint,
234
- token,
235
- secret,
236
- req.headers["x-client-ip"]?.toString() || "127.0.0.1",
237
- email,
238
- req.body[ApiParams.clientSessionId],
239
- );
240
-
241
- if (verified === true) {
242
- // salt
243
- const salt = randomAsHex(32);
244
- // !!!DUMMY CODE!!! - Do not use in production. Use bcrypt or similar for password hashing.
245
- const passwordHash = hashPassword(`${req.body.password}${salt}`);
246
- // No `if (passwordHash)` guard: the hash is always a non-empty hex
247
- // string, and the guard's absent else-branch left the request hanging
248
- // with no response at all.
249
- return User.create({
250
- email: email,
251
- name: req.body.name,
252
- password: passwordHash,
253
- salt: salt,
254
- })
255
- .then(() => {
256
- res.status(200).json({ message: "user created" });
257
- })
258
- .catch((err) => {
259
- logger.error(() => ({
260
- err,
261
- msg: "Error creating user in database",
262
- }));
263
- res.status(502).json({ message: "error while creating the user" });
264
- });
265
- }
266
- res
267
- .status(401)
268
- .json({ message: "user has not completed a captcha", verified });
269
- } catch (err) {
270
- console.error("error", err);
271
- res
272
- .status(500)
273
- .json({ message: (err as Error).message || "internal server error" });
274
- }
275
- };
276
-
277
- const login = async (
278
- mongoose: Connection,
279
- config: ProsopoServerConfigOutput,
280
- verifyEndpoint: string,
281
- verifyType: string,
282
- req: Request,
283
- res: Response,
284
- ) => {
285
- const User = mongoose.model<UserInterface>("User");
286
-
287
- // checks if email exists
288
- await User.findOne({
289
- email: { $eq: req.body.email },
290
- })
291
- .then(async (dbUser) => {
292
- if (dbUser) {
293
- const payload = SubscribeBodySpec.parse(req.body);
294
-
295
- const token = payload[ApiParams.procaptchaResponse];
296
- const siteKey = payload[ApiParams.siteKey];
297
-
298
- if (!config.account.secret) {
299
- throw new ProsopoEnvError("GENERAL.MISSING_SECRET_KEY", {
300
- context: { missingParams: ["PROSOPO_SITE_PRIVATE_KEY"] },
301
- });
302
- }
303
-
304
- const { pair, secret } = getPairAndSecretForSiteKey(
305
- config.account.secret,
306
- siteKey,
307
- );
308
- const prosopoServer = new ProsopoServer(config, pair);
309
-
310
- const verified = await verify(
311
- prosopoServer,
312
- verifyType,
313
- verifyEndpoint,
314
- token,
315
- secret,
316
- req.headers["x-client-ip"]?.toString() || NO_IP,
317
- // login carries no email; the session id still has to reach
318
- // the provider or a widget rendered with data-sessionid is
319
- // silently unverified against its session.
320
- undefined,
321
- req.body[ApiParams.clientSessionId],
322
- );
323
-
324
- logger.info(() => ({
325
- data: {
326
- verified,
327
- },
328
- }));
329
-
330
- if (verified) {
331
- // password hash
332
- // !!!DUMMY CODE!!! - Do not use in production. Use bcrypt or similar for password hashing.
333
- const passwordHash = hashPassword(
334
- `${req.body.password}${dbUser.salt}`,
335
- );
336
- if (passwordHash !== dbUser.password) {
337
- // password doesnt match
338
- return res.status(401).json({ message: "invalid credentials" });
339
- }
340
- // password match
341
- const token = jwt.sign({ email: req.body.email }, "secret", {
342
- expiresIn: "1h",
343
- });
344
- return res
345
- .status(200)
346
- .json({ message: "user logged in", token: token });
347
- }
348
- return res
349
- .status(401)
350
- .json({ message: "user has not completed a captcha", verified });
351
- }
352
- return res.status(404).json({ message: "user not found" });
353
- })
354
- .catch((err) => {
355
- console.error("error", err);
356
- res.status(500).json({ message: err.message || "internal server error" });
357
- });
358
- };
359
-
360
- const isAuth = (req: Request, res: Response) => {
361
- const authHeader = req.get("Authorization") || "";
362
- // Each branch returns: without the returns a missing header still fell
363
- // through to `at(headerParts, 1)`, which throws on a one-element array, and
364
- // a failed verify sent a 500 followed by a 401 on the same response.
365
- if (!authHeader) {
366
- return res.status(401).json({ message: "not authenticated" });
367
- }
368
-
369
- const headerParts = authHeader.split(" ");
370
- if (headerParts.length < 2) {
371
- return res.status(401).json({ message: "not authenticated" });
372
- }
373
-
374
- const token = at(headerParts, 1);
375
- // "Bearer " with nothing after it is a malformed credential, not a server
376
- // fault: jwt.verify("") throws and would otherwise surface as a 500.
377
- if (!token) {
378
- return res.status(401).json({ message: "not authenticated" });
379
- }
380
-
381
- let decodedToken: string | JwtPayload = "";
382
- try {
383
- decodedToken = jwt.verify(token, "secret");
384
- } catch (err) {
385
- return res.status(500).json({
386
- message: (err as Error).message || "could not decode the token",
387
- });
388
- }
389
-
390
- if (!decodedToken) {
391
- return res.status(401).json({ message: "unauthorized" });
392
- }
393
- return res.status(200).json({ message: "here is your resource" });
394
- };
395
-
396
- export { signup, login, isAuth };
@@ -1,45 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
- import mongoose, { Schema } from "mongoose";
15
-
16
- export interface UserInterface {
17
- id: number;
18
- email: string;
19
- name: string;
20
- password: string;
21
- salt: string;
22
- }
23
-
24
- const UserSchema = new Schema<UserInterface>({
25
- email: {
26
- type: mongoose.SchemaTypes.String,
27
- required: true,
28
- },
29
- name: {
30
- type: mongoose.SchemaTypes.String,
31
- },
32
- password: {
33
- type: mongoose.SchemaTypes.String,
34
- required: true,
35
- },
36
- id: {
37
- type: mongoose.SchemaTypes.Number,
38
- },
39
- salt: {
40
- type: mongoose.SchemaTypes.String,
41
- required: true,
42
- },
43
- });
44
-
45
- export default UserSchema;
@@ -1,57 +0,0 @@
1
- import type { ProsopoServerConfigOutput } from "@prosopo/types";
2
- import express from "express";
3
- // Copyright 2021-2026 Prosopo (UK) Ltd.
4
- //
5
- // Licensed under the Apache License, Version 2.0 (the "License");
6
- // you may not use this file except in compliance with the License.
7
- // You may obtain a copy of the License at
8
- //
9
- // http://www.apache.org/licenses/LICENSE-2.0
10
- //
11
- // Unless required by applicable law or agreed to in writing, software
12
- // distributed under the License is distributed on an "AS IS" BASIS,
13
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- // See the License for the specific language governing permissions and
15
- // limitations under the License.
16
- import type { Connection } from "mongoose";
17
- import { isAuth, login, signup } from "../controllers/auth.js";
18
-
19
- function getRoutes(
20
- mongoose: Connection,
21
- config: ProsopoServerConfigOutput,
22
- verifyEndpoint: string,
23
- verifyType: string,
24
- ): express.Router {
25
- // A router per call: the module-level singleton this used to share meant a
26
- // second getRoutes() stacked another set of handlers onto the same router,
27
- // so every request ran the first call's bound config.
28
- const router = express.Router();
29
-
30
- router.post(
31
- "/login",
32
- login.bind(null, mongoose, config, verifyEndpoint, verifyType),
33
- );
34
-
35
- router.post(
36
- "/signup",
37
- signup.bind(null, mongoose, config, verifyEndpoint, verifyType),
38
- );
39
-
40
- router.get("/private", isAuth);
41
-
42
- router.get("/public", (req, res) => {
43
- res.status(200).json({ message: "here is your public resource" });
44
- });
45
-
46
- router.get("/health", (req, res) => {
47
- res.status(200).json({ status: "ok" });
48
- });
49
-
50
- // will match any other path
51
- router.use("/", (req, res) => {
52
- res.status(404).json({ error: "page not found" });
53
- });
54
- return router;
55
- }
56
-
57
- export default getRoutes;