@idp.global/sdk 16.0.0 → 16.1.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
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-09-09 - 16.1.0
4
+
5
+ ### Features
6
+
7
+ - add confidential OIDC introspection client (server)
8
+ - Add a server-only OidcIntrospectionClient with Basic authentication, nonce binding, deadlines, response size limits, and sanitized errors.
9
+ - Validate active and inactive introspection responses against issuer, client, subject, organization, scopes, revisions, roles, and freshness.
10
+ - Export the client and error types from the server entrypoint and document live organization authority usage.
11
+ - Update @idp.global/interfaces to ^29.1.0 for introspection response types.
12
+
13
+ ### Fixes
14
+
15
+ - update development tooling dependencies (tooling)
16
+ - Bump @git.zone/tsdoc to ^2.2.0 and @git.zone/tstest to ^4.1.1
17
+ - Pin @types/node to 26.4.1
18
+ - Update packageManager to pnpm@12.2.1
19
+
3
20
  ## 2026-09-05 - 16.0.0
4
21
 
5
22
  ### Breaking Changes
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@idp.global/sdk',
6
- version: '16.0.0',
6
+ version: '16.1.0',
7
7
  description: 'Reusable browser and server SDK for idp.global authentication.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxpQkFBaUI7SUFDdkIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLGdFQUFnRTtDQUM5RSxDQUFBIn0=
@@ -0,0 +1,24 @@
1
+ import * as plugins from './plugins.js';
2
+ export interface IOidcIntrospectionClientOptions {
3
+ issuer: string;
4
+ clientId: string;
5
+ clientSecret: string;
6
+ timeoutMs?: number;
7
+ maxResponseBytes?: number;
8
+ }
9
+ export interface IOidcIntrospectionClientRequest {
10
+ token: string;
11
+ subject: string;
12
+ organizationId: string;
13
+ }
14
+ export declare class OidcIntrospectionError extends Error {
15
+ readonly code: 'invalid_request' | 'invalid_response' | 'unavailable';
16
+ constructor(code: 'invalid_request' | 'invalid_response' | 'unavailable');
17
+ }
18
+ /** Server-only confidential client. Each result is one live observation, never a cached grant. */
19
+ export declare class OidcIntrospectionClient {
20
+ #private;
21
+ constructor(optionsArg: IOidcIntrospectionClientOptions);
22
+ introspect(requestArg: IOidcIntrospectionClientRequest): Promise<plugins.idpInterfaces.data.TOidcIntrospectionResponse>;
23
+ private validateResponse;
24
+ }
@@ -0,0 +1,144 @@
1
+ import * as plugins from './plugins.js';
2
+ export class OidcIntrospectionError extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(`OIDC introspection ${code.replace(/_/g, ' ')}.`);
6
+ this.code = code;
7
+ this.name = 'OidcIntrospectionError';
8
+ }
9
+ }
10
+ const exactString = (valueArg, maximumArg = 512) => typeof valueArg === 'string' && valueArg.length > 0 && valueArg.length <= maximumArg
11
+ && valueArg.trim() === valueArg;
12
+ const record = (valueArg) => valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg);
13
+ const exactKeys = (valueArg, keysArg) => Object.keys(valueArg).length === keysArg.length && keysArg.every((keyArg) => Object.hasOwn(valueArg, keyArg));
14
+ const revision = (valueArg) => Number.isSafeInteger(valueArg) && valueArg >= 0;
15
+ const sortedUnique = (valuesArg) => Array.isArray(valuesArg)
16
+ && valuesArg.length > 0 && valuesArg.every((valueArg, indexArg) => exactString(valueArg, 64)
17
+ && (indexArg === 0 || valuesArg[indexArg - 1] < valueArg));
18
+ /** Server-only confidential client. Each result is one live observation, never a cached grant. */
19
+ export class OidcIntrospectionClient {
20
+ #issuer;
21
+ #clientId;
22
+ #authorization;
23
+ #timeoutMs;
24
+ #maxResponseBytes;
25
+ constructor(optionsArg) {
26
+ if (!optionsArg || !exactString(optionsArg.issuer, 2048) || !exactString(optionsArg.clientId)
27
+ || !exactString(optionsArg.clientSecret, 4096))
28
+ throw new OidcIntrospectionError('invalid_request');
29
+ let issuer;
30
+ try {
31
+ issuer = new URL(optionsArg.issuer);
32
+ }
33
+ catch {
34
+ throw new OidcIntrospectionError('invalid_request');
35
+ }
36
+ const canonicalIssuer = issuer.origin + (issuer.pathname === '/' ? '' : issuer.pathname);
37
+ if (issuer.protocol !== 'https:' || issuer.username || issuer.password || issuer.search || issuer.hash
38
+ || issuer.pathname.includes('//') || issuer.pathname.endsWith('/') && issuer.pathname !== '/'
39
+ || canonicalIssuer !== optionsArg.issuer)
40
+ throw new OidcIntrospectionError('invalid_request');
41
+ this.#timeoutMs = optionsArg.timeoutMs ?? 5000;
42
+ this.#maxResponseBytes = optionsArg.maxResponseBytes ?? 16384;
43
+ if (!Number.isSafeInteger(this.#timeoutMs) || this.#timeoutMs < 1 || this.#timeoutMs > 30000
44
+ || !Number.isSafeInteger(this.#maxResponseBytes) || this.#maxResponseBytes < 1 || this.#maxResponseBytes > 65536) {
45
+ throw new OidcIntrospectionError('invalid_request');
46
+ }
47
+ this.#issuer = canonicalIssuer;
48
+ this.#clientId = optionsArg.clientId;
49
+ // OAuth Basic components are form-encoded separately before joining with ':'.
50
+ const encode = (valueArg) => new URLSearchParams({ value: valueArg }).toString().slice(6);
51
+ this.#authorization = `Basic ${Buffer.from(`${encode(optionsArg.clientId)}:${encode(optionsArg.clientSecret)}`, 'utf8').toString('base64')}`;
52
+ }
53
+ async introspect(requestArg) {
54
+ if (!requestArg || !exactString(requestArg.token, 16384) || !exactString(requestArg.subject)
55
+ || !exactString(requestArg.organizationId))
56
+ throw new OidcIntrospectionError('invalid_request');
57
+ // Capture bindings before crossing any asynchronous boundary.
58
+ const { token, subject, organizationId } = requestArg;
59
+ const nonce = plugins.crypto.randomBytes(32).toString('base64url');
60
+ const controller = new AbortController();
61
+ let timer;
62
+ const deadline = new Promise((_resolveArg, rejectArg) => {
63
+ timer = setTimeout(() => { controller.abort(); rejectArg(new OidcIntrospectionError('unavailable')); }, this.#timeoutMs);
64
+ });
65
+ let reader;
66
+ try {
67
+ const response = await Promise.race([fetch(`${this.#issuer}/oauth/introspect`, {
68
+ method: 'POST', redirect: 'error', credentials: 'omit', cache: 'no-store', signal: controller.signal,
69
+ headers: { authorization: this.#authorization, 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
70
+ body: new URLSearchParams({ token, subject, organization_id: organizationId, nonce, token_type_hint: 'access_token' }),
71
+ }), deadline]);
72
+ if (response.status !== 200)
73
+ throw new OidcIntrospectionError('unavailable');
74
+ if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json'
75
+ || !response.headers.get('cache-control')?.toLowerCase().split(',').map((partArg) => partArg.trim()).includes('no-store')
76
+ || !response.body)
77
+ throw new OidcIntrospectionError('invalid_response');
78
+ const contentLength = response.headers.get('content-length');
79
+ if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > this.#maxResponseBytes)) {
80
+ throw new OidcIntrospectionError('invalid_response');
81
+ }
82
+ reader = response.body.getReader();
83
+ const chunks = [];
84
+ let size = 0;
85
+ while (true) {
86
+ const part = await Promise.race([reader.read(), deadline]);
87
+ if (part.done)
88
+ break;
89
+ size += part.value.byteLength;
90
+ if (size > this.#maxResponseBytes)
91
+ throw new OidcIntrospectionError('invalid_response');
92
+ chunks.push(part.value);
93
+ }
94
+ let value;
95
+ try {
96
+ value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
97
+ }
98
+ catch {
99
+ throw new OidcIntrospectionError('invalid_response');
100
+ }
101
+ return this.validateResponse(value, subject, organizationId, nonce);
102
+ }
103
+ catch (errorArg) {
104
+ if (errorArg instanceof OidcIntrospectionError)
105
+ throw errorArg;
106
+ // Never include request, credentials, token, transport details or body in an error.
107
+ throw new OidcIntrospectionError('unavailable');
108
+ }
109
+ finally {
110
+ clearTimeout(timer);
111
+ controller.abort();
112
+ if (reader)
113
+ void reader.cancel().catch(() => undefined);
114
+ }
115
+ }
116
+ validateResponse(valueArg, subjectArg, organizationIdArg, nonceArg) {
117
+ const invalid = () => new OidcIntrospectionError('invalid_response');
118
+ if (!record(valueArg))
119
+ throw invalid();
120
+ if (valueArg.active === false && exactKeys(valueArg, ['active']))
121
+ return { active: false };
122
+ if (valueArg.active !== true || !exactKeys(valueArg, ['active', 'token_type', 'client_id', 'scope', 'iss', 'aud', 'sub', 'exp', 'iat', 'idp_global_authority'])
123
+ || valueArg.token_type !== 'Bearer' || valueArg.client_id !== this.#clientId || valueArg.aud !== this.#clientId
124
+ || valueArg.iss !== this.#issuer || valueArg.sub !== subjectArg || !revision(valueArg.exp) || !revision(valueArg.iat)
125
+ || valueArg.exp <= Math.floor(Date.now() / 1000) || valueArg.iat > Math.floor(Date.now() / 1000)
126
+ || valueArg.exp <= valueArg.iat || typeof valueArg.scope !== 'string')
127
+ throw invalid();
128
+ const scopes = valueArg.scope.split(' ');
129
+ if (!sortedUnique(scopes) || !scopes.includes('openid') || !scopes.includes('organizations')
130
+ || !scopes.every((scopeArg) => ['openid', 'profile', 'email', 'organizations', 'roles'].includes(scopeArg)))
131
+ throw invalid();
132
+ const authority = valueArg.idp_global_authority;
133
+ if (!record(authority) || !exactKeys(authority, ['version', 'nonce', 'subject_revision', 'organization_id',
134
+ 'organization_revision', 'membership_id', 'membership_revision', 'app_connection_id', 'app_connection_revision', 'roles'])
135
+ || authority.version !== 1 || authority.nonce !== nonceArg || authority.organization_id !== organizationIdArg
136
+ || !revision(authority.subject_revision) || !revision(authority.organization_revision)
137
+ || !revision(authority.membership_revision) || !revision(authority.app_connection_revision)
138
+ || !exactString(authority.membership_id) || !exactString(authority.app_connection_id) || !sortedUnique(authority.roles)
139
+ || !authority.roles.every((roleArg) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(roleArg)))
140
+ throw invalid();
141
+ return valueArg;
142
+ }
143
+ }
144
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5vaWRjLWludHJvc3BlY3Rpb24tY2xpZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHNfc2VydmVyL2NsYXNzZXMub2lkYy1pbnRyb3NwZWN0aW9uLWNsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQWdCeEMsTUFBTSxPQUFPLHNCQUF1QixTQUFRLEtBQUs7SUFDbkI7SUFBNUIsWUFBNEIsSUFBNEQ7UUFDdEYsS0FBSyxDQUFDLHNCQUFzQixJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7UUFEOUIsU0FBSSxHQUFKLElBQUksQ0FBd0Q7UUFFdEYsSUFBSSxDQUFDLElBQUksR0FBRyx3QkFBd0IsQ0FBQztJQUN2QyxDQUFDO0NBQ0Y7QUFFRCxNQUFNLFdBQVcsR0FBRyxDQUFDLFFBQWlCLEVBQUUsVUFBVSxHQUFHLEdBQUcsRUFBc0IsRUFBRSxDQUM5RSxPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksUUFBUSxDQUFDLE1BQU0sR0FBRyxDQUFDLElBQUksUUFBUSxDQUFDLE1BQU0sSUFBSSxVQUFVO09BQ2pGLFFBQVEsQ0FBQyxJQUFJLEVBQUUsS0FBSyxRQUFRLENBQUM7QUFDbEMsTUFBTSxNQUFNLEdBQUcsQ0FBQyxRQUFpQixFQUF1QyxFQUFFLENBQ3hFLFFBQVEsS0FBSyxJQUFJLElBQUksT0FBTyxRQUFRLEtBQUssUUFBUSxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUNoRixNQUFNLFNBQVMsR0FBRyxDQUFDLFFBQWlDLEVBQUUsT0FBaUIsRUFBRSxFQUFFLENBQ3pFLE1BQU0sQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsTUFBTSxLQUFLLE9BQU8sQ0FBQyxNQUFNLElBQUksT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFF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
@@ -3,4 +3,5 @@ export * from './classes.account-auth-service.js';
3
3
  export * from './classes.account-doc.js';
4
4
  export * from './classes.account-store.js';
5
5
  export * from './classes.idp-global-server-client.js';
6
+ export * from './classes.oidc-introspection-client.js';
6
7
  export * from './classes.password-hasher.js';
@@ -3,5 +3,6 @@ export * from './classes.account-auth-service.js';
3
3
  export * from './classes.account-doc.js';
4
4
  export * from './classes.account-store.js';
5
5
  export * from './classes.idp-global-server-client.js';
6
+ export * from './classes.oidc-introspection-client.js';
6
7
  export * from './classes.password-hasher.js';
7
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zZXJ2ZXIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsOEJBQThCLENBQUMifQ==
8
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zZXJ2ZXIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsd0NBQXdDLENBQUM7QUFDdkQsY0FBYyw4QkFBOEIsQ0FBQyJ9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idp.global/sdk",
3
- "version": "16.0.0",
3
+ "version": "16.1.0",
4
4
  "private": false,
5
5
  "description": "Reusable browser and server SDK for idp.global authentication.",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "@api.global/typedrequest": "^5.2.1",
18
18
  "@api.global/typedrequest-interfaces": "^5.1.1",
19
19
  "@api.global/typedsocket": "^6.0.0",
20
- "@idp.global/interfaces": "^29.0.0",
20
+ "@idp.global/interfaces": "^29.1.0",
21
21
  "@push.rocks/smartdata": "^11.7.0",
22
22
  "@push.rocks/smartjson": "^6.0.1",
23
23
  "@push.rocks/smartpromise": "^4.2.4",
@@ -30,11 +30,11 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@git.zone/tsbuild": "^4.4.2",
33
- "@git.zone/tsdoc": "^2.1.1",
33
+ "@git.zone/tsdoc": "^2.2.0",
34
34
  "@git.zone/tsrun": "^2.0.6",
35
- "@git.zone/tstest": "^4.0.0",
35
+ "@git.zone/tstest": "^4.1.1",
36
36
  "@push.rocks/smartdb": "^2.12.2",
37
- "@types/node": "^26.4.1"
37
+ "@types/node": "26.4.1"
38
38
  },
39
39
  "files": [
40
40
  "ts/**/*",
@@ -44,7 +44,7 @@
44
44
  "dist_*/**/*",
45
45
  "readme.md",
46
46
  "changelog.md",
47
- "license"
47
+ "license.md"
48
48
  ],
49
49
  "repository": {
50
50
  "type": "git",
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @idp.global/sdk
2
2
 
3
- Reusable TypeScript SDK for building against idp.global from both sides of the wire: browser apps that need SSO, JWT housekeeping, transfer-token handoff, and typed IdP requests; and server apps that need explicit account storage, local password auth, and optional idp.global password verification.
3
+ Reusable TypeScript SDK for building against idp.global from both sides of the wire: browser apps that need SSO, JWT housekeeping, transfer-token handoff, and typed IdP requests; and server apps that need explicit account storage, local password auth, optional idp.global password verification, and confidential OIDC membership introspection.
4
4
 
5
5
  The package is intentionally split by runtime. Use `@idp.global/sdk/browser` in browser bundles and `@idp.global/sdk/server` in Node.js services. The root `@idp.global/sdk` export does not expose application APIs.
6
6
 
@@ -13,7 +13,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
13
13
  | Runtime | Import | What it does |
14
14
  | --- | --- | --- |
15
15
  | Browser | `@idp.global/sdk/browser` | SSO client, JWT and refresh-token storage, login redirects, logout, transfer-token exchange, and typed request shortcuts for user, organization, billing, OIDC, and admin flows. |
16
- | Server | `@idp.global/sdk/server` | Smartdata-backed account store, scrypt password hashing, local/idp.global account authentication, and a server-side typed-socket client for idp.global password login. |
16
+ | Server | `@idp.global/sdk/server` | Smartdata-backed account store, scrypt password hashing, local/idp.global account authentication, a server-side typed-socket client for idp.global password login, and confidential OIDC introspection. |
17
17
 
18
18
  ## Install
19
19
 
@@ -291,6 +291,46 @@ themselves.
291
291
  Verification is keyset-only. Replace the cached snapshot on every push, select
292
292
  the verification key by the JWT header `kid`, and reject unknown key IDs.
293
293
 
294
+ ### Live Organization Authority
295
+
296
+ `OidcIntrospectionClient` calls the public issuer's `/oauth/introspect` endpoint
297
+ with confidential OAuth Basic authentication. Keep the client secret and access
298
+ token on the server. The token must belong to that same OAuth client and include
299
+ `openid organizations`; use the exact subject from the validated OIDC login and
300
+ the canonical organization ID.
301
+
302
+ ```ts
303
+ import { OidcIntrospectionClient } from '@idp.global/sdk/server';
304
+
305
+ const introspection = new OidcIntrospectionClient({
306
+ issuer: 'https://idp.global',
307
+ clientId: process.env.IDP_OIDC_CLIENT_ID!,
308
+ clientSecret: process.env.IDP_OIDC_CLIENT_SECRET!,
309
+ });
310
+ const authority = await introspection.introspect({
311
+ token: serverSession.accessToken,
312
+ subject: serverSession.idpSubject,
313
+ organizationId: requestedOrganizationId,
314
+ });
315
+ if (!authority.active) throw new Error('Organization access denied.');
316
+ const currentRoles = authority.idp_global_authority.roles;
317
+ ```
318
+
319
+ Every call generates a fresh nonce and validates the exact issuer, client,
320
+ subject, organization, response shape, revisions, and current roles. The default
321
+ request deadline is five seconds and the response limit is 16 KiB; redirects
322
+ are rejected. `OidcIntrospectionError` exposes only a sanitized `code`:
323
+ `invalid_request`, `invalid_response`, or `unavailable`. Deny the operation when
324
+ introspection fails.
325
+
326
+ An active result observes current authority while the provider holds its
327
+ mutation fences. It is not a cached membership grant: membership can change
328
+ after that observation. `exp` bounds token, session, and refresh-family expiry;
329
+ it does not promise membership remains valid until then. Keep the application's
330
+ existing login and principal binding and make a fresh observation for each
331
+ operation requiring live authority. The response contains no identity profile
332
+ or email-based organization inference.
333
+
294
334
  ### Server API Surface
295
335
 
296
336
  | API | Description |
@@ -298,6 +338,8 @@ the verification key by the JWT header `kid`, and reject unknown key IDs.
298
338
  | `SmartdataAccountStore` | Smartdata-backed account persistence and local password verification. |
299
339
  | `AccountAuthService` | Existing-account authentication orchestration for `local`, `idp.global`, and `auto`. |
300
340
  | `IdpGlobalServerClient` | Typed-socket client for idp.global password login, JWT refresh, `whoIs` lookup, backend JWT validation fetches, and JWT key/blocklist pushes. |
341
+ | `OidcIntrospectionClient` | Confidential server client for an exact live organization authority observation. |
342
+ | `OidcIntrospectionError` | Sanitized introspection failure with a stable error code. |
301
343
  | `IdpMfaRequiredError` | Password-login error carrying the MFA challenge token and available factor methods. |
302
344
  | `defaultIdpGlobalBaseUrl` | Default hosted server origin: `https://app.idp.global`. |
303
345
  | `PasswordHasher` | Static `hashPassword()` and `verifyPassword()` helpers using `scrypt:v1`. |
@@ -310,6 +352,8 @@ The server export includes these TypeScript types:
310
352
 
311
353
  | Type | Values or purpose |
312
354
  | --- | --- |
355
+ | `IOidcIntrospectionClientOptions` | Exact HTTPS issuer, confidential client credentials, and optional deadline/response limits. |
356
+ | `IOidcIntrospectionClientRequest` | Server-held access token, exact subject, and canonical organization ID. |
313
357
  | `TIdpAccountAuthSource` | `'local' | 'idp.global'` |
314
358
  | `TIdpAccountRole` | `'admin' | 'user'` |
315
359
  | `TIdpAccountStatus` | `'active' | 'disabled'` |
@@ -341,7 +385,7 @@ The test suite covers the server account store and auth service, browser transfe
341
385
 
342
386
  ## License and Legal Information
343
387
 
344
- This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
388
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license.md](./license.md) file.
345
389
 
346
390
  **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
347
391
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@idp.global/sdk',
6
- version: '16.0.0',
6
+ version: '16.1.0',
7
7
  description: 'Reusable browser and server SDK for idp.global authentication.'
8
8
  }
@@ -0,0 +1,140 @@
1
+ import * as plugins from './plugins.js';
2
+
3
+ export interface IOidcIntrospectionClientOptions {
4
+ issuer: string;
5
+ clientId: string;
6
+ clientSecret: string;
7
+ timeoutMs?: number;
8
+ maxResponseBytes?: number;
9
+ }
10
+
11
+ export interface IOidcIntrospectionClientRequest {
12
+ token: string;
13
+ subject: string;
14
+ organizationId: string;
15
+ }
16
+
17
+ export class OidcIntrospectionError extends Error {
18
+ constructor(public readonly code: 'invalid_request' | 'invalid_response' | 'unavailable') {
19
+ super(`OIDC introspection ${code.replace(/_/g, ' ')}.`);
20
+ this.name = 'OidcIntrospectionError';
21
+ }
22
+ }
23
+
24
+ const exactString = (valueArg: unknown, maximumArg = 512): valueArg is string =>
25
+ typeof valueArg === 'string' && valueArg.length > 0 && valueArg.length <= maximumArg
26
+ && valueArg.trim() === valueArg;
27
+ const record = (valueArg: unknown): valueArg is Record<string, unknown> =>
28
+ valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg);
29
+ const exactKeys = (valueArg: Record<string, unknown>, keysArg: string[]) =>
30
+ Object.keys(valueArg).length === keysArg.length && keysArg.every((keyArg) => Object.hasOwn(valueArg, keyArg));
31
+ const revision = (valueArg: unknown): valueArg is number => Number.isSafeInteger(valueArg) && (valueArg as number) >= 0;
32
+ const sortedUnique = (valuesArg: unknown): valuesArg is string[] => Array.isArray(valuesArg)
33
+ && valuesArg.length > 0 && valuesArg.every((valueArg, indexArg) => exactString(valueArg, 64)
34
+ && (indexArg === 0 || valuesArg[indexArg - 1] < valueArg));
35
+
36
+ /** Server-only confidential client. Each result is one live observation, never a cached grant. */
37
+ export class OidcIntrospectionClient {
38
+ readonly #issuer: string;
39
+ readonly #clientId: string;
40
+ readonly #authorization: string;
41
+ readonly #timeoutMs: number;
42
+ readonly #maxResponseBytes: number;
43
+
44
+ constructor(optionsArg: IOidcIntrospectionClientOptions) {
45
+ if (!optionsArg || !exactString(optionsArg.issuer, 2048) || !exactString(optionsArg.clientId)
46
+ || !exactString(optionsArg.clientSecret, 4096)) throw new OidcIntrospectionError('invalid_request');
47
+ let issuer: URL;
48
+ try { issuer = new URL(optionsArg.issuer); } catch { throw new OidcIntrospectionError('invalid_request'); }
49
+ const canonicalIssuer = issuer.origin + (issuer.pathname === '/' ? '' : issuer.pathname);
50
+ if (issuer.protocol !== 'https:' || issuer.username || issuer.password || issuer.search || issuer.hash
51
+ || issuer.pathname.includes('//') || issuer.pathname.endsWith('/') && issuer.pathname !== '/'
52
+ || canonicalIssuer !== optionsArg.issuer) throw new OidcIntrospectionError('invalid_request');
53
+ this.#timeoutMs = optionsArg.timeoutMs ?? 5000;
54
+ this.#maxResponseBytes = optionsArg.maxResponseBytes ?? 16384;
55
+ if (!Number.isSafeInteger(this.#timeoutMs) || this.#timeoutMs < 1 || this.#timeoutMs > 30000
56
+ || !Number.isSafeInteger(this.#maxResponseBytes) || this.#maxResponseBytes < 1 || this.#maxResponseBytes > 65536) {
57
+ throw new OidcIntrospectionError('invalid_request');
58
+ }
59
+ this.#issuer = canonicalIssuer;
60
+ this.#clientId = optionsArg.clientId;
61
+ // OAuth Basic components are form-encoded separately before joining with ':'.
62
+ const encode = (valueArg: string) => new URLSearchParams({ value: valueArg }).toString().slice(6);
63
+ this.#authorization = `Basic ${Buffer.from(`${encode(optionsArg.clientId)}:${encode(optionsArg.clientSecret)}`, 'utf8').toString('base64')}`;
64
+ }
65
+
66
+ public async introspect(requestArg: IOidcIntrospectionClientRequest): Promise<plugins.idpInterfaces.data.TOidcIntrospectionResponse> {
67
+ if (!requestArg || !exactString(requestArg.token, 16384) || !exactString(requestArg.subject)
68
+ || !exactString(requestArg.organizationId)) throw new OidcIntrospectionError('invalid_request');
69
+ // Capture bindings before crossing any asynchronous boundary.
70
+ const { token, subject, organizationId } = requestArg;
71
+ const nonce = plugins.crypto.randomBytes(32).toString('base64url');
72
+ const controller = new AbortController();
73
+ let timer: ReturnType<typeof setTimeout>;
74
+ const deadline = new Promise<never>((_resolveArg, rejectArg) => {
75
+ timer = setTimeout(() => { controller.abort(); rejectArg(new OidcIntrospectionError('unavailable')); }, this.#timeoutMs);
76
+ });
77
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
78
+ try {
79
+ const response = await Promise.race([fetch(`${this.#issuer}/oauth/introspect`, {
80
+ method: 'POST', redirect: 'error', credentials: 'omit', cache: 'no-store', signal: controller.signal,
81
+ headers: { authorization: this.#authorization, 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
82
+ body: new URLSearchParams({ token, subject, organization_id: organizationId, nonce, token_type_hint: 'access_token' }),
83
+ }), deadline]);
84
+ if (response.status !== 200) throw new OidcIntrospectionError('unavailable');
85
+ if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json'
86
+ || !response.headers.get('cache-control')?.toLowerCase().split(',').map((partArg) => partArg.trim()).includes('no-store')
87
+ || !response.body) throw new OidcIntrospectionError('invalid_response');
88
+ const contentLength = response.headers.get('content-length');
89
+ if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > this.#maxResponseBytes)) {
90
+ throw new OidcIntrospectionError('invalid_response');
91
+ }
92
+ reader = response.body.getReader();
93
+ const chunks: Uint8Array[] = [];
94
+ let size = 0;
95
+ while (true) {
96
+ const part = await Promise.race([reader.read(), deadline]);
97
+ if (part.done) break;
98
+ size += part.value.byteLength;
99
+ if (size > this.#maxResponseBytes) throw new OidcIntrospectionError('invalid_response');
100
+ chunks.push(part.value);
101
+ }
102
+ let value: unknown;
103
+ try { value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks))); }
104
+ catch { throw new OidcIntrospectionError('invalid_response'); }
105
+ return this.validateResponse(value, subject, organizationId, nonce);
106
+ } catch (errorArg) {
107
+ if (errorArg instanceof OidcIntrospectionError) throw errorArg;
108
+ // Never include request, credentials, token, transport details or body in an error.
109
+ throw new OidcIntrospectionError('unavailable');
110
+ } finally {
111
+ clearTimeout(timer);
112
+ controller.abort();
113
+ if (reader) void reader.cancel().catch(() => undefined);
114
+ }
115
+ }
116
+
117
+ private validateResponse(valueArg: unknown, subjectArg: string, organizationIdArg: string, nonceArg: string): plugins.idpInterfaces.data.TOidcIntrospectionResponse {
118
+ const invalid = () => new OidcIntrospectionError('invalid_response');
119
+ if (!record(valueArg)) throw invalid();
120
+ if (valueArg.active === false && exactKeys(valueArg, ['active'])) return { active: false };
121
+ if (valueArg.active !== true || !exactKeys(valueArg,
122
+ ['active', 'token_type', 'client_id', 'scope', 'iss', 'aud', 'sub', 'exp', 'iat', 'idp_global_authority'])
123
+ || valueArg.token_type !== 'Bearer' || valueArg.client_id !== this.#clientId || valueArg.aud !== this.#clientId
124
+ || valueArg.iss !== this.#issuer || valueArg.sub !== subjectArg || !revision(valueArg.exp) || !revision(valueArg.iat)
125
+ || valueArg.exp <= Math.floor(Date.now() / 1000) || valueArg.iat > Math.floor(Date.now() / 1000)
126
+ || valueArg.exp <= valueArg.iat || typeof valueArg.scope !== 'string') throw invalid();
127
+ const scopes = valueArg.scope.split(' ');
128
+ if (!sortedUnique(scopes) || !scopes.includes('openid') || !scopes.includes('organizations')
129
+ || !scopes.every((scopeArg) => ['openid', 'profile', 'email', 'organizations', 'roles'].includes(scopeArg))) throw invalid();
130
+ const authority = valueArg.idp_global_authority;
131
+ if (!record(authority) || !exactKeys(authority, ['version', 'nonce', 'subject_revision', 'organization_id',
132
+ 'organization_revision', 'membership_id', 'membership_revision', 'app_connection_id', 'app_connection_revision', 'roles'])
133
+ || authority.version !== 1 || authority.nonce !== nonceArg || authority.organization_id !== organizationIdArg
134
+ || !revision(authority.subject_revision) || !revision(authority.organization_revision)
135
+ || !revision(authority.membership_revision) || !revision(authority.app_connection_revision)
136
+ || !exactString(authority.membership_id) || !exactString(authority.app_connection_id) || !sortedUnique(authority.roles)
137
+ || !authority.roles.every((roleArg) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(roleArg))) throw invalid();
138
+ return valueArg as unknown as plugins.idpInterfaces.data.IOidcActiveIntrospectionResponse;
139
+ }
140
+ }
@@ -3,4 +3,5 @@ export * from './classes.account-auth-service.js';
3
3
  export * from './classes.account-doc.js';
4
4
  export * from './classes.account-store.js';
5
5
  export * from './classes.idp-global-server-client.js';
6
+ export * from './classes.oidc-introspection-client.js';
6
7
  export * from './classes.password-hasher.js';
File without changes