Skip to content

Commit 6b81d5a

Browse files
committed
token security: not finished yet
1 parent 8a503d0 commit 6b81d5a

File tree

6 files changed

+481
-333
lines changed

6 files changed

+481
-333
lines changed

.eslintrc.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
module.exports = {
2+
"extends": "eslint:recommended",
3+
"env": {
4+
"es6": true,
5+
"browser": true,
6+
"node": true,
7+
"mocha": true,
8+
"jest": true
9+
},
10+
"globals": {
11+
"artifacts": false,
12+
"contract": false,
13+
"assert": false,
14+
"web3": false
15+
},
16+
"parserOptions": {
17+
"ecmaVersion": 8
18+
},
19+
"rules": {
20+
21+
// Strict mode
22+
"strict": 0,
23+
24+
// Code style
25+
"indent": ["error", 2, {
26+
"SwitchCase": 1
27+
}],
28+
"quotes": [2, "single"],
29+
"semi": ["error", "always"],
30+
"space-before-function-paren": ["error", "always"],
31+
"no-use-before-define": 0,
32+
"eqeqeq": [2, "smart"],
33+
"dot-notation": [2, {
34+
"allowKeywords": true,
35+
"allowPattern": ""
36+
}],
37+
"no-redeclare": [2, {
38+
"builtinGlobals": true
39+
}],
40+
"no-trailing-spaces": [2, {
41+
"skipBlankLines": true
42+
}],
43+
"eol-last": 1,
44+
"comma-spacing": [2, {
45+
"before": false,
46+
"after": true
47+
}],
48+
"camelcase": [2, {
49+
"properties": "always"
50+
}],
51+
"no-mixed-spaces-and-tabs": [2, "smart-tabs"],
52+
"comma-dangle": [1, "only-multiline"],
53+
"no-dupe-args": 2,
54+
"no-dupe-keys": 2,
55+
"no-debugger": 0,
56+
"no-undef": 2,
57+
"one-var": [0],
58+
"object-curly-spacing": [2, "always"],
59+
"generator-star-spacing": ["error", "before"],
60+
"padded-blocks": 0,
61+
"no-unused-expressions": 0,
62+
"arrow-body-style": 0,
63+
"no-extra-semi": 0
64+
}
65+
};

decorators/basic.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
const { verifyJWT, isAuthorized } = require('../helpers/jwt');
1+
const { verifyJWT } = require('../helpers/jwt');
22

33
const basicDecorator = fn => async (req, res) => {
4+
45
try {
56
const { headers } = req;
6-
if (!headers.authorization) throw new Error('Authorization missing');
7+
8+
if (!headers.authorization) {
9+
throw new Error('Authorization missing');
10+
}
11+
712
const auth = headers.authorization.split(' ');
8-
const { payload, signingAddress } = await verifyJWT(...auth);
9-
await isAuthorized(payload.iss, signingAddress);
13+
await verifyJWT(...auth);
1014
await fn(req, res);
1115
} catch (e) {
1216
console.log(e);
1317
res.status(500).json({
1418
message: e.message,
15-
});
19+
});
1620
}
1721
};
1822

1923
module.exports = {
2024
basicDecorator,
21-
}
25+
};

helpers/jwt.js

+125-32
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,137 @@
11
const JWT = require('jsonwebtoken');
22
const ethers = require('ethers');
3-
const { organizationAbi } = require('./contracts/organization');
3+
const KeyEncoder = require('key-encoder').default;
4+
const { ec, eddsa } = require('elliptic');
5+
const Web3 = require('web3');
6+
const { OrgIdResolver, httpFetchMethod } = require('@windingtree/org.id-resolver');
7+
const { addresses } = require('@windingtree/org.id');
8+
const web3 = new Web3(process.env.INFURA_ENDPOINT);
49

5-
const validisTyp = ["jwt", 'application/jwt'];
6-
const verifyJWT = (type, jwt) => {
7-
if (type !== 'Bearer') throw new Error('JWT Token format is not valid');
8-
9-
const decodedToken = JWT.decode(jwt, {complete: true});
10-
if(!decodedToken) throw new Error('JWT Token format is not valid');
11-
const { header, payload, signature} = decodedToken;
12-
13-
if(!validisTyp.includes(header.typ.toLowerCase())) throw new Error('JWT Token header typ invalid');
14-
if(header.alg !== 'ETH') throw new Error('JWT Token algorithm must be ETH');
15-
16-
if (payload.exp < Date.now()/1000) throw new Error('JWT Token has Expired');
10+
// ORG.ID resolver configuration
11+
const orgIdResolver = new OrgIdResolver({
12+
web3,
13+
orgId: addresses.ropsten // @todo Set the network type on the base of environment config
14+
});
15+
orgIdResolver.registerFetchMethod(httpFetchMethod);
1716

18-
const lastPeriod = jwt.lastIndexOf('.');
17+
const validisTyp = ['jwt', 'application/jwt'];
18+
const validAlg = ['ETH', 'ES256K'];
1919

20-
const signedMessage = jwt.substring(0, lastPeriod);
21-
const sigatureB64 = jwt.substring(lastPeriod + 1);
20+
const verifyJWT = async (type, jwt) => {
2221

23-
const signatureB16 = (Buffer.from(sigatureB64.toString().replace('-', '+').replace('_', '/'), 'base64')).toString('hex');
24-
const hashedMessage = ethers.utils.hashMessage(signedMessage);
25-
const signingAddress = ethers.utils.recoverAddress(hashedMessage, `0x${signatureB16}`);
22+
if (type !== 'Bearer') {
23+
throw new Error('JWT Token format is not valid');
24+
};
2625

27-
return { header, payload, signature, signingAddress };
28-
};
26+
const decodedToken = JWT.decode(jwt, {
27+
complete: true
28+
});
2929

30-
const isAuthorized = async (contractAddress, signer) => {
31-
const provider = ethers.getDefaultProvider('ropsten');
32-
const contract = new ethers.Contract(contractAddress, organizationAbi, provider);
33-
34-
const owner = await contract.owner();
35-
if (owner === signer) return;
30+
if (!decodedToken) {
31+
throw new Error('JWT Token format is not valid');
32+
};
33+
34+
const {
35+
header,
36+
payload,
37+
signature
38+
} = decodedToken;
39+
40+
if (!validisTyp.includes(header.typ.toLowerCase())) {
41+
throw new Error('JWT Token header typ invalid');
42+
}
43+
44+
if (!validAlg.includes(header.alg)) {
45+
throw new Error('JWT Token algorithm is invalid');
46+
}
47+
48+
if (payload.exp < Date.now() / 1000) {
49+
throw new Error('JWT Token has Expired');
50+
}
51+
52+
const [ did, fragment ] = payload.iss.split('#');
53+
const didResult = await orgIdResolver.resolve(did);
54+
55+
console.log('>>>', didResult);
56+
57+
const lastPeriod = jwt.lastIndexOf('.');
58+
const signedMessage = jwt.substring(0, lastPeriod);
59+
const signatureB16 = (Buffer.from(
60+
signature
61+
.toString()
62+
.replace('-', '+')
63+
.replace('_', '/'),
64+
'base64')).toString('hex');
3665

37-
const isAssociadtedKey = await contract.hasAssociatedKey(signer);
38-
if(!isAssociadtedKey) throw new Error('JWT Token not authorized');
66+
if (header.alg === 'ETH') {
67+
// Validate signature of the organization owner or director
68+
const hashedMessage = ethers.utils.hashMessage(signedMessage);
69+
const signingAddress = ethers.utils.recoverAddress(hashedMessage, `0x${signatureB16}`);
70+
71+
if (![
72+
didResult.organization.owner,
73+
...didResult.organization.director === '0x0000000000000000000000000000000000000000'
74+
? []
75+
: [didResult.organization.director]
76+
].includes(signingAddress)) {
77+
throw new Error('JWT Token not authorized');
78+
}
79+
80+
} else if (fragment && didResult.didDocument.publicKey) {
81+
// Validate signature using publickKey
82+
let publicKey = didResult.didDocument.publicKey.filter(
83+
p => p.id.match(RegExp(`#${fragment}$`, 'g'))
84+
)[0];
85+
86+
if (!publicKey) {
87+
throw new Error('Public key definition not found in the DID document');
88+
}
89+
90+
let curveType;
91+
92+
switch (publicKey.type) {
93+
case 'X25519':
94+
curveType = 'ed25519';
95+
break;
96+
97+
case 'secp256k1':
98+
curveType = 'secp256k1';
99+
break;
100+
101+
default:
102+
throw new Error('Signature verification method not found');
103+
}
104+
105+
const context = new ec(curveType);
106+
const keyEncoder = new KeyEncoder(curveType);
107+
108+
if (!publicKey.publicKeyPem.match(RegExp('BEGIN PUBLIC KEY', 'gi'))) {
109+
publicKey.publicKeyPem = `-----BEGIN PUBLIC KEY-----\n${publicKey.publicKeyPem}\n-----END PUBLIC KEY-----`;
110+
}
111+
112+
const rawPub = keyEncoder.encodePublic(publicKey.publicKeyPem, 'pem', 'raw');
113+
const key = context.keyFromPublic(rawPub, 'hex');
114+
const sigParts = signatureB16.match(/([a-f\d]{64})/gi);
115+
const sig = {
116+
r: sigParts[0],
117+
s: sigParts[1]
118+
};
119+
120+
if (!key.verify(signedMessage, sig)) {
121+
throw new Error('JWT Token not authorized');
122+
}
123+
124+
} else {
125+
throw new Error('Signature verification method not found');
126+
}
127+
128+
return {
129+
header,
130+
payload,
131+
signature
132+
};
39133
};
40134

41135
module.exports = {
42-
verifyJWT,
43-
isAuthorized,
44-
};
136+
verifyJWT
137+
};

package.json

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
2-
"name": "wt-aggregator",
2+
"name": "glider-aggregator",
33
"version": "0.0.1",
44
"description": "Winding Tree aggregator",
55
"main": "index.js",
66
"repository": {
77
"type": "git",
8-
"url": "git+https://github.com/windingtree/wt-aggregator.git"
8+
"url": "git+https://github.com/windingtree/glider-aggregator.git"
99
},
1010
"keywords": [
1111
"Winding",
@@ -15,9 +15,9 @@
1515
"author": "Winding Tree devs",
1616
"license": "SEE LICENSE IN LICENSE",
1717
"bugs": {
18-
"url": "https://github.com/windingtree/wt-aggregator/issues"
18+
"url": "https://github.com/windingtree/glider-aggregator/issues"
1919
},
20-
"homepage": "https://github.com/windingtree/wt-aggregator#readme",
20+
"homepage": "https://github.com/windingtree/glider-aggregator#readme",
2121
"dependencies": {
2222
"axios": "0.19.2",
2323
"camaro": "^4.1.2",
@@ -28,7 +28,14 @@
2828
"express": "^4.17.1",
2929
"jsonwebtoken": "^8.5.1",
3030
"redis": "^3.0.2",
31-
"uuid": "^7.0.0"
31+
"uuid": "^7.0.0",
32+
"web3": "1.2.6",
33+
"@windingtree/org.id-resolver": "0.3.1",
34+
"key-encoder": "2.0.3",
35+
"elliptic": "6.5.2"
36+
},
37+
"devDependencies": {
38+
"eslint": "6.8.0"
3239
},
3340
"engines": {
3441
"node": "12.x"

0 commit comments

Comments
 (0)