Browse Source

v1.2.7: update docs, handle human readable 'exp' claims, denest required claims

tags/v1.2.7
AJ ONeal 9 months ago
parent
commit
885a00c3ae
4 changed files with 108 additions and 27 deletions
  1. 48
    19
      README.md
  2. 45
    6
      keypairs.js
  3. 1
    2
      package.json
  4. 14
    0
      test.js

+ 48
- 19
README.md View File

@@ -29,25 +29,48 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
29 29
 
30 30
 # Usage
31 31
 
32
-A brief (albeit somewhat nonsensical) introduction to the APIs:
32
+A brief introduction to the APIs:
33 33
 
34 34
 ```
35
+// generate a new keypair as jwk
36
+// (defaults to EC P-256 when no options are specified)
35 37
 Keypairs.generate().then(function (pair) {
36
-  return Keypairs.export({ jwk: pair.private }).then(function (pem) {
37
-    return Keypairs.import({ pem: pem }).then(function (jwk) {
38
-      return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
39
-        console.log(thumb);
40
-        return Keypairs.signJwt({
41
-          jwk: keypair.private
42
-        , claims: {
43
-            iss: 'https://example.com'
44
-          , sub: 'jon.doe@gmail.com'
45
-          , exp: Math.round(Date.now()/1000) + (3 * 24 * 60 * 60)
46
-          }
47
-        });
48
-      });
49
-    });
50
-  });
38
+  console.log(pair.private);
39
+  console.log(pair.public);
40
+});
41
+```
42
+
43
+```
44
+// JWK to PEM
45
+// (supports various 'format' and 'encoding' options)
46
+return Keypairs.export({ jwk: pair.private, format: 'pkcs8' }).then(function (pem) {
47
+  console.log(pem);
48
+});
49
+```
50
+
51
+```
52
+// PEM to JWK
53
+return Keypairs.import({ pem: pem }).then(function (jwk) {
54
+});
55
+```
56
+
57
+```
58
+// Thumbprint a JWK (SHA256)
59
+return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
60
+  console.log(thumb);
61
+});
62
+```
63
+
64
+```
65
+// Sign a JWT (aka compact JWS)
66
+return Keypairs.signJwt({
67
+  jwk: pair.private
68
+, iss: 'https://example.com'
69
+, exp: '1h'
70
+  // optional claims
71
+, claims: {
72
+  , sub: 'jon.doe@gmail.com'
73
+  }
51 74
 });
52 75
 ```
53 76
 
@@ -56,9 +79,9 @@ _much_ longer than RSA has, and they're smaller, and faster to generate.
56 79
 
57 80
 ## API Overview
58 81
 
59
-* generate
60
-* parse
61
-* parseOrGenerate
82
+* generate (JWK)
83
+* parse (PEM)
84
+* parseOrGenerate (PEM to JWK)
62 85
 * import (PEM-to-JWK)
63 86
 * export (JWK-to-PEM, private or public)
64 87
 * publish (Private JWK to Public JWK)
@@ -155,11 +178,17 @@ Returns a JWT (otherwise known as a protected JWS in "compressed" format).
155 178
 
156 179
 ```js
157 180
 { jwk: jwk
181
+  // required claims
182
+, iss: 'https://example.com'
183
+, exp: '15m'
184
+  // all optional claims
158 185
 , claims: {
159 186
   }
160 187
 }
161 188
 ```
162 189
 
190
+Exp may be human readable duration (i.e. 1h, 15m, 30s) or a datetime in seconds.
191
+
163 192
 Header defaults:
164 193
 
165 194
 ```js

+ 45
- 6
keypairs.js View File

@@ -103,10 +103,14 @@ Keypairs.neuter = Keypairs._neuter = function (opts) {
103 103
 Keypairs.publish = function (opts) {
104 104
   if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); }
105 105
 
106
+  // returns a copy
106 107
   var jwk = Keypairs.neuter(opts);
107 108
 
108
-  if (!jwk.exp) {
109
-    if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; }
109
+  if (jwk.exp) {
110
+    jwk.exp = setTime(jwk.exp);
111
+  } else {
112
+    if (opts.exp) { jwk.exp = setTime(opts.exp); }
113
+    else if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; }
110 114
     else { jwk.exp = opts.expiresAt; }
111 115
   }
112 116
   if (!jwk.use && false !== jwk.use) { jwk.use = "sig"; }
@@ -134,17 +138,22 @@ Keypairs.signJwt = function (opts) {
134 138
     if (!header.kid) {
135 139
       header.kid = thumb;
136 140
     }
137
-    if (false === claims.iat) {
141
+    if (!claims.iat && (false === claims.iat || false === opts.iat)) {
138 142
       claims.iat = undefined;
139 143
     } else if (!claims.iat) {
140 144
       claims.iat = Math.round(Date.now()/1000);
141 145
     }
142
-    if (false === claims.exp) {
146
+
147
+    if (opts.exp) {
148
+      claims.exp = setTime(opts.exp);
149
+    } else if (!claims.exp && (false === claims.exp || false === opts.exp)) {
143 150
       claims.exp = undefined;
144 151
     } else if (!claims.exp) {
145
-      throw new Error("opts.claims.exp should be the expiration date (as seconds since the Unix epoch) or false");
152
+      throw new Error("opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false");
146 153
     }
147
-    if (false === claims.iss) {
154
+
155
+    if (opts.iss) { claims.iss = opts.iss; }
156
+    if (!claims.iss && (false === claims.iss || false === opts.iss)) {
148 157
       claims.iss = undefined;
149 158
     } else if (!claims.iss) {
150 159
       throw new Error("opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url");
@@ -229,6 +238,36 @@ Keypairs.signJws = function (opts) {
229 238
   });
230 239
 };
231 240
 
241
+function setTime(time) {
242
+  if ('number' === typeof time) { return time; }
243
+
244
+  var t = time.match(/^(\-?\d+)([dhms])$/i);
245
+  if (!t || !t[0]) {
246
+    throw new Error("'" + time + "' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s");
247
+  }
248
+
249
+  var now = Math.round(Date.now()/1000);
250
+  var num = parseInt(t[1], 10);
251
+  var unit = t[2];
252
+  var mult = 1;
253
+  switch(unit) {
254
+    // fancy fallthrough, what fun!
255
+    case 'd':
256
+      mult *= 24;
257
+      /*falls through*/
258
+    case 'h':
259
+      mult *= 60;
260
+      /*falls through*/
261
+    case 'm':
262
+      mult *= 60;
263
+      /*falls through*/
264
+    case 's':
265
+      mult *= 1;
266
+  }
267
+
268
+  return now + (mult * num);
269
+}
270
+
232 271
 Enc.strToUrlBase64 = function (str) {
233 272
   // node automatically can tell the difference
234 273
   // between uc2 (utf-8) strings and binary strings

+ 1
- 2
package.json View File

@@ -1,10 +1,9 @@
1 1
 {
2 2
   "name": "keypairs",
3
-  "version": "1.2.6",
3
+  "version": "1.2.7",
4 4
   "description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM",
5 5
   "main": "keypairs.js",
6 6
   "files": [
7
-    "CLI.md",
8 7
     "bin/keypairs.js"
9 8
   ],
10 9
   "scripts": {

+ 14
- 0
test.js View File

@@ -90,6 +90,20 @@ Keypairs.parseOrGenerate({ key: '' }).then(function (pair) {
90 90
       if ('NOERR' === e.code) { throw e; }
91 91
       return true;
92 92
     })
93
+  , Keypairs.signJwt({ jwk: pair.private, iss: 'https://example.com/', exp: '1h' }).then(function (jwt) {
94
+      var parts = jwt.split('.');
95
+      var now = Math.round(Date.now()/1000);
96
+      var token = {
97
+        header: JSON.parse(Buffer.from(parts[0], 'base64'))
98
+      , payload: JSON.parse(Buffer.from(parts[1], 'base64'))
99
+      , signature: parts[2] //Buffer.from(parts[2], 'base64')
100
+      };
101
+      // allow some leeway just in case we happen to hit a 1ms boundary
102
+      if (token.payload.exp - now > 60 * 59.99) {
103
+        return true;
104
+      }
105
+      throw new Error("token was not properly generated");
106
+    })
93 107
   ]).then(function (results) {
94 108
     if (results.length && results.every(function (v) { return true === v; })) {
95 109
       console.info("If a warning prints right above this, it's a pass");

Loading…
Cancel
Save