@muze-nl/metro-oidc 0.3.1 → 0.3.3

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2025 Auke van Slooten
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Metro OpenID Connect middleware
2
+
3
+ [![Project stage: Experimental][project-stage-badge: Experimental]][project-stage-page]
4
+
5
+ The OpenID Connect middleware allows you to configure a [metro client](https://github.com/muze-nl/metro) to handle authorization and authentication using OpenID Connect:
6
+
7
+ ```javascript
8
+ import oidcmw from '@muze-nl/metro-oidc'
9
+
10
+ const client = metro.client('https://oauth2api.example.com')
11
+ .with( oidcmw({
12
+ client_info: {
13
+ client_name: 'My Client',
14
+ redirect_uris: [
15
+ 'https://www.example.com/my_app.html'
16
+ ]
17
+ },
18
+ issuer: 'https://solidcommunity.net/'
19
+ }) )
20
+
21
+ async function fetchMovies() {
22
+ return await client.get('https://example.solidcommunity.net/movies/')
23
+ }
24
+ ````
25
+
26
+ The OIDC middleware will automatically discover the configuration of the issuer, as well as do a dynamic client registration, if you haven't set a `client_info.client_id`.
27
+ It will then configure the correct OAuth2 settings and handle the request with [metro oauth2](https://github.com/muze-nl/metro-oauth2) middleware. It may redirect the browser to let the user login with the OIDC issuer. You can skip the automatic configuration step, if you provide the `openid_configuration` parameter set yourself. If you don't, the oidcmw middleware will only run the discovery process once, and store the information in localStorage. The same with the client_info and dynamic registration.
28
+
29
+ ## Security features
30
+
31
+ metro.oidc uses OAuth2.1 by default, including PKCE and DPoP. The Keypair used in DPoP is created non-extractable, so they cannot be leaked. This means that the access_token and refresh_token, even if leaked, cannot be used anywhere else.
32
+
33
+ You can disable PCKE by setting `options.client_info.code_verifier` to false.
34
+ You can disable DPoP by setting `options.use_dpop` to false.
35
+
36
+ ## id_token
37
+
38
+ The metro oidc middleware doesn't use the id_token, or verify any of its contents. This is left up to you, if you need it. You can retrieve the id_token by calling the `idToken` function, after the user is logged in:
39
+
40
+ ```javascript
41
+ import oidc from '@muze-nl/metro-oidc'
42
+
43
+ let id_token = oidc.idToken()
44
+ ```
45
+
46
+ [project-stage-badge: Experimental]: https://img.shields.io/badge/Project%20Stage-Experimental-yellow.svg
47
+ [project-stage-page]: https://blog.pother.ca/project-stages/
package/dist/browser.js CHANGED
@@ -591,19 +591,21 @@
591
591
  thrower
592
592
  }
593
593
  });
594
- globalThis.metro = metro;
594
+ if (!globalThis.metro) {
595
+ globalThis.metro = metro;
596
+ }
595
597
  var everything_default = metro;
596
598
 
597
599
  // node_modules/@muze-nl/metro-oauth2/src/oauth2.mjs
598
600
  var oauth2_exports = {};
599
601
  __export(oauth2_exports, {
600
- authorizePopup: () => authorizePopup,
601
602
  base64url_encode: () => base64url_encode,
602
603
  createState: () => createState,
603
604
  default: () => oauth2mw,
604
605
  generateCodeChallenge: () => generateCodeChallenge,
605
606
  generateCodeVerifier: () => generateCodeVerifier,
606
607
  getExpires: () => getExpires,
608
+ isAuthorized: () => isAuthorized,
607
609
  isExpired: () => isExpired,
608
610
  isRedirected: () => isRedirected
609
611
  });
@@ -630,15 +632,15 @@
630
632
  return false;
631
633
  }
632
634
  }
633
- return error("data does not match oneOf patterns", data, patterns);
635
+ return error2("data does not match oneOf patterns", data, patterns);
634
636
  };
635
637
  var anyOf = (...patterns) => (data) => {
636
638
  if (!Array.isArray(data)) {
637
- return error("data is not an array", data, "anyOf");
639
+ return error2("data is not an array", data, "anyOf");
638
640
  }
639
641
  for (let value of data) {
640
642
  if (oneOf(...patterns)(value)) {
641
- return error("data does not match anyOf patterns", value, patterns);
643
+ return error2("data does not match anyOf patterns", value, patterns);
642
644
  }
643
645
  }
644
646
  return false;
@@ -650,10 +652,10 @@
650
652
  }
651
653
  let url2 = new URL(data);
652
654
  if (url2.href != data) {
653
- return error("data is not a valid url", data, "validURL");
655
+ return error2("data is not a valid url", data, "validURL");
654
656
  }
655
657
  } catch (e) {
656
- return error("data is not a valid url", data, "validURL");
658
+ return error2("data is not a valid url", data, "validURL");
657
659
  }
658
660
  return false;
659
661
  }
@@ -664,28 +666,28 @@
664
666
  let problems = [];
665
667
  if (pattern === Boolean) {
666
668
  if (typeof data != "boolean") {
667
- problems.push(error("data is not a boolean", data, pattern));
669
+ problems.push(error2("data is not a boolean", data, pattern));
668
670
  }
669
671
  } else if (pattern === Number) {
670
672
  if (typeof data != "number") {
671
- problems.push(error("data is not a number", data, pattern));
673
+ problems.push(error2("data is not a number", data, pattern));
672
674
  }
673
675
  } else if (pattern instanceof RegExp) {
674
676
  if (Array.isArray(data)) {
675
677
  let index = data.findIndex((element) => fails(element, pattern, root));
676
678
  if (index > -1) {
677
- problems.push(error("data[" + index + "] does not match pattern", data[index], pattern));
679
+ problems.push(error2("data[" + index + "] does not match pattern", data[index], pattern));
678
680
  }
679
681
  } else if (!pattern.test(data)) {
680
- problems.push(error("data does not match pattern", data, pattern));
682
+ problems.push(error2("data does not match pattern", data, pattern));
681
683
  }
682
684
  } else if (pattern instanceof Function) {
683
685
  if (pattern(data, root)) {
684
- problems.push(error("data does not match function", data, pattern));
686
+ problems.push(error2("data does not match function", data, pattern));
685
687
  }
686
688
  } else if (Array.isArray(pattern)) {
687
689
  if (!Array.isArray(data)) {
688
- problems.push(error("data is not an array", data, []));
690
+ problems.push(error2("data is not an array", data, []));
689
691
  }
690
692
  for (p of pattern) {
691
693
  let problem = fails(data, p, root);
@@ -699,10 +701,10 @@
699
701
  if (Array.isArray(data)) {
700
702
  let index = data.findIndex((element) => fails(element, pattern, root));
701
703
  if (index > -1) {
702
- problems.push(error("data[" + index + "] does not match pattern", data[index], pattern));
704
+ problems.push(error2("data[" + index + "] does not match pattern", data[index], pattern));
703
705
  }
704
706
  } else if (!data || typeof data != "object") {
705
- problems.push(error("data is not an object, pattern is", data, pattern));
707
+ problems.push(error2("data is not an object, pattern is", data, pattern));
706
708
  } else {
707
709
  if (data instanceof URLSearchParams) {
708
710
  data = Object.fromEntries(data);
@@ -713,7 +715,7 @@
713
715
  if (result) {
714
716
  if (!p2 || typeof p2 == "string") {
715
717
  p2 = {};
716
- problems.push(error(p2, data[wKey], wVal));
718
+ problems.push(error2(p2, data[wKey], wVal));
717
719
  }
718
720
  p2[wKey] = result.problems;
719
721
  }
@@ -721,7 +723,7 @@
721
723
  }
722
724
  } else {
723
725
  if (pattern != data) {
724
- problems.push(error("data and pattern are not equal", data, pattern));
726
+ problems.push(error2("data and pattern are not equal", data, pattern));
725
727
  }
726
728
  }
727
729
  if (problems.length) {
@@ -736,7 +738,7 @@
736
738
  this.details = details;
737
739
  }
738
740
  };
739
- function error(message, found, expected) {
741
+ function error2(message, found, expected) {
740
742
  return {
741
743
  message,
742
744
  found,
@@ -1106,13 +1108,19 @@
1106
1108
  }
1107
1109
  return true;
1108
1110
  }
1109
- async function authorizePopup(authorizationCodeURL) {
1110
- return new Promise((resolve, reject) => {
1111
- addEventListener("oauth2authorized", (evt) => {
1112
- resolve(event.authorization_code);
1113
- }, { once: true });
1114
- window.open(authorizationCodeURL);
1115
- });
1111
+ function isAuthorized(tokens) {
1112
+ if (typeof tokens == "string") {
1113
+ tokens = tokenStore(tokens).tokens;
1114
+ }
1115
+ let accessToken = tokens.get("access_token");
1116
+ if (accessToken && !isExpired(accessToken)) {
1117
+ return true;
1118
+ }
1119
+ let refreshToken = tokens.get("refresh_token");
1120
+ if (refreshToken) {
1121
+ return true;
1122
+ }
1123
+ return false;
1116
1124
  }
1117
1125
 
1118
1126
  // node_modules/@muze-nl/metro-oauth2/src/oauth2.mockserver.mjs
@@ -1127,7 +1135,7 @@
1127
1135
  "Content-Type": "application/json"
1128
1136
  }
1129
1137
  };
1130
- var badRequest = (error4) => {
1138
+ var badRequest = (error5) => {
1131
1139
  return {
1132
1140
  status: 400,
1133
1141
  statusText: "Bad Request",
@@ -1136,11 +1144,11 @@
1136
1144
  },
1137
1145
  body: JSON.stringify({
1138
1146
  error: "invalid_request",
1139
- error_description: error4
1147
+ error_description: error5
1140
1148
  })
1141
1149
  };
1142
1150
  };
1143
- var error2;
1151
+ var error3;
1144
1152
  var pkce = {};
1145
1153
  function oauth2mockserver(options = {}) {
1146
1154
  const defaultOptions = {
@@ -1152,12 +1160,12 @@
1152
1160
  let url2 = everything_default.url(req.url);
1153
1161
  switch (url2.pathname) {
1154
1162
  case "/authorize/":
1155
- if (error2 = fails(url2.searchParams, {
1163
+ if (error3 = fails(url2.searchParams, {
1156
1164
  response_type: "code",
1157
1165
  client_id: "mockClientId",
1158
1166
  state: Optional(/.*/)
1159
1167
  })) {
1160
- return everything_default.response(badRequest(error2));
1168
+ return everything_default.response(badRequest(error3));
1161
1169
  }
1162
1170
  if (url2.searchParams.has("code_challenge")) {
1163
1171
  if (!url2.searchParams.has("code_challenge_method")) {
@@ -1179,17 +1187,17 @@
1179
1187
  req.data.forEach((value, key) => body[key] = value);
1180
1188
  req = req.with({ body });
1181
1189
  }
1182
- if (error2 = fails(req, {
1190
+ if (error3 = fails(req, {
1183
1191
  method: "POST",
1184
1192
  data: {
1185
1193
  grant_type: oneOf("refresh_token", "authorization_code")
1186
1194
  }
1187
1195
  })) {
1188
- return everything_default.response(badRequest(error2));
1196
+ return everything_default.response(badRequest(error3));
1189
1197
  }
1190
1198
  switch (req.data.grant_type) {
1191
1199
  case "refresh_token":
1192
- if (error2 = fails(req.data, oneOf({
1200
+ if (error3 = fails(req.data, oneOf({
1193
1201
  refresh_token: "mockRefreshToken",
1194
1202
  client_id: "mockClientId",
1195
1203
  client_secret: "mockClientSecret"
@@ -1198,11 +1206,11 @@
1198
1206
  client_id: "mockClientId",
1199
1207
  code_verifier: /.+/
1200
1208
  }))) {
1201
- return everything_default.response(badRequest(error2));
1209
+ return everything_default.response(badRequest(error3));
1202
1210
  }
1203
1211
  break;
1204
1212
  case "access_token":
1205
- if (error2 = fails(req.data, oneOf({
1213
+ if (error3 = fails(req.data, oneOf({
1206
1214
  client_id: "mockClientId",
1207
1215
  client_secret: "mockClientSecret"
1208
1216
  }, {
@@ -1211,7 +1219,7 @@
1211
1219
  //FIXME: check that this matches code_verifier
1212
1220
  code_challenge_method: "S256"
1213
1221
  }))) {
1214
- return everything_default.response(badRequest(error2));
1222
+ return everything_default.response(badRequest(error3));
1215
1223
  }
1216
1224
  break;
1217
1225
  }
@@ -1327,6 +1335,43 @@
1327
1335
  throw everything_default.metroError("metro.oidcmw: Error while fetching " + issuer + ".wellknown/oauth_authorization_server", res);
1328
1336
  }
1329
1337
 
1338
+ // node_modules/@muze-nl/metro-oauth2/src/oauth2.popup.mjs
1339
+ function handleRedirect() {
1340
+ let params2 = new URLSearchParams(window.location.search);
1341
+ if (!params2.has("code") && window.location.hash) {
1342
+ let query = window.location.hash.substr(1);
1343
+ params2 = new URLSearchParams("?" + query);
1344
+ }
1345
+ let parent = window.parent ? window.parent : window.opener;
1346
+ if (params2.has("code")) {
1347
+ parent.postMessage({
1348
+ authorization_code: params2.get("code")
1349
+ }, window.location.origin);
1350
+ } else if (params2.has("error")) {
1351
+ parent.postMessage({
1352
+ error
1353
+ }, window.location.origin);
1354
+ } else {
1355
+ parent.postMessage({
1356
+ error: "Could not find an authorization_code"
1357
+ }, window.location.origin);
1358
+ }
1359
+ }
1360
+ function authorizePopup(authorizationCodeURL) {
1361
+ return new Promise((resolve, reject) => {
1362
+ addEventListener("message", (evt) => {
1363
+ if (event.data.authorization_code) {
1364
+ resolve(event.data.authorization_code);
1365
+ } else if (event.data.error) {
1366
+ reject(event.data.error);
1367
+ } else {
1368
+ reject("Unknown authorization error");
1369
+ }
1370
+ }, { once: true });
1371
+ window.open(authorizationCodeURL);
1372
+ });
1373
+ }
1374
+
1330
1375
  // node_modules/@muze-nl/metro-oauth2/src/keysstore.mjs
1331
1376
  function keysStore() {
1332
1377
  return new Promise((resolve, reject) => {
@@ -1636,7 +1681,9 @@
1636
1681
  discover: oauth2_discovery_exports,
1637
1682
  tokenstore: tokenStore,
1638
1683
  dpopmw,
1639
- keysstore: keysStore
1684
+ keysstore: keysStore,
1685
+ authorizePopup,
1686
+ popupHandleRedirect: handleRedirect
1640
1687
  });
1641
1688
  if (!globalThis.metro.oauth2) {
1642
1689
  globalThis.metro.oauth2 = oauth2;
@@ -1665,7 +1712,7 @@
1665
1712
  function Required2(pattern) {
1666
1713
  return function _Required(data, root, path) {
1667
1714
  if (data == null || typeof data == "undefined") {
1668
- return error3("data is required", data, pattern || "any value", path);
1715
+ return error4("data is required", data, pattern || "any value", path);
1669
1716
  } else if (typeof pattern != "undefined") {
1670
1717
  return fails2(data, pattern, root, path);
1671
1718
  } else {
@@ -1690,17 +1737,17 @@
1690
1737
  return false;
1691
1738
  }
1692
1739
  }
1693
- return error3("data does not match oneOf patterns", data, patterns, path);
1740
+ return error4("data does not match oneOf patterns", data, patterns, path);
1694
1741
  };
1695
1742
  }
1696
1743
  function anyOf2(...patterns) {
1697
1744
  return function _anyOf(data, root, path) {
1698
1745
  if (!Array.isArray(data)) {
1699
- return error3("data is not an array", data, "anyOf", path);
1746
+ return error4("data is not an array", data, "anyOf", path);
1700
1747
  }
1701
1748
  for (let value of data) {
1702
1749
  if (oneOf2(...patterns)(value)) {
1703
- return error3("data does not match anyOf patterns", value, patterns, path);
1750
+ return error4("data does not match anyOf patterns", value, patterns, path);
1704
1751
  }
1705
1752
  }
1706
1753
  return false;
@@ -1714,7 +1761,7 @@
1714
1761
  }
1715
1762
  problems = problems.filter(Boolean);
1716
1763
  if (problems.length) {
1717
- return error3("data does not match all given patterns", data, patterns, path, problems);
1764
+ return error4("data does not match all given patterns", data, patterns, path, problems);
1718
1765
  }
1719
1766
  };
1720
1767
  }
@@ -1726,29 +1773,29 @@
1726
1773
  let url2 = new URL(data);
1727
1774
  if (url2.href != data) {
1728
1775
  if (!(url2.href + "/" == data || url2.href == data + "/")) {
1729
- return error3("data is not a valid url", data, "validURL", path);
1776
+ return error4("data is not a valid url", data, "validURL", path);
1730
1777
  }
1731
1778
  }
1732
1779
  } catch (e) {
1733
- return error3("data is not a valid url", data, "validURL", path);
1780
+ return error4("data is not a valid url", data, "validURL", path);
1734
1781
  }
1735
1782
  }
1736
1783
  function validEmail(data, root, path) {
1737
1784
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data)) {
1738
- return error3("data is not a valid email", data, "validEmail", path);
1785
+ return error4("data is not a valid email", data, "validEmail", path);
1739
1786
  }
1740
1787
  }
1741
1788
  function instanceOf(constructor) {
1742
1789
  return function _instanceOf(data, root, path) {
1743
1790
  if (!(data instanceof constructor)) {
1744
- return error3("data is not an instanceof pattern", data, constructor, path);
1791
+ return error4("data is not an instanceof pattern", data, constructor, path);
1745
1792
  }
1746
1793
  };
1747
1794
  }
1748
1795
  function not(pattern) {
1749
1796
  return function _not(data, root, path) {
1750
1797
  if (!fails2(data, pattern, root, path)) {
1751
- return error3("data matches pattern, when required not to", data, pattern, path);
1798
+ return error4("data matches pattern, when required not to", data, pattern, path);
1752
1799
  }
1753
1800
  };
1754
1801
  }
@@ -1759,29 +1806,29 @@
1759
1806
  let problems = [];
1760
1807
  if (pattern === Boolean) {
1761
1808
  if (typeof data != "boolean" && !(data instanceof Boolean)) {
1762
- problems.push(error3("data is not a boolean", data, pattern, path));
1809
+ problems.push(error4("data is not a boolean", data, pattern, path));
1763
1810
  }
1764
1811
  } else if (pattern === Number) {
1765
1812
  if (typeof data != "number" && !(data instanceof Number)) {
1766
- problems.push(error3("data is not a number", data, pattern, path));
1813
+ problems.push(error4("data is not a number", data, pattern, path));
1767
1814
  }
1768
1815
  } else if (pattern === String) {
1769
1816
  if (typeof data != "string" && !(data instanceof String)) {
1770
- problems.push(error3("data is not a string", data, pattern, path));
1817
+ problems.push(error4("data is not a string", data, pattern, path));
1771
1818
  }
1772
1819
  if (data == "") {
1773
- problems.push(error3("data is an empty string, which is not allowed", data, pattern, path));
1820
+ problems.push(error4("data is an empty string, which is not allowed", data, pattern, path));
1774
1821
  }
1775
1822
  } else if (pattern instanceof RegExp) {
1776
1823
  if (Array.isArray(data)) {
1777
1824
  let index = data.findIndex((element, index2) => fails2(element, pattern, root, path + "[" + index2 + "]"));
1778
1825
  if (index > -1) {
1779
- problems.push(error3("data[" + index + "] does not match pattern", data[index], pattern, path + "[" + index + "]"));
1826
+ problems.push(error4("data[" + index + "] does not match pattern", data[index], pattern, path + "[" + index + "]"));
1780
1827
  }
1781
1828
  } else if (typeof data == "undefined") {
1782
- problems.push(error3("data is undefined, should match pattern", data, pattern, path));
1829
+ problems.push(error4("data is undefined, should match pattern", data, pattern, path));
1783
1830
  } else if (!pattern.test(data)) {
1784
- problems.push(error3("data does not match pattern", data, pattern, path));
1831
+ problems.push(error4("data does not match pattern", data, pattern, path));
1785
1832
  }
1786
1833
  } else if (pattern instanceof Function) {
1787
1834
  let problem = pattern(data, root, path);
@@ -1794,7 +1841,7 @@
1794
1841
  }
1795
1842
  } else if (Array.isArray(pattern)) {
1796
1843
  if (!Array.isArray(data)) {
1797
- problems.push(error3("data is not an array", data, [], path));
1844
+ problems.push(error4("data is not an array", data, [], path));
1798
1845
  }
1799
1846
  for (let p2 of pattern) {
1800
1847
  for (let index of data.keys()) {
@@ -1810,10 +1857,10 @@
1810
1857
  if (Array.isArray(data)) {
1811
1858
  let index = data.findIndex((element, index2) => fails2(element, pattern, root, path + "[" + index2 + "]"));
1812
1859
  if (index > -1) {
1813
- problems.push(error3("data[" + index + "] does not match pattern", data[index], pattern, path + "[" + index + "]"));
1860
+ problems.push(error4("data[" + index + "] does not match pattern", data[index], pattern, path + "[" + index + "]"));
1814
1861
  }
1815
1862
  } else if (!data || typeof data != "object") {
1816
- problems.push(error3("data is not an object, pattern is", data, pattern, path));
1863
+ problems.push(error4("data is not an object, pattern is", data, pattern, path));
1817
1864
  } else {
1818
1865
  if (data instanceof URLSearchParams) {
1819
1866
  data = Object.fromEntries(data);
@@ -1834,7 +1881,7 @@
1834
1881
  }
1835
1882
  } else {
1836
1883
  if (pattern != data) {
1837
- problems.push(error3("data and pattern are not equal", data, pattern, path));
1884
+ problems.push(error4("data and pattern are not equal", data, pattern, path));
1838
1885
  }
1839
1886
  }
1840
1887
  if (problems.length) {
@@ -1842,7 +1889,7 @@
1842
1889
  }
1843
1890
  return false;
1844
1891
  }
1845
- function error3(message, found, expected, path, problems) {
1892
+ function error4(message, found, expected, path, problems) {
1846
1893
  let result = {
1847
1894
  message,
1848
1895
  found,
@@ -1860,13 +1907,13 @@
1860
1907
  if (options.filter((o) => root.hasOwnKey(o)).length > 0) {
1861
1908
  return false;
1862
1909
  }
1863
- return error3("root data must have all of", root, options);
1910
+ return error4("root data must have all of", root, options);
1864
1911
  };
1865
1912
  var MustInclude = (...options) => (value) => {
1866
1913
  if (Array.isArray(value) && options.filter((o) => !value.includes(o)).length == 0) {
1867
1914
  return false;
1868
1915
  } else {
1869
- return error3("data must be an array which includes", value, options);
1916
+ return error4("data must be an array which includes", value, options);
1870
1917
  }
1871
1918
  };
1872
1919
  var validJWA = [