jose 1.27.0 → 1.28.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ # [1.28.0](https://github.com/panva/jose/compare/v1.27.3...v1.28.0) (2020-08-10)
6
+
7
+
8
+ ### Features
9
+
10
+ * support for validating issuer from a list of values ([#91](https://github.com/panva/jose/issues/91)) ([ce6836a](https://github.com/panva/jose/commit/ce6836af88c9e73c29560233f15ed1760c7dcc13))
11
+
12
+
13
+
14
+ ## [1.27.3](https://github.com/panva/jose/compare/v1.27.2...v1.27.3) (2020-08-04)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * do not mutate unencoded payload when signing for multiple parties ([1695423](https://github.com/panva/jose/commit/169542363f884e4028db9f80086d631e626eb469)), closes [#89](https://github.com/panva/jose/issues/89)
20
+ * ensure "b64" is the same for all recipients edge cases ([d56ec9f](https://github.com/panva/jose/commit/d56ec9f5ddc2612e5ff21fe35d45a56e7153e0e4))
21
+
22
+
23
+
24
+ ## [1.27.2](https://github.com/panva/jose/compare/v1.27.1...v1.27.2) (2020-07-01)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * handle private EC keys without public component ([#86](https://github.com/panva/jose/issues/86)) ([e8ad389](https://github.com/panva/jose/commit/e8ad38993e29747098f7fd1594dde4ce893ba802)), closes [#85](https://github.com/panva/jose/issues/85)
30
+
31
+
32
+
33
+ ## [1.27.1](https://github.com/panva/jose/compare/v1.27.0...v1.27.1) (2020-06-01)
34
+
35
+
36
+ ### Bug Fixes
37
+
38
+ * allow any JSON numeric value for timestamp values ([7ba4922](https://github.com/panva/jose/commit/7ba492237aaf788914166c134d50fb046041efa0))
39
+
40
+
41
+
5
42
  # [1.27.0](https://github.com/panva/jose/compare/v1.26.1...v1.27.0) (2020-05-05)
6
43
 
7
44
 
package/README.md CHANGED
@@ -326,8 +326,8 @@ Legend:
326
326
  <sup>2</sup> Unsecured JWS is [supported][documentation-none] for the JWS and JWT sign and verify
327
327
  operations but it is an entirely opt-in behaviour, downgrade attacks are prevented by the required
328
328
  use of a special `JWK.Key`-like object that cannot be instantiated through the key import API
329
- <sup>3</sup> RSAES OAEP using SHA-2 and MGF1 with SHA-2 is only supported when Node.js >= 12.9.0 runtime is detected
330
- <sup>4</sup> ECDH-ES with X25519 and X448 keys is only supported when Node.js >= 13.9.0 runtime is detected
329
+ <sup>3</sup> RSAES OAEP using SHA-2 and MGF1 with SHA-2 is only supported when Node.js `>=12.9.0` runtime is detected
330
+ <sup>4</sup> ECDH-ES with X25519 and X448 keys is only supported when Node.js `^12.17.0 || >=13.9.0` runtime is detected
331
331
  <sup>5</sup> Draft specification profiles are updated as minor versions of the library, therefore,
332
332
  since they may have breaking changes use the `~` semver operator when using these and pay close
333
333
  attention to changelog and the drafts themselves.
@@ -4,6 +4,7 @@ const { EOL } = require('os')
4
4
 
5
5
  const errors = require('../errors')
6
6
 
7
+ const { keyObjectSupported } = require('./runtime_support')
7
8
  const { createPublicKey } = require('./key_object')
8
9
  const base64url = require('./base64url')
9
10
  const asn1 = require('./asn1')
@@ -89,10 +90,21 @@ const keyObjectToJWK = {
89
90
  const ECPrivateKey = asn1.get('ECPrivateKey')
90
91
 
91
92
  const { privateKey, algorithm: { parameters: { value: crv } } } = PrivateKeyInfo.decode(der)
92
- const { privateKey: d, publicKey: { data: publicKey } } = ECPrivateKey.decode(privateKey)
93
+ const { privateKey: d, publicKey } = ECPrivateKey.decode(privateKey)
94
+
95
+ if (typeof publicKey === 'undefined') {
96
+ if (keyObjectSupported) {
97
+ return {
98
+ ...keyObjectToJWK.ec.public(createPublicKey(keyObject)),
99
+ d: base64url.encodeBuffer(d)
100
+ }
101
+ }
93
102
 
94
- const x = publicKey.slice(1, ((publicKey.length - 1) / 2) + 1)
95
- const y = publicKey.slice(((publicKey.length - 1) / 2) + 1)
103
+ throw new errors.JOSENotSupported('Private EC keys without the public key embedded are not supported in your Node.js runtime version')
104
+ }
105
+
106
+ const x = publicKey.data.slice(1, ((publicKey.data.length - 1) / 2) + 1)
107
+ const y = publicKey.data.slice(((publicKey.data.length - 1) / 2) + 1)
96
108
 
97
109
  return {
98
110
  kty: 'EC',
@@ -190,7 +190,11 @@ class Encrypt {
190
190
  let aad
191
191
  if (this._aad) {
192
192
  final.aad = base64url.encode(this._aad)
193
- aad = Buffer.concat([Buffer.from(final.protected || ''), Buffer.from('.'), Buffer.from(final.aad)])
193
+ aad = Buffer.concat([
194
+ Buffer.from(final.protected || ''),
195
+ Buffer.from('.'),
196
+ Buffer.from(final.aad)
197
+ ])
194
198
  } else {
195
199
  aad = Buffer.from(final.protected || '')
196
200
  }
@@ -44,9 +44,20 @@ const generalSerializer = (payload, recipients) => {
44
44
  }
45
45
  }
46
46
  generalSerializer.validate = (jws, recipients) => {
47
+ let validateB64 = false
47
48
  recipients.forEach(({ protectedHeader, unprotectedHeader }) => {
49
+ if (protectedHeader && !validateB64 && 'b64' in protectedHeader) {
50
+ validateB64 = true
51
+ }
48
52
  validateCrit(protectedHeader, unprotectedHeader, protectedHeader ? protectedHeader.crit : undefined)
49
53
  })
54
+
55
+ if (validateB64) {
56
+ const values = recipients.map(({ protectedHeader }) => protectedHeader && protectedHeader.b64)
57
+ if (!values.every((actual, i, [expected]) => actual === expected)) {
58
+ throw new JWSInvalid('the "b64" Header Parameter value MUST be the same for all recipients')
59
+ }
60
+ }
50
61
  }
51
62
 
52
63
  const isJSON = (input) => {
package/lib/jws/sign.js CHANGED
@@ -57,7 +57,7 @@ class Sign {
57
57
  /*
58
58
  * @private
59
59
  */
60
- [PROCESS_RECIPIENT] (recipient) {
60
+ [PROCESS_RECIPIENT] (recipient, first) {
61
61
  const { key, protectedHeader, unprotectedHeader } = recipient
62
62
 
63
63
  if (key.use === 'enc') {
@@ -87,32 +87,21 @@ class Sign {
87
87
  recipient.header = unprotectedHeader
88
88
  recipient.protected = Object.keys(joseHeader.protected).length ? base64url.JSON.encode(joseHeader.protected) : ''
89
89
 
90
- let toBeSigned
91
- if (joseHeader.protected.crit && joseHeader.protected.crit.includes('b64')) {
92
- if (this._b64 !== undefined && this._b64 !== joseHeader.protected.b64) {
93
- throw new JWSInvalid('the "b64" Header Parameter value MUST be the same for all recipients')
90
+ if (first && joseHeader.protected.crit && joseHeader.protected.crit.includes('b64') && joseHeader.protected.b64 === false) {
91
+ if (this._binary) {
92
+ this._payload = base64url.decodeToBuffer(this._payload)
94
93
  } else {
95
- this._b64 = joseHeader.protected.b64
94
+ this._payload = base64url.decode(this._payload)
96
95
  }
97
-
98
- if (!joseHeader.protected.b64) {
99
- if (this._binary) {
100
- this._payload = base64url.decodeToBuffer(this._payload)
101
- } else {
102
- this._payload = base64url.decode(this._payload)
103
- }
104
- }
105
-
106
- toBeSigned = Buffer.concat([
107
- Buffer.from(recipient.protected || ''),
108
- Buffer.from('.'),
109
- Buffer.isBuffer(this._payload) ? this._payload : Buffer.from(this._payload)
110
- ])
111
- } else {
112
- toBeSigned = `${recipient.protected || ''}.${this._payload}`
113
96
  }
114
97
 
115
- recipient.signature = base64url.encodeBuffer(sign(alg, key, toBeSigned))
98
+ const data = Buffer.concat([
99
+ Buffer.from(recipient.protected || ''),
100
+ Buffer.from('.'),
101
+ Buffer.from(this._payload)
102
+ ])
103
+
104
+ recipient.signature = base64url.encodeBuffer(sign(alg, key, data))
116
105
  }
117
106
 
118
107
  /*
@@ -130,9 +119,9 @@ class Sign {
130
119
 
131
120
  serializer.validate(this, this._recipients)
132
121
 
133
- for (const recipient of this._recipients) {
134
- this[PROCESS_RECIPIENT](recipient)
135
- }
122
+ this._recipients.forEach((recipient, i) => {
123
+ this[PROCESS_RECIPIENT](recipient, i === 0)
124
+ })
136
125
 
137
126
  return serializer(this._payload, this._recipients)
138
127
  }
package/lib/jwt/verify.js CHANGED
@@ -20,8 +20,8 @@ const isTimestamp = (value, label, required = false) => {
20
20
  throw new JWTClaimInvalid(`"${label}" claim is missing`, label, 'missing')
21
21
  }
22
22
 
23
- if (value !== undefined && (typeof value !== 'number' || !Number.isSafeInteger(value))) {
24
- throw new JWTClaimInvalid(`"${label}" claim must be a unix timestamp`, label, 'invalid')
23
+ if (value !== undefined && (typeof value !== 'number')) {
24
+ throw new JWTClaimInvalid(`"${label}" claim must be a JSON numeric value`, label, 'invalid')
25
25
  }
26
26
  }
27
27
 
@@ -63,12 +63,15 @@ const validateOptions = ({
63
63
 
64
64
  isOptionString(maxTokenAge, 'options.maxTokenAge')
65
65
  isOptionString(subject, 'options.subject')
66
- isOptionString(issuer, 'options.issuer')
67
66
  isOptionString(maxAuthAge, 'options.maxAuthAge')
68
67
  isOptionString(jti, 'options.jti')
69
68
  isOptionString(clockTolerance, 'options.clockTolerance')
70
69
  isOptionString(typ, 'options.typ')
71
70
 
71
+ if (issuer !== undefined && (isNotString(issuer) && isNotArrayOfStrings(issuer))) {
72
+ throw new TypeError('options.issuer must be a string or an array of strings')
73
+ }
74
+
72
75
  if (audience !== undefined && (isNotString(audience) && isNotArrayOfStrings(audience))) {
73
76
  throw new TypeError('options.audience must be a string or an array of strings')
74
77
  }
@@ -161,7 +164,7 @@ const validateTypes = ({ header, payload }, profile, options) => {
161
164
  isPayloadString(payload.jti, '"jti" claim', 'jti', profile === LOGOUTTOKEN || profile === ATJWT || !!options.jti)
162
165
  isPayloadString(payload.acr, '"acr" claim', 'acr')
163
166
  isPayloadString(payload.nonce, '"nonce" claim', 'nonce', !!options.nonce)
164
- isPayloadString(payload.iss, '"iss" claim', 'iss', !!options.issuer)
167
+ isStringOrArrayOfStrings(payload.iss, 'iss', !!options.issuer)
165
168
  isPayloadString(payload.sub, '"sub" claim', 'sub', profile === IDTOKEN || profile === ATJWT || !!options.subject)
166
169
  isStringOrArrayOfStrings(payload.aud, 'aud', !!options.audience)
167
170
  isPayloadString(payload.azp, '"azp" claim', 'azp', profile === IDTOKEN && Array.isArray(payload.aud) && payload.aud.length > 1)
@@ -235,7 +238,7 @@ module.exports = (token, key, options = {}) => {
235
238
  const unix = epoch(now)
236
239
  validateTypes(decoded, profile, options)
237
240
 
238
- if (issuer && decoded.payload.iss !== issuer) {
241
+ if (issuer && (typeof decoded.payload.iss !== 'string' || !(typeof issuer === 'string' ? [issuer] : issuer).includes(decoded.payload.iss))) {
239
242
  throw new JWTClaimInvalid('unexpected "iss" claim value', 'iss', 'check_failed')
240
243
  }
241
244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jose",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "description": "JSON Web Almost Everything - JWA, JWS, JWE, JWK, JWT, JWKS for Node.js with minimal dependencies",
5
5
  "keywords": [
6
6
  "access token",
@@ -81,11 +81,11 @@
81
81
  "@panva/asn1.js": "^1.0.0"
82
82
  },
83
83
  "devDependencies": {
84
- "@commitlint/cli": "^8.3.4",
85
- "@commitlint/config-conventional": "^8.3.4",
84
+ "@commitlint/cli": "^9.1.1",
85
+ "@commitlint/config-conventional": "^9.1.1",
86
86
  "ava": "^2.4.0",
87
87
  "babel-eslint": "^10.0.3",
88
- "c8": "^7.0.0",
88
+ "c8": "^7.2.1",
89
89
  "husky": "^4.0.0",
90
90
  "standard": "^14.3.1"
91
91
  },
package/types/index.d.ts CHANGED
@@ -444,7 +444,7 @@ export namespace JWT {
444
444
  ignoreIat?: boolean;
445
445
  maxTokenAge?: string;
446
446
  subject?: string;
447
- issuer?: string;
447
+ issuer?: string | string[];
448
448
  maxAuthAge?: string;
449
449
  jti?: string;
450
450
  clockTolerance?: string;
@@ -479,7 +479,7 @@ export namespace JWT {
479
479
  function sign(payload: object, key: ProduceKeyInputWithNone, options?: SignOptions): string;
480
480
 
481
481
  interface VerifyProfileOptions<profile> {
482
- issuer: string;
482
+ issuer: string | string[];
483
483
  audience: string | string[];
484
484
  profile?: profile;
485
485
  }