@insureco/bio 0.1.0 → 0.2.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/dist/index.d.mts CHANGED
@@ -107,13 +107,22 @@ interface BioClientTokenPayload {
107
107
  orgId?: string;
108
108
  orgSlug?: string;
109
109
  }
110
- /** Options for local JWT verification */
110
+ /** Options for local JWT verification (HS256) */
111
111
  interface VerifyOptions {
112
112
  /** Expected issuer (default: config issuer) */
113
113
  issuer?: string;
114
114
  /** Expected audience (client_id) */
115
115
  audience?: string;
116
116
  }
117
+ /** Options for JWKS-based JWT verification (RS256) */
118
+ interface JWKSVerifyOptions {
119
+ /** JWKS endpoint URL (default: https://bio.tawa.insureco.io/.well-known/jwks.json) */
120
+ jwksUri?: string;
121
+ /** Expected issuer — defaults to accepting both bio.insureco.io and bio.tawa.insureco.io */
122
+ issuer?: string;
123
+ /** Expected audience (client_id) */
124
+ audience?: string;
125
+ }
117
126
  /** User profile from /api/oauth/userinfo or admin API */
118
127
  interface BioUser {
119
128
  sub: string;
@@ -367,8 +376,10 @@ declare function generatePKCE(): {
367
376
  };
368
377
 
369
378
  /**
370
- * Verify a Bio-ID JWT access token using HS256.
379
+ * Verify a Bio-ID JWT access token using HS256 (legacy / internal use).
371
380
  * Checks algorithm, signature (constant-time), expiration, issuer, and audience.
381
+ *
382
+ * @deprecated Prefer verifyTokenJWKS() for RS256 tokens issued by Bio-ID ≥ 0.2.
372
383
  */
373
384
  declare function verifyToken(token: string, secret: string, options?: VerifyOptions): BioTokenPayload;
374
385
  /**
@@ -381,5 +392,20 @@ declare function decodeToken(token: string): BioTokenPayload | null;
381
392
  * Returns true if expired or unparseable.
382
393
  */
383
394
  declare function isTokenExpired(token: string, bufferSeconds?: number): boolean;
395
+ /**
396
+ * Verify a Bio-ID JWT access token using RS256 and the public JWKS endpoint.
397
+ *
398
+ * No shared secret required — fetches Bio-ID's public key and verifies locally.
399
+ * The JWKS is cached in-process for 24 hours and auto-refreshed on key rotation.
400
+ *
401
+ * @example
402
+ * ```ts
403
+ * import { verifyTokenJWKS } from '@insureco/bio'
404
+ *
405
+ * const payload = await verifyTokenJWKS(req.headers.authorization.slice(7))
406
+ * console.log(payload.bioId, payload.orgSlug)
407
+ * ```
408
+ */
409
+ declare function verifyTokenJWKS(token: string, options?: JWKSVerifyOptions): Promise<BioTokenPayload>;
384
410
 
385
- export { type AdminResponse, type AuthorizeOptions, type AuthorizeResult, type BioAddress, BioAdmin, type BioAdminConfig, BioAuth, type BioAuthConfig, type BioClientTokenPayload, type BioDepartment, BioError, type BioMessaging, type BioOAuthClient, type BioRole, type BioTokenPayload, type BioUser, type CreateClientData, type CreateDepartmentData, type CreateRoleData, type IntrospectResult, type TokenResponse, type UpdateUserData, type UserFilters, type VerifyOptions, decodeToken, generatePKCE, isTokenExpired, verifyToken };
411
+ export { type AdminResponse, type AuthorizeOptions, type AuthorizeResult, type BioAddress, BioAdmin, type BioAdminConfig, BioAuth, type BioAuthConfig, type BioClientTokenPayload, type BioDepartment, BioError, type BioMessaging, type BioOAuthClient, type BioRole, type BioTokenPayload, type BioUser, type CreateClientData, type CreateDepartmentData, type CreateRoleData, type IntrospectResult, type JWKSVerifyOptions, type TokenResponse, type UpdateUserData, type UserFilters, type VerifyOptions, decodeToken, generatePKCE, isTokenExpired, verifyToken, verifyTokenJWKS };
package/dist/index.d.ts CHANGED
@@ -107,13 +107,22 @@ interface BioClientTokenPayload {
107
107
  orgId?: string;
108
108
  orgSlug?: string;
109
109
  }
110
- /** Options for local JWT verification */
110
+ /** Options for local JWT verification (HS256) */
111
111
  interface VerifyOptions {
112
112
  /** Expected issuer (default: config issuer) */
113
113
  issuer?: string;
114
114
  /** Expected audience (client_id) */
115
115
  audience?: string;
116
116
  }
117
+ /** Options for JWKS-based JWT verification (RS256) */
118
+ interface JWKSVerifyOptions {
119
+ /** JWKS endpoint URL (default: https://bio.tawa.insureco.io/.well-known/jwks.json) */
120
+ jwksUri?: string;
121
+ /** Expected issuer — defaults to accepting both bio.insureco.io and bio.tawa.insureco.io */
122
+ issuer?: string;
123
+ /** Expected audience (client_id) */
124
+ audience?: string;
125
+ }
117
126
  /** User profile from /api/oauth/userinfo or admin API */
118
127
  interface BioUser {
119
128
  sub: string;
@@ -367,8 +376,10 @@ declare function generatePKCE(): {
367
376
  };
368
377
 
369
378
  /**
370
- * Verify a Bio-ID JWT access token using HS256.
379
+ * Verify a Bio-ID JWT access token using HS256 (legacy / internal use).
371
380
  * Checks algorithm, signature (constant-time), expiration, issuer, and audience.
381
+ *
382
+ * @deprecated Prefer verifyTokenJWKS() for RS256 tokens issued by Bio-ID ≥ 0.2.
372
383
  */
373
384
  declare function verifyToken(token: string, secret: string, options?: VerifyOptions): BioTokenPayload;
374
385
  /**
@@ -381,5 +392,20 @@ declare function decodeToken(token: string): BioTokenPayload | null;
381
392
  * Returns true if expired or unparseable.
382
393
  */
383
394
  declare function isTokenExpired(token: string, bufferSeconds?: number): boolean;
395
+ /**
396
+ * Verify a Bio-ID JWT access token using RS256 and the public JWKS endpoint.
397
+ *
398
+ * No shared secret required — fetches Bio-ID's public key and verifies locally.
399
+ * The JWKS is cached in-process for 24 hours and auto-refreshed on key rotation.
400
+ *
401
+ * @example
402
+ * ```ts
403
+ * import { verifyTokenJWKS } from '@insureco/bio'
404
+ *
405
+ * const payload = await verifyTokenJWKS(req.headers.authorization.slice(7))
406
+ * console.log(payload.bioId, payload.orgSlug)
407
+ * ```
408
+ */
409
+ declare function verifyTokenJWKS(token: string, options?: JWKSVerifyOptions): Promise<BioTokenPayload>;
384
410
 
385
- export { type AdminResponse, type AuthorizeOptions, type AuthorizeResult, type BioAddress, BioAdmin, type BioAdminConfig, BioAuth, type BioAuthConfig, type BioClientTokenPayload, type BioDepartment, BioError, type BioMessaging, type BioOAuthClient, type BioRole, type BioTokenPayload, type BioUser, type CreateClientData, type CreateDepartmentData, type CreateRoleData, type IntrospectResult, type TokenResponse, type UpdateUserData, type UserFilters, type VerifyOptions, decodeToken, generatePKCE, isTokenExpired, verifyToken };
411
+ export { type AdminResponse, type AuthorizeOptions, type AuthorizeResult, type BioAddress, BioAdmin, type BioAdminConfig, BioAuth, type BioAuthConfig, type BioClientTokenPayload, type BioDepartment, BioError, type BioMessaging, type BioOAuthClient, type BioRole, type BioTokenPayload, type BioUser, type CreateClientData, type CreateDepartmentData, type CreateRoleData, type IntrospectResult, type JWKSVerifyOptions, type TokenResponse, type UpdateUserData, type UserFilters, type VerifyOptions, decodeToken, generatePKCE, isTokenExpired, verifyToken, verifyTokenJWKS };
package/dist/index.js CHANGED
@@ -36,7 +36,8 @@ __export(index_exports, {
36
36
  decodeToken: () => decodeToken,
37
37
  generatePKCE: () => generatePKCE,
38
38
  isTokenExpired: () => isTokenExpired,
39
- verifyToken: () => verifyToken
39
+ verifyToken: () => verifyToken,
40
+ verifyTokenJWKS: () => verifyTokenJWKS
40
41
  });
41
42
  module.exports = __toCommonJS(index_exports);
42
43
 
@@ -588,6 +589,57 @@ var DEFAULT_ISSUERS = [
588
589
  "https://bio.tawa.insureco.io",
589
590
  "http://localhost:6100"
590
591
  ];
592
+ var DEFAULT_JWKS_URI = "https://bio.tawa.insureco.io/.well-known/jwks.json";
593
+ var JWKS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
594
+ var jwksCache = /* @__PURE__ */ new Map();
595
+ async function fetchJWKS(uri) {
596
+ const cached = jwksCache.get(uri);
597
+ if (cached && Date.now() - cached.fetchedAt < JWKS_CACHE_TTL_MS) {
598
+ return cached.keys;
599
+ }
600
+ const response = await fetch(uri, {
601
+ headers: { Accept: "application/json" },
602
+ signal: AbortSignal.timeout(5e3)
603
+ });
604
+ if (!response.ok) {
605
+ throw new BioError(`JWKS fetch failed: ${response.status}`, "jwks_fetch_failed");
606
+ }
607
+ const data = await response.json();
608
+ jwksCache.set(uri, { keys: data.keys, fetchedAt: Date.now() });
609
+ return data.keys;
610
+ }
611
+ async function importRS256Key(jwk) {
612
+ return globalThis.crypto.subtle.importKey(
613
+ "jwk",
614
+ jwk,
615
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
616
+ false,
617
+ ["verify"]
618
+ );
619
+ }
620
+ async function verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, kid, retrying = false) {
621
+ const keys = await fetchJWKS(jwksUri);
622
+ const targetKey = kid ? keys.find((k) => k.kid === kid) ?? keys[0] : keys[0];
623
+ if (!targetKey) {
624
+ throw new BioError("No matching key found in JWKS", "invalid_token");
625
+ }
626
+ const cryptoKey = await importRS256Key(targetKey);
627
+ const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
628
+ const signature = Buffer.from(signatureB64, "base64url");
629
+ const valid = await globalThis.crypto.subtle.verify(
630
+ "RSASSA-PKCS1-v1_5",
631
+ cryptoKey,
632
+ signature,
633
+ data
634
+ );
635
+ if (!valid) {
636
+ if (!retrying) {
637
+ jwksCache.delete(jwksUri);
638
+ return verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, kid, true);
639
+ }
640
+ throw new BioError("Invalid JWT signature", "invalid_signature");
641
+ }
642
+ }
591
643
  function base64UrlDecode(str) {
592
644
  return Buffer.from(str, "base64url").toString("utf8");
593
645
  }
@@ -598,6 +650,12 @@ function verifyToken(token, secret, options) {
598
650
  }
599
651
  const [headerB64, payloadB64, signatureB64] = parts;
600
652
  const header = JSON.parse(base64UrlDecode(headerB64));
653
+ if (header.alg === "RS256") {
654
+ throw new BioError(
655
+ "Token uses RS256 \u2014 use verifyTokenJWKS() instead of verifyToken()",
656
+ "unsupported_alg"
657
+ );
658
+ }
601
659
  if (header.alg !== "HS256") {
602
660
  throw new BioError(`Unsupported algorithm: ${header.alg}`, "unsupported_alg");
603
661
  }
@@ -642,6 +700,34 @@ function isTokenExpired(token, bufferSeconds = 30) {
642
700
  const now = Math.floor(Date.now() / 1e3);
643
701
  return payload.exp < now + bufferSeconds;
644
702
  }
703
+ async function verifyTokenJWKS(token, options) {
704
+ const parts = token.split(".");
705
+ if (parts.length !== 3) {
706
+ throw new BioError("Malformed JWT: expected 3 parts", "invalid_token");
707
+ }
708
+ const [headerB64, payloadB64, signatureB64] = parts;
709
+ const header = JSON.parse(base64UrlDecode(headerB64));
710
+ if (header.alg !== "RS256") {
711
+ throw new BioError(
712
+ `Expected RS256 token, got ${header.alg}. Use verifyToken() for HS256.`,
713
+ "unsupported_alg"
714
+ );
715
+ }
716
+ const jwksUri = options?.jwksUri ?? DEFAULT_JWKS_URI;
717
+ await verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, header.kid);
718
+ const payload = JSON.parse(base64UrlDecode(payloadB64));
719
+ const now = Math.floor(Date.now() / 1e3);
720
+ if (!payload.exp) throw new BioError("Token missing expiration claim", "invalid_token");
721
+ if (payload.exp < now) throw new BioError("Token has expired", "token_expired");
722
+ const allowedIssuers = options?.issuer ? [options.issuer] : DEFAULT_ISSUERS;
723
+ if (!allowedIssuers.includes(payload.iss)) {
724
+ throw new BioError(`Unknown issuer: ${payload.iss}`, "invalid_issuer");
725
+ }
726
+ if (options?.audience && payload.aud !== options.audience) {
727
+ throw new BioError(`Invalid audience: ${payload.aud}`, "invalid_audience");
728
+ }
729
+ return payload;
730
+ }
645
731
  // Annotate the CommonJS export names for ESM import in node:
646
732
  0 && (module.exports = {
647
733
  BioAdmin,
@@ -650,5 +736,6 @@ function isTokenExpired(token, bufferSeconds = 30) {
650
736
  decodeToken,
651
737
  generatePKCE,
652
738
  isTokenExpired,
653
- verifyToken
739
+ verifyToken,
740
+ verifyTokenJWKS
654
741
  });
package/dist/index.mjs CHANGED
@@ -546,6 +546,57 @@ var DEFAULT_ISSUERS = [
546
546
  "https://bio.tawa.insureco.io",
547
547
  "http://localhost:6100"
548
548
  ];
549
+ var DEFAULT_JWKS_URI = "https://bio.tawa.insureco.io/.well-known/jwks.json";
550
+ var JWKS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
551
+ var jwksCache = /* @__PURE__ */ new Map();
552
+ async function fetchJWKS(uri) {
553
+ const cached = jwksCache.get(uri);
554
+ if (cached && Date.now() - cached.fetchedAt < JWKS_CACHE_TTL_MS) {
555
+ return cached.keys;
556
+ }
557
+ const response = await fetch(uri, {
558
+ headers: { Accept: "application/json" },
559
+ signal: AbortSignal.timeout(5e3)
560
+ });
561
+ if (!response.ok) {
562
+ throw new BioError(`JWKS fetch failed: ${response.status}`, "jwks_fetch_failed");
563
+ }
564
+ const data = await response.json();
565
+ jwksCache.set(uri, { keys: data.keys, fetchedAt: Date.now() });
566
+ return data.keys;
567
+ }
568
+ async function importRS256Key(jwk) {
569
+ return globalThis.crypto.subtle.importKey(
570
+ "jwk",
571
+ jwk,
572
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
573
+ false,
574
+ ["verify"]
575
+ );
576
+ }
577
+ async function verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, kid, retrying = false) {
578
+ const keys = await fetchJWKS(jwksUri);
579
+ const targetKey = kid ? keys.find((k) => k.kid === kid) ?? keys[0] : keys[0];
580
+ if (!targetKey) {
581
+ throw new BioError("No matching key found in JWKS", "invalid_token");
582
+ }
583
+ const cryptoKey = await importRS256Key(targetKey);
584
+ const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
585
+ const signature = Buffer.from(signatureB64, "base64url");
586
+ const valid = await globalThis.crypto.subtle.verify(
587
+ "RSASSA-PKCS1-v1_5",
588
+ cryptoKey,
589
+ signature,
590
+ data
591
+ );
592
+ if (!valid) {
593
+ if (!retrying) {
594
+ jwksCache.delete(jwksUri);
595
+ return verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, kid, true);
596
+ }
597
+ throw new BioError("Invalid JWT signature", "invalid_signature");
598
+ }
599
+ }
549
600
  function base64UrlDecode(str) {
550
601
  return Buffer.from(str, "base64url").toString("utf8");
551
602
  }
@@ -556,6 +607,12 @@ function verifyToken(token, secret, options) {
556
607
  }
557
608
  const [headerB64, payloadB64, signatureB64] = parts;
558
609
  const header = JSON.parse(base64UrlDecode(headerB64));
610
+ if (header.alg === "RS256") {
611
+ throw new BioError(
612
+ "Token uses RS256 \u2014 use verifyTokenJWKS() instead of verifyToken()",
613
+ "unsupported_alg"
614
+ );
615
+ }
559
616
  if (header.alg !== "HS256") {
560
617
  throw new BioError(`Unsupported algorithm: ${header.alg}`, "unsupported_alg");
561
618
  }
@@ -600,6 +657,34 @@ function isTokenExpired(token, bufferSeconds = 30) {
600
657
  const now = Math.floor(Date.now() / 1e3);
601
658
  return payload.exp < now + bufferSeconds;
602
659
  }
660
+ async function verifyTokenJWKS(token, options) {
661
+ const parts = token.split(".");
662
+ if (parts.length !== 3) {
663
+ throw new BioError("Malformed JWT: expected 3 parts", "invalid_token");
664
+ }
665
+ const [headerB64, payloadB64, signatureB64] = parts;
666
+ const header = JSON.parse(base64UrlDecode(headerB64));
667
+ if (header.alg !== "RS256") {
668
+ throw new BioError(
669
+ `Expected RS256 token, got ${header.alg}. Use verifyToken() for HS256.`,
670
+ "unsupported_alg"
671
+ );
672
+ }
673
+ const jwksUri = options?.jwksUri ?? DEFAULT_JWKS_URI;
674
+ await verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, header.kid);
675
+ const payload = JSON.parse(base64UrlDecode(payloadB64));
676
+ const now = Math.floor(Date.now() / 1e3);
677
+ if (!payload.exp) throw new BioError("Token missing expiration claim", "invalid_token");
678
+ if (payload.exp < now) throw new BioError("Token has expired", "token_expired");
679
+ const allowedIssuers = options?.issuer ? [options.issuer] : DEFAULT_ISSUERS;
680
+ if (!allowedIssuers.includes(payload.iss)) {
681
+ throw new BioError(`Unknown issuer: ${payload.iss}`, "invalid_issuer");
682
+ }
683
+ if (options?.audience && payload.aud !== options.audience) {
684
+ throw new BioError(`Invalid audience: ${payload.aud}`, "invalid_audience");
685
+ }
686
+ return payload;
687
+ }
603
688
  export {
604
689
  BioAdmin,
605
690
  BioAuth,
@@ -607,5 +692,6 @@ export {
607
692
  decodeToken,
608
693
  generatePKCE,
609
694
  isTokenExpired,
610
- verifyToken
695
+ verifyToken,
696
+ verifyTokenJWKS
611
697
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@insureco/bio",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "SDK for Bio-ID SSO integration on the Tawa platform",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",