@insureco/bio 0.1.0 → 0.3.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 +39 -5
- package/dist/index.d.ts +39 -5
- package/dist/index.js +100 -4
- package/dist/index.mjs +98 -3
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ interface BioAuthConfig {
|
|
|
4
4
|
clientId: string;
|
|
5
5
|
/** OAuth client secret (env: BIO_CLIENT_SECRET) */
|
|
6
6
|
clientSecret: string;
|
|
7
|
-
/** Bio-ID issuer URL (env: BIO_ID_URL, default: https://bio.tawa.
|
|
7
|
+
/** Bio-ID issuer URL (env: BIO_ID_URL, default: https://bio.tawa.pro) */
|
|
8
8
|
issuer?: string;
|
|
9
9
|
/** Number of retry attempts on transient failures (default: 2) */
|
|
10
10
|
retries?: number;
|
|
@@ -13,7 +13,7 @@ interface BioAuthConfig {
|
|
|
13
13
|
}
|
|
14
14
|
/** Configuration for BioAdmin (admin API client) */
|
|
15
15
|
interface BioAdminConfig {
|
|
16
|
-
/** Bio-ID base URL (env: BIO_ID_URL, default: https://bio.tawa.
|
|
16
|
+
/** Bio-ID base URL (env: BIO_ID_URL, default: https://bio.tawa.pro) */
|
|
17
17
|
baseUrl?: string;
|
|
18
18
|
/** Internal API key for service-to-service auth (env: INTERNAL_API_KEY) */
|
|
19
19
|
internalKey?: string;
|
|
@@ -32,6 +32,8 @@ interface AuthorizeOptions {
|
|
|
32
32
|
scopes?: string[];
|
|
33
33
|
/** CSRF state parameter (auto-generated if not provided) */
|
|
34
34
|
state?: string;
|
|
35
|
+
/** Optional org slug to pre-select during authorization (for multi-org users) */
|
|
36
|
+
organization?: string;
|
|
35
37
|
}
|
|
36
38
|
/** Result from getAuthorizationUrl() */
|
|
37
39
|
interface AuthorizeResult {
|
|
@@ -85,6 +87,12 @@ interface BioTokenPayload {
|
|
|
85
87
|
permissions: string[];
|
|
86
88
|
orgId?: string;
|
|
87
89
|
orgSlug?: string;
|
|
90
|
+
/** Org-specific role slugs within the user's active org (from OrgMembership) */
|
|
91
|
+
orgRoles?: string[];
|
|
92
|
+
/** Job title at the active org (from OrgMembership.jobTitle) */
|
|
93
|
+
orgTitle?: string;
|
|
94
|
+
/** Additional modules granted by the org specifically (from OrgMembership.enabled_modules) */
|
|
95
|
+
orgModules?: string[];
|
|
88
96
|
client_id: string;
|
|
89
97
|
scope: string;
|
|
90
98
|
enabled_modules?: string[];
|
|
@@ -107,13 +115,22 @@ interface BioClientTokenPayload {
|
|
|
107
115
|
orgId?: string;
|
|
108
116
|
orgSlug?: string;
|
|
109
117
|
}
|
|
110
|
-
/** Options for local JWT verification */
|
|
118
|
+
/** Options for local JWT verification (HS256) */
|
|
111
119
|
interface VerifyOptions {
|
|
112
120
|
/** Expected issuer (default: config issuer) */
|
|
113
121
|
issuer?: string;
|
|
114
122
|
/** Expected audience (client_id) */
|
|
115
123
|
audience?: string;
|
|
116
124
|
}
|
|
125
|
+
/** Options for JWKS-based JWT verification (RS256) */
|
|
126
|
+
interface JWKSVerifyOptions {
|
|
127
|
+
/** JWKS endpoint URL (default: https://bio.tawa.pro/.well-known/jwks.json) */
|
|
128
|
+
jwksUri?: string;
|
|
129
|
+
/** Expected issuer — defaults to accepting bio.insureco.io, bio.tawa.insureco.io, and bio.tawa.pro */
|
|
130
|
+
issuer?: string;
|
|
131
|
+
/** Expected audience (client_id) */
|
|
132
|
+
audience?: string;
|
|
133
|
+
}
|
|
117
134
|
/** User profile from /api/oauth/userinfo or admin API */
|
|
118
135
|
interface BioUser {
|
|
119
136
|
sub: string;
|
|
@@ -367,8 +384,10 @@ declare function generatePKCE(): {
|
|
|
367
384
|
};
|
|
368
385
|
|
|
369
386
|
/**
|
|
370
|
-
* Verify a Bio-ID JWT access token using HS256.
|
|
387
|
+
* Verify a Bio-ID JWT access token using HS256 (legacy / internal use).
|
|
371
388
|
* Checks algorithm, signature (constant-time), expiration, issuer, and audience.
|
|
389
|
+
*
|
|
390
|
+
* @deprecated Prefer verifyTokenJWKS() for RS256 tokens issued by Bio-ID ≥ 0.2.
|
|
372
391
|
*/
|
|
373
392
|
declare function verifyToken(token: string, secret: string, options?: VerifyOptions): BioTokenPayload;
|
|
374
393
|
/**
|
|
@@ -381,5 +400,20 @@ declare function decodeToken(token: string): BioTokenPayload | null;
|
|
|
381
400
|
* Returns true if expired or unparseable.
|
|
382
401
|
*/
|
|
383
402
|
declare function isTokenExpired(token: string, bufferSeconds?: number): boolean;
|
|
403
|
+
/**
|
|
404
|
+
* Verify a Bio-ID JWT access token using RS256 and the public JWKS endpoint.
|
|
405
|
+
*
|
|
406
|
+
* No shared secret required — fetches Bio-ID's public key and verifies locally.
|
|
407
|
+
* The JWKS is cached in-process for 24 hours and auto-refreshed on key rotation.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```ts
|
|
411
|
+
* import { verifyTokenJWKS } from '@insureco/bio'
|
|
412
|
+
*
|
|
413
|
+
* const payload = await verifyTokenJWKS(req.headers.authorization.slice(7))
|
|
414
|
+
* console.log(payload.bioId, payload.orgSlug)
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
declare function verifyTokenJWKS(token: string, options?: JWKSVerifyOptions): Promise<BioTokenPayload>;
|
|
384
418
|
|
|
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 };
|
|
419
|
+
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
|
@@ -4,7 +4,7 @@ interface BioAuthConfig {
|
|
|
4
4
|
clientId: string;
|
|
5
5
|
/** OAuth client secret (env: BIO_CLIENT_SECRET) */
|
|
6
6
|
clientSecret: string;
|
|
7
|
-
/** Bio-ID issuer URL (env: BIO_ID_URL, default: https://bio.tawa.
|
|
7
|
+
/** Bio-ID issuer URL (env: BIO_ID_URL, default: https://bio.tawa.pro) */
|
|
8
8
|
issuer?: string;
|
|
9
9
|
/** Number of retry attempts on transient failures (default: 2) */
|
|
10
10
|
retries?: number;
|
|
@@ -13,7 +13,7 @@ interface BioAuthConfig {
|
|
|
13
13
|
}
|
|
14
14
|
/** Configuration for BioAdmin (admin API client) */
|
|
15
15
|
interface BioAdminConfig {
|
|
16
|
-
/** Bio-ID base URL (env: BIO_ID_URL, default: https://bio.tawa.
|
|
16
|
+
/** Bio-ID base URL (env: BIO_ID_URL, default: https://bio.tawa.pro) */
|
|
17
17
|
baseUrl?: string;
|
|
18
18
|
/** Internal API key for service-to-service auth (env: INTERNAL_API_KEY) */
|
|
19
19
|
internalKey?: string;
|
|
@@ -32,6 +32,8 @@ interface AuthorizeOptions {
|
|
|
32
32
|
scopes?: string[];
|
|
33
33
|
/** CSRF state parameter (auto-generated if not provided) */
|
|
34
34
|
state?: string;
|
|
35
|
+
/** Optional org slug to pre-select during authorization (for multi-org users) */
|
|
36
|
+
organization?: string;
|
|
35
37
|
}
|
|
36
38
|
/** Result from getAuthorizationUrl() */
|
|
37
39
|
interface AuthorizeResult {
|
|
@@ -85,6 +87,12 @@ interface BioTokenPayload {
|
|
|
85
87
|
permissions: string[];
|
|
86
88
|
orgId?: string;
|
|
87
89
|
orgSlug?: string;
|
|
90
|
+
/** Org-specific role slugs within the user's active org (from OrgMembership) */
|
|
91
|
+
orgRoles?: string[];
|
|
92
|
+
/** Job title at the active org (from OrgMembership.jobTitle) */
|
|
93
|
+
orgTitle?: string;
|
|
94
|
+
/** Additional modules granted by the org specifically (from OrgMembership.enabled_modules) */
|
|
95
|
+
orgModules?: string[];
|
|
88
96
|
client_id: string;
|
|
89
97
|
scope: string;
|
|
90
98
|
enabled_modules?: string[];
|
|
@@ -107,13 +115,22 @@ interface BioClientTokenPayload {
|
|
|
107
115
|
orgId?: string;
|
|
108
116
|
orgSlug?: string;
|
|
109
117
|
}
|
|
110
|
-
/** Options for local JWT verification */
|
|
118
|
+
/** Options for local JWT verification (HS256) */
|
|
111
119
|
interface VerifyOptions {
|
|
112
120
|
/** Expected issuer (default: config issuer) */
|
|
113
121
|
issuer?: string;
|
|
114
122
|
/** Expected audience (client_id) */
|
|
115
123
|
audience?: string;
|
|
116
124
|
}
|
|
125
|
+
/** Options for JWKS-based JWT verification (RS256) */
|
|
126
|
+
interface JWKSVerifyOptions {
|
|
127
|
+
/** JWKS endpoint URL (default: https://bio.tawa.pro/.well-known/jwks.json) */
|
|
128
|
+
jwksUri?: string;
|
|
129
|
+
/** Expected issuer — defaults to accepting bio.insureco.io, bio.tawa.insureco.io, and bio.tawa.pro */
|
|
130
|
+
issuer?: string;
|
|
131
|
+
/** Expected audience (client_id) */
|
|
132
|
+
audience?: string;
|
|
133
|
+
}
|
|
117
134
|
/** User profile from /api/oauth/userinfo or admin API */
|
|
118
135
|
interface BioUser {
|
|
119
136
|
sub: string;
|
|
@@ -367,8 +384,10 @@ declare function generatePKCE(): {
|
|
|
367
384
|
};
|
|
368
385
|
|
|
369
386
|
/**
|
|
370
|
-
* Verify a Bio-ID JWT access token using HS256.
|
|
387
|
+
* Verify a Bio-ID JWT access token using HS256 (legacy / internal use).
|
|
371
388
|
* Checks algorithm, signature (constant-time), expiration, issuer, and audience.
|
|
389
|
+
*
|
|
390
|
+
* @deprecated Prefer verifyTokenJWKS() for RS256 tokens issued by Bio-ID ≥ 0.2.
|
|
372
391
|
*/
|
|
373
392
|
declare function verifyToken(token: string, secret: string, options?: VerifyOptions): BioTokenPayload;
|
|
374
393
|
/**
|
|
@@ -381,5 +400,20 @@ declare function decodeToken(token: string): BioTokenPayload | null;
|
|
|
381
400
|
* Returns true if expired or unparseable.
|
|
382
401
|
*/
|
|
383
402
|
declare function isTokenExpired(token: string, bufferSeconds?: number): boolean;
|
|
403
|
+
/**
|
|
404
|
+
* Verify a Bio-ID JWT access token using RS256 and the public JWKS endpoint.
|
|
405
|
+
*
|
|
406
|
+
* No shared secret required — fetches Bio-ID's public key and verifies locally.
|
|
407
|
+
* The JWKS is cached in-process for 24 hours and auto-refreshed on key rotation.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```ts
|
|
411
|
+
* import { verifyTokenJWKS } from '@insureco/bio'
|
|
412
|
+
*
|
|
413
|
+
* const payload = await verifyTokenJWKS(req.headers.authorization.slice(7))
|
|
414
|
+
* console.log(payload.bioId, payload.orgSlug)
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
declare function verifyTokenJWKS(token: string, options?: JWKSVerifyOptions): Promise<BioTokenPayload>;
|
|
384
418
|
|
|
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 };
|
|
419
|
+
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
|
|
|
@@ -89,7 +90,7 @@ async function parseJsonResponse(response) {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
// src/auth.ts
|
|
92
|
-
var DEFAULT_ISSUER = "https://bio.tawa.
|
|
93
|
+
var DEFAULT_ISSUER = "https://bio.tawa.pro";
|
|
93
94
|
var DEFAULT_SCOPES = ["openid", "profile", "email"];
|
|
94
95
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
95
96
|
var BioAuth = class _BioAuth {
|
|
@@ -159,6 +160,9 @@ var BioAuth = class _BioAuth {
|
|
|
159
160
|
code_challenge: codeChallenge,
|
|
160
161
|
code_challenge_method: "S256"
|
|
161
162
|
});
|
|
163
|
+
if (opts.organization) {
|
|
164
|
+
params.set("organization", opts.organization);
|
|
165
|
+
}
|
|
162
166
|
return {
|
|
163
167
|
url: `${this.issuer}/oauth/authorize?${params.toString()}`,
|
|
164
168
|
state,
|
|
@@ -380,7 +384,7 @@ function mapIntrospectResponse(raw) {
|
|
|
380
384
|
}
|
|
381
385
|
|
|
382
386
|
// src/admin.ts
|
|
383
|
-
var DEFAULT_BASE_URL = "https://bio.tawa.
|
|
387
|
+
var DEFAULT_BASE_URL = "https://bio.tawa.pro";
|
|
384
388
|
var DEFAULT_TIMEOUT_MS2 = 1e4;
|
|
385
389
|
var BioAdmin = class _BioAdmin {
|
|
386
390
|
baseUrl;
|
|
@@ -586,8 +590,60 @@ var import_node_crypto3 = __toESM(require("crypto"));
|
|
|
586
590
|
var DEFAULT_ISSUERS = [
|
|
587
591
|
"https://bio.insureco.io",
|
|
588
592
|
"https://bio.tawa.insureco.io",
|
|
593
|
+
"https://bio.tawa.pro",
|
|
589
594
|
"http://localhost:6100"
|
|
590
595
|
];
|
|
596
|
+
var DEFAULT_JWKS_URI = "https://bio.tawa.pro/.well-known/jwks.json";
|
|
597
|
+
var JWKS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
598
|
+
var jwksCache = /* @__PURE__ */ new Map();
|
|
599
|
+
async function fetchJWKS(uri) {
|
|
600
|
+
const cached = jwksCache.get(uri);
|
|
601
|
+
if (cached && Date.now() - cached.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
602
|
+
return cached.keys;
|
|
603
|
+
}
|
|
604
|
+
const response = await fetch(uri, {
|
|
605
|
+
headers: { Accept: "application/json" },
|
|
606
|
+
signal: AbortSignal.timeout(5e3)
|
|
607
|
+
});
|
|
608
|
+
if (!response.ok) {
|
|
609
|
+
throw new BioError(`JWKS fetch failed: ${response.status}`, "jwks_fetch_failed");
|
|
610
|
+
}
|
|
611
|
+
const data = await response.json();
|
|
612
|
+
jwksCache.set(uri, { keys: data.keys, fetchedAt: Date.now() });
|
|
613
|
+
return data.keys;
|
|
614
|
+
}
|
|
615
|
+
async function importRS256Key(jwk) {
|
|
616
|
+
return globalThis.crypto.subtle.importKey(
|
|
617
|
+
"jwk",
|
|
618
|
+
jwk,
|
|
619
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
620
|
+
false,
|
|
621
|
+
["verify"]
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
async function verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, kid, retrying = false) {
|
|
625
|
+
const keys = await fetchJWKS(jwksUri);
|
|
626
|
+
const targetKey = kid ? keys.find((k) => k.kid === kid) ?? keys[0] : keys[0];
|
|
627
|
+
if (!targetKey) {
|
|
628
|
+
throw new BioError("No matching key found in JWKS", "invalid_token");
|
|
629
|
+
}
|
|
630
|
+
const cryptoKey = await importRS256Key(targetKey);
|
|
631
|
+
const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
632
|
+
const signature = Buffer.from(signatureB64, "base64url");
|
|
633
|
+
const valid = await globalThis.crypto.subtle.verify(
|
|
634
|
+
"RSASSA-PKCS1-v1_5",
|
|
635
|
+
cryptoKey,
|
|
636
|
+
signature,
|
|
637
|
+
data
|
|
638
|
+
);
|
|
639
|
+
if (!valid) {
|
|
640
|
+
if (!retrying) {
|
|
641
|
+
jwksCache.delete(jwksUri);
|
|
642
|
+
return verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, kid, true);
|
|
643
|
+
}
|
|
644
|
+
throw new BioError("Invalid JWT signature", "invalid_signature");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
591
647
|
function base64UrlDecode(str) {
|
|
592
648
|
return Buffer.from(str, "base64url").toString("utf8");
|
|
593
649
|
}
|
|
@@ -598,6 +654,12 @@ function verifyToken(token, secret, options) {
|
|
|
598
654
|
}
|
|
599
655
|
const [headerB64, payloadB64, signatureB64] = parts;
|
|
600
656
|
const header = JSON.parse(base64UrlDecode(headerB64));
|
|
657
|
+
if (header.alg === "RS256") {
|
|
658
|
+
throw new BioError(
|
|
659
|
+
"Token uses RS256 \u2014 use verifyTokenJWKS() instead of verifyToken()",
|
|
660
|
+
"unsupported_alg"
|
|
661
|
+
);
|
|
662
|
+
}
|
|
601
663
|
if (header.alg !== "HS256") {
|
|
602
664
|
throw new BioError(`Unsupported algorithm: ${header.alg}`, "unsupported_alg");
|
|
603
665
|
}
|
|
@@ -642,6 +704,39 @@ function isTokenExpired(token, bufferSeconds = 30) {
|
|
|
642
704
|
const now = Math.floor(Date.now() / 1e3);
|
|
643
705
|
return payload.exp < now + bufferSeconds;
|
|
644
706
|
}
|
|
707
|
+
async function verifyTokenJWKS(token, options) {
|
|
708
|
+
const parts = token.split(".");
|
|
709
|
+
if (parts.length !== 3) {
|
|
710
|
+
throw new BioError("Malformed JWT: expected 3 parts", "invalid_token");
|
|
711
|
+
}
|
|
712
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
713
|
+
let header;
|
|
714
|
+
try {
|
|
715
|
+
header = JSON.parse(base64UrlDecode(headerB64));
|
|
716
|
+
} catch {
|
|
717
|
+
throw new BioError("Malformed JWT: invalid header encoding", "invalid_token");
|
|
718
|
+
}
|
|
719
|
+
if (header.alg !== "RS256") {
|
|
720
|
+
throw new BioError(
|
|
721
|
+
`Expected RS256 token, got ${header.alg}. Use verifyToken() for HS256.`,
|
|
722
|
+
"unsupported_alg"
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
const jwksUri = options?.jwksUri ?? DEFAULT_JWKS_URI;
|
|
726
|
+
await verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, header.kid);
|
|
727
|
+
const payload = JSON.parse(base64UrlDecode(payloadB64));
|
|
728
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
729
|
+
if (!payload.exp) throw new BioError("Token missing expiration claim", "invalid_token");
|
|
730
|
+
if (payload.exp < now) throw new BioError("Token has expired", "token_expired");
|
|
731
|
+
const allowedIssuers = options?.issuer ? [options.issuer] : DEFAULT_ISSUERS;
|
|
732
|
+
if (!allowedIssuers.includes(payload.iss)) {
|
|
733
|
+
throw new BioError(`Unknown issuer: ${payload.iss}`, "invalid_issuer");
|
|
734
|
+
}
|
|
735
|
+
if (options?.audience && payload.aud !== options.audience) {
|
|
736
|
+
throw new BioError(`Invalid audience: ${payload.aud}`, "invalid_audience");
|
|
737
|
+
}
|
|
738
|
+
return payload;
|
|
739
|
+
}
|
|
645
740
|
// Annotate the CommonJS export names for ESM import in node:
|
|
646
741
|
0 && (module.exports = {
|
|
647
742
|
BioAdmin,
|
|
@@ -650,5 +745,6 @@ function isTokenExpired(token, bufferSeconds = 30) {
|
|
|
650
745
|
decodeToken,
|
|
651
746
|
generatePKCE,
|
|
652
747
|
isTokenExpired,
|
|
653
|
-
verifyToken
|
|
748
|
+
verifyToken,
|
|
749
|
+
verifyTokenJWKS
|
|
654
750
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -47,7 +47,7 @@ async function parseJsonResponse(response) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// src/auth.ts
|
|
50
|
-
var DEFAULT_ISSUER = "https://bio.tawa.
|
|
50
|
+
var DEFAULT_ISSUER = "https://bio.tawa.pro";
|
|
51
51
|
var DEFAULT_SCOPES = ["openid", "profile", "email"];
|
|
52
52
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
53
53
|
var BioAuth = class _BioAuth {
|
|
@@ -117,6 +117,9 @@ var BioAuth = class _BioAuth {
|
|
|
117
117
|
code_challenge: codeChallenge,
|
|
118
118
|
code_challenge_method: "S256"
|
|
119
119
|
});
|
|
120
|
+
if (opts.organization) {
|
|
121
|
+
params.set("organization", opts.organization);
|
|
122
|
+
}
|
|
120
123
|
return {
|
|
121
124
|
url: `${this.issuer}/oauth/authorize?${params.toString()}`,
|
|
122
125
|
state,
|
|
@@ -338,7 +341,7 @@ function mapIntrospectResponse(raw) {
|
|
|
338
341
|
}
|
|
339
342
|
|
|
340
343
|
// src/admin.ts
|
|
341
|
-
var DEFAULT_BASE_URL = "https://bio.tawa.
|
|
344
|
+
var DEFAULT_BASE_URL = "https://bio.tawa.pro";
|
|
342
345
|
var DEFAULT_TIMEOUT_MS2 = 1e4;
|
|
343
346
|
var BioAdmin = class _BioAdmin {
|
|
344
347
|
baseUrl;
|
|
@@ -544,8 +547,60 @@ import crypto3 from "crypto";
|
|
|
544
547
|
var DEFAULT_ISSUERS = [
|
|
545
548
|
"https://bio.insureco.io",
|
|
546
549
|
"https://bio.tawa.insureco.io",
|
|
550
|
+
"https://bio.tawa.pro",
|
|
547
551
|
"http://localhost:6100"
|
|
548
552
|
];
|
|
553
|
+
var DEFAULT_JWKS_URI = "https://bio.tawa.pro/.well-known/jwks.json";
|
|
554
|
+
var JWKS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
555
|
+
var jwksCache = /* @__PURE__ */ new Map();
|
|
556
|
+
async function fetchJWKS(uri) {
|
|
557
|
+
const cached = jwksCache.get(uri);
|
|
558
|
+
if (cached && Date.now() - cached.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
559
|
+
return cached.keys;
|
|
560
|
+
}
|
|
561
|
+
const response = await fetch(uri, {
|
|
562
|
+
headers: { Accept: "application/json" },
|
|
563
|
+
signal: AbortSignal.timeout(5e3)
|
|
564
|
+
});
|
|
565
|
+
if (!response.ok) {
|
|
566
|
+
throw new BioError(`JWKS fetch failed: ${response.status}`, "jwks_fetch_failed");
|
|
567
|
+
}
|
|
568
|
+
const data = await response.json();
|
|
569
|
+
jwksCache.set(uri, { keys: data.keys, fetchedAt: Date.now() });
|
|
570
|
+
return data.keys;
|
|
571
|
+
}
|
|
572
|
+
async function importRS256Key(jwk) {
|
|
573
|
+
return globalThis.crypto.subtle.importKey(
|
|
574
|
+
"jwk",
|
|
575
|
+
jwk,
|
|
576
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
577
|
+
false,
|
|
578
|
+
["verify"]
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
async function verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, kid, retrying = false) {
|
|
582
|
+
const keys = await fetchJWKS(jwksUri);
|
|
583
|
+
const targetKey = kid ? keys.find((k) => k.kid === kid) ?? keys[0] : keys[0];
|
|
584
|
+
if (!targetKey) {
|
|
585
|
+
throw new BioError("No matching key found in JWKS", "invalid_token");
|
|
586
|
+
}
|
|
587
|
+
const cryptoKey = await importRS256Key(targetKey);
|
|
588
|
+
const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
589
|
+
const signature = Buffer.from(signatureB64, "base64url");
|
|
590
|
+
const valid = await globalThis.crypto.subtle.verify(
|
|
591
|
+
"RSASSA-PKCS1-v1_5",
|
|
592
|
+
cryptoKey,
|
|
593
|
+
signature,
|
|
594
|
+
data
|
|
595
|
+
);
|
|
596
|
+
if (!valid) {
|
|
597
|
+
if (!retrying) {
|
|
598
|
+
jwksCache.delete(jwksUri);
|
|
599
|
+
return verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, kid, true);
|
|
600
|
+
}
|
|
601
|
+
throw new BioError("Invalid JWT signature", "invalid_signature");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
549
604
|
function base64UrlDecode(str) {
|
|
550
605
|
return Buffer.from(str, "base64url").toString("utf8");
|
|
551
606
|
}
|
|
@@ -556,6 +611,12 @@ function verifyToken(token, secret, options) {
|
|
|
556
611
|
}
|
|
557
612
|
const [headerB64, payloadB64, signatureB64] = parts;
|
|
558
613
|
const header = JSON.parse(base64UrlDecode(headerB64));
|
|
614
|
+
if (header.alg === "RS256") {
|
|
615
|
+
throw new BioError(
|
|
616
|
+
"Token uses RS256 \u2014 use verifyTokenJWKS() instead of verifyToken()",
|
|
617
|
+
"unsupported_alg"
|
|
618
|
+
);
|
|
619
|
+
}
|
|
559
620
|
if (header.alg !== "HS256") {
|
|
560
621
|
throw new BioError(`Unsupported algorithm: ${header.alg}`, "unsupported_alg");
|
|
561
622
|
}
|
|
@@ -600,6 +661,39 @@ function isTokenExpired(token, bufferSeconds = 30) {
|
|
|
600
661
|
const now = Math.floor(Date.now() / 1e3);
|
|
601
662
|
return payload.exp < now + bufferSeconds;
|
|
602
663
|
}
|
|
664
|
+
async function verifyTokenJWKS(token, options) {
|
|
665
|
+
const parts = token.split(".");
|
|
666
|
+
if (parts.length !== 3) {
|
|
667
|
+
throw new BioError("Malformed JWT: expected 3 parts", "invalid_token");
|
|
668
|
+
}
|
|
669
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
670
|
+
let header;
|
|
671
|
+
try {
|
|
672
|
+
header = JSON.parse(base64UrlDecode(headerB64));
|
|
673
|
+
} catch {
|
|
674
|
+
throw new BioError("Malformed JWT: invalid header encoding", "invalid_token");
|
|
675
|
+
}
|
|
676
|
+
if (header.alg !== "RS256") {
|
|
677
|
+
throw new BioError(
|
|
678
|
+
`Expected RS256 token, got ${header.alg}. Use verifyToken() for HS256.`,
|
|
679
|
+
"unsupported_alg"
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
const jwksUri = options?.jwksUri ?? DEFAULT_JWKS_URI;
|
|
683
|
+
await verifyRS256Signature(headerB64, payloadB64, signatureB64, jwksUri, header.kid);
|
|
684
|
+
const payload = JSON.parse(base64UrlDecode(payloadB64));
|
|
685
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
686
|
+
if (!payload.exp) throw new BioError("Token missing expiration claim", "invalid_token");
|
|
687
|
+
if (payload.exp < now) throw new BioError("Token has expired", "token_expired");
|
|
688
|
+
const allowedIssuers = options?.issuer ? [options.issuer] : DEFAULT_ISSUERS;
|
|
689
|
+
if (!allowedIssuers.includes(payload.iss)) {
|
|
690
|
+
throw new BioError(`Unknown issuer: ${payload.iss}`, "invalid_issuer");
|
|
691
|
+
}
|
|
692
|
+
if (options?.audience && payload.aud !== options.audience) {
|
|
693
|
+
throw new BioError(`Invalid audience: ${payload.aud}`, "invalid_audience");
|
|
694
|
+
}
|
|
695
|
+
return payload;
|
|
696
|
+
}
|
|
603
697
|
export {
|
|
604
698
|
BioAdmin,
|
|
605
699
|
BioAuth,
|
|
@@ -607,5 +701,6 @@ export {
|
|
|
607
701
|
decodeToken,
|
|
608
702
|
generatePKCE,
|
|
609
703
|
isTokenExpired,
|
|
610
|
-
verifyToken
|
|
704
|
+
verifyToken,
|
|
705
|
+
verifyTokenJWKS
|
|
611
706
|
};
|