Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions migrations/2026-07-27_10-00-00_add_password_backup_public_key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//2026-07-27_10-00-00_add_password_backup_public_key

exports.up = function (db) {
return db.query(
'ALTER TABLE user_devices ADD COLUMN IF NOT EXISTS password_backup_public_key TEXT',
);
};

exports.down = function (db) {
return db.query('ALTER TABLE user_devices DROP COLUMN IF EXISTS password_backup_public_key');
};
19 changes: 19 additions & 0 deletions migrations/2026-07-28_14-11-47_sso_passwordless.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//2026-07-28_14-11-47_sso_passwordless

exports.up = async function (db) {
await db.query(
`ALTER TABLE allowed_emails ADD COLUMN IF NOT EXISTS uses_passwordless_auth BOOLEAN NOT NULL DEFAULT FALSE`,
);
await db.query(
`ALTER TABLE banks ADD COLUMN IF NOT EXISTS ms_entra_vaults_use_passwordless_auth BOOLEAN NOT NULL DEFAULT FALSE`,
);
await db.query(`ALTER TABLE bank_sso_config DROP COLUMN IF EXISTS is_sso_v2`);
};

exports.down = async function (db) {
await db.query(`ALTER TABLE allowed_emails DROP COLUMN IF EXISTS uses_passwordless_auth`);
await db.query(`ALTER TABLE banks DROP COLUMN IF EXISTS ms_entra_vaults_use_passwordless_auth`);
await db.query(
`ALTER TABLE bank_sso_config ADD COLUMN IF NOT EXISTS is_sso_v2 BOOLEAN NOT NULL DEFAULT FALSE`,
);
};
21 changes: 21 additions & 0 deletions src/api2/routes/authentication/authenticateWithOpenidAuthCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ export const authenticateWithOpenidAuthCode = async (
res.status(400).end();
return;
}

// // BYPASS CODE FOR DEBUGGING PURPOSES
// if (safeBody.authCode === 'bypass_code') {
// // bypass code for debugging purposes
// const userEmail = youremail@domain.com;
// const openidSession = await SessionStore.createOpenIdSession(
// {
// bankId: bankIds.internalId,
// accessToken: 'bypass_access_token',
// userEmail,
// },
// Date.now() + 3600 * 1000,
// );

// res.status(200).json({
// openidSession,
// email: userEmail,
// });
// return;
// }

const clientId = bankConfigRes.rows[0]!.client_id;

const openidConfig = await fetchOpenIdConfig(safeBody.openidConfigurationUrl);
Expand Down
81 changes: 81 additions & 0 deletions src/api2/routes/authentication/usesPasswordlessUnlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Request, Response } from 'express';
import Joi from 'joi';
import { getBankIds } from '../../helpers/bankUUID';
import { db } from '../../../helpers/db';
import { logError, logInfo } from '../../../helpers/logger';

export const usesPasswordlessUnlock = async (req: Request, res: Response): Promise<void> => {
try {
const joiRes = Joi.object({ userEmail: Joi.string().email().lowercase().required() }).validate(
req.body,
);
if (joiRes.error) {
res.status(400).json({ error: joiRes.error.details });
return;
}
const email = joiRes.value.userEmail;
const bankIds = await getBankIds(req);
const usesPasswordless = await usesPasswordlessUnlockForEmail(email, bankIds.internalId);
if (usesPasswordless === null) {
res.status(404).end();
return;
}

res.status(200).json({
usesPasswordlessUnlock: usesPasswordless,
});
return;
} catch (e) {
logError('usesPasswordlessUnlock', e);
res.status(400).end();
return;
}
};

export const usesPasswordlessUnlockForEmail = async (
email: string,
bankId: number,
): Promise<boolean | null> => {
const bankRes = await db.query(
`SELECT
b.ms_entra_vaults_use_passwordless_auth,
EXISTS (SELECT 1 FROM bank_sso_config WHERE bank_id = b.id) AS sso_configured
FROM banks AS b
WHERE b.id = $1`,
[bankId],
);
if (bankRes.rowCount === 0) {
logInfo(email, 'usesPasswordlessUnlock fail: bad bank');
return null;
}

const userRes = await db.query(
`SELECT deactivated, ms_entra_id FROM users WHERE email = $1 AND bank_id = $2`,
[email, bankId],
);
if ((userRes.rowCount ?? 0) === 0 || userRes.rows[0].deactivated) {
logInfo(email, 'usesPasswordlessUnlock fail: email not found or deactivated');
return null;
}

const msEntraPasswordless =
bankRes.rows[0].sso_configured &&
bankRes.rows[0].ms_entra_vaults_use_passwordless_auth &&
(userRes.rowCount ?? 0) > 0 &&
userRes.rows[0].ms_entra_id != null;

const patternRes = await db.query(
`SELECT 1 FROM allowed_emails
WHERE bank_id = $1
AND uses_passwordless_auth = true
AND (
(LEFT(pattern, 2) != '*@' AND pattern = $2) OR
(LEFT(pattern, 2) = '*@' AND SPLIT_PART($2, '@', 2) = SUBSTRING(pattern FROM 3))
)
LIMIT 1`,
[bankId, email],
);
const patternPasswordless = (patternRes.rowCount ?? 0) > 0;
const usesPasswordlessUnlock = msEntraPasswordless || patternPasswordless;
return usesPasswordlessUnlock;
};
17 changes: 9 additions & 8 deletions src/api2/routes/bank/getBankConfig.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Request, Response } from 'express';
import { db } from '../../../helpers/db';
import env from '../../../helpers/env';
import { logError, logInfo } from '../../../helpers/logger';
import { getBankIds } from '../../helpers/bankUUID';

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export const getBankConfig = async (req: any, res: any): Promise<void> => {
export const getBankConfig = async (req: Request, res: Response): Promise<void> => {
try {
const bankIds = await getBankIds(req);
const bankRes = await db.query(
Expand All @@ -16,8 +15,7 @@ export const getBankConfig = async (req: any, res: any): Promise<void> => {
json_agg(
json_build_object(
'openid_configuration_url', sso.openid_configuration_url,
'client_id', sso.client_id,
'is_sso_v2', sso.is_sso_v2
'client_id', sso.client_id
)
) FILTER (WHERE sso.id IS NOT NULL),
'[]'
Expand All @@ -30,17 +28,20 @@ export const getBankConfig = async (req: any, res: any): Promise<void> => {
);
if (bankRes.rowCount === 0) {
logInfo(req.body?.userEmail, 'getBankConfig fail: bad bank');
return res.status(400).end();
res.status(400).end();
return;
}
logInfo(req.body?.userEmail, 'getBankConfig OK');
return res.status(200).json({
res.status(200).json({
newUrl: bankRes.rows[0].redirect_url,
bankName: bankRes.rows[0].name,
preventUpdatePopup: bankRes.rows[0]?.settings?.PREVENT_UPDATE_POPUP || false,
ssoConfigs: bankRes.rows[0]?.sso_configs.length == 0 ? null : bankRes.rows[0]?.sso_configs,
});
return;
} catch (e) {
logError('getBankConfig', e);
return res.status(400).end();
res.status(400).end();
return;
}
};
48 changes: 48 additions & 0 deletions src/api2/routes/data/getVaultData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ export const getVaultData = async (req: any, res: any): Promise<void> => {
}

const sharedVaults = await getSharedVaults(dbRes.rows[0].user_id, bankIds.internalId);
const pendingSsoDevices = await getPendingSsoDevices(
dbRes.rows[0].user_id,
bankIds.internalId,
deviceId,
);

const userResultingSetting = getDefaultSettingOrUserOverride(
dbRes.rows[0].bank_settings,
Expand All @@ -142,6 +147,7 @@ export const getVaultData = async (req: any, res: any): Promise<void> => {
defaultAutolockDelay: userResultingSetting?.defaultAutolockDelay,
maxAutolockDelay: userResultingSetting?.maxAutolockDelay,
sharedVaults,
pendingSsoDevices,
needsPasswordBackup:
!dbRes.rows[0].encrypted_password_backup_2 ||
dbRes.rows[0].encrypted_password_backup_2.length == 512,
Expand Down Expand Up @@ -202,6 +208,48 @@ export const getSharedVaults = async (
}));
};

export const getPendingSsoDevices = async (
userId: number,
bankId: number,
callingDeviceUniqueId: string,
): Promise<
{
deviceId: string;
deviceName: string;
deviceType: string;
osFamily: string;
osNameAndVersion: string;
createdAt: any;
passwordBackupPublicKey: string;
}[]
> => {
const pendingSsoDevicesRes = await db.query(
`SELECT
device_unique_id AS device_id,
device_name,
device_type,
os_family,
os_version AS os_name_and_version,
created_at,
password_backup_public_key
FROM user_devices
WHERE user_id=$1 AND bank_id=$2 AND device_unique_id != $3
AND authorization_status = 'AUTHORIZED'
AND password_backup_public_key IS NOT NULL
AND (encrypted_password_backup_2 IS NULL OR encrypted_password_backup_2 = '')`,
[userId, bankId, callingDeviceUniqueId],
);
return pendingSsoDevicesRes.rows.map((d) => ({
deviceId: d.device_id,
deviceName: d.device_name,
deviceType: d.device_type,
osFamily: d.os_family,
osNameAndVersion: d.os_name_and_version,
createdAt: d.created_at,
passwordBackupPublicKey: d.password_backup_public_key,
}));
};

const cleanChangedEmails = async (userId: number, deviceUniqueId: string, bankId: number) => {
try {
const changedEmails = await db.query(
Expand Down
Loading
Loading