@arcblock/jwt 1.6.10
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.
- package/LICENSE +13 -0
- package/README.md +148 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.js +206 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2018-2019 ArcBlock
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[](https://github.com/prettier/prettier)
|
|
4
|
+
[](https://docs.arcblock.io)
|
|
5
|
+
[](https://gitter.im/ArcBlock/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
|
6
|
+
|
|
7
|
+
This library aims to ease the process of handling `Did-Auth` process between different parts, its implemented according to [ABT-DID-Protocol](https://github.com/ArcBlock/abt-did-spec), and can eliminate the threat of middle-man attach if properly used, there are typically 2 use case for the library:
|
|
8
|
+
|
|
9
|
+
- `dApp <--> dApp`: for inter application communication, we provide `AppAuthenticator` and `AppHandlers`
|
|
10
|
+
- `dApp <--> DID Wallet`: for application and wallet communication, we provide `WalletAuthenticator` and `WalletHandlers`
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [Table of Contents](#table-of-contents)
|
|
15
|
+
- [Install](#install)
|
|
16
|
+
- [Usage](#usage)
|
|
17
|
+
- [Between dApp and DID Wallet](#between-dapp-and-did-wallet)
|
|
18
|
+
- [Between dApp and dApp](#between-dapp-and-dapp)
|
|
19
|
+
- [Initialize authenticator and handlers](#initialize-authenticator-and-handlers)
|
|
20
|
+
- [For the server](#for-the-server)
|
|
21
|
+
- [For the client](#for-the-client)
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install @arcblock/jwt
|
|
27
|
+
// or
|
|
28
|
+
yarn add @arcblock/jwt
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Between dApp and DID Wallet
|
|
34
|
+
|
|
35
|
+
`WalletAuthenticator` and `WalletHandlers` should be used together with [@ocap/react-forge](https://www.npmjs.com/package/@ocap/react-forge).
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const { fromRandom } = require('@ocap/wallet');
|
|
39
|
+
const { WalletAuthenticator, WalletHandlers } = require('@arcblock/jwt');
|
|
40
|
+
|
|
41
|
+
// First setup authenticator and handler factory
|
|
42
|
+
const wallet = fromRandom().toJSON();
|
|
43
|
+
const authenticator = new WalletAuthenticator({
|
|
44
|
+
wallet,
|
|
45
|
+
baseUrl: 'http://wangshijun.natapp1.cc',
|
|
46
|
+
appInfo: {
|
|
47
|
+
description: 'Starter projects to develop web application on forge',
|
|
48
|
+
icon: '/images/logo@2x.png',
|
|
49
|
+
name: 'Forge Web Starter',
|
|
50
|
+
},
|
|
51
|
+
chainInfo: {
|
|
52
|
+
host: 'http://did-workshop.arcblock.co:8210/api',
|
|
53
|
+
id: 'forge',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const handlers = new WalletHandlers({
|
|
58
|
+
authenticator,
|
|
59
|
+
tokenStorage: new MongoStorage({ url: process.env.MONGO_URI }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Then attach handler to express server
|
|
63
|
+
const express = require('express');
|
|
64
|
+
const app = express();
|
|
65
|
+
|
|
66
|
+
// This is required if you want to use dynamic baseUrl inference
|
|
67
|
+
app.set('trust proxy', true);
|
|
68
|
+
|
|
69
|
+
handlers.attach({
|
|
70
|
+
prefix: '/api/did',
|
|
71
|
+
action: 'login',
|
|
72
|
+
claims: {
|
|
73
|
+
profile: () => ({
|
|
74
|
+
fields: ['fullName', 'email'],
|
|
75
|
+
description: 'Please provide your name and email to continue',
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
onAuth: async ({ claims, userDid }) => {
|
|
79
|
+
try {
|
|
80
|
+
const profile = claims.find((x) => x.type === 'profile');
|
|
81
|
+
console.info('login.success', { userDid, profile });
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('login.error', err);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Then your app will have 5 api endpoints that can be consumed by AuthComponent
|
|
89
|
+
// - `GET /api/did/login/token` create new token
|
|
90
|
+
// - `GET /api/did/login/status` check for token status
|
|
91
|
+
// - `GET /api/did/login/timeout` expire a token
|
|
92
|
+
// - `GET /api/did/login/auth` create auth response
|
|
93
|
+
// - `POST /api/did/login/auth` process login request
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Between dApp and dApp
|
|
97
|
+
|
|
98
|
+
Please note that `AppAuthenticator` and `AppHandlers` should be used to sign and verify the message sent between dApps, so there must are both a client and a server.
|
|
99
|
+
|
|
100
|
+
#### Initialize authenticator and handlers
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const { fromRandom } = require('@ocap/wallet');
|
|
104
|
+
const { AppAuthenticator, AppHandlers } = require('@arcblock/jwt');
|
|
105
|
+
|
|
106
|
+
// First setup authenticator and handler factory
|
|
107
|
+
const wallet = fromRandom().toJSON();
|
|
108
|
+
const authenticator = new AppAuthenticator(wallet);
|
|
109
|
+
const handlers = new AppHandlers(authenticator);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### For the server
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
const express = require('express');
|
|
116
|
+
const app = express();
|
|
117
|
+
|
|
118
|
+
app.post('/api/endpoint', handlers.attach(), (req, res) => {
|
|
119
|
+
console.log('client.appPk', req.appPk);
|
|
120
|
+
console.log('verified payload', req.payload);
|
|
121
|
+
|
|
122
|
+
// Sent signed response: sensitive info should not be here
|
|
123
|
+
res.jsonSecure({
|
|
124
|
+
key: 'value',
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### For the client
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
const axios = require('axios');
|
|
133
|
+
|
|
134
|
+
const signedPayload = authenticator.sign({
|
|
135
|
+
amount,
|
|
136
|
+
depositorDid,
|
|
137
|
+
depositorPk,
|
|
138
|
+
withdrawer: appAuth.wallet.address,
|
|
139
|
+
merchantId: process.env.MERCHANT_ID,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const res = await axios.post('http://example.com/api/endpoint', signedPayload);
|
|
143
|
+
const payload = await authenticator.verify(res.data);
|
|
144
|
+
if (payload.error) {
|
|
145
|
+
throw new Error(payload.error);
|
|
146
|
+
}
|
|
147
|
+
// Do something with the payload
|
|
148
|
+
```
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generate by [js2dts@0.3.3](https://github.com/whxaxes/js2dts#readme)
|
|
2
|
+
|
|
3
|
+
declare const _Lib: _Lib.T102;
|
|
4
|
+
declare namespace _Lib {
|
|
5
|
+
export interface T101 {
|
|
6
|
+
tolerance?: number;
|
|
7
|
+
enforceTimestamp?: boolean;
|
|
8
|
+
signerKey?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface T102 {
|
|
11
|
+
sign: (signer: string, sk: string, payload?: any, doSign?: boolean, version?: string) => string;
|
|
12
|
+
verify: (token: string, signerPk: string, T100?: _Lib.T101) => boolean;
|
|
13
|
+
decode: (token: string, payloadOnly?: boolean) => any;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export = _Lib;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
const semver = require('semver');
|
|
2
|
+
const stringify = require('json-stable-stringify');
|
|
3
|
+
const Mcrypto = require('@ocap/mcrypto');
|
|
4
|
+
const { toHex, toBase64, fromBase64 } = require('@ocap/util');
|
|
5
|
+
const { toDid, toStrictHex, toTypeInfo, isValid, isFromPublicKey } = require('@arcblock/did');
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line
|
|
8
|
+
const debug = require('debug')(require('../package.json').name);
|
|
9
|
+
|
|
10
|
+
const { types, getSigner } = Mcrypto;
|
|
11
|
+
|
|
12
|
+
// we start hashing before signing after 1.1
|
|
13
|
+
const JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN = '1.1.0';
|
|
14
|
+
|
|
15
|
+
// since ios requires a fixed length of input to sign, we use sha3 here before sign
|
|
16
|
+
const hasher = Mcrypto.Hasher.SHA3.hash256;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate and sign a jwt token
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
* @static
|
|
23
|
+
* @param {string} signer - address string
|
|
24
|
+
* @param {string} sk - hex encoded secret key
|
|
25
|
+
* @param {object} [payload={}] - data to be included before signing
|
|
26
|
+
* @param {boolean} [doSign=true] - do we need to sign the payload or just return the content to be signed
|
|
27
|
+
* @returns {string} hex encoded signature
|
|
28
|
+
*/
|
|
29
|
+
const sign = (signer, sk, payload = {}, doSign = true, version = '1.0.0') => {
|
|
30
|
+
if (isValid(signer) === false) {
|
|
31
|
+
throw new Error('Cannot do sign with invalid signer');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const type = toTypeInfo(signer);
|
|
35
|
+
const headers = {
|
|
36
|
+
[types.KeyType.SECP256K1]: {
|
|
37
|
+
alg: 'ES256K',
|
|
38
|
+
type: 'JWT',
|
|
39
|
+
},
|
|
40
|
+
[types.KeyType.ED25519]: {
|
|
41
|
+
alg: 'Ed25519',
|
|
42
|
+
type: 'JWT',
|
|
43
|
+
},
|
|
44
|
+
[types.KeyType.ETHEREUM]: {
|
|
45
|
+
alg: 'Ethereum',
|
|
46
|
+
type: 'JWT',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// make header
|
|
51
|
+
const header = headers[type.pk];
|
|
52
|
+
const headerB64 = toBase64(stringify(header));
|
|
53
|
+
|
|
54
|
+
// make body
|
|
55
|
+
const now = Math.floor(Date.now() / 1000);
|
|
56
|
+
let body = {
|
|
57
|
+
iss: toDid(signer),
|
|
58
|
+
iat: String(now),
|
|
59
|
+
nbf: String(now),
|
|
60
|
+
exp: String(now + 5 * 60),
|
|
61
|
+
version,
|
|
62
|
+
...(payload || {}),
|
|
63
|
+
};
|
|
64
|
+
// remove empty keys
|
|
65
|
+
body = Object.keys(body)
|
|
66
|
+
.filter((x) => {
|
|
67
|
+
if (typeof body[x] === 'undefined' || body[x] == null || body[x] === '') {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
})
|
|
73
|
+
.reduce((acc, x) => {
|
|
74
|
+
acc[x] = body[x];
|
|
75
|
+
return acc;
|
|
76
|
+
}, {});
|
|
77
|
+
|
|
78
|
+
const bodyB64 = toBase64(stringify(body));
|
|
79
|
+
debug('sign.body', body);
|
|
80
|
+
|
|
81
|
+
// istanbul ignore if
|
|
82
|
+
if (!doSign) {
|
|
83
|
+
return `${headerB64}.${bodyB64}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// make signature
|
|
87
|
+
const msgHex = toHex(`${headerB64}.${bodyB64}`);
|
|
88
|
+
const msgHash = semver.gte(semver.coerce(version).version, JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN)
|
|
89
|
+
? hasher(msgHex)
|
|
90
|
+
: msgHex;
|
|
91
|
+
const sigHex = getSigner(type.pk).sign(msgHash, sk);
|
|
92
|
+
const sigB64 = toBase64(sigHex);
|
|
93
|
+
|
|
94
|
+
return [headerB64, bodyB64, sigB64].join('.');
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decode info from jwt token
|
|
99
|
+
*
|
|
100
|
+
* @public
|
|
101
|
+
* @static
|
|
102
|
+
* @param {string} token - jwt string
|
|
103
|
+
* @param {boolean} [payloadOnly=false]
|
|
104
|
+
* @returns {object}
|
|
105
|
+
*/
|
|
106
|
+
const decode = (token, payloadOnly = true) => {
|
|
107
|
+
const [headerB64, bodyB64, sigB64] = token.split('.');
|
|
108
|
+
const header = JSON.parse(fromBase64(headerB64));
|
|
109
|
+
const body = JSON.parse(fromBase64(bodyB64));
|
|
110
|
+
const sig = Buffer.from(fromBase64(sigB64)).toString('hex');
|
|
111
|
+
if (payloadOnly) {
|
|
112
|
+
return body;
|
|
113
|
+
}
|
|
114
|
+
return { header, body, signature: `0x${toStrictHex(sig)}` };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Verify a jwt token signed with signerPk and signerDid
|
|
119
|
+
*
|
|
120
|
+
* @public
|
|
121
|
+
* @static
|
|
122
|
+
* @param {string} token - the jwt token
|
|
123
|
+
* @param {string} signerPk - signer public key
|
|
124
|
+
* @param {object} options - options to customize the verify
|
|
125
|
+
* @param {number} [options.tolerance=5] - number of seconds to tolerant expire
|
|
126
|
+
* @param {boolean} [options.enforceTimestamp=true] - whether should be verify timestamps?
|
|
127
|
+
* @param {string} [options.signerKey='iss'] - which field should be used to pick the signer
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
const verify = (token, signerPk, { tolerance = 5, enforceTimestamp = true, signerKey = 'iss' } = {}) => {
|
|
131
|
+
try {
|
|
132
|
+
const [headerB64, bodyB64] = token.split('.');
|
|
133
|
+
const { header, body, signature } = decode(token, false);
|
|
134
|
+
if (!signature) {
|
|
135
|
+
debug('verify.error.emptySig');
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (!header.alg) {
|
|
139
|
+
debug('verify.error.emptyAlg');
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const signerDid = body[signerKey];
|
|
144
|
+
if (!signerDid) {
|
|
145
|
+
debug('verify.error.emptySignerDid');
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (isFromPublicKey(signerDid, signerPk) === false) {
|
|
150
|
+
debug('verify.error.signerDidAndPkNotMatch');
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (enforceTimestamp) {
|
|
155
|
+
const now = Math.ceil(Date.now() / 1000);
|
|
156
|
+
const exp = Number(body.exp) || 0;
|
|
157
|
+
const iat = Number(body.iat) || 0;
|
|
158
|
+
const nbf = Number(body.nbf) || 0;
|
|
159
|
+
debug('verify.enforceTimestamp', { now, exp, iat, nbf });
|
|
160
|
+
if (exp && exp + tolerance < now) {
|
|
161
|
+
debug('verify.error.expired');
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (iat && iat > now && iat - now > tolerance) {
|
|
165
|
+
debug('verify.error.issuedAt');
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (nbf && nbf > now && nbf - now > tolerance) {
|
|
169
|
+
debug('verify.error.notBefore');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const signers = {
|
|
175
|
+
secp256k1: getSigner(types.KeyType.SECP256K1),
|
|
176
|
+
es256k: getSigner(types.KeyType.SECP256K1),
|
|
177
|
+
ed25519: getSigner(types.KeyType.ED25519),
|
|
178
|
+
ethereum: getSigner(types.KeyType.ETHEREUM),
|
|
179
|
+
};
|
|
180
|
+
const alg = header.alg.toLowerCase();
|
|
181
|
+
if (signers[alg]) {
|
|
182
|
+
const msgHex = toHex(`${headerB64}.${bodyB64}`);
|
|
183
|
+
|
|
184
|
+
// If we are using v1.1 protocol, the message should be hashed before verify
|
|
185
|
+
const version = body.version && semver.coerce(body.version) ? semver.coerce(body.version).version : '';
|
|
186
|
+
if (version && version === JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) {
|
|
187
|
+
return signers[alg].verify(hasher(msgHex), signature, signerPk);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return signers[alg].verify(msgHex, signature, signerPk);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
debug('verify.error.crypto');
|
|
194
|
+
return false;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
debug('verify.error.exception');
|
|
197
|
+
debug(err);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
sign,
|
|
204
|
+
verify,
|
|
205
|
+
decode,
|
|
206
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arcblock/jwt",
|
|
3
|
+
"description": "JSON Web Token variant for arcblock DID solutions",
|
|
4
|
+
"version": "1.6.10",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "wangshijun",
|
|
7
|
+
"email": "shijun@arcblock.io",
|
|
8
|
+
"url": "https://github.com/wangshijun"
|
|
9
|
+
},
|
|
10
|
+
"contributors": [
|
|
11
|
+
"wangshijun <shijun@arcblock.io> (https://github.com/wangshijun)"
|
|
12
|
+
],
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ArcBlock/asset-chain/issues",
|
|
15
|
+
"email": "shijun@arcblock.io"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@arcblock/did": "1.6.10",
|
|
22
|
+
"@ocap/mcrypto": "1.6.10",
|
|
23
|
+
"@ocap/util": "1.6.10",
|
|
24
|
+
"debug": "^4.3.3",
|
|
25
|
+
"json-stable-stringify": "^1.0.1",
|
|
26
|
+
"semver": "^7.3.4"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"jest": "^27.3.1"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/ArcBlock/asset-chain/tree/master/did/jwt",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"blockchain",
|
|
34
|
+
"arcblock",
|
|
35
|
+
"sdk",
|
|
36
|
+
"nodejs"
|
|
37
|
+
],
|
|
38
|
+
"license": "Apache-2.0",
|
|
39
|
+
"main": "./lib/index.js",
|
|
40
|
+
"files": [
|
|
41
|
+
"lib"
|
|
42
|
+
],
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/ArcBlock/asset-chain/tree/master/did/jwt"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"lint": "eslint lib tests",
|
|
49
|
+
"lint:fix": "eslint --fix lib tests",
|
|
50
|
+
"test": "jest --forceExit --detectOpenHandles",
|
|
51
|
+
"coverage": "yarn test -- --coverage"
|
|
52
|
+
},
|
|
53
|
+
"gitHead": "ab272e8db3a15c6571cc7fae7cc3d3e0fdd4bdb1"
|
|
54
|
+
}
|