@tidecloak/verify 0.13.28 → 0.13.31

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A lightweight utility for server‑side verification of TideCloak‑issued JSON Web Tokens (JWTs).
4
4
 
5
- This package exports a single function, `verifyTideCloakToken`, which you can use in your Next.js API routes, Node.js servers, or any backend to verify the authenticity, issuer, audience, and roles of a JWT issued by your TideCloak realm.
5
+ This package exports a single function, `verifyTideCloakToken`, which you can use in your Next.js API routes, Node.js servers, or any backend to verify the signature, issuer, authorized party (`azp`), and roles of a JWT issued by your TideCloak realm.
6
6
 
7
7
  ---
8
8
 
@@ -47,11 +47,23 @@ Internally, `verifyTideCloakToken` uses the [jose](https://github.com/panva/jose
47
47
  1. Ensure a token is present.
48
48
  2. Construct the correct issuer URL from `config['auth-server-url']` and `config.realm`.
49
49
  3. Choose between a local JWK Set (`config.jwk.keys`) or fetch the JWK Set remotely from Tidecloak.
50
- 4. Verify the token's signature, issuer, and `azp` (authorized party) against `config.resource`.
51
- 5. Extract realm (`payload.realm_access.roles`) and client (`payload.resource_access[resource].roles`) roles.
52
- 6. Check for at least one matching role if `allowedRoles` is specified.
50
+ 4. Verify the token's signature against a **pinned algorithm allowlist** (`ES256`, `ES384`, `ES512`, `EdDSA` by default), the `issuer`, and the standard time claims (`exp`/`nbf`) with a small `clockTolerance`.
51
+ 5. Verify the `azp` (authorized party) against `config.resource` — **only when `resource` is configured**.
52
+ 6. Extract realm (`payload.realm_access.roles`) and client (`payload.resource_access[resource].roles`) roles.
53
+ 7. Check for at least one matching role if `allowedRoles` is specified.
53
54
 
54
- On any failure, it logs an error to the console and returns `null`.
55
+ On any failure, it logs the error message to the console and returns `null`.
56
+
57
+ > **Note:** the algorithm allowlist closes algorithm-confusion attacks — without it, `jose` would accept any algorithm a key in the set can validate. A `null` result collapses both invalid tokens and infrastructure failures (e.g. an unreachable remote JWKS endpoint), so treat `null` as "not authorized" and monitor your JWKS reachability separately.
58
+
59
+ ### Optional config fields
60
+
61
+ You can tune verification by adding these optional fields to the `config` object:
62
+
63
+ | Field | Type | Default | Description |
64
+ | --------------------------- | ---------------------- | ---------------------------------------- | --------------------------------------------------------------------------- |
65
+ | `tokenSignatureAlgorithms` | `string[]` | `['ES256','ES384','ES512','EdDSA']` | Allowed JWS signature algorithms. Override if your realm signs with others (e.g. `['RS256']`). |
66
+ | `clockTolerance` | `number` \| `string` | `'5s'` | Allowed clock skew between issuer and verifier (seconds, or a jose duration string). |
55
67
 
56
68
  ---
57
69
 
@@ -172,10 +184,14 @@ export async function GET(req: NextRequest) {
172
184
  interface TidecloakConfig {
173
185
  realm: string;
174
186
  'auth-server-url': string;
175
- resource: string;
187
+ resource?: string;
176
188
  publicClient?: boolean;
177
189
  confidentialPort?: number;
178
- jwk?: { keys: Array<{ kid: string; kty: string; alg: string; use: string; x: string; crv?: string }> };
190
+ jwk?: { keys: Array<{ kid: string; kty: string; alg?: string; use?: string; x?: string; crv?: string; n?: string; e?: string }> };
191
+ /** Allowed JWS signature algorithms. Default: ['ES256','ES384','ES512','EdDSA']. */
192
+ tokenSignatureAlgorithms?: string[];
193
+ /** Allowed clock skew (seconds or a jose duration string). Default: '5s'. */
194
+ clockTolerance?: number | string;
179
195
  [key: string]: unknown;
180
196
  }
181
197
 
@@ -1,14 +1,26 @@
1
- import { jwtVerify, createLocalJWKSet, createRemoteJWKSet } from "jose";
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyTideCloakToken = verifyTideCloakToken;
4
+ const jose_1 = require("jose");
5
+ /**
6
+ * Signature algorithms TideCloak realms issue tokens with. Pinning these prevents
7
+ * algorithm-confusion attacks: without an explicit allowlist `jose` accepts any
8
+ * algorithm a key in the set can validate (and falls back to permissive behaviour
9
+ * for keys that omit an `alg`). Override via `config.tokenSignatureAlgorithms`.
10
+ */
11
+ const DEFAULT_ALLOWED_ALGORITHMS = ["ES256", "ES384", "ES512", "EdDSA"];
2
12
  /**
3
13
  * Verify a TideCloak-issued JWT on the server side using your imported config object.
4
14
  *
5
- * @param {object} config - Imported TideCloak configuration (parsed JSON).
15
+ * @param {object} config - Imported TideCloak configuration (parsed JSON). May also
16
+ * carry optional `tokenSignatureAlgorithms` (string[]) and `clockTolerance`
17
+ * (number of seconds, or a jose duration string) to tune verification.
6
18
  * @param {string} token - access token to verify.
7
19
  * @param {string[]} [allowedRoles] - Array of Tidecloak realm or client roles; user must have at least one.
8
20
  * @returns {Promise<object|null>} - The token payload if valid and role-check passes, otherwise null.
9
21
  */
10
- export async function verifyTideCloakToken(config, token, allowedRoles = []) {
11
- var _a, _b, _c;
22
+ async function verifyTideCloakToken(config, token, allowedRoles = []) {
23
+ var _a, _b, _c, _d;
12
24
  try {
13
25
  // Ensure token is provided
14
26
  if (!token) {
@@ -24,18 +36,27 @@ export async function verifyTideCloakToken(config, token, allowedRoles = []) {
24
36
  const issuer = `${baseUrl}${sep}realms/${config.realm}`;
25
37
  // Determine JWK set (use local JWKs if provided, otherwise fetch remotely)
26
38
  const jwkSet = config.jwk
27
- ? createLocalJWKSet(config.jwk)
28
- : createRemoteJWKSet(new URL(`${issuer}/protocol/openid-connect/certs`));
29
- // Verify token signature and issuer
30
- const { payload } = await jwtVerify(token, jwkSet, { issuer });
31
- // Verify authorized party (client)
39
+ ? (0, jose_1.createLocalJWKSet)(config.jwk)
40
+ : (0, jose_1.createRemoteJWKSet)(new URL(`${issuer}/protocol/openid-connect/certs`));
41
+ // Verify signature (with a pinned algorithm allowlist), issuer and time claims.
42
+ // `clockTolerance` allows a small amount of clock drift between issuer and verifier.
43
+ const algorithms = Array.isArray(config.tokenSignatureAlgorithms) && config.tokenSignatureAlgorithms.length > 0
44
+ ? config.tokenSignatureAlgorithms
45
+ : DEFAULT_ALLOWED_ALGORITHMS;
46
+ const { payload } = await (0, jose_1.jwtVerify)(token, jwkSet, {
47
+ issuer,
48
+ algorithms,
49
+ clockTolerance: (_a = config.clockTolerance) !== null && _a !== void 0 ? _a : "5s",
50
+ });
51
+ // Verify authorized party (client). Only enforced when a `resource` (client id)
52
+ // is configured; without this guard an undefined client would reject every token.
32
53
  const client = config["resource"];
33
- if (payload.azp !== client) {
34
- throw new Error(`AZP mismatch: expected '${config.resource}', got '${payload.azp}'`);
54
+ if (client !== undefined && client !== null && payload.azp !== client) {
55
+ throw new Error(`AZP mismatch: expected '${client}', got '${payload.azp}'`);
35
56
  }
36
57
  // Gather all user roles from realm and client roles for the specified resource from the config.
37
- const realmRoles = ((_a = payload.realm_access) === null || _a === void 0 ? void 0 : _a.roles) || [];
38
- const clientRoles = ((_c = (_b = payload.resource_access) === null || _b === void 0 ? void 0 : _b[client]) === null || _c === void 0 ? void 0 : _c.roles) || [];
58
+ const realmRoles = ((_b = payload.realm_access) === null || _b === void 0 ? void 0 : _b.roles) || [];
59
+ const clientRoles = ((_d = (_c = payload.resource_access) === null || _c === void 0 ? void 0 : _c[client]) === null || _d === void 0 ? void 0 : _d.roles) || [];
39
60
  const allRoles = new Set([...realmRoles, ...clientRoles]);
40
61
  // If allowedRoles specified, ensure at least one match
41
62
  if (allowedRoles.length > 0) {
@@ -47,7 +68,11 @@ export async function verifyTideCloakToken(config, token, allowedRoles = []) {
47
68
  return payload;
48
69
  }
49
70
  catch (err) {
50
- console.error("[TideJWT] Token verification failed:", err);
71
+ // Log only the message (not the error object, which can echo token-derived
72
+ // data). Note: this collapses both invalid tokens and infrastructure failures
73
+ // (e.g. an unreachable JWKS endpoint) into a `null` result, so callers cannot
74
+ // distinguish "forbidden" from "auth backend down".
75
+ console.error("[TideJWT] Token verification failed:", err instanceof Error ? err.message : err);
51
76
  return null;
52
77
  }
53
78
  }
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -1,14 +1,23 @@
1
1
  import { jwtVerify, createLocalJWKSet, createRemoteJWKSet } from "jose";
2
+ /**
3
+ * Signature algorithms TideCloak realms issue tokens with. Pinning these prevents
4
+ * algorithm-confusion attacks: without an explicit allowlist `jose` accepts any
5
+ * algorithm a key in the set can validate (and falls back to permissive behaviour
6
+ * for keys that omit an `alg`). Override via `config.tokenSignatureAlgorithms`.
7
+ */
8
+ const DEFAULT_ALLOWED_ALGORITHMS = ["ES256", "ES384", "ES512", "EdDSA"];
2
9
  /**
3
10
  * Verify a TideCloak-issued JWT on the server side using your imported config object.
4
11
  *
5
- * @param {object} config - Imported TideCloak configuration (parsed JSON).
12
+ * @param {object} config - Imported TideCloak configuration (parsed JSON). May also
13
+ * carry optional `tokenSignatureAlgorithms` (string[]) and `clockTolerance`
14
+ * (number of seconds, or a jose duration string) to tune verification.
6
15
  * @param {string} token - access token to verify.
7
16
  * @param {string[]} [allowedRoles] - Array of Tidecloak realm or client roles; user must have at least one.
8
17
  * @returns {Promise<object|null>} - The token payload if valid and role-check passes, otherwise null.
9
18
  */
10
19
  export async function verifyTideCloakToken(config, token, allowedRoles = []) {
11
- var _a, _b, _c;
20
+ var _a, _b, _c, _d;
12
21
  try {
13
22
  // Ensure token is provided
14
23
  if (!token) {
@@ -26,16 +35,25 @@ export async function verifyTideCloakToken(config, token, allowedRoles = []) {
26
35
  const jwkSet = config.jwk
27
36
  ? createLocalJWKSet(config.jwk)
28
37
  : createRemoteJWKSet(new URL(`${issuer}/protocol/openid-connect/certs`));
29
- // Verify token signature and issuer
30
- const { payload } = await jwtVerify(token, jwkSet, { issuer });
31
- // Verify authorized party (client)
38
+ // Verify signature (with a pinned algorithm allowlist), issuer and time claims.
39
+ // `clockTolerance` allows a small amount of clock drift between issuer and verifier.
40
+ const algorithms = Array.isArray(config.tokenSignatureAlgorithms) && config.tokenSignatureAlgorithms.length > 0
41
+ ? config.tokenSignatureAlgorithms
42
+ : DEFAULT_ALLOWED_ALGORITHMS;
43
+ const { payload } = await jwtVerify(token, jwkSet, {
44
+ issuer,
45
+ algorithms,
46
+ clockTolerance: (_a = config.clockTolerance) !== null && _a !== void 0 ? _a : "5s",
47
+ });
48
+ // Verify authorized party (client). Only enforced when a `resource` (client id)
49
+ // is configured; without this guard an undefined client would reject every token.
32
50
  const client = config["resource"];
33
- if (payload.azp !== client) {
34
- throw new Error(`AZP mismatch: expected '${config.resource}', got '${payload.azp}'`);
51
+ if (client !== undefined && client !== null && payload.azp !== client) {
52
+ throw new Error(`AZP mismatch: expected '${client}', got '${payload.azp}'`);
35
53
  }
36
54
  // Gather all user roles from realm and client roles for the specified resource from the config.
37
- const realmRoles = ((_a = payload.realm_access) === null || _a === void 0 ? void 0 : _a.roles) || [];
38
- const clientRoles = ((_c = (_b = payload.resource_access) === null || _b === void 0 ? void 0 : _b[client]) === null || _c === void 0 ? void 0 : _c.roles) || [];
55
+ const realmRoles = ((_b = payload.realm_access) === null || _b === void 0 ? void 0 : _b.roles) || [];
56
+ const clientRoles = ((_d = (_c = payload.resource_access) === null || _c === void 0 ? void 0 : _c[client]) === null || _d === void 0 ? void 0 : _d.roles) || [];
39
57
  const allRoles = new Set([...realmRoles, ...clientRoles]);
40
58
  // If allowedRoles specified, ensure at least one match
41
59
  if (allowedRoles.length > 0) {
@@ -47,7 +65,11 @@ export async function verifyTideCloakToken(config, token, allowedRoles = []) {
47
65
  return payload;
48
66
  }
49
67
  catch (err) {
50
- console.error("[TideJWT] Token verification failed:", err);
68
+ // Log only the message (not the error object, which can echo token-derived
69
+ // data). Note: this collapses both invalid tokens and infrastructure failures
70
+ // (e.g. an unreachable JWKS endpoint) into a `null` result, so callers cannot
71
+ // distinguish "forbidden" from "auth backend down".
72
+ console.error("[TideJWT] Token verification failed:", err instanceof Error ? err.message : err);
51
73
  return null;
52
74
  }
53
75
  }
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Verify a TideCloak-issued JWT on the server side using your imported config object.
3
3
  *
4
- * @param {object} config - Imported TideCloak configuration (parsed JSON).
4
+ * @param {object} config - Imported TideCloak configuration (parsed JSON). May also
5
+ * carry optional `tokenSignatureAlgorithms` (string[]) and `clockTolerance`
6
+ * (number of seconds, or a jose duration string) to tune verification.
5
7
  * @param {string} token - access token to verify.
6
8
  * @param {string[]} [allowedRoles] - Array of Tidecloak realm or client roles; user must have at least one.
7
9
  * @returns {Promise<object|null>} - The token payload if valid and role-check passes, otherwise null.
@@ -1 +1 @@
1
- {"version":3,"file":"TideJWT.d.ts","sourceRoot":"","sources":["../../src/TideJWT.js"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,6CALW,MAAM,SACN,MAAM,iBACN,MAAM,EAAE,GACN,OAAO,CAAC,MAAM,GAAC,IAAI,CAAC,CAsDhC"}
1
+ {"version":3,"file":"TideJWT.d.ts","sourceRoot":"","sources":["../../src/TideJWT.js"],"names":[],"mappings":"AAUA;;;;;;;;;GASG;AACH,6CAPW,MAAM,SAGN,MAAM,iBACN,MAAM,EAAE,GACN,OAAO,CAAC,MAAM,GAAC,IAAI,CAAC,CAmEhC"}
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@tidecloak/verify",
3
- "version": "0.13.28",
3
+ "version": "0.13.31",
4
4
  "description": "A lightweight utility for server-side verification of TideCloak-issued JSON Web Tokens (JWTs).",
5
+ "main": "./dist/cjs/TideJWT.js",
6
+ "module": "./dist/esm/TideJWT.js",
7
+ "types": "./dist/types/TideJWT.d.ts",
5
8
  "exports": {
6
9
  ".": {
7
- "import": "./dist/esm/TideJWT.js",
8
10
  "types": "./dist/types/TideJWT.d.ts",
9
- "require": "./dist/cjs/TideJWT.js"
11
+ "import": "./dist/esm/TideJWT.js",
12
+ "require": "./dist/cjs/TideJWT.js",
13
+ "default": "./dist/esm/TideJWT.js"
10
14
  }
11
15
  },
12
16
  "files": [
@@ -15,7 +19,8 @@
15
19
  "scripts": {
16
20
  "build:cjs": "tsc -p tsconfig.cjs.json",
17
21
  "build:esm": "tsc -p tsconfig.esm.json",
18
- "build": "npm run build:cjs && npm run build:esm",
22
+ "build": "npm run build:cjs && npm run build:esm && node scripts/postbuild.cjs",
23
+ "test": "node --test test/*.test.mjs",
19
24
  "prepare": "npm run build"
20
25
  },
21
26
  "publishConfig": {