tlc-claude-code 1.3.0 → 1.4.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/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
- package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSO Session Manager Tests
|
|
3
|
+
* Enhanced session management for SSO with IdP integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
createSsoSessionManager,
|
|
9
|
+
SESSION_DEFAULTS,
|
|
10
|
+
} from './sso-session.js';
|
|
11
|
+
|
|
12
|
+
describe('sso-session', () => {
|
|
13
|
+
let sessionManager;
|
|
14
|
+
let mockIdpManager;
|
|
15
|
+
let mockMfaStore;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.useFakeTimers();
|
|
19
|
+
vi.setSystemTime(new Date('2024-01-15T10:00:00.000Z'));
|
|
20
|
+
|
|
21
|
+
// Mock IdP manager
|
|
22
|
+
mockIdpManager = {
|
|
23
|
+
getProvider: vi.fn(),
|
|
24
|
+
handleCallback: vi.fn(),
|
|
25
|
+
oauthRegistry: {
|
|
26
|
+
getProvider: vi.fn(),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Mock MFA store
|
|
31
|
+
mockMfaStore = {
|
|
32
|
+
getMfaStatus: vi.fn().mockResolvedValue({ enabled: false }),
|
|
33
|
+
verifyMfa: vi.fn().mockResolvedValue({ valid: true }),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
sessionManager = createSsoSessionManager({
|
|
37
|
+
idpManager: mockIdpManager,
|
|
38
|
+
mfaStore: mockMfaStore,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.useRealTimers();
|
|
44
|
+
vi.restoreAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('SESSION_DEFAULTS', () => {
|
|
48
|
+
it('defines default configuration values', () => {
|
|
49
|
+
expect(SESSION_DEFAULTS.sessionDuration).toBe(86400000); // 24 hours
|
|
50
|
+
expect(SESSION_DEFAULTS.maxConcurrentSessions).toBe(5);
|
|
51
|
+
expect(SESSION_DEFAULTS.tokenRefreshThreshold).toBe(300000); // 5 minutes
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('createSession', () => {
|
|
56
|
+
it('stores user and IdP info', async () => {
|
|
57
|
+
const authResult = {
|
|
58
|
+
profile: {
|
|
59
|
+
id: 'user-123',
|
|
60
|
+
email: 'user@example.com',
|
|
61
|
+
name: 'Test User',
|
|
62
|
+
},
|
|
63
|
+
tokens: {
|
|
64
|
+
accessToken: 'access-token-123',
|
|
65
|
+
refreshToken: 'refresh-token-456',
|
|
66
|
+
expiresIn: 3600,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const session = await sessionManager.createSession('github', authResult, {
|
|
71
|
+
userAgent: 'Mozilla/5.0',
|
|
72
|
+
ipAddress: '192.168.1.1',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(session.id).toBeDefined();
|
|
76
|
+
expect(session.userId).toBe('user-123');
|
|
77
|
+
expect(session.provider).toBe('github');
|
|
78
|
+
expect(session.accessToken).toBe('access-token-123');
|
|
79
|
+
expect(session.refreshToken).toBe('refresh-token-456');
|
|
80
|
+
expect(session.userAgent).toBe('Mozilla/5.0');
|
|
81
|
+
expect(session.ipAddress).toBe('192.168.1.1');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('sets expiry from config', async () => {
|
|
85
|
+
const customManager = createSsoSessionManager({
|
|
86
|
+
idpManager: mockIdpManager,
|
|
87
|
+
mfaStore: mockMfaStore,
|
|
88
|
+
sessionDuration: 3600000, // 1 hour
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const authResult = {
|
|
92
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
93
|
+
tokens: { accessToken: 'token', expiresIn: 3600 },
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const session = await customManager.createSession('github', authResult, {});
|
|
97
|
+
|
|
98
|
+
const expectedExpiry = Date.now() + 3600000;
|
|
99
|
+
expect(session.expiresAt).toBe(expectedExpiry);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('calculates token expiry from expiresIn', async () => {
|
|
103
|
+
const authResult = {
|
|
104
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
105
|
+
tokens: { accessToken: 'token', expiresIn: 7200 }, // 2 hours in seconds
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const session = await sessionManager.createSession('github', authResult, {});
|
|
109
|
+
|
|
110
|
+
const expectedTokenExpiry = Date.now() + (7200 * 1000);
|
|
111
|
+
expect(session.tokenExpiry).toBe(expectedTokenExpiry);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('sets createdAt and lastActivityAt', async () => {
|
|
115
|
+
const authResult = {
|
|
116
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
117
|
+
tokens: { accessToken: 'token' },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const session = await sessionManager.createSession('github', authResult, {});
|
|
121
|
+
|
|
122
|
+
expect(session.createdAt).toBe(Date.now());
|
|
123
|
+
expect(session.lastActivityAt).toBe(Date.now());
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('sets mfaVerified from MFA status', async () => {
|
|
127
|
+
mockMfaStore.getMfaStatus.mockResolvedValue({ enabled: true });
|
|
128
|
+
mockMfaStore.verifyMfa.mockResolvedValue({ valid: true });
|
|
129
|
+
|
|
130
|
+
const authResult = {
|
|
131
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
132
|
+
tokens: { accessToken: 'token' },
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const session = await sessionManager.createSession('github', authResult, {
|
|
136
|
+
mfaCode: '123456',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(session.mfaVerified).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('sets mfaVerified false when no MFA enabled', async () => {
|
|
143
|
+
mockMfaStore.getMfaStatus.mockResolvedValue({ enabled: false });
|
|
144
|
+
|
|
145
|
+
const authResult = {
|
|
146
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
147
|
+
tokens: { accessToken: 'token' },
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const session = await sessionManager.createSession('github', authResult, {});
|
|
151
|
+
|
|
152
|
+
expect(session.mfaVerified).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('getSession', () => {
|
|
157
|
+
it('returns valid session', async () => {
|
|
158
|
+
const authResult = {
|
|
159
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
160
|
+
tokens: { accessToken: 'token', expiresIn: 3600 },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const created = await sessionManager.createSession('github', authResult, {});
|
|
164
|
+
const session = await sessionManager.getSession(created.id);
|
|
165
|
+
|
|
166
|
+
expect(session).not.toBeNull();
|
|
167
|
+
expect(session.id).toBe(created.id);
|
|
168
|
+
expect(session.userId).toBe('user-123');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('returns null for expired session', async () => {
|
|
172
|
+
const authResult = {
|
|
173
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
174
|
+
tokens: { accessToken: 'token' },
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const created = await sessionManager.createSession('github', authResult, {});
|
|
178
|
+
|
|
179
|
+
// Advance time past session expiry
|
|
180
|
+
vi.advanceTimersByTime(SESSION_DEFAULTS.sessionDuration + 1000);
|
|
181
|
+
|
|
182
|
+
const session = await sessionManager.getSession(created.id);
|
|
183
|
+
|
|
184
|
+
expect(session).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('returns null for non-existent session', async () => {
|
|
188
|
+
const session = await sessionManager.getSession('non-existent-id');
|
|
189
|
+
|
|
190
|
+
expect(session).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('updates lastActivityAt on access', async () => {
|
|
194
|
+
const authResult = {
|
|
195
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
196
|
+
tokens: { accessToken: 'token' },
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const created = await sessionManager.createSession('github', authResult, {});
|
|
200
|
+
const originalLastActivity = created.lastActivityAt;
|
|
201
|
+
|
|
202
|
+
// Advance time
|
|
203
|
+
vi.advanceTimersByTime(60000); // 1 minute
|
|
204
|
+
|
|
205
|
+
const session = await sessionManager.getSession(created.id);
|
|
206
|
+
|
|
207
|
+
expect(session.lastActivityAt).toBeGreaterThan(originalLastActivity);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('refreshSession', () => {
|
|
212
|
+
it('extends session lifetime', async () => {
|
|
213
|
+
const authResult = {
|
|
214
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
215
|
+
tokens: { accessToken: 'token', expiresIn: 3600 },
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const created = await sessionManager.createSession('github', authResult, {});
|
|
219
|
+
const originalExpiry = created.expiresAt;
|
|
220
|
+
|
|
221
|
+
// Advance time
|
|
222
|
+
vi.advanceTimersByTime(60000); // 1 minute
|
|
223
|
+
|
|
224
|
+
const refreshed = await sessionManager.refreshSession(created.id);
|
|
225
|
+
|
|
226
|
+
expect(refreshed.expiresAt).toBeGreaterThan(originalExpiry);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('refreshes IdP tokens when near expiry', async () => {
|
|
230
|
+
// Set up provider with refresh capability
|
|
231
|
+
mockIdpManager.oauthRegistry.getProvider.mockReturnValue({
|
|
232
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
233
|
+
clientId: 'client-123',
|
|
234
|
+
clientSecret: 'secret-456',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Mock fetch for token refresh
|
|
238
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
239
|
+
ok: true,
|
|
240
|
+
json: () => Promise.resolve({
|
|
241
|
+
access_token: 'new-access-token',
|
|
242
|
+
refresh_token: 'new-refresh-token',
|
|
243
|
+
expires_in: 3600,
|
|
244
|
+
}),
|
|
245
|
+
});
|
|
246
|
+
global.fetch = mockFetch;
|
|
247
|
+
|
|
248
|
+
const authResult = {
|
|
249
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
250
|
+
tokens: {
|
|
251
|
+
accessToken: 'old-token',
|
|
252
|
+
refreshToken: 'refresh-token',
|
|
253
|
+
expiresIn: 600, // 10 minutes - will be near expiry after time advance
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const created = await sessionManager.createSession('github', authResult, {});
|
|
258
|
+
|
|
259
|
+
// Advance time close to token expiry (past threshold)
|
|
260
|
+
vi.advanceTimersByTime(400000); // 6.67 minutes - within 5 minute threshold
|
|
261
|
+
|
|
262
|
+
const refreshed = await sessionManager.refreshSession(created.id);
|
|
263
|
+
|
|
264
|
+
expect(refreshed.accessToken).toBe('new-access-token');
|
|
265
|
+
expect(refreshed.refreshToken).toBe('new-refresh-token');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('returns null for non-existent session', async () => {
|
|
269
|
+
const result = await sessionManager.refreshSession('non-existent-id');
|
|
270
|
+
|
|
271
|
+
expect(result).toBeNull();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('updates lastActivityAt', async () => {
|
|
275
|
+
const authResult = {
|
|
276
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
277
|
+
tokens: { accessToken: 'token' },
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const created = await sessionManager.createSession('github', authResult, {});
|
|
281
|
+
const originalLastActivity = created.lastActivityAt;
|
|
282
|
+
|
|
283
|
+
vi.advanceTimersByTime(60000);
|
|
284
|
+
|
|
285
|
+
const refreshed = await sessionManager.refreshSession(created.id);
|
|
286
|
+
|
|
287
|
+
expect(refreshed.lastActivityAt).toBeGreaterThan(originalLastActivity);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('destroySession', () => {
|
|
292
|
+
it('removes session', async () => {
|
|
293
|
+
const authResult = {
|
|
294
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
295
|
+
tokens: { accessToken: 'token' },
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const created = await sessionManager.createSession('github', authResult, {});
|
|
299
|
+
await sessionManager.destroySession(created.id);
|
|
300
|
+
|
|
301
|
+
const session = await sessionManager.getSession(created.id);
|
|
302
|
+
expect(session).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('triggers IdP logout for SAML provider', async () => {
|
|
306
|
+
const mockSamlLogout = vi.fn().mockReturnValue({
|
|
307
|
+
url: 'https://idp.example.com/logout',
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
mockIdpManager.samlProvider = {
|
|
311
|
+
getIdP: vi.fn().mockReturnValue({ sloUrl: 'https://idp.example.com/logout' }),
|
|
312
|
+
createLogoutRequest: mockSamlLogout,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const authResult = {
|
|
316
|
+
profile: { id: 'user-123', email: 'user@example.com', nameId: 'name-id-123' },
|
|
317
|
+
tokens: {},
|
|
318
|
+
providerType: 'saml',
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const created = await sessionManager.createSession('okta', authResult, {});
|
|
322
|
+
const result = await sessionManager.destroySession(created.id, { triggerIdpLogout: true });
|
|
323
|
+
|
|
324
|
+
expect(result.logoutUrl).toBeDefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('triggers token revocation for OAuth provider', async () => {
|
|
328
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
|
329
|
+
global.fetch = mockFetch;
|
|
330
|
+
|
|
331
|
+
mockIdpManager.oauthRegistry.getProvider.mockReturnValue({
|
|
332
|
+
revokeUrl: 'https://github.com/login/oauth/revoke',
|
|
333
|
+
clientId: 'client-123',
|
|
334
|
+
clientSecret: 'secret-456',
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const authResult = {
|
|
338
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
339
|
+
tokens: { accessToken: 'token-to-revoke' },
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const created = await sessionManager.createSession('github', authResult, {});
|
|
343
|
+
await sessionManager.destroySession(created.id, { triggerIdpLogout: true });
|
|
344
|
+
|
|
345
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('returns success for non-existent session', async () => {
|
|
349
|
+
const result = await sessionManager.destroySession('non-existent-id');
|
|
350
|
+
|
|
351
|
+
expect(result.success).toBe(true);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('enforceSessionLimit', () => {
|
|
356
|
+
it('limits concurrent sessions', async () => {
|
|
357
|
+
const customManager = createSsoSessionManager({
|
|
358
|
+
idpManager: mockIdpManager,
|
|
359
|
+
mfaStore: mockMfaStore,
|
|
360
|
+
maxConcurrentSessions: 2,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const authResult = {
|
|
364
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
365
|
+
tokens: { accessToken: 'token' },
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Create 3 sessions for the same user
|
|
369
|
+
const session1 = await customManager.createSession('github', authResult, {});
|
|
370
|
+
vi.advanceTimersByTime(1000);
|
|
371
|
+
const session2 = await customManager.createSession('github', authResult, {});
|
|
372
|
+
vi.advanceTimersByTime(1000);
|
|
373
|
+
const session3 = await customManager.createSession('github', authResult, {});
|
|
374
|
+
|
|
375
|
+
// Session 1 should be removed (oldest)
|
|
376
|
+
const found1 = await customManager.getSession(session1.id);
|
|
377
|
+
const found2 = await customManager.getSession(session2.id);
|
|
378
|
+
const found3 = await customManager.getSession(session3.id);
|
|
379
|
+
|
|
380
|
+
expect(found1).toBeNull();
|
|
381
|
+
expect(found2).not.toBeNull();
|
|
382
|
+
expect(found3).not.toBeNull();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('removes oldest session when limit exceeded', async () => {
|
|
386
|
+
const customManager = createSsoSessionManager({
|
|
387
|
+
idpManager: mockIdpManager,
|
|
388
|
+
mfaStore: mockMfaStore,
|
|
389
|
+
maxConcurrentSessions: 2,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const authResult = {
|
|
393
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
394
|
+
tokens: { accessToken: 'token' },
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const session1 = await customManager.createSession('github', authResult, {});
|
|
398
|
+
vi.advanceTimersByTime(1000);
|
|
399
|
+
await customManager.createSession('github', authResult, {});
|
|
400
|
+
vi.advanceTimersByTime(1000);
|
|
401
|
+
await customManager.createSession('github', authResult, {});
|
|
402
|
+
|
|
403
|
+
// Verify oldest was removed
|
|
404
|
+
const remaining = await customManager.getActiveSessions('user-123');
|
|
405
|
+
const sessionIds = remaining.map(s => s.id);
|
|
406
|
+
|
|
407
|
+
expect(sessionIds).not.toContain(session1.id);
|
|
408
|
+
expect(remaining.length).toBe(2);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('does not affect sessions from different users', async () => {
|
|
412
|
+
const customManager = createSsoSessionManager({
|
|
413
|
+
idpManager: mockIdpManager,
|
|
414
|
+
mfaStore: mockMfaStore,
|
|
415
|
+
maxConcurrentSessions: 2,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const authResult1 = {
|
|
419
|
+
profile: { id: 'user-1', email: 'user1@example.com' },
|
|
420
|
+
tokens: { accessToken: 'token1' },
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const authResult2 = {
|
|
424
|
+
profile: { id: 'user-2', email: 'user2@example.com' },
|
|
425
|
+
tokens: { accessToken: 'token2' },
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// Create 2 sessions for user 1
|
|
429
|
+
await customManager.createSession('github', authResult1, {});
|
|
430
|
+
await customManager.createSession('github', authResult1, {});
|
|
431
|
+
|
|
432
|
+
// Create 2 sessions for user 2
|
|
433
|
+
await customManager.createSession('github', authResult2, {});
|
|
434
|
+
await customManager.createSession('github', authResult2, {});
|
|
435
|
+
|
|
436
|
+
// Each user should have their full allowed sessions
|
|
437
|
+
const user1Sessions = await customManager.getActiveSessions('user-1');
|
|
438
|
+
const user2Sessions = await customManager.getActiveSessions('user-2');
|
|
439
|
+
|
|
440
|
+
expect(user1Sessions.length).toBe(2);
|
|
441
|
+
expect(user2Sessions.length).toBe(2);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('getActiveSessions', () => {
|
|
446
|
+
it('returns user\'s sessions', async () => {
|
|
447
|
+
const authResult = {
|
|
448
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
449
|
+
tokens: { accessToken: 'token' },
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
await sessionManager.createSession('github', authResult, {});
|
|
453
|
+
await sessionManager.createSession('google', authResult, {});
|
|
454
|
+
|
|
455
|
+
const sessions = await sessionManager.getActiveSessions('user-123');
|
|
456
|
+
|
|
457
|
+
expect(sessions.length).toBe(2);
|
|
458
|
+
expect(sessions.every(s => s.userId === 'user-123')).toBe(true);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('returns empty array for user with no sessions', async () => {
|
|
462
|
+
const sessions = await sessionManager.getActiveSessions('no-sessions-user');
|
|
463
|
+
|
|
464
|
+
expect(sessions).toEqual([]);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('excludes expired sessions', async () => {
|
|
468
|
+
const shortDurationManager = createSsoSessionManager({
|
|
469
|
+
idpManager: mockIdpManager,
|
|
470
|
+
mfaStore: mockMfaStore,
|
|
471
|
+
sessionDuration: 60000, // 1 minute
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const authResult = {
|
|
475
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
476
|
+
tokens: { accessToken: 'token' },
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
await shortDurationManager.createSession('github', authResult, {});
|
|
480
|
+
vi.advanceTimersByTime(30000); // 30 seconds
|
|
481
|
+
await shortDurationManager.createSession('google', authResult, {});
|
|
482
|
+
|
|
483
|
+
// Advance past first session expiry
|
|
484
|
+
vi.advanceTimersByTime(40000); // 40 more seconds
|
|
485
|
+
|
|
486
|
+
const sessions = await shortDurationManager.getActiveSessions('user-123');
|
|
487
|
+
|
|
488
|
+
expect(sessions.length).toBe(1);
|
|
489
|
+
expect(sessions[0].provider).toBe('google');
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('returns sessions with sanitized data', async () => {
|
|
493
|
+
const authResult = {
|
|
494
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
495
|
+
tokens: { accessToken: 'secret-token', refreshToken: 'secret-refresh' },
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
await sessionManager.createSession('github', authResult, {});
|
|
499
|
+
|
|
500
|
+
const sessions = await sessionManager.getActiveSessions('user-123');
|
|
501
|
+
|
|
502
|
+
// Should include metadata but not expose tokens in list view
|
|
503
|
+
expect(sessions[0].id).toBeDefined();
|
|
504
|
+
expect(sessions[0].provider).toBeDefined();
|
|
505
|
+
expect(sessions[0].createdAt).toBeDefined();
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe('cleanupExpiredSessions', () => {
|
|
510
|
+
it('removes old sessions', async () => {
|
|
511
|
+
const shortDurationManager = createSsoSessionManager({
|
|
512
|
+
idpManager: mockIdpManager,
|
|
513
|
+
mfaStore: mockMfaStore,
|
|
514
|
+
sessionDuration: 60000, // 1 minute
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const authResult = {
|
|
518
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
519
|
+
tokens: { accessToken: 'token' },
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
await shortDurationManager.createSession('github', authResult, {});
|
|
523
|
+
vi.advanceTimersByTime(30000);
|
|
524
|
+
await shortDurationManager.createSession('google', authResult, {});
|
|
525
|
+
|
|
526
|
+
// Advance past first session expiry
|
|
527
|
+
vi.advanceTimersByTime(40000);
|
|
528
|
+
|
|
529
|
+
const removed = await shortDurationManager.cleanupExpiredSessions();
|
|
530
|
+
|
|
531
|
+
expect(removed).toBe(1);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('returns count of removed sessions', async () => {
|
|
535
|
+
const shortDurationManager = createSsoSessionManager({
|
|
536
|
+
idpManager: mockIdpManager,
|
|
537
|
+
mfaStore: mockMfaStore,
|
|
538
|
+
sessionDuration: 60000,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const authResult1 = {
|
|
542
|
+
profile: { id: 'user-1', email: 'user1@example.com' },
|
|
543
|
+
tokens: { accessToken: 'token1' },
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const authResult2 = {
|
|
547
|
+
profile: { id: 'user-2', email: 'user2@example.com' },
|
|
548
|
+
tokens: { accessToken: 'token2' },
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
await shortDurationManager.createSession('github', authResult1, {});
|
|
552
|
+
await shortDurationManager.createSession('github', authResult2, {});
|
|
553
|
+
|
|
554
|
+
// Expire all sessions
|
|
555
|
+
vi.advanceTimersByTime(70000);
|
|
556
|
+
|
|
557
|
+
const removed = await shortDurationManager.cleanupExpiredSessions();
|
|
558
|
+
|
|
559
|
+
expect(removed).toBe(2);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('does not remove active sessions', async () => {
|
|
563
|
+
const authResult = {
|
|
564
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
565
|
+
tokens: { accessToken: 'token' },
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
await sessionManager.createSession('github', authResult, {});
|
|
569
|
+
|
|
570
|
+
// Don't advance time past expiry
|
|
571
|
+
vi.advanceTimersByTime(1000);
|
|
572
|
+
|
|
573
|
+
const removed = await sessionManager.cleanupExpiredSessions();
|
|
574
|
+
|
|
575
|
+
expect(removed).toBe(0);
|
|
576
|
+
|
|
577
|
+
const sessions = await sessionManager.getActiveSessions('user-123');
|
|
578
|
+
expect(sessions.length).toBe(1);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe('getSessionByToken', () => {
|
|
583
|
+
it('returns session by access token', async () => {
|
|
584
|
+
const authResult = {
|
|
585
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
586
|
+
tokens: { accessToken: 'unique-token-123' },
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const created = await sessionManager.createSession('github', authResult, {});
|
|
590
|
+
const session = await sessionManager.getSessionByToken('unique-token-123');
|
|
591
|
+
|
|
592
|
+
expect(session).not.toBeNull();
|
|
593
|
+
expect(session.id).toBe(created.id);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('returns null for unknown token', async () => {
|
|
597
|
+
const session = await sessionManager.getSessionByToken('unknown-token');
|
|
598
|
+
|
|
599
|
+
expect(session).toBeNull();
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
describe('destroyAllUserSessions', () => {
|
|
604
|
+
it('removes all sessions for a user', async () => {
|
|
605
|
+
const authResult = {
|
|
606
|
+
profile: { id: 'user-123', email: 'user@example.com' },
|
|
607
|
+
tokens: { accessToken: 'token' },
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
await sessionManager.createSession('github', authResult, {});
|
|
611
|
+
await sessionManager.createSession('google', authResult, {});
|
|
612
|
+
await sessionManager.createSession('azuread', authResult, {});
|
|
613
|
+
|
|
614
|
+
const removed = await sessionManager.destroyAllUserSessions('user-123');
|
|
615
|
+
|
|
616
|
+
expect(removed).toBe(3);
|
|
617
|
+
|
|
618
|
+
const sessions = await sessionManager.getActiveSessions('user-123');
|
|
619
|
+
expect(sessions.length).toBe(0);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('does not affect other users sessions', async () => {
|
|
623
|
+
const authResult1 = {
|
|
624
|
+
profile: { id: 'user-1', email: 'user1@example.com' },
|
|
625
|
+
tokens: { accessToken: 'token1' },
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const authResult2 = {
|
|
629
|
+
profile: { id: 'user-2', email: 'user2@example.com' },
|
|
630
|
+
tokens: { accessToken: 'token2' },
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
await sessionManager.createSession('github', authResult1, {});
|
|
634
|
+
await sessionManager.createSession('github', authResult2, {});
|
|
635
|
+
|
|
636
|
+
await sessionManager.destroyAllUserSessions('user-1');
|
|
637
|
+
|
|
638
|
+
const user1Sessions = await sessionManager.getActiveSessions('user-1');
|
|
639
|
+
const user2Sessions = await sessionManager.getActiveSessions('user-2');
|
|
640
|
+
|
|
641
|
+
expect(user1Sessions.length).toBe(0);
|
|
642
|
+
expect(user2Sessions.length).toBe(1);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
describe('getSessionStats', () => {
|
|
647
|
+
it('returns session statistics', async () => {
|
|
648
|
+
const authResult1 = {
|
|
649
|
+
profile: { id: 'user-1', email: 'user1@example.com' },
|
|
650
|
+
tokens: { accessToken: 'token1' },
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const authResult2 = {
|
|
654
|
+
profile: { id: 'user-2', email: 'user2@example.com' },
|
|
655
|
+
tokens: { accessToken: 'token2' },
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
await sessionManager.createSession('github', authResult1, {});
|
|
659
|
+
await sessionManager.createSession('google', authResult1, {});
|
|
660
|
+
await sessionManager.createSession('github', authResult2, {});
|
|
661
|
+
|
|
662
|
+
const stats = await sessionManager.getSessionStats();
|
|
663
|
+
|
|
664
|
+
expect(stats.totalSessions).toBe(3);
|
|
665
|
+
expect(stats.uniqueUsers).toBe(2);
|
|
666
|
+
expect(stats.byProvider.github).toBe(2);
|
|
667
|
+
expect(stats.byProvider.google).toBe(1);
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
});
|