@idp.global/sdk 16.0.0 → 16.1.1
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 +24 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_server/classes.oidc-introspection-client.d.ts +24 -0
- package/dist_ts_server/classes.oidc-introspection-client.js +144 -0
- package/dist_ts_server/index.d.ts +1 -0
- package/dist_ts_server/index.js +2 -1
- package/package.json +6 -6
- package/readme.md +47 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_server/classes.oidc-introspection-client.ts +140 -0
- package/ts_server/index.ts +1 -0
- /package/{license → license.md} +0 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-09-09 - 16.1.1
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- bump @git.zone/tstest to ^5.0.0 (deps-dev)
|
|
8
|
+
- Updates the tstest development dependency from ^4.1.1 to ^5.0.0.
|
|
9
|
+
|
|
10
|
+
## 2026-09-09 - 16.1.0
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- add confidential OIDC introspection client (server)
|
|
15
|
+
- Add a server-only OidcIntrospectionClient with Basic authentication, nonce binding, deadlines, response size limits, and sanitized errors.
|
|
16
|
+
- Validate active and inactive introspection responses against issuer, client, subject, organization, scopes, revisions, roles, and freshness.
|
|
17
|
+
- Export the client and error types from the server entrypoint and document live organization authority usage.
|
|
18
|
+
- Update @idp.global/interfaces to ^29.1.0 for introspection response types.
|
|
19
|
+
|
|
20
|
+
### Fixes
|
|
21
|
+
|
|
22
|
+
- update development tooling dependencies (tooling)
|
|
23
|
+
- Bump @git.zone/tsdoc to ^2.2.0 and @git.zone/tstest to ^4.1.1
|
|
24
|
+
- Pin @types/node to 26.4.1
|
|
25
|
+
- Update packageManager to pnpm@12.2.1
|
|
26
|
+
|
|
3
27
|
## 2026-09-05 - 16.0.0
|
|
4
28
|
|
|
5
29
|
### Breaking Changes
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@idp.global/sdk',
|
|
6
|
-
version: '16.
|
|
6
|
+
version: '16.1.1',
|
|
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';
|
package/dist_ts_server/index.js
CHANGED
|
@@ -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,
|
|
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.
|
|
3
|
+
"version": "16.1.1",
|
|
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.
|
|
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.
|
|
33
|
+
"@git.zone/tsdoc": "^2.2.0",
|
|
34
34
|
"@git.zone/tsrun": "^2.0.6",
|
|
35
|
-
"@git.zone/tstest": "^
|
|
35
|
+
"@git.zone/tstest": "^5.0.0",
|
|
36
36
|
"@push.rocks/smartdb": "^2.12.2",
|
|
37
|
-
"@types/node": "
|
|
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,
|
|
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,
|
|
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
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -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
|
+
}
|
package/ts_server/index.ts
CHANGED
|
@@ -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';
|
/package/{license → license.md}
RENAMED
|
File without changes
|