@ibgib/web-gib 0.0.49 → 0.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AUTO-GENERATED-version.d.mts +1 -1
- package/dist/AUTO-GENERATED-version.mjs +1 -1
- package/dist/common/settings/settings-constants.d.mts +8 -1
- package/dist/common/settings/settings-constants.d.mts.map +1 -1
- package/dist/common/settings/settings-constants.mjs +7 -0
- package/dist/common/settings/settings-constants.mjs.map +1 -1
- package/dist/common/settings/settings-helpers.d.mts.map +1 -1
- package/dist/common/settings/settings-helpers.mjs +3 -1
- package/dist/common/settings/settings-helpers.mjs.map +1 -1
- package/dist/common/settings/settings-types.d.mts +5 -2
- package/dist/common/settings/settings-types.d.mts.map +1 -1
- package/dist/identity/sso/sso-config-helper.d.mts +7 -0
- package/dist/identity/sso/sso-config-helper.d.mts.map +1 -0
- package/dist/identity/sso/sso-config-helper.mjs +42 -0
- package/dist/identity/sso/sso-config-helper.mjs.map +1 -0
- package/dist/identity/sso/sso-custodian-service.d.mts +125 -0
- package/dist/identity/sso/sso-custodian-service.d.mts.map +1 -0
- package/dist/identity/sso/sso-custodian-service.mjs +373 -0
- package/dist/identity/sso/sso-custodian-service.mjs.map +1 -0
- package/dist/identity/sso/sso-custodian-service.respec.d.mts +2 -0
- package/dist/identity/sso/sso-custodian-service.respec.d.mts.map +1 -0
- package/dist/identity/sso/sso-custodian-service.respec.mjs +167 -0
- package/dist/identity/sso/sso-custodian-service.respec.mjs.map +1 -0
- package/dist/identity/sso/sso-types.d.mts +40 -0
- package/dist/identity/sso/sso-types.d.mts.map +1 -0
- package/dist/identity/sso/sso-types.mjs +7 -0
- package/dist/identity/sso/sso-types.mjs.map +1 -0
- package/package.json +4 -3
- package/src/AUTO-GENERATED-version.mts +1 -1
- package/src/common/settings/settings-constants.mts +9 -0
- package/src/common/settings/settings-helpers.mts +4 -1
- package/src/common/settings/settings-types.mts +7 -2
- package/src/identity/sso/sso-config-helper.mts +52 -0
- package/src/identity/sso/sso-custodian-service.mts +453 -0
- package/src/identity/sso/sso-custodian-service.respec.mts +206 -0
- package/src/identity/sso/sso-types.mts +44 -0
- package/src/ui/component/README.md +53 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { generateKeyPairSync, createSign } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
respecfully,
|
|
4
|
+
ifWeMight,
|
|
5
|
+
iReckon,
|
|
6
|
+
firstOfAll,
|
|
7
|
+
lastOfAll
|
|
8
|
+
} from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
|
|
9
|
+
import { SsoCustodianService } from './sso-custodian-service.mjs';
|
|
10
|
+
import { SsoServerConfig } from './sso-types.mjs';
|
|
11
|
+
import { POOL_ID_CUSTODIAN_MANAGE } from '@ibgib/core-gib/dist/keystone/keystone-constants.mjs';
|
|
12
|
+
|
|
13
|
+
const maam = `[${import.meta.url}]`, sir = maam;
|
|
14
|
+
|
|
15
|
+
await respecfully(sir, 'SsoCustodianService', async () => {
|
|
16
|
+
let mockPrivateKey: any;
|
|
17
|
+
let mockPublicKeyJwk: any;
|
|
18
|
+
let originalFetch: any;
|
|
19
|
+
|
|
20
|
+
firstOfAll(sir, async () => {
|
|
21
|
+
// Generate mock RSA key pair
|
|
22
|
+
const { privateKey, publicKey } = generateKeyPairSync('rsa', {
|
|
23
|
+
modulusLength: 2048
|
|
24
|
+
});
|
|
25
|
+
mockPrivateKey = privateKey;
|
|
26
|
+
|
|
27
|
+
// Export public key as JWK
|
|
28
|
+
mockPublicKeyJwk = publicKey.export({ format: 'jwk' });
|
|
29
|
+
mockPublicKeyJwk.kid = 'mock-kid-123';
|
|
30
|
+
mockPublicKeyJwk.alg = 'RS256';
|
|
31
|
+
mockPublicKeyJwk.use = 'sig';
|
|
32
|
+
|
|
33
|
+
// Mock global fetch to return JWKS containing the mock public key
|
|
34
|
+
originalFetch = (globalThis as any).fetch;
|
|
35
|
+
(globalThis as any).fetch = async (url: string, options?: any) => {
|
|
36
|
+
if (url === 'https://www.googleapis.com/oauth2/v3/certs') {
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
status: 200,
|
|
40
|
+
json: async () => ({
|
|
41
|
+
keys: [mockPublicKeyJwk]
|
|
42
|
+
})
|
|
43
|
+
} as any;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
status: 404,
|
|
48
|
+
text: async () => 'Not Found'
|
|
49
|
+
} as any;
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
lastOfAll(sir, async () => {
|
|
54
|
+
// Restore global fetch
|
|
55
|
+
if (originalFetch) {
|
|
56
|
+
(globalThis as any).fetch = originalFetch;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await ifWeMight(sir, 'verify dynamic RSA RS256 JWT signature successfully', async () => {
|
|
61
|
+
const config: SsoServerConfig = {
|
|
62
|
+
ssoServerKdfSecret: 'mock-kdf-secret',
|
|
63
|
+
sessionSecret: 'mock-session-secret',
|
|
64
|
+
providers: {
|
|
65
|
+
google: {
|
|
66
|
+
providerId: 'google',
|
|
67
|
+
clientId: 'google-client-id-123',
|
|
68
|
+
clientSecret: 'google-client-secret-123',
|
|
69
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
70
|
+
userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
|
|
71
|
+
jwksUrl: 'https://www.googleapis.com/oauth2/v3/certs'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const service = new SsoCustodianService(config);
|
|
77
|
+
|
|
78
|
+
// 1. Build JWT Header & Payload
|
|
79
|
+
const header = { alg: 'RS256', kid: 'mock-kid-123', typ: 'JWT' };
|
|
80
|
+
const payload = {
|
|
81
|
+
sub: 'user-sub-999',
|
|
82
|
+
email: 'user@example.com',
|
|
83
|
+
name: 'John Doe',
|
|
84
|
+
aud: 'google-client-id-123',
|
|
85
|
+
iss: 'https://accounts.google.com',
|
|
86
|
+
exp: Math.floor(Date.now() / 1000) + 3600 // expires in 1 hour
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
90
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
91
|
+
const data = `${headerB64}.${payloadB64}`;
|
|
92
|
+
|
|
93
|
+
// 2. Sign JWT using private key
|
|
94
|
+
const sign = createSign('RSA-SHA256');
|
|
95
|
+
sign.update(data);
|
|
96
|
+
const signatureB64 = sign.sign(mockPrivateKey, 'base64url');
|
|
97
|
+
|
|
98
|
+
const mockJwtToken = `${data}.${signatureB64}`;
|
|
99
|
+
|
|
100
|
+
// 3. Verify signature via service
|
|
101
|
+
const decodedPayload = await service.verifyJwt(mockJwtToken, config.providers.google!);
|
|
102
|
+
|
|
103
|
+
iReckon(sir, decodedPayload.sub).isGonnaBe('user-sub-999');
|
|
104
|
+
iReckon(sir, decodedPayload.email).isGonnaBe('user@example.com');
|
|
105
|
+
iReckon(sir, decodedPayload.name).isGonnaBe('John Doe');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await ifWeMight(sir, 'fail validation if JWT has invalid signature', async () => {
|
|
109
|
+
const config: SsoServerConfig = {
|
|
110
|
+
ssoServerKdfSecret: 'mock-kdf-secret',
|
|
111
|
+
sessionSecret: 'mock-session-secret',
|
|
112
|
+
providers: {
|
|
113
|
+
google: {
|
|
114
|
+
providerId: 'google',
|
|
115
|
+
clientId: 'google-client-id-123',
|
|
116
|
+
clientSecret: 'google-client-secret-123',
|
|
117
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
118
|
+
userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
|
|
119
|
+
jwksUrl: 'https://www.googleapis.com/oauth2/v3/certs'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const service = new SsoCustodianService(config);
|
|
125
|
+
|
|
126
|
+
const header = { alg: 'RS256', kid: 'mock-kid-123', typ: 'JWT' };
|
|
127
|
+
const payload = {
|
|
128
|
+
sub: 'user-sub-999',
|
|
129
|
+
aud: 'google-client-id-123',
|
|
130
|
+
exp: Math.floor(Date.now() / 1000) + 3600
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
134
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
135
|
+
const data = `${headerB64}.${payloadB64}`;
|
|
136
|
+
|
|
137
|
+
// Create signature using a different/unregistered RSA key
|
|
138
|
+
const { privateKey: badPrivateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
|
|
139
|
+
const sign = createSign('RSA-SHA256');
|
|
140
|
+
sign.update(data);
|
|
141
|
+
const badSignatureB64 = sign.sign(badPrivateKey, 'base64url');
|
|
142
|
+
|
|
143
|
+
const badJwtToken = `${data}.${badSignatureB64}`;
|
|
144
|
+
|
|
145
|
+
let verifyError: any;
|
|
146
|
+
try {
|
|
147
|
+
await service.verifyJwt(badJwtToken, config.providers.google!);
|
|
148
|
+
} catch (error: any) {
|
|
149
|
+
verifyError = error;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
iReckon(sir, verifyError).not.isGonnaBeUndefined();
|
|
153
|
+
iReckon(sir, verifyError.message).includes('Cryptographic signature verification failed');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await ifWeMight(sir, 'derive server-delegate secret deterministically', async () => {
|
|
157
|
+
const config: SsoServerConfig = {
|
|
158
|
+
ssoServerKdfSecret: 'high-entropy-server-kdf-secret',
|
|
159
|
+
sessionSecret: 'session-secret-123',
|
|
160
|
+
providers: {}
|
|
161
|
+
};
|
|
162
|
+
const service = new SsoCustodianService(config);
|
|
163
|
+
|
|
164
|
+
const key = 'google:123456789';
|
|
165
|
+
const nonce1 = 'nonce-abc-123';
|
|
166
|
+
const nonce2 = 'nonce-def-456';
|
|
167
|
+
|
|
168
|
+
const secret1_a = await service.deriveServerDelegateSecret(key, nonce1);
|
|
169
|
+
const secret1_b = await service.deriveServerDelegateSecret(key, nonce1);
|
|
170
|
+
const secret2 = await service.deriveServerDelegateSecret(key, nonce2);
|
|
171
|
+
|
|
172
|
+
// Identical inputs must yield identical outputs
|
|
173
|
+
iReckon(sir, secret1_a).isGonnaBe(secret1_b);
|
|
174
|
+
// Different nonces must yield different outputs
|
|
175
|
+
iReckon(sir, secret1_a).not.isGonnaBe(secret2);
|
|
176
|
+
// Derived secrets must be string type with high entropy
|
|
177
|
+
iReckon(sir, typeof secret1_a).isGonnaBe('string');
|
|
178
|
+
iReckon(sir, secret1_a.length).isGonnaBe(128); // sha512 output in hex/base64 wrap
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await ifWeMight(sir, 'create custodian challenge pool successfully', async () => {
|
|
182
|
+
const config: SsoServerConfig = {
|
|
183
|
+
ssoServerKdfSecret: 'high-entropy-server-kdf-secret',
|
|
184
|
+
sessionSecret: 'session-secret-123',
|
|
185
|
+
providers: {}
|
|
186
|
+
};
|
|
187
|
+
const service = new SsoCustodianService(config);
|
|
188
|
+
|
|
189
|
+
const key = 'google:123456789';
|
|
190
|
+
const nonce = 'nonce-abc-123';
|
|
191
|
+
|
|
192
|
+
const custodianPool = await service.createCustodianChallengePool(
|
|
193
|
+
key,
|
|
194
|
+
nonce
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
iReckon(sir, custodianPool).not.isGonnaBeUndefined();
|
|
198
|
+
iReckon(sir, custodianPool.id).willEqual(POOL_ID_CUSTODIAN_MANAGE);
|
|
199
|
+
iReckon(sir, custodianPool.isForeign).isGonnaBeTrue();
|
|
200
|
+
iReckon(sir, custodianPool.metadata?.providerKey).willEqual(key);
|
|
201
|
+
|
|
202
|
+
// Structural validation of challenges
|
|
203
|
+
const challengesCount = Object.keys(custodianPool.challenges).length;
|
|
204
|
+
iReckon(sir, challengesCount).willEqual(custodianPool.config.behavior.size);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module identity/sso/sso-types
|
|
3
|
+
*
|
|
4
|
+
* Core types and interfaces for extensible SSO (OAuth2) authentication.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type SsoProviderId = 'google' | 'github';
|
|
8
|
+
|
|
9
|
+
export interface SsoProviderClientConfig {
|
|
10
|
+
providerId: SsoProviderId;
|
|
11
|
+
clientId: string;
|
|
12
|
+
authUrl: string;
|
|
13
|
+
redirectUri: string;
|
|
14
|
+
scope: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SsoProviderServerConfig {
|
|
18
|
+
providerId: SsoProviderId;
|
|
19
|
+
clientId: string;
|
|
20
|
+
clientSecret: string;
|
|
21
|
+
tokenUrl: string;
|
|
22
|
+
userInfoUrl: string;
|
|
23
|
+
jwksUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SsoServerConfig {
|
|
27
|
+
/** Server's private KDF secret used to derive server-delegate keystones. */
|
|
28
|
+
ssoServerKdfSecret: string;
|
|
29
|
+
/** Server's private session encryption secret. */
|
|
30
|
+
sessionSecret: string;
|
|
31
|
+
/** Map of active OAuth2 providers. */
|
|
32
|
+
providers: Partial<Record<SsoProviderId, SsoProviderServerConfig>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OAuthUserInfo {
|
|
36
|
+
/** Unique identity key combining provider and sub ID (e.g., `google:107632...`). */
|
|
37
|
+
providerKey: string;
|
|
38
|
+
providerId: SsoProviderId;
|
|
39
|
+
/** Unique user ID within the provider. */
|
|
40
|
+
sub: string;
|
|
41
|
+
email?: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
picture?: string;
|
|
44
|
+
}
|
|
@@ -192,10 +192,62 @@ export class MyFormInstance extends IbGibFormInstanceBase {
|
|
|
192
192
|
// ... perform business logic / space evolution ...
|
|
193
193
|
this.setStatus("Form successfully submitted!", "success");
|
|
194
194
|
} catch (err) {
|
|
195
|
-
|
|
195
|
+
// ...
|
|
196
196
|
} finally {
|
|
197
197
|
this.setLoading(false, '#btn-submit');
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 🗂️ Parent / Child Tab Components
|
|
206
|
+
|
|
207
|
+
For complex views that act as shells containing other interactive component tabs (e.g. `ibgib-projects` or `ibgib-identity-manager`), the framework provides specialized parent classes:
|
|
208
|
+
|
|
209
|
+
* **`IbGibDynamicComponentInstanceBase_Parent<TIbGib, TElements, TChildInfo>`**:
|
|
210
|
+
Base class coordinating child injection, child components mapping, and activation of a specific backing child `ibgib` address.
|
|
211
|
+
* **`IbGibDynamicComponentInstanceBase_ParentOfTabs<TSettings, TIbGib, TElements, TChildInfo>`**:
|
|
212
|
+
Extends the Parent base class to integrate tab state persistence using `SettingsWithTabs` (e.g. saving which child tabs are open and which one is active).
|
|
213
|
+
|
|
214
|
+
### Core Tab Integration Steps
|
|
215
|
+
|
|
216
|
+
1. **Define Tab Info**: Define a tab interface that extends `ChildInfoBase<TComponent>`:
|
|
217
|
+
```typescript
|
|
218
|
+
interface MyTabInfo extends ChildInfoBase<IbGibDynamicComponentInstance<any, any>> {
|
|
219
|
+
// Add custom metadata if needed (e.g. component cache, lens modes)
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
2. **Parent Class Configuration**: Extend `IbGibDynamicComponentInstanceBase_ParentOfTabs` and define `settingsType` and `getLoadedChildInfo()`:
|
|
223
|
+
```typescript
|
|
224
|
+
export class MyParentComponent extends IbGibDynamicComponentInstanceBase_ParentOfTabs<
|
|
225
|
+
Settings_MyParent, IbGib_V1, MyElements, MyTabInfo
|
|
226
|
+
> {
|
|
227
|
+
protected get settingsType() { return SettingsType.myParent; }
|
|
228
|
+
|
|
229
|
+
protected async getLoadedChildInfo({ addr, ibGib }) {
|
|
230
|
+
// Check if tab info exists in this.childInfos, else instantiate
|
|
231
|
+
// the child component using componentSvc.getComponentInstance
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
protected async addChild({ addr, ibGib }) {
|
|
235
|
+
// Create tab button element, attach listener, append to DOM, and return it
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
protected async removeTabBtn({ tabInfo }) {
|
|
239
|
+
// Remove tab button element from DOM
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
3. **Dynamic Delegate Tab Navigation**:
|
|
244
|
+
To allow deep links or delegate views (e.g. clicking a delegate key to open its details in a temporary sub-tab), have the child component dispatch a bubbling Custom Event:
|
|
245
|
+
```typescript
|
|
246
|
+
this.dispatchEvent(new CustomEvent('ibgib-view-details-request', {
|
|
247
|
+
detail: { addr: delegateAddr },
|
|
248
|
+
bubbles: true,
|
|
249
|
+
composed: true
|
|
250
|
+
}));
|
|
251
|
+
```
|
|
252
|
+
The parent component listens to this event in `created()` and triggers `activateIbGib({ addr })` to dynamically open and activate the sub-tab.
|
|
253
|
+
|