collabmd 0.1.21 → 0.1.23

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.
Files changed (99) hide show
  1. package/README.md +94 -7
  2. package/bin/collabmd.js +3 -2
  3. package/docker-compose.yml +13 -1
  4. package/package.json +2 -1
  5. package/public/assets/css/style.css +1 -1
  6. package/public/assets/js/chunks/{chunk-HEWVH67U.js → chunk-CYK7KO7W.js} +8 -8
  7. package/public/assets/js/chunks/editor-session-S5AT7BMV.js +22 -0
  8. package/public/assets/js/{excalidraw-editor-TCIH6GJL.js → excalidraw-editor-5YWMGLKF.js} +145 -62
  9. package/public/assets/js/excalidraw-editor.js +1 -1
  10. package/public/assets/js/main.js +503 -182
  11. package/public/index.html +104 -84
  12. package/src/client/application/app-shell/git-feature.js +247 -9
  13. package/src/client/application/app-shell/ui-feature.js +65 -7
  14. package/src/client/application/app-shell/workspace-feature.js +46 -0
  15. package/src/client/application/app-shell-elements.js +2 -0
  16. package/src/client/application/mermaid-preview-hydrator.js +1 -1
  17. package/src/client/application/plantuml-preview-hydrator.js +1 -1
  18. package/src/client/application/workspace-coordinator.js +147 -18
  19. package/src/client/application/workspace-preview-controller.js +34 -0
  20. package/src/client/application/workspace-route-controller.js +104 -0
  21. package/src/client/bootstrap/collabmd-app-shell.js +95 -0
  22. package/src/client/domain/runtime-paths.js +5 -0
  23. package/src/client/excalidraw-editor.js +50 -2
  24. package/src/client/infrastructure/auth-client.js +219 -52
  25. package/src/client/infrastructure/browser-navigation-port.js +20 -0
  26. package/src/client/infrastructure/editor-collaboration-client.js +4 -0
  27. package/src/client/infrastructure/editor-session.js +47 -7
  28. package/src/client/infrastructure/editor-view-adapter.js +122 -58
  29. package/src/client/infrastructure/runtime-config.js +66 -0
  30. package/src/client/presentation/comment-ui-controller.js +82 -26
  31. package/src/client/presentation/comments-panel.js +2 -2
  32. package/src/client/presentation/excalidraw-embed-controller.js +60 -2
  33. package/src/client/presentation/file-history-view-controller.js +806 -0
  34. package/src/client/presentation/git-diff-view-controller.js +718 -121
  35. package/src/client/presentation/git-panel-controller.js +458 -66
  36. package/src/client/presentation/layout-controller.js +1 -1
  37. package/src/client/styles/components/actions.css +89 -0
  38. package/src/client/styles/components/feedback.css +43 -0
  39. package/src/client/styles/components/navigation.css +68 -0
  40. package/src/client/styles/components/scrollbars.css +104 -0
  41. package/src/client/styles/components/states.css +289 -0
  42. package/src/client/styles/components/surfaces.css +155 -0
  43. package/src/client/styles/features/backlinks.css +213 -0
  44. package/src/client/styles/features/collaboration-chat.css +228 -0
  45. package/src/client/styles/features/collaboration-dialogs.css +154 -0
  46. package/src/client/styles/features/collaboration-presence.css +103 -0
  47. package/src/client/styles/features/collaboration-ui.css +3 -0
  48. package/src/client/styles/features/comment-card.css +236 -0
  49. package/src/client/styles/features/comment-markdown.css +127 -0
  50. package/src/client/styles/features/comment-overlays.css +60 -0
  51. package/src/client/styles/features/comments-drawer.css +120 -0
  52. package/src/client/styles/features/comments.css +4 -0
  53. package/src/client/styles/features/diagram-preview.css +227 -0
  54. package/src/client/styles/features/diff-commit.css +268 -0
  55. package/src/client/styles/features/diff-lines.css +160 -0
  56. package/src/client/styles/features/diff-toolbar.css +98 -0
  57. package/src/client/styles/features/diff.css +3 -0
  58. package/src/client/styles/features/embedded-preview.css +218 -0
  59. package/src/client/styles/features/git-branch.css +155 -0
  60. package/src/client/styles/features/git-files.css +105 -0
  61. package/src/client/styles/features/git-history.css +339 -0
  62. package/src/client/styles/features/git.css +3 -0
  63. package/src/client/styles/features/media-lightbox.css +157 -0
  64. package/src/client/styles/features/outline.css +115 -0
  65. package/src/client/styles/features/preview-content.css +3 -0
  66. package/src/client/styles/features/preview-markdown.css +201 -0
  67. package/src/client/styles/features/preview-sidebars.css +2 -0
  68. package/src/client/styles/features/quick-switcher.css +167 -0
  69. package/src/client/styles/foundation/themes.css +347 -0
  70. package/src/client/styles/foundation/tokens.css +66 -0
  71. package/src/client/styles/foundation/utilities.css +36 -0
  72. package/src/client/styles/layout/editor-page.css +203 -0
  73. package/src/client/styles/layout/overlays.css +132 -0
  74. package/src/client/styles/layout/responsive.css +251 -0
  75. package/src/client/styles/layout/shell.css +314 -0
  76. package/src/client/styles/layout/sidebar.css +176 -0
  77. package/src/client/styles/layout/view-modes.css +92 -0
  78. package/src/client/styles/overrides/codemirror.css +53 -0
  79. package/src/client/styles/overrides/highlightjs.css +7 -0
  80. package/src/client/styles/overrides/vendor.css +2 -0
  81. package/src/client/styles/primitives/controls.css +276 -0
  82. package/src/client/styles/style.css +25 -4945
  83. package/src/server/auth/create-auth-service.js +373 -15
  84. package/src/server/auth/session-cookie.js +30 -4
  85. package/src/server/config/env.js +121 -5
  86. package/src/server/create-app-server.js +8 -0
  87. package/src/server/domain/collaboration/collaboration-room.js +90 -47
  88. package/src/server/index.js +3 -1
  89. package/src/server/infrastructure/git/command-runner.js +2 -1
  90. package/src/server/infrastructure/git/git-service.js +48 -2
  91. package/src/server/infrastructure/git/history-service.js +683 -0
  92. package/src/server/infrastructure/git/parsers.js +30 -0
  93. package/src/server/infrastructure/git/responses.js +73 -0
  94. package/src/server/infrastructure/http/create-auth-api-handler.js +30 -2
  95. package/src/server/infrastructure/http/create-git-api-command-handler.js +2 -0
  96. package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
  97. package/src/server/infrastructure/http/create-git-api-query-handler.js +61 -0
  98. package/src/server/infrastructure/http/create-request-handler.js +15 -0
  99. package/public/assets/js/chunks/editor-session-EAUBJCHH.js +0 -22
@@ -1,6 +1,8 @@
1
1
  import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
2
 
3
- import { createSessionCookieManager } from './session-cookie.js';
3
+ import * as oidc from 'openid-client';
4
+
5
+ import { createSessionCookieManager, createSignedCookieManager } from './session-cookie.js';
4
6
 
5
7
  export const AUTH_STRATEGY_NONE = 'none';
6
8
  export const AUTH_STRATEGY_PASSWORD = 'password';
@@ -12,6 +14,9 @@ export const SUPPORTED_AUTH_STRATEGIES = new Set([
12
14
  AUTH_STRATEGY_OIDC,
13
15
  ]);
14
16
 
17
+ const OIDC_FLOW_TTL_MS = 10 * 60 * 1000;
18
+ const OIDC_PROVIDER_GOOGLE = 'google';
19
+
15
20
  export function createRandomAuthPassword(length = 18) {
16
21
  const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
17
22
  const bytes = randomBytes(length);
@@ -36,6 +41,7 @@ function createResponse(statusCode, body, options = {}) {
36
41
  return {
37
42
  body,
38
43
  kind: options.kind || 'response',
44
+ redirectTo: options.redirectTo || '',
39
45
  setCookie: options.setCookie || null,
40
46
  statusCode,
41
47
  };
@@ -60,7 +66,127 @@ function prependBasePath(basePath, pathname) {
60
66
  return pathname === '/' ? basePath : `${basePath}${pathname}`;
61
67
  }
62
68
 
69
+ function appendHashParam(path, key, value, publicBaseUrl) {
70
+ const url = new URL(path, publicBaseUrl);
71
+ const hashParams = new URLSearchParams(url.hash.startsWith('#') ? url.hash.slice(1) : url.hash);
72
+ hashParams.set(key, value);
73
+ url.hash = hashParams.toString();
74
+ return `${url.pathname}${url.search}${url.hash}`;
75
+ }
76
+
77
+ function getDefaultReturnTo(basePath) {
78
+ return basePath ? `${basePath}/` : '/';
79
+ }
80
+
81
+ function sanitizeReturnTo(rawValue, { basePath = '', publicBaseUrl = '' } = {}) {
82
+ const fallback = getDefaultReturnTo(basePath);
83
+ const candidate = String(rawValue ?? '').trim();
84
+ if (!candidate) {
85
+ return fallback;
86
+ }
87
+
88
+ try {
89
+ const resolved = new URL(candidate, `${publicBaseUrl || 'http://localhost'}/`);
90
+ const publicOrigin = publicBaseUrl ? new URL(publicBaseUrl).origin : resolved.origin;
91
+ if (resolved.origin !== publicOrigin) {
92
+ return fallback;
93
+ }
94
+
95
+ if (basePath) {
96
+ const withinBasePath = resolved.pathname === basePath || resolved.pathname.startsWith(`${basePath}/`);
97
+ if (!withinBasePath) {
98
+ return fallback;
99
+ }
100
+ }
101
+
102
+ return `${resolved.pathname}${resolved.search}${resolved.hash}`;
103
+ } catch {
104
+ return fallback;
105
+ }
106
+ }
107
+
108
+ function isNonEmptyString(value) {
109
+ return typeof value === 'string' && value.trim().length > 0;
110
+ }
111
+
112
+ function normalizeEmailAddress(value) {
113
+ return String(value ?? '').trim().toLowerCase();
114
+ }
115
+
116
+ function getEmailDomain(email) {
117
+ const normalized = normalizeEmailAddress(email);
118
+ const atIndex = normalized.lastIndexOf('@');
119
+ if (atIndex <= 0 || atIndex === normalized.length - 1) {
120
+ return '';
121
+ }
122
+
123
+ return normalized.slice(atIndex + 1);
124
+ }
125
+
126
+ function isOidcUserAllowed(oidcConfig, email) {
127
+ const normalizedEmail = normalizeEmailAddress(email);
128
+ const emailDomain = getEmailDomain(normalizedEmail);
129
+ const allowedEmails = Array.isArray(oidcConfig?.allowedEmails) ? oidcConfig.allowedEmails : [];
130
+ const allowedDomains = Array.isArray(oidcConfig?.allowedDomains) ? oidcConfig.allowedDomains : [];
131
+
132
+ if (allowedEmails.length === 0 && allowedDomains.length === 0) {
133
+ return { allowed: true };
134
+ }
135
+
136
+ if (allowedEmails.includes(normalizedEmail)) {
137
+ return { allowed: true, reason: 'email' };
138
+ }
139
+
140
+ if (allowedDomains.includes(emailDomain)) {
141
+ return { allowed: true, reason: 'domain' };
142
+ }
143
+
144
+ return {
145
+ allowed: false,
146
+ error: allowedEmails.length > 0 && allowedDomains.length > 0
147
+ ? 'This Google account is not in the allowed email or domain list.'
148
+ : allowedEmails.length > 0
149
+ ? 'This Google account is not in the allowed email list.'
150
+ : 'This Google account is not in an allowed domain.',
151
+ };
152
+ }
153
+
154
+ function readAuthenticatedOidcSession(sessionCookieManager, req) {
155
+ const session = sessionCookieManager.readSession(req);
156
+ if (session?.strategy !== AUTH_STRATEGY_OIDC) {
157
+ return null;
158
+ }
159
+
160
+ const user = session.user;
161
+ if (
162
+ !user
163
+ || typeof user !== 'object'
164
+ || !isNonEmptyString(user.sub)
165
+ || !isNonEmptyString(user.email)
166
+ || !isNonEmptyString(user.name)
167
+ ) {
168
+ return null;
169
+ }
170
+
171
+ const expiresAt = Number(session.expiresAt);
172
+ if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
173
+ return null;
174
+ }
175
+
176
+ return {
177
+ ...session,
178
+ user: {
179
+ email: user.email,
180
+ emailVerified: user.emailVerified === true,
181
+ name: user.name,
182
+ picture: typeof user.picture === 'string' ? user.picture : '',
183
+ sub: user.sub,
184
+ },
185
+ };
186
+ }
187
+
63
188
  function buildClientConfig(authConfig, { basePath = '' } = {}) {
189
+ const loginEndpoint = prependBasePath(basePath, '/api/auth/oidc/login');
64
190
  const sessionEndpoint = prependBasePath(basePath, '/api/auth/session');
65
191
  const statusEndpoint = prependBasePath(basePath, '/api/auth/status');
66
192
 
@@ -90,11 +216,14 @@ function buildClientConfig(authConfig, { basePath = '' } = {}) {
90
216
 
91
217
  return {
92
218
  enabled: true,
93
- implemented: false,
219
+ implemented: true,
220
+ loginEndpoint,
221
+ provider: OIDC_PROVIDER_GOOGLE,
94
222
  requiresLogin: true,
95
223
  sessionEndpoint,
96
224
  statusEndpoint,
97
225
  strategy: AUTH_STRATEGY_OIDC,
226
+ submitLabel: 'Continue with Google',
98
227
  };
99
228
  }
100
229
 
@@ -107,6 +236,13 @@ function createPasswordStrategy(authConfig, sessionCookieManager, clientConfig)
107
236
  }
108
237
 
109
238
  return {
239
+ clearSession(req) {
240
+ return createResponse(200, { ok: true }, {
241
+ kind: 'logout_ok',
242
+ setCookie: sessionCookieManager.clearSession(req),
243
+ });
244
+ },
245
+
110
246
  createSession(req, body = {}) {
111
247
  if (typeof body.password !== 'string' || !body.password) {
112
248
  return createResponse(400, { error: 'Missing password' }, { kind: 'invalid_request' });
@@ -128,6 +264,7 @@ function createPasswordStrategy(authConfig, sessionCookieManager, clientConfig)
128
264
  auth: clientConfig,
129
265
  authenticated: true,
130
266
  ok: true,
267
+ user: null,
131
268
  }, {
132
269
  kind: 'session_created',
133
270
  setCookie: sessionCookieManager.createSessionCookie(req, {
@@ -137,10 +274,15 @@ function createPasswordStrategy(authConfig, sessionCookieManager, clientConfig)
137
274
  });
138
275
  },
139
276
 
277
+ getAuthenticatedUser() {
278
+ return null;
279
+ },
280
+
140
281
  getStatus(req) {
141
282
  return createResponse(200, {
142
283
  authenticated: hasValidSession(req),
143
284
  auth: clientConfig,
285
+ user: null,
144
286
  }, { kind: 'status' });
145
287
  },
146
288
 
@@ -152,14 +294,23 @@ function createPasswordStrategy(authConfig, sessionCookieManager, clientConfig)
152
294
 
153
295
  function createNoneStrategy(clientConfig) {
154
296
  return {
297
+ clearSession() {
298
+ return createResponse(200, { ok: true, user: null }, { kind: 'logout_ok' });
299
+ },
300
+
155
301
  createSession() {
156
302
  return createResponse(405, { error: 'Authentication is disabled' }, { kind: 'disabled' });
157
303
  },
158
304
 
305
+ getAuthenticatedUser() {
306
+ return null;
307
+ },
308
+
159
309
  getStatus() {
160
310
  return createResponse(200, {
161
311
  authenticated: true,
162
312
  auth: clientConfig,
313
+ user: null,
163
314
  }, { kind: 'status' });
164
315
  },
165
316
 
@@ -169,25 +320,204 @@ function createNoneStrategy(clientConfig) {
169
320
  };
170
321
  }
171
322
 
172
- function createOidcStrategy(clientConfig, sessionCookieManager) {
323
+ function createOidcStrategy(authConfig, sessionCookieManager, flowCookieManager, clientConfig, { basePath = '' } = {}) {
324
+ let oidcConfigPromise = null;
325
+
326
+ function clearOidcCookies(req, includeSession = true) {
327
+ const cookies = [flowCookieManager.clear(req)];
328
+ if (includeSession) {
329
+ cookies.unshift(sessionCookieManager.clearSession(req));
330
+ }
331
+ return cookies;
332
+ }
333
+
334
+ function createErrorRedirect(req, message, flowPayload = null) {
335
+ const returnTo = sanitizeReturnTo(flowPayload?.returnTo, {
336
+ basePath,
337
+ publicBaseUrl: authConfig.oidc.publicBaseUrl,
338
+ });
339
+ return createResponse(302, null, {
340
+ kind: 'redirect',
341
+ redirectTo: appendHashParam(returnTo, 'auth_error', message, authConfig.oidc.publicBaseUrl),
342
+ setCookie: clearOidcCookies(req),
343
+ });
344
+ }
345
+
346
+ async function getOidcConfiguration() {
347
+ if (!oidcConfigPromise) {
348
+ const allowInsecureRequests = new URL(authConfig.oidc.issuer).protocol !== 'https:';
349
+ oidcConfigPromise = oidc.discovery(
350
+ new URL(authConfig.oidc.issuer),
351
+ authConfig.oidc.clientId,
352
+ authConfig.oidc.clientSecret,
353
+ undefined,
354
+ allowInsecureRequests
355
+ ? { execute: [oidc.allowInsecureRequests] }
356
+ : undefined,
357
+ ).catch((error) => {
358
+ oidcConfigPromise = null;
359
+ throw error;
360
+ });
361
+ }
362
+
363
+ return oidcConfigPromise;
364
+ }
365
+
366
+ function getAuthenticatedSession(req) {
367
+ return readAuthenticatedOidcSession(sessionCookieManager, req);
368
+ }
369
+
370
+ function getAuthenticatedUser(req) {
371
+ return getAuthenticatedSession(req)?.user ?? null;
372
+ }
373
+
173
374
  return {
375
+ async beginLogin(req, requestUrl) {
376
+ const config = await getOidcConfiguration();
377
+ const returnTo = sanitizeReturnTo(requestUrl.searchParams.get('returnTo'), {
378
+ basePath,
379
+ publicBaseUrl: authConfig.oidc.publicBaseUrl,
380
+ });
381
+ const codeVerifier = oidc.randomPKCECodeVerifier();
382
+ const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
383
+ const nonce = oidc.randomNonce();
384
+ const state = oidc.randomState();
385
+ const redirectTo = oidc.buildAuthorizationUrl(config, {
386
+ code_challenge: codeChallenge,
387
+ code_challenge_method: 'S256',
388
+ ...(authConfig.oidc.allowedDomains.length === 1 ? { hd: authConfig.oidc.allowedDomains[0] } : {}),
389
+ nonce,
390
+ redirect_uri: authConfig.oidc.callbackUrl,
391
+ response_type: 'code',
392
+ scope: 'openid email profile',
393
+ state,
394
+ });
395
+
396
+ return createResponse(302, null, {
397
+ kind: 'redirect',
398
+ redirectTo: redirectTo.href,
399
+ setCookie: flowCookieManager.create(req, {
400
+ createdAt: Date.now(),
401
+ nonce,
402
+ pkceCodeVerifier: codeVerifier,
403
+ returnTo,
404
+ state,
405
+ }, {
406
+ expires: new Date(Date.now() + OIDC_FLOW_TTL_MS),
407
+ }),
408
+ });
409
+ },
410
+
411
+ async completeLogin(req, requestUrl) {
412
+ const flowPayload = flowCookieManager.read(req);
413
+ if (!flowPayload) {
414
+ return createErrorRedirect(req, 'Authentication session expired. Try again.');
415
+ }
416
+
417
+ if (requestUrl.searchParams.get('error')) {
418
+ const errorMessage = requestUrl.searchParams.get('error_description')
419
+ || requestUrl.searchParams.get('error')
420
+ || 'Authentication failed';
421
+ return createErrorRedirect(req, errorMessage, flowPayload);
422
+ }
423
+
424
+ if (
425
+ !isNonEmptyString(flowPayload.state)
426
+ || !isNonEmptyString(flowPayload.nonce)
427
+ || !isNonEmptyString(flowPayload.pkceCodeVerifier)
428
+ ) {
429
+ return createErrorRedirect(req, 'Authentication session expired. Try again.', flowPayload);
430
+ }
431
+
432
+ try {
433
+ const config = await getOidcConfiguration();
434
+ const callbackUrl = new URL(authConfig.oidc.callbackUrl);
435
+ callbackUrl.search = requestUrl.search;
436
+ const tokens = await oidc.authorizationCodeGrant(config, callbackUrl, {
437
+ expectedNonce: flowPayload.nonce,
438
+ expectedState: flowPayload.state,
439
+ idTokenExpected: true,
440
+ pkceCodeVerifier: flowPayload.pkceCodeVerifier,
441
+ });
442
+ const claims = tokens.claims();
443
+
444
+ if (
445
+ !claims
446
+ || !isNonEmptyString(claims.sub)
447
+ || !isNonEmptyString(claims.email)
448
+ || claims.email_verified !== true
449
+ || !isNonEmptyString(claims.name)
450
+ ) {
451
+ return createErrorRedirect(req, 'Google account is missing a verified email or name.', flowPayload);
452
+ }
453
+
454
+ const accessDecision = isOidcUserAllowed(authConfig.oidc, claims.email);
455
+ if (!accessDecision.allowed) {
456
+ return createErrorRedirect(req, accessDecision.error, flowPayload);
457
+ }
458
+
459
+ const expiresAt = Number.isFinite(claims.exp)
460
+ ? claims.exp * 1000
461
+ : Date.now() + (Math.max(Number(tokens.expires_in) || 3600, 1) * 1000);
462
+ const returnTo = sanitizeReturnTo(flowPayload.returnTo, {
463
+ basePath,
464
+ publicBaseUrl: authConfig.oidc.publicBaseUrl,
465
+ });
466
+ const user = {
467
+ email: claims.email,
468
+ emailVerified: true,
469
+ name: claims.name,
470
+ picture: typeof claims.picture === 'string' ? claims.picture : '',
471
+ sub: claims.sub,
472
+ };
473
+
474
+ return createResponse(302, null, {
475
+ kind: 'redirect',
476
+ redirectTo: returnTo,
477
+ setCookie: [
478
+ sessionCookieManager.createSessionCookie(req, {
479
+ authenticatedAt: Date.now(),
480
+ expiresAt,
481
+ strategy: AUTH_STRATEGY_OIDC,
482
+ user,
483
+ }),
484
+ flowCookieManager.clear(req),
485
+ ],
486
+ });
487
+ } catch (error) {
488
+ console.error('[auth] OIDC callback failed:', error.message);
489
+ return createErrorRedirect(req, 'Google sign-in failed. Try again.', flowPayload);
490
+ }
491
+ },
492
+
493
+ clearSession(req) {
494
+ return createResponse(200, { ok: true, user: null }, {
495
+ kind: 'logout_ok',
496
+ setCookie: clearOidcCookies(req),
497
+ });
498
+ },
499
+
174
500
  createSession() {
175
- return createResponse(501, {
501
+ return createResponse(405, {
176
502
  auth: clientConfig,
177
- code: 'AUTH_NOT_IMPLEMENTED',
178
- error: 'OIDC authentication is not implemented yet',
179
- }, { kind: 'not_implemented' });
503
+ code: 'AUTH_LOGIN_REDIRECT_REQUIRED',
504
+ error: 'Use the OIDC login endpoint to authenticate.',
505
+ }, { kind: 'invalid_request' });
180
506
  },
181
507
 
182
- getStatus() {
508
+ getAuthenticatedUser,
509
+
510
+ getStatus(req) {
511
+ const user = getAuthenticatedUser(req);
183
512
  return createResponse(200, {
184
- authenticated: false,
513
+ authenticated: Boolean(user),
185
514
  auth: clientConfig,
515
+ user,
186
516
  }, { kind: 'status' });
187
517
  },
188
518
 
189
519
  isAuthenticated(req) {
190
- return sessionCookieManager.readSession(req)?.strategy === AUTH_STRATEGY_OIDC;
520
+ return Boolean(getAuthenticatedSession(req));
191
521
  },
192
522
  };
193
523
  }
@@ -207,7 +537,18 @@ export function createAuthService(config) {
207
537
  if (authConfig.strategy === AUTH_STRATEGY_PASSWORD) {
208
538
  strategy = createPasswordStrategy(authConfig, sessionCookieManager, clientConfig);
209
539
  } else if (authConfig.strategy === AUTH_STRATEGY_OIDC) {
210
- strategy = createOidcStrategy(clientConfig, sessionCookieManager);
540
+ const flowCookieManager = createSignedCookieManager({
541
+ cookieName: authConfig.oidc.flowCookieName,
542
+ cookiePath: config.basePath || '/',
543
+ secret: authConfig.sessionSecret,
544
+ });
545
+ strategy = createOidcStrategy(
546
+ authConfig,
547
+ sessionCookieManager,
548
+ flowCookieManager,
549
+ clientConfig,
550
+ { basePath: config.basePath ?? '' },
551
+ );
211
552
  } else {
212
553
  strategy = createNoneStrategy(clientConfig);
213
554
  }
@@ -241,17 +582,34 @@ export function createAuthService(config) {
241
582
  };
242
583
  },
243
584
 
585
+ beginOidcLogin(req, requestUrl) {
586
+ if (authConfig.strategy !== AUTH_STRATEGY_OIDC || typeof strategy.beginLogin !== 'function') {
587
+ return createResponse(404, { error: 'OIDC endpoint not found' }, { kind: 'not_found' });
588
+ }
589
+
590
+ return strategy.beginLogin(req, requestUrl);
591
+ },
592
+
244
593
  clearSession(req) {
245
- return createResponse(200, { ok: true }, {
246
- kind: 'logout_ok',
247
- setCookie: sessionCookieManager.clearSession(req),
248
- });
594
+ return strategy.clearSession(req);
595
+ },
596
+
597
+ completeOidcLogin(req, requestUrl) {
598
+ if (authConfig.strategy !== AUTH_STRATEGY_OIDC || typeof strategy.completeLogin !== 'function') {
599
+ return createResponse(404, { error: 'OIDC endpoint not found' }, { kind: 'not_found' });
600
+ }
601
+
602
+ return strategy.completeLogin(req, requestUrl);
249
603
  },
250
604
 
251
605
  createSession(req, body) {
252
606
  return strategy.createSession(req, body);
253
607
  },
254
608
 
609
+ getAuthenticatedUser(req) {
610
+ return strategy.getAuthenticatedUser(req);
611
+ },
612
+
255
613
  getClientConfig() {
256
614
  return clientConfig;
257
615
  },
@@ -51,6 +51,32 @@ export function createSessionCookieManager({
51
51
  cookieName,
52
52
  cookiePath = '/',
53
53
  secret,
54
+ }) {
55
+ const signedCookieManager = createSignedCookieManager({
56
+ cookieName,
57
+ cookiePath,
58
+ secret,
59
+ });
60
+
61
+ return {
62
+ clearSession(req) {
63
+ return signedCookieManager.clear(req);
64
+ },
65
+
66
+ createSessionCookie(req, payload, options = {}) {
67
+ return signedCookieManager.create(req, payload, options);
68
+ },
69
+
70
+ readSession(req) {
71
+ return signedCookieManager.read(req);
72
+ },
73
+ };
74
+ }
75
+
76
+ export function createSignedCookieManager({
77
+ cookieName,
78
+ cookiePath = '/',
79
+ secret,
54
80
  }) {
55
81
  function createCookieAttributes(req, { expires = null } = {}) {
56
82
  const attributes = [
@@ -71,24 +97,24 @@ export function createSessionCookieManager({
71
97
  }
72
98
 
73
99
  return {
74
- clearSession(req) {
100
+ clear(req) {
75
101
  return [
76
102
  `${cookieName}=`,
77
103
  ...createCookieAttributes(req, { expires: new Date(0) }),
78
104
  ].join('; ');
79
105
  },
80
106
 
81
- createSessionCookie(req, payload) {
107
+ create(req, payload, { expires = null } = {}) {
82
108
  const serializedPayload = JSON.stringify(payload);
83
109
  const encodedPayload = encodeBase64Url(serializedPayload);
84
110
  const signature = encodeBase64Url(createSignature(encodedPayload, secret));
85
111
  return [
86
112
  `${cookieName}=${encodedPayload}.${signature}`,
87
- ...createCookieAttributes(req),
113
+ ...createCookieAttributes(req, { expires }),
88
114
  ].join('; ');
89
115
  },
90
116
 
91
- readSession(req) {
117
+ read(req) {
92
118
  const token = parseCookieHeader(req.headers.cookie).get(cookieName);
93
119
  if (!token) {
94
120
  return null;