@astronautlabs/jwt 0.0.12

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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/demo.key +27 -0
  4. package/demo8.key +28 -0
  5. package/dist/browser/base64url.d.ts +4 -0
  6. package/dist/browser/base64url.js +18 -0
  7. package/dist/browser/base64url.js.map +1 -0
  8. package/dist/browser/index.d.ts +5 -0
  9. package/dist/browser/index.js +21 -0
  10. package/dist/browser/index.js.map +1 -0
  11. package/dist/browser/utils.d.ts +6 -0
  12. package/dist/browser/utils.js +24 -0
  13. package/dist/browser/utils.js.map +1 -0
  14. package/dist/browser/webcrypto-jwt.d.ts +22 -0
  15. package/dist/browser/webcrypto-jwt.js +378 -0
  16. package/dist/browser/webcrypto-jwt.js.map +1 -0
  17. package/dist/browser/webcrypto-jwt.test.d.ts +1 -0
  18. package/dist/browser/webcrypto-jwt.test.js +6 -0
  19. package/dist/browser/webcrypto-jwt.test.js.map +1 -0
  20. package/dist/common/expiry.d.ts +1 -0
  21. package/dist/common/expiry.js +25 -0
  22. package/dist/common/expiry.js.map +1 -0
  23. package/dist/common/index.d.ts +2 -0
  24. package/dist/common/index.js +15 -0
  25. package/dist/common/index.js.map +1 -0
  26. package/dist/common/interface.d.ts +38 -0
  27. package/dist/common/interface.js +3 -0
  28. package/dist/common/interface.js.map +1 -0
  29. package/dist/engine.test.d.ts +10 -0
  30. package/dist/engine.test.js +295 -0
  31. package/dist/engine.test.js.map +1 -0
  32. package/dist/fixtures/es256.fixture.d.ts +6 -0
  33. package/dist/fixtures/es256.fixture.js +10 -0
  34. package/dist/fixtures/es256.fixture.js.map +1 -0
  35. package/dist/fixtures/hs256.fixture.d.ts +6 -0
  36. package/dist/fixtures/hs256.fixture.js +10 -0
  37. package/dist/fixtures/hs256.fixture.js.map +1 -0
  38. package/dist/fixtures/hs384.fixture.d.ts +6 -0
  39. package/dist/fixtures/hs384.fixture.js +10 -0
  40. package/dist/fixtures/hs384.fixture.js.map +1 -0
  41. package/dist/fixtures/hs512.fixture.d.ts +6 -0
  42. package/dist/fixtures/hs512.fixture.js +10 -0
  43. package/dist/fixtures/hs512.fixture.js.map +1 -0
  44. package/dist/fixtures/rs256.fixture.d.ts +6 -0
  45. package/dist/fixtures/rs256.fixture.js +10 -0
  46. package/dist/fixtures/rs256.fixture.js.map +1 -0
  47. package/dist/fixtures/rs512.fixture.d.ts +6 -0
  48. package/dist/fixtures/rs512.fixture.js +10 -0
  49. package/dist/fixtures/rs512.fixture.js.map +1 -0
  50. package/dist/index.d.ts +3 -0
  51. package/dist/index.js +18 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/node/engine.test.d.ts +1 -0
  54. package/dist/node/engine.test.js +6 -0
  55. package/dist/node/engine.test.js.map +1 -0
  56. package/dist/node/index.d.ts +9 -0
  57. package/dist/node/index.js +117 -0
  58. package/dist/node/index.js.map +1 -0
  59. package/dist/test.d.ts +1 -0
  60. package/dist/test.js +9 -0
  61. package/dist/test.js.map +1 -0
  62. package/karma.conf.ts +59 -0
  63. package/package.json +46 -0
  64. package/src/browser/base64url.ts +12 -0
  65. package/src/browser/index.ts +6 -0
  66. package/src/browser/utils.ts +20 -0
  67. package/src/browser/webcrypto-jwt.test.ts +4 -0
  68. package/src/browser/webcrypto-jwt.ts +351 -0
  69. package/src/common/expiry.ts +25 -0
  70. package/src/common/index.ts +2 -0
  71. package/src/common/interface.ts +47 -0
  72. package/src/engine.test.ts +173 -0
  73. package/src/fixtures/es256.fixture.ts +25 -0
  74. package/src/fixtures/hs256.fixture.ts +6 -0
  75. package/src/fixtures/hs384.fixture.ts +6 -0
  76. package/src/fixtures/hs512.fixture.ts +6 -0
  77. package/src/fixtures/rs256.fixture.ts +81 -0
  78. package/src/fixtures/rs512.fixture.ts +81 -0
  79. package/src/index.ts +5 -0
  80. package/src/node/engine.test.ts +4 -0
  81. package/src/node/index.ts +56 -0
  82. package/src/test.ts +7 -0
  83. package/tsconfig.json +37 -0
@@ -0,0 +1,20 @@
1
+ import { Base64URL } from "./base64url";
2
+
3
+ export class Utils {
4
+ public static isString(s) {
5
+ return typeof s === 'string';
6
+ }
7
+
8
+ public static utf8ToUint8Array(str) {
9
+ str = btoa(unescape(encodeURIComponent(str)));
10
+ return Base64URL.parse(str);
11
+ }
12
+
13
+ public static isFunction(fn) {
14
+ return typeof fn === 'function';
15
+ }
16
+
17
+ public static isObject(arg) {
18
+ return arg !== null && typeof arg === 'object';
19
+ }
20
+ }
@@ -0,0 +1,4 @@
1
+ import { engineTest } from "../engine.test";
2
+ import { JWT } from "./index";
3
+
4
+ engineTest('WebCryptoJWT', JWT);
@@ -0,0 +1,351 @@
1
+ // Originally from https://github.com/pose/webcrypto-jwt
2
+ // (C) Copyright (c) 2015 Alberto Pose albertopose@gmail.com
3
+ // Used under the terms of the MIT License (https://github.com/pose/webcrypto-jwt/blob/master/LICENSE.md)
4
+
5
+ import { JWTEngine, EncodeOptions, Token, DecodeOptions, Options, DecodedToken } from "../common/interface";
6
+ import { Base64URL } from "./base64url";
7
+ import { Utils } from "./utils";
8
+ import { validateExpiry } from "../common";
9
+
10
+ const ALGORITHMS = {
11
+ none: {},
12
+ HS256: {
13
+ importKey: {
14
+ name: 'HMAC',
15
+ hash: 'SHA-256'
16
+ },
17
+ operation: {
18
+ name: 'HMAC',
19
+ hash: 'SHA-256'
20
+ }
21
+ },
22
+ HS384: {
23
+ importKey: {
24
+ name: 'HMAC',
25
+ hash: 'SHA-384'
26
+ },
27
+ operation: {
28
+ name: 'HMAC',
29
+ hash: 'SHA-384'
30
+ }
31
+ },
32
+ HS512: {
33
+ importKey: {
34
+ name: 'HMAC',
35
+ hash: 'SHA-512'
36
+ },
37
+ operation: {
38
+ name: 'HMAC',
39
+ hash: 'SHA-512'
40
+ }
41
+ },
42
+ RS256: {
43
+ importKey: {
44
+ name: 'RSASSA-PKCS1-v1_5',
45
+ hash: 'SHA-256'
46
+ },
47
+ operation: {
48
+ name: 'RSASSA-PKCS1-v1_5',
49
+ hash: 'SHA-256'
50
+ }
51
+ },
52
+ RS512: {
53
+ importKey: {
54
+ name: 'RSASSA-PKCS1-v1_5',
55
+ hash: 'SHA-512'
56
+ },
57
+ operation: {
58
+ name: 'RSASSA-PKCS1-v1_5',
59
+ hash: 'SHA-512'
60
+ }
61
+ },
62
+ ES256: {
63
+ importKey: {
64
+ name: 'ECDSA',
65
+ namedCurve: 'P-256'
66
+ },
67
+ operation: {
68
+ name: 'ECDSA',
69
+ namedCurve: 'P-256',
70
+ hash: 'SHA-256'
71
+ }
72
+ }
73
+ };
74
+
75
+ export class WebCryptoJWT implements JWTEngine {
76
+ constructor(
77
+ private subtleCrypto? : SubtleCrypto
78
+ ) {
79
+ if (!subtleCrypto)
80
+ this.findSubtleCrypto();
81
+ }
82
+
83
+ async decodeUntrusted(token: string): Promise<Token> {
84
+ let decodedToken = this._decode(token);
85
+ return { claims: decodedToken.payload, string: token };
86
+ }
87
+
88
+ private findSubtleCrypto() {
89
+ if ('crypto' in window)
90
+ this.subtleCrypto = crypto.subtle || crypto['webkitSubtle'];
91
+ if (!this.subtleCrypto && 'msCrypto' in window)
92
+ this.subtleCrypto = window['msCrypto'].Subtle;
93
+ }
94
+
95
+
96
+ async encode(payload: Record<string, any>, options: EncodeOptions): Promise<Token> {
97
+ return {
98
+ string: await this._sign(payload, options.secretOrKey, this.algorithmOf(options)),
99
+ claims: payload
100
+ }
101
+ }
102
+
103
+ async validate(string: string, options: DecodeOptions): Promise<Token> {
104
+ let decodedToken = this._decode(string);
105
+ let algorithm = this.algorithmOf(options);
106
+ let secretOrKey = options.secretOrKey;
107
+ let claims = decodedToken.payload;
108
+
109
+ // Signature must match
110
+
111
+ if (decodedToken.header.alg !== algorithm)
112
+ throw new Error(`Cannot validate JWT '${string}': Token has incorrect algorithm`);
113
+
114
+ if (algorithm !== 'none' && !(await this._verify(decodedToken, secretOrKey, algorithm)))
115
+ throw new Error(`Cannot validate JWT '${string}': Invalid signature`);
116
+
117
+ // Algorithm must match
118
+
119
+ // Expiration
120
+
121
+ try {
122
+ validateExpiry(claims.exp, options.now, options.validate?.exp);
123
+ } catch (e) {
124
+ throw new Error(`Cannot validate JWT '${string}': ${e.message}`);
125
+ }
126
+
127
+ return {
128
+ string,
129
+ claims: decodedToken.payload
130
+ };
131
+ }
132
+
133
+ private algorithmOf(options : Options) {
134
+ return options.algorithm || 'HS256';
135
+ }
136
+
137
+ private str2ab(str : string) {
138
+ const buf = new ArrayBuffer(str.length);
139
+ const bufView = new Uint8Array(buf);
140
+ for (let i = 0, strLen = str.length; i < strLen; i++) {
141
+ bufView[i] = str.charCodeAt(i);
142
+ }
143
+ return buf;
144
+ }
145
+
146
+ /**
147
+ * Adapted from https://chromium.googlesource.com/chromium/blink/+/master/LayoutTests/crypto/subtle/hmac/sign-verify.html
148
+ *
149
+ * @param token
150
+ * @param secret
151
+ * @param alg
152
+ */
153
+ private async _verify(token : DecodedToken, secret : string, alg : string): Promise<boolean> {
154
+ if (alg === 'none')
155
+ return true;
156
+
157
+ let importAlgorithm = ALGORITHMS[alg];
158
+ if (!importAlgorithm)
159
+ throw new Error(`Algorithm ${alg} is not supported`);
160
+
161
+ let keyFormat : string;
162
+ let secretBuf : ArrayBuffer;
163
+ let encoder = new TextEncoder();
164
+
165
+ // TODO Test utf8ToUint8Array function
166
+ if (secret.includes('-----BEGIN PUBLIC KEY-----')) {
167
+ secretBuf = this.str2ab(atob(
168
+ secret
169
+ .replace(/^-----BEGIN PUBLIC KEY-----\n/, '')
170
+ .replace(/\n-----END PUBLIC KEY-----/, '')
171
+ .replace(/\n/g, '')
172
+ ));
173
+ keyFormat = 'spki';
174
+ } else if (secret.includes('-----BEGIN RSA PUBLIC KEY-----')) {
175
+ throw new Error(
176
+ `PKCS#1 keys are not supported. `
177
+ + `Please convert the key to PKCS#8 instead: `
178
+ + `openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in pkcs1.key -out pkcs8.key`
179
+ );
180
+ } else {
181
+ secretBuf = encoder.encode(secret);
182
+ keyFormat = 'raw';
183
+ }
184
+
185
+ let key : CryptoKey;
186
+
187
+ if (!this.subtleCrypto)
188
+ throw new Error(`Not supported: No Subtle Crypto support`);
189
+
190
+ try {
191
+ key = await this.subtleCrypto.importKey(
192
+ keyFormat,
193
+ secretBuf,
194
+ importAlgorithm.importKey,
195
+ false,
196
+ ['verify']
197
+ );
198
+ } catch (e) {
199
+ let identifier = `jwtUncaughtError${Math.floor(10000 + Math.random() * 10000)}`;
200
+
201
+ console.error(`JWT.verify(): Caught error while importing ${alg} key: format=${keyFormat}, importAlgorithm: ${JSON.stringify(importAlgorithm.importKey)}.`);
202
+ console.error(e);
203
+
204
+ if (typeof window !== 'undefined') {
205
+ window[identifier] = e;
206
+ console.error(`To aid in debugging, this error was saved to ${identifier}`);
207
+ }
208
+
209
+ throw e;
210
+ }
211
+
212
+ let partialToken = `${token.encodedHeader}.${token.encodedPayload}`;
213
+ let signaturePart = token.signature;
214
+
215
+ // TODO Test utf8ToUint8Array function
216
+ let messageAsUint8Array = encoder.encode(partialToken);
217
+ // TODO Test utf8ToUint8Array function
218
+ let signatureAsUint8Array = Base64URL.parse(signaturePart);
219
+
220
+ if (!this.subtleCrypto)
221
+ throw new Error(`Not supported: No Subtle Crypto support`);
222
+
223
+ try {
224
+ return await this.subtleCrypto.verify(
225
+ importAlgorithm.operation,
226
+ key,
227
+ signatureAsUint8Array,
228
+ messageAsUint8Array
229
+ );
230
+ } catch (e) {
231
+ console.error(`JWT.verify(): Caught error while verifying token:`);
232
+ console.error(e);
233
+ throw e;
234
+ }
235
+ };
236
+
237
+ private async _sign(payload : Record<string,any>, secret : string, alg : string): Promise<string> {
238
+ let importAlgorithm = ALGORITHMS[alg];
239
+ if (!importAlgorithm)
240
+ throw new Error(`Algorithm '${alg}' is not supported`);
241
+
242
+ let payloadAsJSON = JSON.stringify(payload);
243
+ let header = { alg: alg, typ: 'JWT' };
244
+ let headerAsJSON = JSON.stringify(header);
245
+ let partialToken = Base64URL.stringify(Utils.utf8ToUint8Array(headerAsJSON)) + '.' +
246
+ Base64URL.stringify(Utils.utf8ToUint8Array(payloadAsJSON));
247
+
248
+ if (alg === 'none')
249
+ return `${partialToken}.`;
250
+
251
+ let keyFormat = 'raw';
252
+ let encoder = new TextEncoder();
253
+ let secretBuf : ArrayBuffer;
254
+
255
+ // TODO Test utf8ToUint8Array function
256
+ if (secret.includes('-----BEGIN RSA PRIVATE KEY-----')) {
257
+ throw new Error(`PKCS#1 keys are not supported. Please convert the key to PKCS#8 instead: openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in pkcs1.key -out pkcs8.key`);
258
+ } else if (secret.includes('-----BEGIN PRIVATE KEY-----')) {
259
+ secretBuf = this.str2ab(atob(
260
+ secret
261
+ .replace(/^-----BEGIN PRIVATE KEY-----\n/, '')
262
+ .replace(/\n-----END PRIVATE KEY-----/, '')
263
+ .replace(/\n/g, '')
264
+ ));
265
+ keyFormat = 'pkcs8';
266
+ } else {
267
+ secretBuf = encoder.encode(secret);
268
+ }
269
+
270
+
271
+ if (!this.subtleCrypto)
272
+ throw new Error(`Not supported: No Subtle Crypto support`);
273
+
274
+ let key : CryptoKey;
275
+
276
+ try {
277
+ key = await this.subtleCrypto.importKey(
278
+ keyFormat,
279
+ secretBuf,
280
+ importAlgorithm.importKey,
281
+ false,
282
+ ['sign']
283
+ )
284
+ } catch (e) {
285
+ console.error(`JWT.sign(): Caught error while importing ${alg} key: format=${keyFormat}, importAlgorithm: ${JSON.stringify(importAlgorithm.importKey)}`);
286
+ console.error(e);
287
+ throw e;
288
+ }
289
+
290
+ let messageAsUint8Array = Utils.utf8ToUint8Array(partialToken);
291
+
292
+
293
+ if (!this.subtleCrypto)
294
+ throw new Error(`Not supported: No Subtle Crypto support`);
295
+
296
+ let signature : ArrayBuffer;
297
+
298
+ try {
299
+ signature = await this.subtleCrypto.sign(
300
+ importAlgorithm.operation,
301
+ key,
302
+ messageAsUint8Array
303
+ );
304
+ } catch (e) {
305
+ console.error(`JWT.sign(): Caught error while signing token:`);
306
+ console.error(e);
307
+ throw e;
308
+ }
309
+
310
+ // TODO Test
311
+ let signatureAsBase64 = Base64URL.stringify(new Uint8Array(signature));
312
+ let token = `${partialToken}.${signatureAsBase64}`;
313
+
314
+ return token;
315
+ };
316
+
317
+ private _decode(token : string): DecodedToken {
318
+ let parts = token.split('.');
319
+ if (parts.length !== 3)
320
+ throw new Error(`Invalid token '${token}': must have 3 parts separated by '.'`);
321
+
322
+ return {
323
+ encodedHeader: parts[0],
324
+ encodedPayload: parts[1],
325
+ signature: parts[2],
326
+
327
+ header: JSON.parse(this._decodeBase64URL(parts[0])),
328
+ payload: JSON.parse(this._decodeBase64URL(parts[1])),
329
+ };
330
+ };
331
+
332
+ private _decodeBase64URL(string : string) {
333
+ string = string.replace(/-/g, '+').replace(/_/g, '/');
334
+
335
+ switch (string.length % 4) {
336
+ case 0:
337
+ break;
338
+ case 2:
339
+ string += '==';
340
+ break;
341
+ case 3:
342
+ string += '=';
343
+ break;
344
+ default:
345
+ throw new Error(`Illegal Base64URL string '${string}'`);
346
+ }
347
+
348
+ // TODO Use shim or document incompatible browsers
349
+ return decodeURIComponent(escape(atob(string)));
350
+ }
351
+ }
@@ -0,0 +1,25 @@
1
+ export function validateExpiry(exp : number, now : number, policy : 'ignore' | 'when-present' | 'force') {
2
+
3
+ // Define acceptable policies
4
+
5
+ const supportedPolicies = [
6
+ 'when-present', 'force', 'ignore'
7
+ ];
8
+
9
+ // Set defaults
10
+
11
+ policy = policy || 'when-present';
12
+ now = now || Date.now();
13
+
14
+ if (!supportedPolicies.includes(policy))
15
+ throw new Error(`Unsupported 'exp' validation strategy '${policy}'`);
16
+
17
+ if (policy !== 'ignore') {
18
+ if (typeof exp !== 'undefined') {
19
+ if (exp * 1000 < now)
20
+ throw new Error(`Token is expired`);
21
+ } else if (policy === 'force') {
22
+ throw new Error(`Non-expiring tokens are not acceptable`);
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,2 @@
1
+ export * from './interface';
2
+ export * from './expiry';
@@ -0,0 +1,47 @@
1
+ export interface Options {
2
+ secretOrKey? : string;
3
+ algorithm? : string;
4
+
5
+ /**
6
+ * Assume the given UNIX clock time in milliseconds (as would be returned by Date.now()).
7
+ * When not specified, Date.now() is used to get the current time.
8
+ */
9
+ now? : number;
10
+ }
11
+
12
+ export interface EncodeOptions extends Options {
13
+ }
14
+
15
+ export interface DecodeOptions extends Options {
16
+ validate?: ValidateOptions;
17
+ }
18
+
19
+ export interface ValidateOptions {
20
+ /**
21
+ * Whether to automatically verify the `exp` claim.
22
+ * When not specified, default is 'when-present'.
23
+ * Set `now` option to override the current time.
24
+ */
25
+ exp? : 'when-present' | 'force' | 'ignore';
26
+ }
27
+
28
+ export interface Token {
29
+ string : string;
30
+ claims : Record<string, any>;
31
+ }
32
+
33
+ export interface DecodedToken {
34
+ encodedHeader : string;
35
+ encodedPayload : string;
36
+ signature : string;
37
+
38
+ header : Record<string,any>;
39
+ payload : Record<string,any>;
40
+ }
41
+
42
+
43
+ export interface JWTEngine {
44
+ encode(claims : Record<string,any>, options : EncodeOptions) : Promise<Token>;
45
+ validate(token : string, options : DecodeOptions) : Promise<Token>;
46
+ decodeUntrusted(token : string) : Promise<Token>;
47
+ }