@mcp-z/oauth 1.0.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.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/dist/cjs/account-utils.d.cts +107 -0
  4. package/dist/cjs/account-utils.d.ts +107 -0
  5. package/dist/cjs/account-utils.js +481 -0
  6. package/dist/cjs/account-utils.js.map +1 -0
  7. package/dist/cjs/index.d.cts +19 -0
  8. package/dist/cjs/index.d.ts +19 -0
  9. package/dist/cjs/index.js +149 -0
  10. package/dist/cjs/index.js.map +1 -0
  11. package/dist/cjs/jwt-auth.d.cts +53 -0
  12. package/dist/cjs/jwt-auth.d.ts +53 -0
  13. package/dist/cjs/jwt-auth.js +417 -0
  14. package/dist/cjs/jwt-auth.js.map +1 -0
  15. package/dist/cjs/key-utils.d.cts +131 -0
  16. package/dist/cjs/key-utils.d.ts +131 -0
  17. package/dist/cjs/key-utils.js +421 -0
  18. package/dist/cjs/key-utils.js.map +1 -0
  19. package/dist/cjs/lib/account-server/index.d.cts +45 -0
  20. package/dist/cjs/lib/account-server/index.d.ts +45 -0
  21. package/dist/cjs/lib/account-server/index.js +67 -0
  22. package/dist/cjs/lib/account-server/index.js.map +1 -0
  23. package/dist/cjs/lib/account-server/loopback.d.cts +22 -0
  24. package/dist/cjs/lib/account-server/loopback.d.ts +22 -0
  25. package/dist/cjs/lib/account-server/loopback.js +778 -0
  26. package/dist/cjs/lib/account-server/loopback.js.map +1 -0
  27. package/dist/cjs/lib/account-server/me.d.cts +23 -0
  28. package/dist/cjs/lib/account-server/me.d.ts +23 -0
  29. package/dist/cjs/lib/account-server/me.js +412 -0
  30. package/dist/cjs/lib/account-server/me.js.map +1 -0
  31. package/dist/cjs/lib/account-server/shared-utils.d.cts +6 -0
  32. package/dist/cjs/lib/account-server/shared-utils.d.ts +6 -0
  33. package/dist/cjs/lib/account-server/shared-utils.js +235 -0
  34. package/dist/cjs/lib/account-server/shared-utils.js.map +1 -0
  35. package/dist/cjs/lib/account-server/stateless.d.cts +20 -0
  36. package/dist/cjs/lib/account-server/stateless.d.ts +20 -0
  37. package/dist/cjs/lib/account-server/stateless.js +32 -0
  38. package/dist/cjs/lib/account-server/stateless.js.map +1 -0
  39. package/dist/cjs/lib/account-server/types.d.cts +32 -0
  40. package/dist/cjs/lib/account-server/types.d.ts +32 -0
  41. package/dist/cjs/lib/account-server/types.js +7 -0
  42. package/dist/cjs/lib/account-server/types.js.map +1 -0
  43. package/dist/cjs/lib/dcr-types.d.cts +126 -0
  44. package/dist/cjs/lib/dcr-types.d.ts +126 -0
  45. package/dist/cjs/lib/dcr-types.js +12 -0
  46. package/dist/cjs/lib/dcr-types.js.map +1 -0
  47. package/dist/cjs/lib/rfc-metadata-types.d.cts +46 -0
  48. package/dist/cjs/lib/rfc-metadata-types.d.ts +46 -0
  49. package/dist/cjs/lib/rfc-metadata-types.js +8 -0
  50. package/dist/cjs/lib/rfc-metadata-types.js.map +1 -0
  51. package/dist/cjs/package.json +1 -0
  52. package/dist/cjs/pkce.d.cts +36 -0
  53. package/dist/cjs/pkce.d.ts +36 -0
  54. package/dist/cjs/pkce.js +25 -0
  55. package/dist/cjs/pkce.js.map +1 -0
  56. package/dist/cjs/sanitizer.d.cts +37 -0
  57. package/dist/cjs/sanitizer.d.ts +37 -0
  58. package/dist/cjs/sanitizer.js +407 -0
  59. package/dist/cjs/sanitizer.js.map +1 -0
  60. package/dist/cjs/schemas/index.d.cts +36 -0
  61. package/dist/cjs/schemas/index.d.ts +36 -0
  62. package/dist/cjs/schemas/index.js +28 -0
  63. package/dist/cjs/schemas/index.js.map +1 -0
  64. package/dist/cjs/session-auth.d.cts +79 -0
  65. package/dist/cjs/session-auth.d.ts +79 -0
  66. package/dist/cjs/session-auth.js +354 -0
  67. package/dist/cjs/session-auth.js.map +1 -0
  68. package/dist/cjs/templates.d.cts +18 -0
  69. package/dist/cjs/templates.d.ts +18 -0
  70. package/dist/cjs/templates.js +38 -0
  71. package/dist/cjs/templates.js.map +1 -0
  72. package/dist/cjs/types.d.cts +343 -0
  73. package/dist/cjs/types.d.ts +343 -0
  74. package/dist/cjs/types.js +210 -0
  75. package/dist/cjs/types.js.map +1 -0
  76. package/dist/esm/account-utils.d.ts +107 -0
  77. package/dist/esm/account-utils.js +179 -0
  78. package/dist/esm/account-utils.js.map +1 -0
  79. package/dist/esm/index.d.ts +19 -0
  80. package/dist/esm/index.js +23 -0
  81. package/dist/esm/index.js.map +1 -0
  82. package/dist/esm/jwt-auth.d.ts +53 -0
  83. package/dist/esm/jwt-auth.js +164 -0
  84. package/dist/esm/jwt-auth.js.map +1 -0
  85. package/dist/esm/key-utils.d.ts +131 -0
  86. package/dist/esm/key-utils.js +143 -0
  87. package/dist/esm/key-utils.js.map +1 -0
  88. package/dist/esm/lib/account-server/index.d.ts +45 -0
  89. package/dist/esm/lib/account-server/index.js +41 -0
  90. package/dist/esm/lib/account-server/index.js.map +1 -0
  91. package/dist/esm/lib/account-server/loopback.d.ts +22 -0
  92. package/dist/esm/lib/account-server/loopback.js +372 -0
  93. package/dist/esm/lib/account-server/loopback.js.map +1 -0
  94. package/dist/esm/lib/account-server/me.d.ts +23 -0
  95. package/dist/esm/lib/account-server/me.js +170 -0
  96. package/dist/esm/lib/account-server/me.js.map +1 -0
  97. package/dist/esm/lib/account-server/shared-utils.d.ts +6 -0
  98. package/dist/esm/lib/account-server/shared-utils.js +24 -0
  99. package/dist/esm/lib/account-server/shared-utils.js.map +1 -0
  100. package/dist/esm/lib/account-server/stateless.d.ts +20 -0
  101. package/dist/esm/lib/account-server/stateless.js +25 -0
  102. package/dist/esm/lib/account-server/stateless.js.map +1 -0
  103. package/dist/esm/lib/account-server/types.d.ts +32 -0
  104. package/dist/esm/lib/account-server/types.js +6 -0
  105. package/dist/esm/lib/account-server/types.js.map +1 -0
  106. package/dist/esm/lib/dcr-types.d.ts +126 -0
  107. package/dist/esm/lib/dcr-types.js +13 -0
  108. package/dist/esm/lib/dcr-types.js.map +1 -0
  109. package/dist/esm/lib/rfc-metadata-types.d.ts +46 -0
  110. package/dist/esm/lib/rfc-metadata-types.js +7 -0
  111. package/dist/esm/lib/rfc-metadata-types.js.map +1 -0
  112. package/dist/esm/package.json +1 -0
  113. package/dist/esm/pkce.d.ts +36 -0
  114. package/dist/esm/pkce.js +33 -0
  115. package/dist/esm/pkce.js.map +1 -0
  116. package/dist/esm/sanitizer.d.ts +37 -0
  117. package/dist/esm/sanitizer.js +256 -0
  118. package/dist/esm/sanitizer.js.map +1 -0
  119. package/dist/esm/schemas/index.d.ts +36 -0
  120. package/dist/esm/schemas/index.js +19 -0
  121. package/dist/esm/schemas/index.js.map +1 -0
  122. package/dist/esm/session-auth.d.ts +79 -0
  123. package/dist/esm/session-auth.js +141 -0
  124. package/dist/esm/session-auth.js.map +1 -0
  125. package/dist/esm/templates.d.ts +18 -0
  126. package/dist/esm/templates.js +132 -0
  127. package/dist/esm/templates.js.map +1 -0
  128. package/dist/esm/types.d.ts +343 -0
  129. package/dist/esm/types.js +34 -0
  130. package/dist/esm/types.js.map +1 -0
  131. package/package.json +82 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Data sanitization utilities for secure logging.
3
+ * Redacts sensitive OAuth tokens, API keys, and credentials from log output.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * sanitizeData({ accountId: 'test@example.com', access_token: 'secret_token_value' })
8
+ * // { accountId: 'test@example.com', access_token: 'secr****alue' }
9
+ *
10
+ * sanitizeForLogging('Processing token', { token: 'secret_value' })
11
+ * // { message: 'Processing token', meta: { token: 'secr****alue' } }
12
+ * ```
13
+ */ /** Regex patterns for sensitive data that should be redacted from logs */ const SENSITIVE_PATTERNS = [
14
+ // OAuth tokens, codes, and secrets
15
+ /access_token['":\s]*['"]\s*([^'"]+)['"]/gi,
16
+ /(access_token_[a-zA-Z0-9_]+)/gi,
17
+ /refresh_token['":\s]*['"]\s*([^'"]+)['"]/gi,
18
+ /client_secret['":\s]*['"]\s*([^'"]+)['"]/gi,
19
+ /id_token['":\s]*['"]\s*([^'"]+)['"]/gi,
20
+ /\bcode['":\s]*['"]\s*([^'"]+)['"]/gi,
21
+ /\bstate['":\s]*['"]\s*([^'"]+)['"]/gi,
22
+ /code_verifier['":\s]*['"]\s*([^'"]+)['"]/gi,
23
+ /code_challenge['":\s]*['"]\s*([^'"]+)['"]/gi,
24
+ /codeVerifier['":\s]*['"]\s*([^'"]+)['"]/gi,
25
+ /codeChallenge['":\s]*['"]\s*([^'"]+)['"]/gi,
26
+ /device_code['":\s]*['"]\s*([^'"]+)['"]/gi,
27
+ /user_code['":\s]*['"]\s*([^'"]+)['"]/gi,
28
+ /verification_uri['":\s]*['"]\s*([^'"]+)['"]/gi,
29
+ /verification_uri_complete['":\s]*['"]\s*([^'"]+)['"]/gi,
30
+ // Provider credentials and identifiers
31
+ /app_secret['":\s]*['"]\s*([^'"]+)['"]/gi,
32
+ /appSecret['":\s]*['"]\s*([^'"]+)['"]/gi,
33
+ /tenant_id['":\s]*['"]\s*([^'"]+)['"]/gi,
34
+ /tenantId['":\s]*['"]\s*([^'"]+)['"]/gi,
35
+ /client_id['":\s]*['"]\s*([^'"]+)['"]/gi,
36
+ /clientId['":\s]*['"]\s*([^'"]+)['"]/gi,
37
+ /app_id['":\s]*['"]\s*([^'"]+)['"]/gi,
38
+ /appId['":\s]*['"]\s*([^'"]+)['"]/gi,
39
+ /redirect_uri['":\s]*['"]\s*([^'"]+)['"]/gi,
40
+ /redirectUri['":\s]*['"]\s*([^'"]+)['"]/gi,
41
+ /subscription_key['":\s]*['"]\s*([^'"]+)['"]/gi,
42
+ /subscriptionKey['":\s]*['"]\s*([^'"]+)['"]/gi,
43
+ // Security secrets and keys
44
+ /webhook_secret['":\s]*['"]\s*([^'"]+)['"]/gi,
45
+ /webhookSecret['":\s]*['"]\s*([^'"]+)['"]/gi,
46
+ /signing_secret['":\s]*['"]\s*([^'"]+)['"]/gi,
47
+ /signingSecret['":\s]*['"]\s*([^'"]+)['"]/gi,
48
+ /encryption_key['":\s]*['"]\s*([^'"]+)['"]/gi,
49
+ /encryptionKey['":\s]*['"]\s*([^'"]+)['"]/gi,
50
+ /private_key['":\s]*['"]\s*([^'"]+)['"]/gi,
51
+ /privateKey['":\s]*['"]\s*([^'"]+)['"]/gi,
52
+ /certificate['":\s]*['"]\s*([^'"]+)['"]/gi,
53
+ /cert['":\s]*['"]\s*([^'"]+)['"]/gi,
54
+ // Authorization headers
55
+ /Authorization['":\s]*['"]\s*Bearer\s+([^'"]+)['"]/gi,
56
+ /authorization['":\s]*['"]\s*Bearer\s+([^'"]+)['"]/gi,
57
+ /Bearer\s+([A-Za-z0-9+/=\-_.]+)/gi,
58
+ /Authorization:\s*Bearer\s+([A-Za-z0-9+/=\-_.]+)/gi,
59
+ /[A-Z_]+_(SECRET|KEY|TOKEN|PASSWORD)['":\s]*['"]\s*([^'"]+)['"]/gi,
60
+ // Session and CSRF tokens
61
+ /\bnonce['":\s]*['"]\s*([^'"]+)['"]/gi,
62
+ /session[_-]?id['":\s]*['"]\s*([^'"]+)['"]/gi,
63
+ /csrf[_-]?token['":\s]*['"]\s*([^'"]+)['"]/gi,
64
+ // Other sensitive patterns
65
+ /"email"\s*:\s*"([^@"]{1,64}@[^."]{1,63}\.[a-z]{2,6})"/gi,
66
+ /api[_-]?key['":\s]*['"]\s*([^'"]+)['"]/gi,
67
+ /password['":\s]*['"]\s*([^'"]+)['"]/gi,
68
+ /\b(ey[A-Za-z0-9+/=]+\.[A-Za-z0-9+/=]+\.[A-Za-z0-9+/=\-_]+)/g,
69
+ // Base64 secrets (split into length ranges for practical matching)
70
+ /\b([A-Za-z0-9+/]{60,200}={0,2})\b/g,
71
+ /\b([A-Za-z0-9+/]{201,1000}={0,2})\b/g,
72
+ /\b([A-Za-z0-9+/]{1001,5000}={0,2})\b/g,
73
+ // Connection identifiers
74
+ /connection[_-]?id['":\s]*['"]\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})['"]/gi
75
+ ];
76
+ /** Field names that should be redacted when found as object keys */ const SENSITIVE_FIELDS = new Set([
77
+ 'access_token',
78
+ 'accessToken',
79
+ 'refresh_token',
80
+ 'refreshToken',
81
+ 'client_secret',
82
+ 'clientSecret',
83
+ 'id_token',
84
+ 'idToken',
85
+ 'code',
86
+ 'authorization_code',
87
+ 'authorizationCode',
88
+ 'device_code',
89
+ 'deviceCode',
90
+ 'user_code',
91
+ 'userCode',
92
+ 'verification_uri',
93
+ 'verificationUri',
94
+ 'verification_uri_complete',
95
+ 'verificationUriComplete',
96
+ 'client_id',
97
+ 'clientId',
98
+ 'app_id',
99
+ 'appId',
100
+ 'app_secret',
101
+ 'appSecret',
102
+ 'tenant_id',
103
+ 'tenantId',
104
+ 'bot_id',
105
+ 'botId',
106
+ 'workspace_id',
107
+ 'workspaceId',
108
+ 'organization_id',
109
+ 'organizationId',
110
+ 'redirect_uri',
111
+ 'redirectUri',
112
+ 'audience',
113
+ 'realm',
114
+ 'domain',
115
+ 'webhook_secret',
116
+ 'webhookSecret',
117
+ 'signing_secret',
118
+ 'signingSecret',
119
+ 'subscription_key',
120
+ 'subscriptionKey',
121
+ 'encryption_key',
122
+ 'encryptionKey',
123
+ 'private_key',
124
+ 'privateKey',
125
+ 'certificate',
126
+ 'cert',
127
+ 'stripe-signature',
128
+ 'x-hub-signature',
129
+ 'x-hub-signature-256',
130
+ 'x-slack-signature',
131
+ 'x-mcp-z-webhook-secret',
132
+ 'password',
133
+ 'secret',
134
+ 'token',
135
+ 'authorization',
136
+ 'credential',
137
+ 'auth',
138
+ 'verifier',
139
+ 'challenge',
140
+ 'code_verifier',
141
+ 'codeVerifier',
142
+ 'code_challenge',
143
+ 'codeChallenge',
144
+ 'nonce',
145
+ 'session_id',
146
+ 'sessionId',
147
+ 'csrf_token',
148
+ 'csrfToken',
149
+ 'api_key',
150
+ 'apiKey',
151
+ 'state',
152
+ 'connection_id',
153
+ 'connectionId',
154
+ 'gmail_connection_id',
155
+ 'gmailConnectionId'
156
+ ]);
157
+ function isAlreadySanitized(value) {
158
+ return value.includes('****') || value.includes('[REDACTED]') || value === '[REDACTED]';
159
+ }
160
+ function redactValue(value) {
161
+ if (isAlreadySanitized(value)) {
162
+ return value;
163
+ }
164
+ if (value.length <= 8) {
165
+ return '*'.repeat(value.length);
166
+ }
167
+ // Show first 4 and last 4 characters
168
+ return `${value.substring(0, 4)}****${value.substring(value.length - 4)}`;
169
+ }
170
+ export function sanitizeData(data) {
171
+ if (typeof data === 'string') {
172
+ if (isAlreadySanitized(data)) {
173
+ return data;
174
+ }
175
+ let sanitized = data;
176
+ for (const pattern of SENSITIVE_PATTERNS){
177
+ sanitized = sanitized.replace(pattern, (match, captured)=>{
178
+ if (typeof captured === 'string') {
179
+ const redacted = redactValue(captured);
180
+ return match.replace(captured, redacted);
181
+ }
182
+ return match;
183
+ });
184
+ }
185
+ return sanitized;
186
+ }
187
+ if (Array.isArray(data)) {
188
+ return data.map(sanitizeData);
189
+ }
190
+ if (data && typeof data === 'object') {
191
+ const sanitized = {};
192
+ for (const [key, value] of Object.entries(data)){
193
+ const lowerKey = key.toLowerCase();
194
+ if (SENSITIVE_FIELDS.has(lowerKey) || SENSITIVE_FIELDS.has(key)) {
195
+ if (typeof value === 'string') {
196
+ sanitized[key] = redactValue(value);
197
+ } else {
198
+ sanitized[key] = '[REDACTED]';
199
+ }
200
+ } else {
201
+ sanitized[key] = sanitizeData(value);
202
+ }
203
+ }
204
+ return sanitized;
205
+ }
206
+ return data;
207
+ }
208
+ /**
209
+ * Prevent log injection attacks by escaping control characters
210
+ * SECURITY: Critical for preventing CRLF injection (OWASP A03)
211
+ */ export function sanitizeLogMessage(message, maxLength = 50000) {
212
+ if (typeof message !== 'string') {
213
+ return String(message);
214
+ }
215
+ // Truncation protection - prevent log poisoning via huge payloads
216
+ let processedMessage = message;
217
+ if (processedMessage.length > maxLength) {
218
+ processedMessage = `${processedMessage.substring(0, maxLength)} [TRUNCATED]`;
219
+ }
220
+ return processedMessage.normalize('NFKC').replace(/\r\n|\r|\n/g, ' ').replace(/\t/g, ' ')// biome-ignore lint/suspicious/noControlCharactersInRegex: Security sanitization requires control character removal
221
+ .replace(/[\x00-\x1F\x7F-\x9F]/g, '').replace(/[\u200B-\u200D\uFEFF]/g, '') // Zero-width chars used for obfuscation
222
+ .trim();
223
+ }
224
+ /**
225
+ * Sanitize log message and metadata for safe logging
226
+ * Applies both CRLF protection and sensitive data redaction
227
+ *
228
+ * @param message - The log message to sanitize
229
+ * @param meta - Optional metadata object to sanitize
230
+ * @param enableDataSanitization - Whether to apply sensitive data redaction (default: true)
231
+ * @returns Sanitized message and metadata ready for logging
232
+ */ export function sanitizeForLogging(message, meta, enableDataSanitization = true) {
233
+ const cleanMessage = sanitizeLogMessage(message);
234
+ if (!enableDataSanitization) {
235
+ return {
236
+ message: cleanMessage,
237
+ meta: meta || {}
238
+ };
239
+ }
240
+ return {
241
+ message: sanitizeData(cleanMessage),
242
+ meta: sanitizeData(meta || {})
243
+ };
244
+ }
245
+ export function sanitizeForLoggingFormatter() {
246
+ return {
247
+ log: (obj)=>{
248
+ const message = obj.msg || obj.message || '';
249
+ const { message: clean, meta } = sanitizeForLogging(message, obj);
250
+ return {
251
+ ...meta,
252
+ msg: clean
253
+ };
254
+ }
255
+ };
256
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth/src/sanitizer.ts"],"sourcesContent":["/**\n * Data sanitization utilities for secure logging.\n * Redacts sensitive OAuth tokens, API keys, and credentials from log output.\n *\n * @example\n * ```typescript\n * sanitizeData({ accountId: 'test@example.com', access_token: 'secret_token_value' })\n * // { accountId: 'test@example.com', access_token: 'secr****alue' }\n *\n * sanitizeForLogging('Processing token', { token: 'secret_value' })\n * // { message: 'Processing token', meta: { token: 'secr****alue' } }\n * ```\n */\n\n/** Regex patterns for sensitive data that should be redacted from logs */\nconst SENSITIVE_PATTERNS = [\n // OAuth tokens, codes, and secrets\n /access_token['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /(access_token_[a-zA-Z0-9_]+)/gi,\n /refresh_token['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /client_secret['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /id_token['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /\\bcode['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /\\bstate['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /code_verifier['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /code_challenge['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /codeVerifier['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /codeChallenge['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /device_code['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /user_code['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /verification_uri['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /verification_uri_complete['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n\n // Provider credentials and identifiers\n /app_secret['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /appSecret['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /tenant_id['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /tenantId['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /client_id['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /clientId['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /app_id['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /appId['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /redirect_uri['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /redirectUri['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /subscription_key['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /subscriptionKey['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n\n // Security secrets and keys\n /webhook_secret['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /webhookSecret['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /signing_secret['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /signingSecret['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /encryption_key['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /encryptionKey['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /private_key['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /privateKey['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /certificate['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /cert['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n\n // Authorization headers\n /Authorization['\":\\s]*['\"]\\s*Bearer\\s+([^'\"]+)['\"]/gi,\n /authorization['\":\\s]*['\"]\\s*Bearer\\s+([^'\"]+)['\"]/gi,\n /Bearer\\s+([A-Za-z0-9+/=\\-_.]+)/gi,\n /Authorization:\\s*Bearer\\s+([A-Za-z0-9+/=\\-_.]+)/gi,\n /[A-Z_]+_(SECRET|KEY|TOKEN|PASSWORD)['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n\n // Session and CSRF tokens\n /\\bnonce['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /session[_-]?id['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /csrf[_-]?token['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n\n // Other sensitive patterns\n /\"email\"\\s*:\\s*\"([^@\"]{1,64}@[^.\"]{1,63}\\.[a-z]{2,6})\"/gi,\n /api[_-]?key['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /password['\":\\s]*['\"]\\s*([^'\"]+)['\"]/gi,\n /\\b(ey[A-Za-z0-9+/=]+\\.[A-Za-z0-9+/=]+\\.[A-Za-z0-9+/=\\-_]+)/g,\n\n // Base64 secrets (split into length ranges for practical matching)\n /\\b([A-Za-z0-9+/]{60,200}={0,2})\\b/g,\n /\\b([A-Za-z0-9+/]{201,1000}={0,2})\\b/g,\n /\\b([A-Za-z0-9+/]{1001,5000}={0,2})\\b/g,\n\n // Connection identifiers\n /connection[_-]?id['\":\\s]*['\"]\\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})['\"]/gi,\n];\n\n/** Field names that should be redacted when found as object keys */\nconst SENSITIVE_FIELDS = new Set([\n 'access_token',\n 'accessToken',\n 'refresh_token',\n 'refreshToken',\n 'client_secret',\n 'clientSecret',\n 'id_token',\n 'idToken',\n 'code',\n 'authorization_code',\n 'authorizationCode',\n 'device_code',\n 'deviceCode',\n 'user_code',\n 'userCode',\n 'verification_uri',\n 'verificationUri',\n 'verification_uri_complete',\n 'verificationUriComplete',\n 'client_id',\n 'clientId',\n 'app_id',\n 'appId',\n 'app_secret',\n 'appSecret',\n 'tenant_id',\n 'tenantId',\n 'bot_id',\n 'botId',\n 'workspace_id',\n 'workspaceId',\n 'organization_id',\n 'organizationId',\n 'redirect_uri',\n 'redirectUri',\n 'audience',\n 'realm',\n 'domain',\n 'webhook_secret',\n 'webhookSecret',\n 'signing_secret',\n 'signingSecret',\n 'subscription_key',\n 'subscriptionKey',\n 'encryption_key',\n 'encryptionKey',\n 'private_key',\n 'privateKey',\n 'certificate',\n 'cert',\n 'stripe-signature',\n 'x-hub-signature',\n 'x-hub-signature-256',\n 'x-slack-signature',\n 'x-mcp-z-webhook-secret',\n 'password',\n 'secret',\n 'token',\n 'authorization',\n 'credential',\n 'auth',\n 'verifier',\n 'challenge',\n 'code_verifier',\n 'codeVerifier',\n 'code_challenge',\n 'codeChallenge',\n 'nonce',\n 'session_id',\n 'sessionId',\n 'csrf_token',\n 'csrfToken',\n 'api_key',\n 'apiKey',\n 'state',\n 'connection_id',\n 'connectionId',\n 'gmail_connection_id',\n 'gmailConnectionId',\n]);\n\nfunction isAlreadySanitized(value: string): boolean {\n return value.includes('****') || value.includes('[REDACTED]') || value === '[REDACTED]';\n}\n\nfunction redactValue(value: string): string {\n if (isAlreadySanitized(value)) {\n return value;\n }\n\n if (value.length <= 8) {\n return '*'.repeat(value.length);\n }\n\n // Show first 4 and last 4 characters\n return `${value.substring(0, 4)}****${value.substring(value.length - 4)}`;\n}\n\nexport function sanitizeData(data: unknown): unknown {\n if (typeof data === 'string') {\n if (isAlreadySanitized(data)) {\n return data;\n }\n\n let sanitized = data;\n for (const pattern of SENSITIVE_PATTERNS) {\n sanitized = sanitized.replace(pattern, (match, captured) => {\n if (typeof captured === 'string') {\n const redacted = redactValue(captured);\n return match.replace(captured, redacted);\n }\n return match;\n });\n }\n\n return sanitized;\n }\n\n if (Array.isArray(data)) {\n return data.map(sanitizeData);\n }\n\n if (data && typeof data === 'object') {\n const sanitized: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(data)) {\n const lowerKey = key.toLowerCase();\n\n if (SENSITIVE_FIELDS.has(lowerKey) || SENSITIVE_FIELDS.has(key)) {\n if (typeof value === 'string') {\n sanitized[key] = redactValue(value);\n } else {\n sanitized[key] = '[REDACTED]';\n }\n } else {\n sanitized[key] = sanitizeData(value);\n }\n }\n\n return sanitized;\n }\n\n return data;\n}\n\n/**\n * Prevent log injection attacks by escaping control characters\n * SECURITY: Critical for preventing CRLF injection (OWASP A03)\n */\nexport function sanitizeLogMessage(message: string, maxLength = 50000): string {\n if (typeof message !== 'string') {\n return String(message);\n }\n\n // Truncation protection - prevent log poisoning via huge payloads\n let processedMessage = message;\n if (processedMessage.length > maxLength) {\n processedMessage = `${processedMessage.substring(0, maxLength)} [TRUNCATED]`;\n }\n\n return (\n processedMessage\n .normalize('NFKC')\n .replace(/\\r\\n|\\r|\\n/g, ' ')\n .replace(/\\t/g, ' ')\n // biome-ignore lint/suspicious/noControlCharactersInRegex: Security sanitization requires control character removal\n .replace(/[\\x00-\\x1F\\x7F-\\x9F]/g, '')\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Zero-width chars used for obfuscation\n .trim()\n );\n}\n\n/**\n * Sanitize log message and metadata for safe logging\n * Applies both CRLF protection and sensitive data redaction\n *\n * @param message - The log message to sanitize\n * @param meta - Optional metadata object to sanitize\n * @param enableDataSanitization - Whether to apply sensitive data redaction (default: true)\n * @returns Sanitized message and metadata ready for logging\n */\nexport function sanitizeForLogging(message: string, meta?: Record<string, unknown>, enableDataSanitization = true): { message: string; meta: Record<string, unknown> } {\n const cleanMessage = sanitizeLogMessage(message);\n\n if (!enableDataSanitization) {\n return {\n message: cleanMessage,\n meta: meta || {},\n };\n }\n\n return {\n message: sanitizeData(cleanMessage) as string,\n meta: sanitizeData(meta || {}) as Record<string, unknown>,\n };\n}\n\nexport function sanitizeForLoggingFormatter() {\n return {\n log: (obj) => {\n const message = (obj.msg || obj.message || '') as string;\n const { message: clean, meta } = sanitizeForLogging(message, obj as Record<string, unknown>);\n return { ...meta, msg: clean };\n },\n };\n}\n"],"names":["SENSITIVE_PATTERNS","SENSITIVE_FIELDS","Set","isAlreadySanitized","value","includes","redactValue","length","repeat","substring","sanitizeData","data","sanitized","pattern","replace","match","captured","redacted","Array","isArray","map","key","Object","entries","lowerKey","toLowerCase","has","sanitizeLogMessage","message","maxLength","String","processedMessage","normalize","trim","sanitizeForLogging","meta","enableDataSanitization","cleanMessage","sanitizeForLoggingFormatter","log","obj","msg","clean"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GAED,wEAAwE,GACxE,MAAMA,qBAAqB;IACzB,mCAAmC;IACnC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAEA,uCAAuC;IACvC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAEA,4BAA4B;IAC5B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAEA,wBAAwB;IACxB;IACA;IACA;IACA;IACA;IAEA,0BAA0B;IAC1B;IACA;IACA;IAEA,2BAA2B;IAC3B;IACA;IACA;IACA;IAEA,mEAAmE;IACnE;IACA;IACA;IAEA,yBAAyB;IACzB;CACD;AAED,kEAAkE,GAClE,MAAMC,mBAAmB,IAAIC,IAAI;IAC/B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED,SAASC,mBAAmBC,KAAa;IACvC,OAAOA,MAAMC,QAAQ,CAAC,WAAWD,MAAMC,QAAQ,CAAC,iBAAiBD,UAAU;AAC7E;AAEA,SAASE,YAAYF,KAAa;IAChC,IAAID,mBAAmBC,QAAQ;QAC7B,OAAOA;IACT;IAEA,IAAIA,MAAMG,MAAM,IAAI,GAAG;QACrB,OAAO,IAAIC,MAAM,CAACJ,MAAMG,MAAM;IAChC;IAEA,qCAAqC;IACrC,OAAO,GAAGH,MAAMK,SAAS,CAAC,GAAG,GAAG,IAAI,EAAEL,MAAMK,SAAS,CAACL,MAAMG,MAAM,GAAG,IAAI;AAC3E;AAEA,OAAO,SAASG,aAAaC,IAAa;IACxC,IAAI,OAAOA,SAAS,UAAU;QAC5B,IAAIR,mBAAmBQ,OAAO;YAC5B,OAAOA;QACT;QAEA,IAAIC,YAAYD;QAChB,KAAK,MAAME,WAAWb,mBAAoB;YACxCY,YAAYA,UAAUE,OAAO,CAACD,SAAS,CAACE,OAAOC;gBAC7C,IAAI,OAAOA,aAAa,UAAU;oBAChC,MAAMC,WAAWX,YAAYU;oBAC7B,OAAOD,MAAMD,OAAO,CAACE,UAAUC;gBACjC;gBACA,OAAOF;YACT;QACF;QAEA,OAAOH;IACT;IAEA,IAAIM,MAAMC,OAAO,CAACR,OAAO;QACvB,OAAOA,KAAKS,GAAG,CAACV;IAClB;IAEA,IAAIC,QAAQ,OAAOA,SAAS,UAAU;QACpC,MAAMC,YAAqC,CAAC;QAE5C,KAAK,MAAM,CAACS,KAAKjB,MAAM,IAAIkB,OAAOC,OAAO,CAACZ,MAAO;YAC/C,MAAMa,WAAWH,IAAII,WAAW;YAEhC,IAAIxB,iBAAiByB,GAAG,CAACF,aAAavB,iBAAiByB,GAAG,CAACL,MAAM;gBAC/D,IAAI,OAAOjB,UAAU,UAAU;oBAC7BQ,SAAS,CAACS,IAAI,GAAGf,YAAYF;gBAC/B,OAAO;oBACLQ,SAAS,CAACS,IAAI,GAAG;gBACnB;YACF,OAAO;gBACLT,SAAS,CAACS,IAAI,GAAGX,aAAaN;YAChC;QACF;QAEA,OAAOQ;IACT;IAEA,OAAOD;AACT;AAEA;;;CAGC,GACD,OAAO,SAASgB,mBAAmBC,OAAe,EAAEC,YAAY,KAAK;IACnE,IAAI,OAAOD,YAAY,UAAU;QAC/B,OAAOE,OAAOF;IAChB;IAEA,kEAAkE;IAClE,IAAIG,mBAAmBH;IACvB,IAAIG,iBAAiBxB,MAAM,GAAGsB,WAAW;QACvCE,mBAAmB,GAAGA,iBAAiBtB,SAAS,CAAC,GAAGoB,WAAW,YAAY,CAAC;IAC9E;IAEA,OACEE,iBACGC,SAAS,CAAC,QACVlB,OAAO,CAAC,eAAe,KACvBA,OAAO,CAAC,OAAO,IAChB,oHAAoH;KACnHA,OAAO,CAAC,yBAAyB,IACjCA,OAAO,CAAC,0BAA0B,IAAI,wCAAwC;KAC9EmB,IAAI;AAEX;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,mBAAmBN,OAAe,EAAEO,IAA8B,EAAEC,yBAAyB,IAAI;IAC/G,MAAMC,eAAeV,mBAAmBC;IAExC,IAAI,CAACQ,wBAAwB;QAC3B,OAAO;YACLR,SAASS;YACTF,MAAMA,QAAQ,CAAC;QACjB;IACF;IAEA,OAAO;QACLP,SAASlB,aAAa2B;QACtBF,MAAMzB,aAAayB,QAAQ,CAAC;IAC9B;AACF;AAEA,OAAO,SAASG;IACd,OAAO;QACLC,KAAK,CAACC;YACJ,MAAMZ,UAAWY,IAAIC,GAAG,IAAID,IAAIZ,OAAO,IAAI;YAC3C,MAAM,EAAEA,SAASc,KAAK,EAAEP,IAAI,EAAE,GAAGD,mBAAmBN,SAASY;YAC7D,OAAO;gBAAE,GAAGL,IAAI;gBAAEM,KAAKC;YAAM;QAC/B;IACF;AACF"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * OAuth-specific schemas and response builders
3
+ *
4
+ * Provider-agnostic schemas and utilities for building OAuth auth_required responses.
5
+ * Individual OAuth packages (oauth-google, oauth-microsoft) wrap these with provider-specific defaults.
6
+ */
7
+ import { z } from 'zod';
8
+ export type { z };
9
+ /**
10
+ * Authentication required response type
11
+ */
12
+ export interface AuthRequired {
13
+ type: 'auth_required';
14
+ provider: string;
15
+ message: string;
16
+ url: string;
17
+ flow?: string;
18
+ instructions: string;
19
+ user_code?: string;
20
+ expires_in?: number;
21
+ accountId?: string;
22
+ }
23
+ /**
24
+ * Zod schema for auth_required responses
25
+ */
26
+ export declare const AuthRequiredSchema: z.ZodObject<{
27
+ type: z.ZodLiteral<"auth_required">;
28
+ provider: z.ZodString;
29
+ message: z.ZodString;
30
+ url: z.ZodString;
31
+ flow: z.ZodOptional<z.ZodString>;
32
+ instructions: z.ZodString;
33
+ user_code: z.ZodOptional<z.ZodString>;
34
+ expires_in: z.ZodOptional<z.ZodNumber>;
35
+ accountId: z.ZodOptional<z.ZodString>;
36
+ }, z.core.$strip>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * OAuth-specific schemas and response builders
3
+ *
4
+ * Provider-agnostic schemas and utilities for building OAuth auth_required responses.
5
+ * Individual OAuth packages (oauth-google, oauth-microsoft) wrap these with provider-specific defaults.
6
+ */ import { z } from 'zod';
7
+ /**
8
+ * Zod schema for auth_required responses
9
+ */ export const AuthRequiredSchema = z.object({
10
+ type: z.literal('auth_required'),
11
+ provider: z.string().describe('OAuth provider name (e.g., "google", "microsoft")'),
12
+ message: z.string().describe('Human-readable message explaining why auth is needed'),
13
+ url: z.string().url().describe('Authentication URL to open in browser'),
14
+ flow: z.string().optional().describe('Authentication flow type (e.g., "auth_url", "device_code")'),
15
+ instructions: z.string().describe('Clear instructions for the user'),
16
+ user_code: z.string().optional().describe('Code user must enter at verification URL (device flows only)'),
17
+ expires_in: z.number().optional().describe('Seconds until code expires (device flows only)'),
18
+ accountId: z.string().optional().describe('Account identifier (email) that requires authentication')
19
+ }).describe('Authentication required with clear actionable instructions for user');
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth/src/schemas/index.ts"],"sourcesContent":["/**\n * OAuth-specific schemas and response builders\n *\n * Provider-agnostic schemas and utilities for building OAuth auth_required responses.\n * Individual OAuth packages (oauth-google, oauth-microsoft) wrap these with provider-specific defaults.\n */\n\nimport { z } from 'zod';\n\n// Re-export z for use in middleware (MCP requires zod)\nexport type { z };\n\n/**\n * Authentication required response type\n */\nexport interface AuthRequired {\n type: 'auth_required';\n provider: string;\n message: string;\n url: string;\n flow?: string;\n instructions: string;\n user_code?: string;\n expires_in?: number;\n accountId?: string;\n}\n\n/**\n * Zod schema for auth_required responses\n */\nexport const AuthRequiredSchema = z\n .object({\n type: z.literal('auth_required'),\n provider: z.string().describe('OAuth provider name (e.g., \"google\", \"microsoft\")'),\n message: z.string().describe('Human-readable message explaining why auth is needed'),\n url: z.string().url().describe('Authentication URL to open in browser'),\n flow: z.string().optional().describe('Authentication flow type (e.g., \"auth_url\", \"device_code\")'),\n instructions: z.string().describe('Clear instructions for the user'),\n user_code: z.string().optional().describe('Code user must enter at verification URL (device flows only)'),\n expires_in: z.number().optional().describe('Seconds until code expires (device flows only)'),\n accountId: z.string().optional().describe('Account identifier (email) that requires authentication'),\n })\n .describe('Authentication required with clear actionable instructions for user');\n"],"names":["z","AuthRequiredSchema","object","type","literal","provider","string","describe","message","url","flow","optional","instructions","user_code","expires_in","number","accountId"],"mappings":"AAAA;;;;;CAKC,GAED,SAASA,CAAC,QAAQ,MAAM;AAoBxB;;CAEC,GACD,OAAO,MAAMC,qBAAqBD,EAC/BE,MAAM,CAAC;IACNC,MAAMH,EAAEI,OAAO,CAAC;IAChBC,UAAUL,EAAEM,MAAM,GAAGC,QAAQ,CAAC;IAC9BC,SAASR,EAAEM,MAAM,GAAGC,QAAQ,CAAC;IAC7BE,KAAKT,EAAEM,MAAM,GAAGG,GAAG,GAAGF,QAAQ,CAAC;IAC/BG,MAAMV,EAAEM,MAAM,GAAGK,QAAQ,GAAGJ,QAAQ,CAAC;IACrCK,cAAcZ,EAAEM,MAAM,GAAGC,QAAQ,CAAC;IAClCM,WAAWb,EAAEM,MAAM,GAAGK,QAAQ,GAAGJ,QAAQ,CAAC;IAC1CO,YAAYd,EAAEe,MAAM,GAAGJ,QAAQ,GAAGJ,QAAQ,CAAC;IAC3CS,WAAWhB,EAAEM,MAAM,GAAGK,QAAQ,GAAGJ,QAAQ,CAAC;AAC5C,GACCA,QAAQ,CAAC,uEAAuE"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Session-based user authentication for multi-tenant deployments
3
+ *
4
+ * Extracts user ID from HTTP session cookies with HMAC signature verification.
5
+ * Compatible with Express/Connect session middleware patterns.
6
+ */
7
+ import type { SessionUserAuthConfig, UserAuthProvider } from './types.js';
8
+ /**
9
+ * Session-based user authentication provider
10
+ *
11
+ * Verifies signed session cookies and extracts user IDs.
12
+ * Use for multi-tenant deployments where users authenticate via web sessions.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // Multi-tenant server setup with session-based user authentication
17
+ * const userAuth = new SessionUserAuth({
18
+ * sessionSecret: process.env.SESSION_SECRET!,
19
+ * cookieName: 'app_session',
20
+ * });
21
+ *
22
+ * // Create OAuth adapters with session-based user identification
23
+ * const oauthAdapters = await createOAuthAdapters(
24
+ * config.transport,
25
+ * {
26
+ * service: 'gmail',
27
+ * clientId: process.env.GOOGLE_CLIENT_ID!,
28
+ * clientSecret: process.env.GOOGLE_CLIENT_SECRET,
29
+ * scope: GOOGLE_SCOPE,
30
+ * auth: 'loopback-oauth',
31
+ * headless: false,
32
+ * redirectUri: undefined,
33
+ * },
34
+ * {
35
+ * logger,
36
+ * tokenStore,
37
+ * userAuth, // Session-based user identification for multi-tenant deployments
38
+ * }
39
+ * );
40
+ *
41
+ * // Use auth middleware with tools
42
+ * const { middleware: authMiddleware } = oauthAdapters;
43
+ * const tools = toolFactories.map(f => f()).map(authMiddleware.withToolAuth);
44
+ * ```
45
+ */
46
+ export declare class SessionUserAuth implements UserAuthProvider {
47
+ private readonly secret;
48
+ private readonly cookieName;
49
+ private readonly algorithm;
50
+ constructor(config: SessionUserAuthConfig);
51
+ /**
52
+ * Extract and verify user ID from session cookie
53
+ *
54
+ * @param req - HTTP request object with cookie header
55
+ * @returns User ID from verified session
56
+ * @throws Error if session missing, invalid, or expired
57
+ */
58
+ getUserId(req: unknown): Promise<string>;
59
+ /**
60
+ * Parse cookie from header string
61
+ */
62
+ private parseCookie;
63
+ /**
64
+ * Generate HMAC signature for session data
65
+ */
66
+ private sign;
67
+ /**
68
+ * Constant-time string comparison to prevent timing attacks
69
+ */
70
+ private constantTimeCompare;
71
+ /**
72
+ * Helper for creating session cookies (for testing/integration)
73
+ *
74
+ * @param userId - User ID to encode in session
75
+ * @param expiresInMs - Optional expiration time in milliseconds from now
76
+ * @returns Signed session cookie value
77
+ */
78
+ createSessionCookie(userId: string, expiresInMs?: number): string;
79
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Session-based user authentication for multi-tenant deployments
3
+ *
4
+ * Extracts user ID from HTTP session cookies with HMAC signature verification.
5
+ * Compatible with Express/Connect session middleware patterns.
6
+ */ import { createHmac, timingSafeEqual } from 'crypto';
7
+ /**
8
+ * Session-based user authentication provider
9
+ *
10
+ * Verifies signed session cookies and extracts user IDs.
11
+ * Use for multi-tenant deployments where users authenticate via web sessions.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Multi-tenant server setup with session-based user authentication
16
+ * const userAuth = new SessionUserAuth({
17
+ * sessionSecret: process.env.SESSION_SECRET!,
18
+ * cookieName: 'app_session',
19
+ * });
20
+ *
21
+ * // Create OAuth adapters with session-based user identification
22
+ * const oauthAdapters = await createOAuthAdapters(
23
+ * config.transport,
24
+ * {
25
+ * service: 'gmail',
26
+ * clientId: process.env.GOOGLE_CLIENT_ID!,
27
+ * clientSecret: process.env.GOOGLE_CLIENT_SECRET,
28
+ * scope: GOOGLE_SCOPE,
29
+ * auth: 'loopback-oauth',
30
+ * headless: false,
31
+ * redirectUri: undefined,
32
+ * },
33
+ * {
34
+ * logger,
35
+ * tokenStore,
36
+ * userAuth, // Session-based user identification for multi-tenant deployments
37
+ * }
38
+ * );
39
+ *
40
+ * // Use auth middleware with tools
41
+ * const { middleware: authMiddleware } = oauthAdapters;
42
+ * const tools = toolFactories.map(f => f()).map(authMiddleware.withToolAuth);
43
+ * ```
44
+ */ export class SessionUserAuth {
45
+ /**
46
+ * Extract and verify user ID from session cookie
47
+ *
48
+ * @param req - HTTP request object with cookie header
49
+ * @returns User ID from verified session
50
+ * @throws Error if session missing, invalid, or expired
51
+ */ async getUserId(req) {
52
+ var _httpReq_headers;
53
+ const httpReq = req;
54
+ const cookieHeader = (_httpReq_headers = httpReq.headers) === null || _httpReq_headers === void 0 ? void 0 : _httpReq_headers.cookie;
55
+ if (!cookieHeader) {
56
+ throw new Error('SessionUserAuth: No cookie header found');
57
+ }
58
+ const sessionCookie = this.parseCookie(cookieHeader, this.cookieName);
59
+ if (!sessionCookie) {
60
+ throw new Error(`SessionUserAuth: Session cookie '${this.cookieName}' not found`);
61
+ }
62
+ // Format: base64(data).signature
63
+ const parts = sessionCookie.split('.');
64
+ if (parts.length !== 2) {
65
+ throw new Error('SessionUserAuth: Invalid session format (expected data.signature)');
66
+ }
67
+ const [dataB64, signature] = parts;
68
+ const expectedSignature = this.sign(dataB64);
69
+ if (!this.constantTimeCompare(signature, expectedSignature)) {
70
+ throw new Error('SessionUserAuth: Invalid session signature');
71
+ }
72
+ let sessionData;
73
+ try {
74
+ const dataJson = Buffer.from(dataB64, 'base64').toString('utf8');
75
+ sessionData = JSON.parse(dataJson);
76
+ } catch {
77
+ throw new Error('SessionUserAuth: Invalid session data encoding');
78
+ }
79
+ if (sessionData.exp !== undefined && Date.now() > sessionData.exp) {
80
+ throw new Error('SessionUserAuth: Session expired');
81
+ }
82
+ if (!sessionData.userId || typeof sessionData.userId !== 'string') {
83
+ throw new Error('SessionUserAuth: Session missing userId field');
84
+ }
85
+ return sessionData.userId;
86
+ }
87
+ /**
88
+ * Parse cookie from header string
89
+ */ parseCookie(cookieHeader, name) {
90
+ const cookies = cookieHeader.split(';');
91
+ for (const cookie of cookies){
92
+ const [key, ...valueParts] = cookie.trim().split('=');
93
+ if (key === name) {
94
+ return valueParts.join('='); // Handle values with = in them
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ /**
100
+ * Generate HMAC signature for session data
101
+ */ sign(data) {
102
+ return createHmac(this.algorithm, this.secret).update(data).digest('hex');
103
+ }
104
+ /**
105
+ * Constant-time string comparison to prevent timing attacks
106
+ */ constantTimeCompare(a, b) {
107
+ if (a.length !== b.length) {
108
+ return false;
109
+ }
110
+ const bufA = Buffer.from(a);
111
+ const bufB = Buffer.from(b);
112
+ return timingSafeEqual(bufA, bufB);
113
+ }
114
+ /**
115
+ * Helper for creating session cookies (for testing/integration)
116
+ *
117
+ * @param userId - User ID to encode in session
118
+ * @param expiresInMs - Optional expiration time in milliseconds from now
119
+ * @returns Signed session cookie value
120
+ */ createSessionCookie(userId, expiresInMs) {
121
+ const sessionData = {
122
+ userId,
123
+ ...expiresInMs !== undefined && {
124
+ exp: Date.now() + expiresInMs
125
+ }
126
+ };
127
+ const dataJson = JSON.stringify(sessionData);
128
+ const dataB64 = Buffer.from(dataJson).toString('base64');
129
+ const signature = this.sign(dataB64);
130
+ return `${dataB64}.${signature}`;
131
+ }
132
+ constructor(config){
133
+ var _config_cookieName, _config_algorithm;
134
+ if (!config.sessionSecret || config.sessionSecret.length < 32) {
135
+ throw new Error('SessionUserAuth: sessionSecret must be at least 32 characters');
136
+ }
137
+ this.secret = config.sessionSecret;
138
+ this.cookieName = (_config_cookieName = config.cookieName) !== null && _config_cookieName !== void 0 ? _config_cookieName : 'session';
139
+ this.algorithm = (_config_algorithm = config.algorithm) !== null && _config_algorithm !== void 0 ? _config_algorithm : 'sha256';
140
+ }
141
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth/src/session-auth.ts"],"sourcesContent":["/**\n * Session-based user authentication for multi-tenant deployments\n *\n * Extracts user ID from HTTP session cookies with HMAC signature verification.\n * Compatible with Express/Connect session middleware patterns.\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto';\nimport type { SessionUserAuthConfig, UserAuthProvider } from './types.ts';\n\n/**\n * HTTP request interface (subset needed for session auth)\n */\ninterface HttpRequest {\n headers?: {\n cookie?: string;\n };\n}\n\n/**\n * Session cookie structure\n */\ninterface SessionData {\n userId: string;\n exp?: number; // Optional expiration timestamp (ms)\n}\n\n/**\n * Session-based user authentication provider\n *\n * Verifies signed session cookies and extracts user IDs.\n * Use for multi-tenant deployments where users authenticate via web sessions.\n *\n * @example\n * ```typescript\n * // Multi-tenant server setup with session-based user authentication\n * const userAuth = new SessionUserAuth({\n * sessionSecret: process.env.SESSION_SECRET!,\n * cookieName: 'app_session',\n * });\n *\n * // Create OAuth adapters with session-based user identification\n * const oauthAdapters = await createOAuthAdapters(\n * config.transport,\n * {\n * service: 'gmail',\n * clientId: process.env.GOOGLE_CLIENT_ID!,\n * clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n * scope: GOOGLE_SCOPE,\n * auth: 'loopback-oauth',\n * headless: false,\n * redirectUri: undefined,\n * },\n * {\n * logger,\n * tokenStore,\n * userAuth, // Session-based user identification for multi-tenant deployments\n * }\n * );\n *\n * // Use auth middleware with tools\n * const { middleware: authMiddleware } = oauthAdapters;\n * const tools = toolFactories.map(f => f()).map(authMiddleware.withToolAuth);\n * ```\n */\nexport class SessionUserAuth implements UserAuthProvider {\n private readonly secret: string;\n private readonly cookieName: string;\n private readonly algorithm: 'sha256' | 'sha512';\n\n constructor(config: SessionUserAuthConfig) {\n if (!config.sessionSecret || config.sessionSecret.length < 32) {\n throw new Error('SessionUserAuth: sessionSecret must be at least 32 characters');\n }\n\n this.secret = config.sessionSecret;\n this.cookieName = config.cookieName ?? 'session';\n this.algorithm = config.algorithm ?? 'sha256';\n }\n\n /**\n * Extract and verify user ID from session cookie\n *\n * @param req - HTTP request object with cookie header\n * @returns User ID from verified session\n * @throws Error if session missing, invalid, or expired\n */\n async getUserId(req: unknown): Promise<string> {\n const httpReq = req as HttpRequest;\n\n const cookieHeader = httpReq.headers?.cookie;\n if (!cookieHeader) {\n throw new Error('SessionUserAuth: No cookie header found');\n }\n\n const sessionCookie = this.parseCookie(cookieHeader, this.cookieName);\n if (!sessionCookie) {\n throw new Error(`SessionUserAuth: Session cookie '${this.cookieName}' not found`);\n }\n\n // Format: base64(data).signature\n const parts = sessionCookie.split('.');\n if (parts.length !== 2) {\n throw new Error('SessionUserAuth: Invalid session format (expected data.signature)');\n }\n\n const [dataB64, signature] = parts as [string, string];\n\n const expectedSignature = this.sign(dataB64);\n if (!this.constantTimeCompare(signature, expectedSignature)) {\n throw new Error('SessionUserAuth: Invalid session signature');\n }\n\n let sessionData: SessionData;\n try {\n const dataJson = Buffer.from(dataB64, 'base64').toString('utf8');\n sessionData = JSON.parse(dataJson) as SessionData;\n } catch {\n throw new Error('SessionUserAuth: Invalid session data encoding');\n }\n\n if (sessionData.exp !== undefined && Date.now() > sessionData.exp) {\n throw new Error('SessionUserAuth: Session expired');\n }\n\n if (!sessionData.userId || typeof sessionData.userId !== 'string') {\n throw new Error('SessionUserAuth: Session missing userId field');\n }\n\n return sessionData.userId;\n }\n\n /**\n * Parse cookie from header string\n */\n private parseCookie(cookieHeader: string, name: string): string | null {\n const cookies = cookieHeader.split(';');\n for (const cookie of cookies) {\n const [key, ...valueParts] = cookie.trim().split('=');\n if (key === name) {\n return valueParts.join('='); // Handle values with = in them\n }\n }\n return null;\n }\n\n /**\n * Generate HMAC signature for session data\n */\n private sign(data: string): string {\n return createHmac(this.algorithm, this.secret).update(data).digest('hex');\n }\n\n /**\n * Constant-time string comparison to prevent timing attacks\n */\n private constantTimeCompare(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n const bufA = Buffer.from(a);\n const bufB = Buffer.from(b);\n\n return timingSafeEqual(bufA, bufB);\n }\n\n /**\n * Helper for creating session cookies (for testing/integration)\n *\n * @param userId - User ID to encode in session\n * @param expiresInMs - Optional expiration time in milliseconds from now\n * @returns Signed session cookie value\n */\n createSessionCookie(userId: string, expiresInMs?: number): string {\n const sessionData: SessionData = {\n userId,\n ...(expiresInMs !== undefined && { exp: Date.now() + expiresInMs }),\n };\n\n const dataJson = JSON.stringify(sessionData);\n const dataB64 = Buffer.from(dataJson).toString('base64');\n const signature = this.sign(dataB64);\n\n return `${dataB64}.${signature}`;\n }\n}\n"],"names":["createHmac","timingSafeEqual","SessionUserAuth","getUserId","req","httpReq","cookieHeader","headers","cookie","Error","sessionCookie","parseCookie","cookieName","parts","split","length","dataB64","signature","expectedSignature","sign","constantTimeCompare","sessionData","dataJson","Buffer","from","toString","JSON","parse","exp","undefined","Date","now","userId","name","cookies","key","valueParts","trim","join","data","algorithm","secret","update","digest","a","b","bufA","bufB","createSessionCookie","expiresInMs","stringify","config","sessionSecret"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * HTML templates for OAuth callback pages
3
+ *
4
+ * These templates are shown in the browser after OAuth authorization
5
+ * for loopback OAuth flows (RFC 8252).
6
+ */
7
+ /**
8
+ * HTML template for successful OAuth authorization
9
+ */
10
+ export declare function getSuccessTemplate(): string;
11
+ /**
12
+ * HTML template for OAuth error
13
+ */
14
+ export declare function getErrorTemplate(error: string): string;
15
+ /**
16
+ * Escape HTML special characters to prevent XSS
17
+ */
18
+ export declare function escapeHtml(unsafe: string): string;