-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathorganizationCredentials.ts
More file actions
472 lines (404 loc) · 13.2 KB
/
Copy pathorganizationCredentials.ts
File metadata and controls
472 lines (404 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
import { safeStorage } from 'electron';
import { getPrismaClient } from '@main/db/prisma';
import { Organization, OrganizationCredentials } from '@prisma/client';
import { jwtDecode } from 'jwt-decode';
import { login } from '@main/services/organization/auth';
import { getUseKeychainClaim } from '@main/services/localUser/claim';
import { createLogger } from '@main/modules/logger';
import {
decrypt,
encrypt,
isClearTextToken,
isLegacyBlob,
} from '@main/utils/crypto';
const logger = createLogger('main.organizationCredentials');
/* Returns the organization that the user is connected to */
export const getOrganizationTokens = async (user_id: string, decryptPassword: string | null) => {
const prisma = getPrismaClient();
try {
const orgs = await prisma.organizationCredentials.findMany({
where: { user_id },
select: {
id: true,
organization_id: true,
jwtToken: true,
},
});
const result: { organization_id: string; jwtToken: string | null }[] = [];
for (const o of orgs) {
result.push({
organization_id: o.organization_id,
jwtToken: await decryptMigrateJwtToken(o, decryptPassword),
});
}
return result;
} catch (error) {
logger.error('Failed to get organization tokens', { error });
return [];
}
};
/* Returns the organizations that the user should sign into */
export const organizationsToSignIn = async (user_id: string, decryptPassword: string | null) => {
const prisma = getPrismaClient();
try {
const credentials = await prisma.organizationCredentials.findMany({
where: { user_id },
include: {
organization: true,
},
});
const finalCredentials: typeof credentials = [];
for (let i = 0; i < credentials.length; i++) {
if (await organizationCredentialsInvalid(credentials[i], decryptPassword))
finalCredentials.push(credentials[i]);
}
return finalCredentials;
} catch (error) {
logger.error('Failed to get organizations to sign in', { error });
return [];
}
};
/* Returns whether the user should sign in a specific organization */
export const shouldSignInOrganization = async (user_id: string, organization_id: string, decryptPassword: string | null) => {
const prisma = getPrismaClient();
try {
const org = await prisma.organizationCredentials.findFirst({
where: { user_id, organization_id },
include: {
organization: true,
},
});
return await organizationCredentialsInvalid(org, decryptPassword);
} catch {
return true;
}
};
/* Returns the access token of a user for an organization */
export const getAccessToken = async (serverUrl: string, decryptPassword: string | null) => {
const prisma = getPrismaClient();
try {
const credentials = await prisma.organizationCredentials.findFirst({
where: { organization: { serverUrl } },
});
if (!credentials) return null;
return await decryptMigrateJwtToken(credentials, decryptPassword);
} catch (error) {
logger.error('Failed to get access token', { error });
return null;
}
};
/* Returns the current user of an organization */
export const getCurrentUser = async (organizationServerUrl: string, decryptPassword: string | null) => {
const token = await getAccessToken(organizationServerUrl, decryptPassword);
if (!token) return null;
try {
const decoded: any = jwtDecode(token);
return decoded;
} catch {
return null;
}
};
/* Returns credentials for organization */
export const getOrganizationCredentials = async (
organization_id: string,
user_id: string,
decryptPassword: string | null,
) => {
const prisma = getPrismaClient();
try {
const credentials = await prisma.organizationCredentials.findFirst({
where: { user_id, organization_id },
});
if (!credentials) return null;
const password = await decryptMigratePassword(credentials, decryptPassword);
const jwtToken = await decryptMigrateJwtToken(credentials, decryptPassword);
return {
...credentials,
password,
jwtToken,
};
} catch (error) {
logger.error('Failed to get organization credentials', { error });
return null;
}
};
/* Returns whether organization credentials exists */
export const organizationCredentialsExists = async (organization_id: string, user_id: string) => {
const prisma = getPrismaClient();
try {
return (
(await prisma.organizationCredentials.count({
where: { user_id, organization_id },
})) > 0
);
} catch (error) {
logger.error('Failed to check organization credentials existence', { error });
return false;
}
};
/* Adds a new organization credentials to the user */
export const addOrganizationCredentials = async (
email: string,
password: string,
organization_id: string,
user_id: string,
jwtToken: string,
encryptPassword: string | null,
updateIfExists: boolean = false,
) => {
const prisma = getPrismaClient();
if (updateIfExists) {
const exists = await organizationCredentialsExists(organization_id, user_id);
if (exists) {
await updateOrganizationCredentials(
organization_id,
user_id,
email,
password,
jwtToken,
encryptPassword,
);
return;
}
}
try {
password = await encryptData(password, encryptPassword);
jwtToken = await encryptData(jwtToken, encryptPassword);
await prisma.organizationCredentials.create({
data: {
email,
password,
jwtToken,
organization_id,
user_id,
},
});
return true;
} catch (error) {
logger.error('Failed to add organization credentials', { error });
throw new Error('Failed to add organization credentials');
}
};
/* Updates the organization credentials */
export const updateOrganizationCredentials = async (
organization_id: string,
user_id: string,
email?: string,
password?: string | null,
jwtToken?: string | null,
encryptPassword?: string | null,
passwordIsEncrypted: boolean = false,
) => {
const prisma = getPrismaClient();
try {
if (password && !passwordIsEncrypted) {
password = await encryptData(password, encryptPassword);
}
if (jwtToken && !passwordIsEncrypted) {
jwtToken = await encryptData(jwtToken, encryptPassword);
}
const credentials = await prisma.organizationCredentials.findFirst({
where: { user_id, organization_id },
});
if (!credentials) {
logger.warn('User credentials for this organization not found');
return false;
}
await prisma.organizationCredentials.update({
where: { id: credentials.id },
data: {
email: email || credentials.email,
password: password ?? credentials.password,
jwtToken: jwtToken ?? credentials.jwtToken,
},
});
return true;
} catch (error) {
logger.error('Failed to update organization credentials', { error });
throw new Error('Failed to update organization credentials');
}
};
/* Deletes the organization credentials */
export const deleteOrganizationCredentials = async (organization_id: string, user_id: string) => {
const prisma = getPrismaClient();
try {
await prisma.organizationCredentials.deleteMany({
where: { user_id, organization_id },
});
return true;
} catch (error) {
logger.error('Failed to delete organization credentials', { error });
throw new Error('Failed to delete organization credentials');
}
};
/* Tries to auto sign in to all organizations that should sign in */
export const tryAutoSignIn = async (user_id: string, decryptPassword: string | null) => {
const prisma = getPrismaClient();
const invalidCredentials = await organizationsToSignIn(user_id, decryptPassword);
const failedLogins: Organization[] = [];
for (let i = 0; i < invalidCredentials.length; i++) {
const invalidCredential = invalidCredentials[i];
let password = '';
try {
password = await decryptMigratePassword(
invalidCredential,
decryptPassword,
);
} catch {
throw new Error('Incorrect decryption password');
}
try {
const { accessToken } = await login(
invalidCredential.organization.serverUrl,
invalidCredential.email,
password,
);
const encryptedAccessToken = await encryptData(accessToken, decryptPassword);
await prisma.organizationCredentials.update({
where: { id: invalidCredential.id },
data: { jwtToken: encryptedAccessToken },
});
} catch {
failedLogins.push(invalidCredential.organization);
}
}
return failedLogins;
};
// Surfaces keychain / personal-password failures up front so callers can abort
// before triggering irreversible side effects (e.g. the backend password rotation).
export const encryptOrganizationPassword = async (
password: string,
encryptPassword?: string | null,
) => {
if (!password) {
throw new Error('Password is required to encrypt');
}
let useKeychain = false;
try {
useKeychain = await getUseKeychainClaim();
} catch (error) {
logger.error('Failed to encrypt organization password', { error });
throw new Error('Keychain access denied or unavailable', { cause: error });
}
if (!useKeychain && !encryptPassword) {
logger.warn('encryptOrganizationPassword called without a viable encryption method', {
useKeychain,
});
throw new Error('No encryption method available');
}
try {
if (useKeychain) {
const buffer = safeStorage.encryptString(password);
return buffer.toString('base64');
}
return await encrypt(password, encryptPassword as string);
} catch (error) {
logger.error('Failed to encrypt organization password', { error, useKeychain });
if (useKeychain) {
throw new Error('Keychain access denied or unavailable', { cause: error });
}
throw new Error('Failed to encrypt with application password', { cause: error });
}
};
/* Encrypt data */
async function encryptData(data: string, encryptPassword?: string | null) {
const useKeychain = await getUseKeychainClaim();
if (useKeychain) {
const passwordBuffer = safeStorage.encryptString(data);
return passwordBuffer.toString('base64');
} else if (encryptPassword) {
return await encrypt(data, encryptPassword);
} else {
throw new Error('Password is required to store sensitive data');
}
}
/* Decrypt data */
export async function decryptData(
data: string,
decryptPassword: string | null,
) {
// if no data was stored (password cleared), just return empty string
if (data.length === 0) {
return '';
}
const useKeychain = await getUseKeychainClaim();
if (useKeychain) {
const buffer = Buffer.from(data, 'base64');
return safeStorage.decryptString(buffer);
} else if (decryptPassword) {
return decrypt(data, decryptPassword);
} else {
throw new Error('Password is required to decrypt sensitive');
}
}
/* Decrypt credentials password. Update its encryption if needed. */
export async function decryptMigratePassword(
credential: { id: string, password: string },
decryptPassword: string | null,
) {
// if password was cleared, just return empty string
if (credential.password.length === 0) {
return '';
}
const useKeychain = await getUseKeychainClaim();
if (useKeychain) {
const buffer = Buffer.from(credential.password, 'base64');
return safeStorage.decryptString(buffer);
} else if (decryptPassword) {
const decrypted = await decrypt(credential.password, decryptPassword);
if (isLegacyBlob(credential.password)) {
try {
await getPrismaClient().organizationCredentials.update({
where: { id: credential.id },
data: { password: await encrypt(decrypted, decryptPassword) },
});
} catch {
// migration failure is non-fatal
}
}
return decrypted;
} else {
throw new Error('Password is required to decrypt sensitive');
}
}
/* Decrypt credentials JWT token. Update its encryption if needed. */
export async function decryptMigrateJwtToken(
credential: { id: string; jwtToken: string | null},
decryptPassword: string | null,
) {
// if token is null, returns null
if (credential.jwtToken === null) {
return null;
}
if (isClearTextToken(credential.jwtToken)) {
// JWT token is not encrypted => we encrypt it
try {
await getPrismaClient().organizationCredentials.update({
where: { id: credential.id },
data: { jwtToken: await encryptData(credential.jwtToken, decryptPassword) },
});
} catch {
// migration failure is non-fatal
}
return credential.jwtToken;
} else {
return decryptData(credential.jwtToken, decryptPassword);
}
}
/* Validate organization credentials */
export async function organizationCredentialsInvalid(
org: (OrganizationCredentials & { organization: Organization }) | null,
decryptPassword: string | null,
) {
if (!org) return true;
if (org.password.length === 0 || org.email.length === 0) return true;
const token = await getAccessToken(org.organization.serverUrl, decryptPassword);
if (!token) return true;
try {
const decoded: any = jwtDecode(token);
if (decoded.exp * 1000 < Date.now()) return true;
} catch {
return true;
}
return false;
}