Skip to content

Commit 6991838

Browse files
authored
TW-1612: Temple Tap AirDrop (#183)
* TW-1612: Temple Tap AirDrop. + SigAuth middleware * TW-1612: Temple Tap AirDrop. + \/temple-tap\/check-airdrop-confirmation proxy endpoint * TW-1612: Temple Tap AirDrop. ++ Sig Auth. Forbidding non-recognized messages
1 parent 9863286 commit 6991838

File tree

5 files changed

+151
-60
lines changed

5 files changed

+151
-60
lines changed

src/index.ts

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import swaggerJSDoc from 'swagger-jsdoc';
1010
import swaggerUi from 'swagger-ui-express';
1111

1212
import { getAdvertisingInfo } from './advertising/advertising';
13-
import { EnvVars, MIN_ANDROID_APP_VERSION, MIN_IOS_APP_VERSION } from './config';
13+
import { MIN_ANDROID_APP_VERSION, MIN_IOS_APP_VERSION } from './config';
1414
import getDAppsStats from './getDAppsStats';
1515
import { getMagicSquareQuestParticipants, startMagicSquareQuest } from './magic-square';
1616
import { basicAuth } from './middlewares/basic-auth.middleware';
@@ -23,6 +23,8 @@ import { redisClient } from './redis';
2323
import { evmRouter } from './routers/evm';
2424
import { adRulesRouter } from './routers/slise-ad-rules';
2525
import { templeWalletAdsRouter } from './routers/temple-wallet-ads';
26+
import { getSigningNonce, tezosSigAuthMiddleware } from './sig-auth';
27+
import { handleTempleTapApiProxyRequest } from './temple-tap';
2628
import { getTkeyStats } from './tkey-stats';
2729
import { getABData } from './utils/ab-test';
2830
import { cancelAliceBobOrder } from './utils/alice-bob/cancel-alice-bob-order';
@@ -39,7 +41,6 @@ import { coinGeckoTokens } from './utils/gecko-tokens';
3941
import { getExternalApiErrorPayload, isDefined, isNonEmptyString } from './utils/helpers';
4042
import logger from './utils/logger';
4143
import { getSignedMoonPayUrl } from './utils/moonpay/get-signed-moonpay-url';
42-
import { getSigningNonce } from './utils/signing-nonce';
4344
import SingleQueryDataProvider from './utils/SingleQueryDataProvider';
4445
import { getExchangeRates } from './utils/tokens';
4546

@@ -398,32 +399,13 @@ app.get('/api/signing-nonce', (req, res) => {
398399
}
399400
});
400401

401-
app.post('/api/temple-tap/confirm-airdrop-username', async (req, res) => {
402-
try {
403-
const response = await fetch(new URL('v1/confirm-airdrop-address', EnvVars.TEMPLE_TAP_API_URL + '/'), {
404-
method: 'POST',
405-
body: JSON.stringify(req.body),
406-
headers: {
407-
'Content-Type': 'application/json'
408-
}
409-
});
410-
411-
const statusCode = String(response.status);
412-
const responseBody = await response.text();
413-
414-
if (statusCode.startsWith('2') || statusCode.startsWith('4')) {
415-
res.status(response.status).send(responseBody);
416-
417-
return;
418-
}
419-
420-
throw new Error(responseBody);
421-
} catch (error) {
422-
console.error('Temple Tap API proxy endpoint exception:', error);
402+
app.post('/api/temple-tap/confirm-airdrop-username', tezosSigAuthMiddleware, (req, res) =>
403+
handleTempleTapApiProxyRequest(req, res, 'v1/confirm-airdrop-address')
404+
);
423405

424-
res.status(500).send({ message: 'Unknown error' });
425-
}
426-
});
406+
app.post('/api/temple-tap/check-airdrop-confirmation', tezosSigAuthMiddleware, (req, res) =>
407+
handleTempleTapApiProxyRequest(req, res, 'v1/check-airdrop-address-confirmation')
408+
);
427409

428410
const swaggerOptions = {
429411
swaggerDefinition: {

src/magic-square.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { verifySignature, getPkhfromPk } from '@taquito/utils';
66
import { StatusCodes } from 'http-status-codes';
77

88
import { objectStorageMethodsFactory } from './redis';
9+
import { getSigningNonce, removeSigningNonce } from './sig-auth';
910
import { CodedError } from './utils/errors';
1011
import { safeCheck } from './utils/helpers';
11-
import { getSigningNonce, removeSigningNonce } from './utils/signing-nonce';
1212

1313
interface Participant {
1414
pkh: string;

src/sig-auth.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { randomStringForEntropy } from '@stablelib/random';
2+
import { getPkhfromPk, validateAddress, ValidationResult, verifySignature } from '@taquito/utils';
3+
import { NextFunction, Request, Response } from 'express';
4+
import { StatusCodes } from 'http-status-codes';
5+
import memoizee from 'memoizee';
6+
import * as yup from 'yup';
7+
8+
import { CodedError } from './utils/errors';
9+
10+
const SIGNING_NONCE_TTL = 5 * 60_000;
11+
12+
/** Result for packing (via `import('@taquito/michel-codec').packDataBytes({ string })`) in bytes for message:
13+
* `Tezos Signed Message: Confirming my identity as ${Account PKH}.\n\nNonce: ${nonce}`
14+
*/
15+
const TEZ_SIG_AUTH_MSG_PATTERN =
16+
/^0501[a-f0-9]{8}54657a6f73205369676e6564204d6573736167653a20436f6e6669726d696e67206d79206964656e7469747920617320[a-f0-9]{72}2e0a0a4e6f6e63653a20[a-f0-9]{16,40}$/;
17+
18+
export const getSigningNonce = memoizee(
19+
(pkh: string) => {
20+
if (validateAddress(pkh) !== ValidationResult.VALID) throw new CodedError(400, 'Invalid address');
21+
22+
return buildNonce();
23+
},
24+
{
25+
max: 1_000_000,
26+
maxAge: SIGNING_NONCE_TTL
27+
}
28+
);
29+
30+
export function removeSigningNonce(pkh: string) {
31+
getSigningNonce.delete(pkh);
32+
}
33+
34+
export async function tezosSigAuthMiddleware(req: Request, res: Response, next: NextFunction) {
35+
const sigHeaders = await sigAuthHeadersSchema.validate(req.headers).catch(() => null);
36+
37+
if (!sigHeaders) {
38+
logInvalidSigAuthValues('values (empty)', req.headers);
39+
40+
return void res.status(StatusCodes.UNAUTHORIZED).send();
41+
}
42+
43+
const {
44+
'tw-sig-auth-tez-pk': publicKey,
45+
'tw-sig-auth-tez-msg': messageBytes,
46+
'tw-sig-auth-tez-sig': signature
47+
} = sigHeaders;
48+
49+
let pkh: string;
50+
try {
51+
pkh = getPkhfromPk(publicKey);
52+
} catch (err) {
53+
console.error(err);
54+
55+
return void res.status(StatusCodes.BAD_REQUEST).send({ message: 'Invalid public key' });
56+
}
57+
58+
// Nonce
59+
const { value: nonce } = getSigningNonce(pkh);
60+
const nonceBytes = Buffer.from(nonce, 'utf-8').toString('hex');
61+
62+
if (!messageBytes.endsWith(nonceBytes)) {
63+
logInvalidSigAuthValues('nonce', req.headers);
64+
65+
return void res.status(StatusCodes.UNAUTHORIZED).send({ code: 'INVALID_NONCE', message: 'Invalid nonce' });
66+
}
67+
68+
// Message
69+
if (!TEZ_SIG_AUTH_MSG_PATTERN.test(messageBytes)) {
70+
logInvalidSigAuthValues('pattern', req.headers);
71+
72+
return void res.status(StatusCodes.UNAUTHORIZED).send({ code: 'INVALID_MSG', message: 'Invalid message' });
73+
}
74+
75+
// Signature
76+
try {
77+
verifySignature(messageBytes, publicKey, signature);
78+
} catch (error) {
79+
logInvalidSigAuthValues('signature', req.headers);
80+
console.error(error);
81+
82+
return void res
83+
.status(StatusCodes.UNAUTHORIZED)
84+
.send({ code: 'INVALID_SIG', message: 'Invalid signature or message' });
85+
}
86+
87+
removeSigningNonce(pkh);
88+
89+
next();
90+
}
91+
92+
const sigAuthHeadersSchema = yup.object().shape({
93+
'tw-sig-auth-tez-pk': yup.string().required(),
94+
'tw-sig-auth-tez-msg': yup.string().required(),
95+
'tw-sig-auth-tez-sig': yup.string().required()
96+
});
97+
98+
function buildNonce() {
99+
// Same as in in SIWE.generateNonce()
100+
const value = randomStringForEntropy(96);
101+
102+
const expiresAt = new Date(Date.now() + SIGNING_NONCE_TTL).toISOString();
103+
104+
return { value, expiresAt };
105+
}
106+
107+
function logInvalidSigAuthValues(title: string, reqHeaders: Record<string, unknown>) {
108+
console.error(`[SIG-AUTH] Received invalid message ${title}. Request headers:`);
109+
110+
console.info(JSON.stringify(reqHeaders, null, 2));
111+
}

src/temple-tap.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Request, Response } from 'express';
2+
3+
import { EnvVars } from './config';
4+
5+
export async function handleTempleTapApiProxyRequest(req: Request, res: Response, endpoint: string, method = 'POST') {
6+
try {
7+
const response = await fetch(new URL(endpoint, EnvVars.TEMPLE_TAP_API_URL + '/'), {
8+
method,
9+
body: JSON.stringify(req.body),
10+
headers: {
11+
'Content-Type': 'application/json'
12+
}
13+
});
14+
15+
const statusCode = String(response.status);
16+
const responseBody = await response.text();
17+
18+
if (statusCode.startsWith('2') || statusCode.startsWith('4')) {
19+
res.status(response.status).send(responseBody);
20+
21+
return;
22+
}
23+
24+
throw new Error(responseBody);
25+
} catch (error) {
26+
console.error('Temple Tap API proxy endpoint exception:', error);
27+
28+
res.status(500).send({ message: 'Unknown error' });
29+
}
30+
}

src/utils/signing-nonce.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)