@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 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
+ ![did-auth](https://www.arcblock.io/.netlify/functions/badge/?text=did-auth)
2
+
3
+ [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
4
+ [![docs](https://img.shields.io/badge/powered%20by-arcblock-green.svg)](https://docs.arcblock.io)
5
+ [![Gitter](https://badges.gitter.im/ArcBlock/community.svg)](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
+ }