@idp.global/sdk 16.1.1 → 17.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 +20 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_server/classes.oidc-userinfo-client.d.ts +22 -0
- package/dist_ts_server/classes.oidc-userinfo-client.js +154 -0
- package/dist_ts_server/index.d.ts +1 -0
- package/dist_ts_server/index.js +2 -1
- package/package.json +10 -9
- package/readme.md +46 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_server/classes.oidc-userinfo-client.ts +143 -0
- package/ts_server/index.ts +1 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-09-13 - 17.1.0
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- Update release tooling, require build and test preflight, and qualify account persistence against NoSQLDB 10.2.0.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Add a subject-bound server UserInfo client with bounded transport and validated organization display claims.
|
|
12
|
+
|
|
13
|
+
## 2026-09-09 - 17.0.0
|
|
14
|
+
|
|
15
|
+
### Breaking Changes
|
|
16
|
+
|
|
17
|
+
- migrate SDK transports to TypedRequest 8 and TypedSocket 8 (transport)
|
|
18
|
+
- Upgrades the typed transport dependency stack, including TypedRequest, TypedSocket, SmartServe, SmartDB, and TypedServer test support.
|
|
19
|
+
- Adds native transport integration coverage for server handshake, login, public key retrieval, targeted public key pushes, and browser HTTP/WebSocket RPCs.
|
|
20
|
+
- Updates account-store test fixtures to use an in-memory SmartdbServer with cleanup coverage for failed startup paths.
|
|
21
|
+
- Documents that SDK clients and IdP listeners must be upgraded together because earlier transport majors are incompatible.
|
|
22
|
+
|
|
3
23
|
## 2026-09-09 - 16.1.1
|
|
4
24
|
|
|
5
25
|
### Fixes
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@idp.global/sdk',
|
|
6
|
-
version: '
|
|
6
|
+
version: '17.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,22 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
export interface IOidcUserInfoClientOptions {
|
|
3
|
+
issuer: string;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
maxResponseBytes?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface IOidcUserInfoClientRequest {
|
|
8
|
+
token: string;
|
|
9
|
+
/** Subject already established by the application's verified OIDC login. */
|
|
10
|
+
subject: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class OidcUserInfoError extends Error {
|
|
13
|
+
readonly code: 'invalid_request' | 'invalid_response' | 'unauthenticated' | 'unavailable';
|
|
14
|
+
constructor(code: 'invalid_request' | 'invalid_response' | 'unauthenticated' | 'unavailable');
|
|
15
|
+
}
|
|
16
|
+
/** Fetches display claims using a server-held token; results are never authorization grants. */
|
|
17
|
+
export declare class OidcUserInfoClient {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(optionsArg: IOidcUserInfoClientOptions);
|
|
20
|
+
getUserInfo(requestArg: IOidcUserInfoClientRequest): Promise<plugins.idpInterfaces.data.IUserInfoResponse>;
|
|
21
|
+
private validateResponse;
|
|
22
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
export class OidcUserInfoError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code) {
|
|
5
|
+
super(`OIDC userinfo ${code.replace(/_/g, ' ')}.`);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'OidcUserInfoError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const record = (valueArg) => valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg);
|
|
11
|
+
const exactString = (valueArg, maximumArg = 512) => typeof valueArg === 'string' && valueArg.length > 0 && valueArg.length <= maximumArg
|
|
12
|
+
&& valueArg.trim() === valueArg && !/[\u0000-\u001f\u007f]/.test(valueArg);
|
|
13
|
+
const strings = (valueArg) => Array.isArray(valueArg) && valueArg.every((entryArg) => exactString(entryArg))
|
|
14
|
+
&& new Set(valueArg).size === valueArg.length;
|
|
15
|
+
/** Fetches display claims using a server-held token; results are never authorization grants. */
|
|
16
|
+
export class OidcUserInfoClient {
|
|
17
|
+
#endpoint;
|
|
18
|
+
#timeoutMs;
|
|
19
|
+
#maxResponseBytes;
|
|
20
|
+
constructor(optionsArg) {
|
|
21
|
+
if (!optionsArg || !exactString(optionsArg.issuer, 2048))
|
|
22
|
+
throw new OidcUserInfoError('invalid_request');
|
|
23
|
+
let issuer;
|
|
24
|
+
try {
|
|
25
|
+
issuer = new URL(optionsArg.issuer);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new OidcUserInfoError('invalid_request');
|
|
29
|
+
}
|
|
30
|
+
const canonicalIssuer = issuer.origin + (issuer.pathname === '/' ? '' : issuer.pathname);
|
|
31
|
+
if (issuer.protocol !== 'https:' || issuer.username || issuer.password || issuer.search || issuer.hash
|
|
32
|
+
|| issuer.pathname.includes('//') || issuer.pathname.endsWith('/') && issuer.pathname !== '/'
|
|
33
|
+
|| canonicalIssuer !== optionsArg.issuer)
|
|
34
|
+
throw new OidcUserInfoError('invalid_request');
|
|
35
|
+
this.#timeoutMs = optionsArg.timeoutMs ?? 5000;
|
|
36
|
+
this.#maxResponseBytes = optionsArg.maxResponseBytes ?? 65536;
|
|
37
|
+
if (!Number.isSafeInteger(this.#timeoutMs) || this.#timeoutMs < 1 || this.#timeoutMs > 30000
|
|
38
|
+
|| !Number.isSafeInteger(this.#maxResponseBytes) || this.#maxResponseBytes < 1 || this.#maxResponseBytes > 1048576) {
|
|
39
|
+
throw new OidcUserInfoError('invalid_request');
|
|
40
|
+
}
|
|
41
|
+
this.#endpoint = `${canonicalIssuer}/oauth/userinfo`;
|
|
42
|
+
}
|
|
43
|
+
async getUserInfo(requestArg) {
|
|
44
|
+
if (!requestArg || !exactString(requestArg.subject) || !exactString(requestArg.token, 16384)
|
|
45
|
+
|| !/^[A-Za-z0-9\-._~+/]+=*$/.test(requestArg.token))
|
|
46
|
+
throw new OidcUserInfoError('invalid_request');
|
|
47
|
+
// Snapshot the principal and token before any asynchronous work.
|
|
48
|
+
const { token, subject } = requestArg;
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
let timer;
|
|
51
|
+
const deadline = new Promise((_resolveArg, rejectArg) => {
|
|
52
|
+
timer = setTimeout(() => {
|
|
53
|
+
controller.abort();
|
|
54
|
+
rejectArg(new OidcUserInfoError('unavailable'));
|
|
55
|
+
}, this.#timeoutMs);
|
|
56
|
+
});
|
|
57
|
+
let reader;
|
|
58
|
+
try {
|
|
59
|
+
const response = await Promise.race([fetch(this.#endpoint, {
|
|
60
|
+
method: 'GET', redirect: 'error', credentials: 'omit', cache: 'no-store', signal: controller.signal,
|
|
61
|
+
headers: { authorization: `Bearer ${token}`, accept: 'application/json' },
|
|
62
|
+
}), deadline]);
|
|
63
|
+
if (response.status === 401 || response.status === 403)
|
|
64
|
+
throw new OidcUserInfoError('unauthenticated');
|
|
65
|
+
if (response.status !== 200)
|
|
66
|
+
throw new OidcUserInfoError('unavailable');
|
|
67
|
+
if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json'
|
|
68
|
+
|| !response.headers.get('cache-control')?.toLowerCase().split(',').map((partArg) => partArg.trim()).includes('no-store')
|
|
69
|
+
|| !response.body)
|
|
70
|
+
throw new OidcUserInfoError('invalid_response');
|
|
71
|
+
const contentLength = response.headers.get('content-length');
|
|
72
|
+
if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > this.#maxResponseBytes)) {
|
|
73
|
+
throw new OidcUserInfoError('invalid_response');
|
|
74
|
+
}
|
|
75
|
+
reader = response.body.getReader();
|
|
76
|
+
const chunks = [];
|
|
77
|
+
let size = 0;
|
|
78
|
+
while (true) {
|
|
79
|
+
const part = await Promise.race([reader.read(), deadline]);
|
|
80
|
+
if (part.done)
|
|
81
|
+
break;
|
|
82
|
+
size += part.value.byteLength;
|
|
83
|
+
if (size > this.#maxResponseBytes)
|
|
84
|
+
throw new OidcUserInfoError('invalid_response');
|
|
85
|
+
chunks.push(part.value);
|
|
86
|
+
}
|
|
87
|
+
let value;
|
|
88
|
+
try {
|
|
89
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new OidcUserInfoError('invalid_response');
|
|
93
|
+
}
|
|
94
|
+
return this.validateResponse(value, subject);
|
|
95
|
+
}
|
|
96
|
+
catch (errorArg) {
|
|
97
|
+
if (errorArg instanceof OidcUserInfoError)
|
|
98
|
+
throw errorArg;
|
|
99
|
+
// Transport errors can contain credentials, URLs or response bodies.
|
|
100
|
+
throw new OidcUserInfoError('unavailable');
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
controller.abort();
|
|
105
|
+
if (reader)
|
|
106
|
+
void reader.cancel().catch(() => undefined);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
validateResponse(valueArg, subjectArg) {
|
|
110
|
+
const invalid = () => new OidcUserInfoError('invalid_response');
|
|
111
|
+
if (!record(valueArg) || valueArg.sub !== subjectArg)
|
|
112
|
+
throw invalid();
|
|
113
|
+
const result = { sub: subjectArg };
|
|
114
|
+
for (const key of ['name', 'preferred_username', 'picture', 'email']) {
|
|
115
|
+
if (!Object.hasOwn(valueArg, key))
|
|
116
|
+
continue;
|
|
117
|
+
const value = valueArg[key];
|
|
118
|
+
if (typeof value !== 'string' || value.length > 4096)
|
|
119
|
+
throw invalid();
|
|
120
|
+
result[key] = value;
|
|
121
|
+
}
|
|
122
|
+
if (Object.hasOwn(valueArg, 'email_verified')) {
|
|
123
|
+
if (typeof valueArg.email_verified !== 'boolean')
|
|
124
|
+
throw invalid();
|
|
125
|
+
result.email_verified = valueArg.email_verified;
|
|
126
|
+
}
|
|
127
|
+
for (const key of ['roles', 'app_roles', 'app_permissions', 'app_scopes']) {
|
|
128
|
+
if (!Object.hasOwn(valueArg, key))
|
|
129
|
+
continue;
|
|
130
|
+
const value = valueArg[key];
|
|
131
|
+
if (!strings(value))
|
|
132
|
+
throw invalid();
|
|
133
|
+
result[key] = [...value];
|
|
134
|
+
}
|
|
135
|
+
if (Object.hasOwn(valueArg, 'organizations')) {
|
|
136
|
+
if (!Array.isArray(valueArg.organizations))
|
|
137
|
+
throw invalid();
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
result.organizations = valueArg.organizations.map((organizationArg) => {
|
|
140
|
+
if (!record(organizationArg) || !exactString(organizationArg.id)
|
|
141
|
+
|| typeof organizationArg.name !== 'string' || organizationArg.name.length > 4096
|
|
142
|
+
|| !exactString(organizationArg.slug) || !strings(organizationArg.roles)
|
|
143
|
+
|| seen.has(organizationArg.id)
|
|
144
|
+
|| Object.keys(organizationArg).some((keyArg) => !['id', 'name', 'slug', 'roles'].includes(keyArg)))
|
|
145
|
+
throw invalid();
|
|
146
|
+
seen.add(organizationArg.id);
|
|
147
|
+
return { id: organizationArg.id, name: organizationArg.name, slug: organizationArg.slug, roles: [...organizationArg.roles] };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// OIDC allows additional claims; only the documented, validated fields leave the SDK.
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5vaWRjLXVzZXJpbmZvLWNsaWVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX3NlcnZlci9jbGFzc2VzLm9pZGMtdXNlcmluZm8tY2xpZW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBY3hDLE1BQU0sT0FBTyxpQkFBa0IsU0FBUSxLQUFLO0lBQ2Q7SUFBNUIsWUFBNEIsSUFBZ0Y7UUFDMUcsS0FBSyxDQUFDLGlCQUFpQixJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7UUFEekIsU0FBSSxHQUFKLElBQUksQ0FBNEU7UUFFMUcsSUFBSSxDQUFDLElBQUksR0FBRyxtQkFBbUIsQ0FBQztJQUNsQyxDQUFDO0NBQ0Y7QUFFRCxNQUFNLE1BQU0sR0FBRyxDQUFDLFFBQWlCLEVBQXVDLEVBQUUsQ0FDeEUsUUFBUSxLQUFLLElBQUksSUFBSSxPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0FBQ2hGLE1BQU0sV0FBVyxHQUFHLENBQUMsUUFBaUIsRUFBRSxVQUFVLEdBQUcsR0FBRyxFQUFzQixFQUFFLENBQzlFLE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsSUFBSSxRQUFRLENBQUMsTUFBTSxJQUFJLFVBQVU7T0FDakYsUUFBUSxDQUFDLElBQUksRUFBRSxLQUFLLFFBQVEsSUFBSSxDQUFDLHVCQUF1QixDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUM3RSxNQUFNLE9BQU8sR0FBRyxDQUFDLFFBQWlCLEVBQXdCLEVBQUUsQ0FDMUQsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSSxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUMsUUFBUSxFQUFFLEVBQUUsQ0FBQyxXQUFX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
|
|
@@ -4,4 +4,5 @@ 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
6
|
export * from './classes.oidc-introspection-client.js';
|
|
7
|
+
export * from './classes.oidc-userinfo-client.js';
|
|
7
8
|
export * from './classes.password-hasher.js';
|
package/dist_ts_server/index.js
CHANGED
|
@@ -4,5 +4,6 @@ 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
6
|
export * from './classes.oidc-introspection-client.js';
|
|
7
|
+
export * from './classes.oidc-userinfo-client.js';
|
|
7
8
|
export * from './classes.password-hasher.js';
|
|
8
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
9
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zZXJ2ZXIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsd0NBQXdDLENBQUM7QUFDdkQsY0FBYyxtQ0FBbUMsQ0FBQztBQUNsRCxjQUFjLDhCQUE4QixDQUFDIn0=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idp.global/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable browser and server SDK for idp.global authentication.",
|
|
6
6
|
"type": "module",
|
|
@@ -14,27 +14,28 @@
|
|
|
14
14
|
"author": "Task Venture Capital GmbH",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@api.global/typedrequest": "^
|
|
18
|
-
"@api.global/typedrequest-interfaces": "^
|
|
19
|
-
"@api.global/typedsocket": "^
|
|
17
|
+
"@api.global/typedrequest": "^8.0.4",
|
|
18
|
+
"@api.global/typedrequest-interfaces": "^7.1.0",
|
|
19
|
+
"@api.global/typedsocket": "^8.1.0",
|
|
20
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",
|
|
24
24
|
"@push.rocks/smartrx": "^3.0.12",
|
|
25
|
-
"@push.rocks/smartserve": "^
|
|
25
|
+
"@push.rocks/smartserve": "^6.2.2",
|
|
26
26
|
"@push.rocks/smarttime": "^4.2.3",
|
|
27
27
|
"@push.rocks/smarturl": "^3.1.0",
|
|
28
28
|
"@push.rocks/webjwt": "^1.0.10",
|
|
29
29
|
"@push.rocks/webstore": "^2.1.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
+
"@api.global/typedserver": "^11.4.0",
|
|
32
33
|
"@git.zone/tsbuild": "^4.4.2",
|
|
33
34
|
"@git.zone/tsdoc": "^2.2.0",
|
|
34
35
|
"@git.zone/tsrun": "^2.0.6",
|
|
35
|
-
"@git.zone/tstest": "^
|
|
36
|
-
"@
|
|
37
|
-
"@
|
|
36
|
+
"@git.zone/tstest": "^6.0.0",
|
|
37
|
+
"@types/node": "26.4.1",
|
|
38
|
+
"@lossless.org/nosqldb": "10.2.0"
|
|
38
39
|
},
|
|
39
40
|
"files": [
|
|
40
41
|
"ts/**/*",
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
],
|
|
67
68
|
"scripts": {
|
|
68
69
|
"test": "pnpm run build && tstest test/ --verbose --logfile --timeout 60",
|
|
69
|
-
"build": "tsbuild tsfolders --web
|
|
70
|
+
"build": "tsbuild tsfolders --web",
|
|
70
71
|
"buildDocs": "tsdoc"
|
|
71
72
|
}
|
|
72
73
|
}
|
package/readme.md
CHANGED
|
@@ -23,6 +23,13 @@ pnpm add @idp.global/sdk
|
|
|
23
23
|
|
|
24
24
|
This is an ESM TypeScript package. Published builds expose declaration files and runtime JavaScript through package exports.
|
|
25
25
|
|
|
26
|
+
Version 17 requires an IdP listener using TypedSocket 8 and TypedRequest 8
|
|
27
|
+
(TypedServer 11). Earlier listener and SDK transport majors cannot share the
|
|
28
|
+
WebSocket handshake. Upgrade the IdP app, its browser bundle, and services using
|
|
29
|
+
`IdpGlobalServerClient` together; this includes dcrouter's IdP password-login
|
|
30
|
+
flow. HTTP typed requests also use TypedRequest 8 response identities. The
|
|
31
|
+
confidential OIDC introspection client continues to use its separate HTTP API.
|
|
32
|
+
|
|
26
33
|
## Browser SDK
|
|
27
34
|
|
|
28
35
|
Import the browser client from the browser subpath:
|
|
@@ -331,6 +338,41 @@ existing login and principal binding and make a fresh observation for each
|
|
|
331
338
|
operation requiring live authority. The response contains no identity profile
|
|
332
339
|
or email-based organization inference.
|
|
333
340
|
|
|
341
|
+
### Server-held OIDC UserInfo
|
|
342
|
+
|
|
343
|
+
Use `OidcUserInfoClient` to obtain current profile and organization picker data
|
|
344
|
+
with an access token held by your application's server session:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { OidcUserInfoClient } from '@idp.global/sdk/server';
|
|
348
|
+
|
|
349
|
+
const userInfoClient = new OidcUserInfoClient({ issuer: 'https://idp.global' });
|
|
350
|
+
const claims = await userInfoClient.getUserInfo({
|
|
351
|
+
token: serverSession.accessToken,
|
|
352
|
+
subject: serverSession.idpSubject,
|
|
353
|
+
});
|
|
354
|
+
const ownerOrganizations = claims.organizations?.filter((organization) =>
|
|
355
|
+
organization.roles.includes('owner')) ?? [];
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The expected subject must come from the verified OIDC login. Keep the token
|
|
359
|
+
server-side; the SDK sends it only in the bearer header to the issuer's
|
|
360
|
+
`/oauth/userinfo` endpoint, rejects redirects, and validates the returned subject
|
|
361
|
+
and documented claims. Unknown top-level OIDC extension claims are omitted.
|
|
362
|
+
The client retains no access token or claim cache. Calls have a five-second
|
|
363
|
+
deadline, including body reads, and a 64 KiB response limit. Options allow a
|
|
364
|
+
deadline up to 30 seconds and a response limit up to 1 MiB.
|
|
365
|
+
|
|
366
|
+
Organization claims are display data within the token's original organization
|
|
367
|
+
grant. Newly granted organizations require a new OIDC login; removed memberships
|
|
368
|
+
or app connections disappear from subsequent responses. Authorize each protected
|
|
369
|
+
operation separately with fresh `OidcIntrospectionClient` introspection. UserInfo
|
|
370
|
+
does not turn a picker selection or an observed role into an authorization grant.
|
|
371
|
+
|
|
372
|
+
`OidcUserInfoError.code` is `invalid_request`, `invalid_response`,
|
|
373
|
+
`unauthenticated`, or `unavailable`. Provider 401/403 responses indicate that the
|
|
374
|
+
server-held token is no longer accepted. Errors omit token and provider content.
|
|
375
|
+
|
|
334
376
|
### Server API Surface
|
|
335
377
|
|
|
336
378
|
| API | Description |
|
|
@@ -340,6 +382,8 @@ or email-based organization inference.
|
|
|
340
382
|
| `IdpGlobalServerClient` | Typed-socket client for idp.global password login, JWT refresh, `whoIs` lookup, backend JWT validation fetches, and JWT key/blocklist pushes. |
|
|
341
383
|
| `OidcIntrospectionClient` | Confidential server client for an exact live organization authority observation. |
|
|
342
384
|
| `OidcIntrospectionError` | Sanitized introspection failure with a stable error code. |
|
|
385
|
+
| `OidcUserInfoClient` | Subject-bound profile and organization display claims using a server-held OIDC token. |
|
|
386
|
+
| `OidcUserInfoError` | Sanitized UserInfo failure with a stable error code. |
|
|
343
387
|
| `IdpMfaRequiredError` | Password-login error carrying the MFA challenge token and available factor methods. |
|
|
344
388
|
| `defaultIdpGlobalBaseUrl` | Default hosted server origin: `https://app.idp.global`. |
|
|
345
389
|
| `PasswordHasher` | Static `hashPassword()` and `verifyPassword()` helpers using `scrypt:v1`. |
|
|
@@ -354,6 +398,8 @@ The server export includes these TypeScript types:
|
|
|
354
398
|
| --- | --- |
|
|
355
399
|
| `IOidcIntrospectionClientOptions` | Exact HTTPS issuer, confidential client credentials, and optional deadline/response limits. |
|
|
356
400
|
| `IOidcIntrospectionClientRequest` | Server-held access token, exact subject, and canonical organization ID. |
|
|
401
|
+
| `IOidcUserInfoClientOptions` | Exact HTTPS issuer and optional deadline/response limits. |
|
|
402
|
+
| `IOidcUserInfoClientRequest` | Server-held access token and the subject established by verified OIDC login. |
|
|
357
403
|
| `TIdpAccountAuthSource` | `'local' | 'idp.global'` |
|
|
358
404
|
| `TIdpAccountRole` | `'admin' | 'user'` |
|
|
359
405
|
| `TIdpAccountStatus` | `'active' | 'disabled'` |
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
export interface IOidcUserInfoClientOptions {
|
|
4
|
+
issuer: string;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
maxResponseBytes?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface IOidcUserInfoClientRequest {
|
|
10
|
+
token: string;
|
|
11
|
+
/** Subject already established by the application's verified OIDC login. */
|
|
12
|
+
subject: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class OidcUserInfoError extends Error {
|
|
16
|
+
constructor(public readonly code: 'invalid_request' | 'invalid_response' | 'unauthenticated' | 'unavailable') {
|
|
17
|
+
super(`OIDC userinfo ${code.replace(/_/g, ' ')}.`);
|
|
18
|
+
this.name = 'OidcUserInfoError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const record = (valueArg: unknown): valueArg is Record<string, unknown> =>
|
|
23
|
+
valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg);
|
|
24
|
+
const exactString = (valueArg: unknown, maximumArg = 512): valueArg is string =>
|
|
25
|
+
typeof valueArg === 'string' && valueArg.length > 0 && valueArg.length <= maximumArg
|
|
26
|
+
&& valueArg.trim() === valueArg && !/[\u0000-\u001f\u007f]/.test(valueArg);
|
|
27
|
+
const strings = (valueArg: unknown): valueArg is string[] =>
|
|
28
|
+
Array.isArray(valueArg) && valueArg.every((entryArg) => exactString(entryArg))
|
|
29
|
+
&& new Set(valueArg).size === valueArg.length;
|
|
30
|
+
|
|
31
|
+
/** Fetches display claims using a server-held token; results are never authorization grants. */
|
|
32
|
+
export class OidcUserInfoClient {
|
|
33
|
+
readonly #endpoint: string;
|
|
34
|
+
readonly #timeoutMs: number;
|
|
35
|
+
readonly #maxResponseBytes: number;
|
|
36
|
+
|
|
37
|
+
constructor(optionsArg: IOidcUserInfoClientOptions) {
|
|
38
|
+
if (!optionsArg || !exactString(optionsArg.issuer, 2048)) throw new OidcUserInfoError('invalid_request');
|
|
39
|
+
let issuer: URL;
|
|
40
|
+
try { issuer = new URL(optionsArg.issuer); } catch { throw new OidcUserInfoError('invalid_request'); }
|
|
41
|
+
const canonicalIssuer = issuer.origin + (issuer.pathname === '/' ? '' : issuer.pathname);
|
|
42
|
+
if (issuer.protocol !== 'https:' || issuer.username || issuer.password || issuer.search || issuer.hash
|
|
43
|
+
|| issuer.pathname.includes('//') || issuer.pathname.endsWith('/') && issuer.pathname !== '/'
|
|
44
|
+
|| canonicalIssuer !== optionsArg.issuer) throw new OidcUserInfoError('invalid_request');
|
|
45
|
+
this.#timeoutMs = optionsArg.timeoutMs ?? 5000;
|
|
46
|
+
this.#maxResponseBytes = optionsArg.maxResponseBytes ?? 65536;
|
|
47
|
+
if (!Number.isSafeInteger(this.#timeoutMs) || this.#timeoutMs < 1 || this.#timeoutMs > 30000
|
|
48
|
+
|| !Number.isSafeInteger(this.#maxResponseBytes) || this.#maxResponseBytes < 1 || this.#maxResponseBytes > 1048576) {
|
|
49
|
+
throw new OidcUserInfoError('invalid_request');
|
|
50
|
+
}
|
|
51
|
+
this.#endpoint = `${canonicalIssuer}/oauth/userinfo`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public async getUserInfo(requestArg: IOidcUserInfoClientRequest): Promise<plugins.idpInterfaces.data.IUserInfoResponse> {
|
|
55
|
+
if (!requestArg || !exactString(requestArg.subject) || !exactString(requestArg.token, 16384)
|
|
56
|
+
|| !/^[A-Za-z0-9\-._~+/]+=*$/.test(requestArg.token)) throw new OidcUserInfoError('invalid_request');
|
|
57
|
+
// Snapshot the principal and token before any asynchronous work.
|
|
58
|
+
const { token, subject } = requestArg;
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
61
|
+
const deadline = new Promise<never>((_resolveArg, rejectArg) => {
|
|
62
|
+
timer = setTimeout(() => {
|
|
63
|
+
controller.abort();
|
|
64
|
+
rejectArg(new OidcUserInfoError('unavailable'));
|
|
65
|
+
}, this.#timeoutMs);
|
|
66
|
+
});
|
|
67
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
68
|
+
try {
|
|
69
|
+
const response = await Promise.race([fetch(this.#endpoint, {
|
|
70
|
+
method: 'GET', redirect: 'error', credentials: 'omit', cache: 'no-store', signal: controller.signal,
|
|
71
|
+
headers: { authorization: `Bearer ${token}`, accept: 'application/json' },
|
|
72
|
+
}), deadline]);
|
|
73
|
+
if (response.status === 401 || response.status === 403) throw new OidcUserInfoError('unauthenticated');
|
|
74
|
+
if (response.status !== 200) throw new OidcUserInfoError('unavailable');
|
|
75
|
+
if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json'
|
|
76
|
+
|| !response.headers.get('cache-control')?.toLowerCase().split(',').map((partArg) => partArg.trim()).includes('no-store')
|
|
77
|
+
|| !response.body) throw new OidcUserInfoError('invalid_response');
|
|
78
|
+
const contentLength = response.headers.get('content-length');
|
|
79
|
+
if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > this.#maxResponseBytes)) {
|
|
80
|
+
throw new OidcUserInfoError('invalid_response');
|
|
81
|
+
}
|
|
82
|
+
reader = response.body.getReader();
|
|
83
|
+
const chunks: Uint8Array[] = [];
|
|
84
|
+
let size = 0;
|
|
85
|
+
while (true) {
|
|
86
|
+
const part = await Promise.race([reader.read(), deadline]);
|
|
87
|
+
if (part.done) break;
|
|
88
|
+
size += part.value.byteLength;
|
|
89
|
+
if (size > this.#maxResponseBytes) throw new OidcUserInfoError('invalid_response');
|
|
90
|
+
chunks.push(part.value);
|
|
91
|
+
}
|
|
92
|
+
let value: unknown;
|
|
93
|
+
try { value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks))); }
|
|
94
|
+
catch { throw new OidcUserInfoError('invalid_response'); }
|
|
95
|
+
return this.validateResponse(value, subject);
|
|
96
|
+
} catch (errorArg) {
|
|
97
|
+
if (errorArg instanceof OidcUserInfoError) throw errorArg;
|
|
98
|
+
// Transport errors can contain credentials, URLs or response bodies.
|
|
99
|
+
throw new OidcUserInfoError('unavailable');
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
controller.abort();
|
|
103
|
+
if (reader) void reader.cancel().catch(() => undefined);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private validateResponse(valueArg: unknown, subjectArg: string): plugins.idpInterfaces.data.IUserInfoResponse {
|
|
108
|
+
const invalid = () => new OidcUserInfoError('invalid_response');
|
|
109
|
+
if (!record(valueArg) || valueArg.sub !== subjectArg) throw invalid();
|
|
110
|
+
const result: plugins.idpInterfaces.data.IUserInfoResponse = { sub: subjectArg };
|
|
111
|
+
for (const key of ['name', 'preferred_username', 'picture', 'email'] as const) {
|
|
112
|
+
if (!Object.hasOwn(valueArg, key)) continue;
|
|
113
|
+
const value = valueArg[key];
|
|
114
|
+
if (typeof value !== 'string' || value.length > 4096) throw invalid();
|
|
115
|
+
result[key] = value;
|
|
116
|
+
}
|
|
117
|
+
if (Object.hasOwn(valueArg, 'email_verified')) {
|
|
118
|
+
if (typeof valueArg.email_verified !== 'boolean') throw invalid();
|
|
119
|
+
result.email_verified = valueArg.email_verified;
|
|
120
|
+
}
|
|
121
|
+
for (const key of ['roles', 'app_roles', 'app_permissions', 'app_scopes'] as const) {
|
|
122
|
+
if (!Object.hasOwn(valueArg, key)) continue;
|
|
123
|
+
const value = valueArg[key];
|
|
124
|
+
if (!strings(value)) throw invalid();
|
|
125
|
+
result[key] = [...value];
|
|
126
|
+
}
|
|
127
|
+
if (Object.hasOwn(valueArg, 'organizations')) {
|
|
128
|
+
if (!Array.isArray(valueArg.organizations)) throw invalid();
|
|
129
|
+
const seen = new Set<string>();
|
|
130
|
+
result.organizations = valueArg.organizations.map((organizationArg: unknown) => {
|
|
131
|
+
if (!record(organizationArg) || !exactString(organizationArg.id)
|
|
132
|
+
|| typeof organizationArg.name !== 'string' || organizationArg.name.length > 4096
|
|
133
|
+
|| !exactString(organizationArg.slug) || !strings(organizationArg.roles)
|
|
134
|
+
|| seen.has(organizationArg.id)
|
|
135
|
+
|| Object.keys(organizationArg).some((keyArg) => !['id', 'name', 'slug', 'roles'].includes(keyArg))) throw invalid();
|
|
136
|
+
seen.add(organizationArg.id);
|
|
137
|
+
return { id: organizationArg.id, name: organizationArg.name, slug: organizationArg.slug, roles: [...organizationArg.roles] };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// OIDC allows additional claims; only the documented, validated fields leave the SDK.
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
}
|
package/ts_server/index.ts
CHANGED
|
@@ -4,4 +4,5 @@ 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
6
|
export * from './classes.oidc-introspection-client.js';
|
|
7
|
+
export * from './classes.oidc-userinfo-client.js';
|
|
7
8
|
export * from './classes.password-hasher.js';
|