diff --git a/migrations/2026-07-27_10-00-00_add_password_backup_public_key.js b/migrations/2026-07-27_10-00-00_add_password_backup_public_key.js new file mode 100644 index 00000000..6e030fbc --- /dev/null +++ b/migrations/2026-07-27_10-00-00_add_password_backup_public_key.js @@ -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'); +}; diff --git a/migrations/2026-07-28_14-11-47_sso_passwordless.js b/migrations/2026-07-28_14-11-47_sso_passwordless.js new file mode 100644 index 00000000..72d8b5f7 --- /dev/null +++ b/migrations/2026-07-28_14-11-47_sso_passwordless.js @@ -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`, + ); +}; diff --git a/src/api2/routes/authentication/authenticateWithOpenidAuthCode.ts b/src/api2/routes/authentication/authenticateWithOpenidAuthCode.ts index 3346ddc1..547606b8 100644 --- a/src/api2/routes/authentication/authenticateWithOpenidAuthCode.ts +++ b/src/api2/routes/authentication/authenticateWithOpenidAuthCode.ts @@ -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); diff --git a/src/api2/routes/authentication/usesPasswordlessUnlock.ts b/src/api2/routes/authentication/usesPasswordlessUnlock.ts new file mode 100644 index 00000000..d1b4b847 --- /dev/null +++ b/src/api2/routes/authentication/usesPasswordlessUnlock.ts @@ -0,0 +1,83 @@ +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 => { + 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) { + // return 200 to prevent account enumeration. + res.status(200).json({ usesPasswordlessUnlock: false }); + return; + } + + res.status(200).json({ + usesPasswordlessUnlock: usesPasswordless, + }); + return; + } catch (e) { + logError('usesPasswordlessUnlock', e); + // return 200 to prevent account enumeration. + res.status(200).json({ usesPasswordlessUnlock: false }); + return; + } +}; + +export const usesPasswordlessUnlockForEmail = async ( + email: string, + bankId: number, +): Promise => { + 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; +}; diff --git a/src/api2/routes/bank/getBankConfig.ts b/src/api2/routes/bank/getBankConfig.ts index 20e604b8..224585cb 100644 --- a/src/api2/routes/bank/getBankConfig.ts +++ b/src/api2/routes/bank/getBankConfig.ts @@ -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 => { +export const getBankConfig = async (req: Request, res: Response): Promise => { try { const bankIds = await getBankIds(req); const bankRes = await db.query( @@ -16,8 +15,7 @@ export const getBankConfig = async (req: any, res: any): Promise => { 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), '[]' @@ -30,17 +28,20 @@ export const getBankConfig = async (req: any, res: any): Promise => { ); 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; } }; diff --git a/src/api2/routes/data/getVaultData.ts b/src/api2/routes/data/getVaultData.ts index 815f7ced..c3ecbe71 100644 --- a/src/api2/routes/data/getVaultData.ts +++ b/src/api2/routes/data/getVaultData.ts @@ -126,6 +126,11 @@ export const getVaultData = async (req: any, res: any): Promise => { } 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, @@ -142,6 +147,7 @@ export const getVaultData = async (req: any, res: any): Promise => { 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, @@ -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( diff --git a/src/api2/routes/deviceAuthorization/rejectSsoDevice.ts b/src/api2/routes/deviceAuthorization/rejectSsoDevice.ts new file mode 100644 index 00000000..3dccbafc --- /dev/null +++ b/src/api2/routes/deviceAuthorization/rejectSsoDevice.ts @@ -0,0 +1,41 @@ +import { db } from '../../../helpers/db'; +import { logError, logInfo } from '../../../helpers/logger'; +import { inputSanitizer } from '../../../helpers/sanitizer'; +import { checkBasicAuth2 } from '../../helpers/authorizationChecks'; + +// Lets an already-authorized device (A) reject a peer device (B) that is itself already +// AUTHORIZED (identity verified via SSO or email) but still waiting for a password backup +// (see getPendingSsoDevices) — ie. before the device ever actually gains access to the vault. +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any +export const rejectSsoDevice = async (req: any, res: any) => { + try { + const deviceIdToReject = inputSanitizer.getString(req.body?.deviceIdToReject); + if (!deviceIdToReject) { + logInfo(req.body?.userEmail, 'rejectSsoDevice fail: missing deviceIdToReject'); + return res.status(403).end(); + } + + const basicAuth = await checkBasicAuth2(req); + if (!basicAuth.granted) { + logInfo(req.body?.userEmail, 'rejectSsoDevice fail: auth not granted'); + return res.status(401).end(); + } + + const result = await db.query( + `UPDATE user_devices + SET device_unique_id=null, authorization_status='REVOKED_BY_USER', device_public_key_2=null, + encrypted_password_backup_2='', password_backup_public_key=null, revocation_date=$1 + WHERE device_unique_id=$2 AND user_id=$3 AND bank_id=$4 + AND authorization_status='AUTHORIZED' AND password_backup_public_key IS NOT NULL`, + [new Date().toISOString(), deviceIdToReject, basicAuth.userId, basicAuth.bankIds.internalId], + ); + // applied=false means another authorized device already approved (or also rejected) this + // same device in the meantime. + const applied = (result.rowCount ?? 0) > 0; + logInfo(req.body?.userEmail, 'rejectSsoDevice OK'); + return res.status(200).json({ applied }); + } catch (e) { + logError(req.body?.userEmail, 'rejectSsoDevice', e); + return res.status(400).end(); + } +}; diff --git a/src/api2/routes/deviceAuthorization/requestDeviceAccess.ts b/src/api2/routes/deviceAuthorization/requestDeviceAccess.ts index 28e326b0..03b0ef8d 100644 --- a/src/api2/routes/deviceAuthorization/requestDeviceAccess.ts +++ b/src/api2/routes/deviceAuthorization/requestDeviceAccess.ts @@ -1,6 +1,9 @@ import { db } from '../../../helpers/db'; import { getExpirationDate, isExpired } from '../../../helpers/dateHelper'; -import { sendDeviceRequestEmail } from '../../../emails/sendDeviceRequestEmail'; +import { + sendDeviceRequestAdminEmail, + sendDeviceRequestEmail, +} from '../../../emails/sendDeviceRequestEmail'; import { logError, logInfo } from '../../../helpers/logger'; import { getRandomString } from '../../../helpers/randomString'; import { @@ -133,6 +136,22 @@ export const requestDeviceAccess2 = async (req: Request, res: Response) => { } if (isOpenidAuthenticated) { + const otherActiveDevicesRes = await db.query( + `SELECT COUNT(*) AS device_count FROM user_devices + WHERE user_id=$1 AND bank_id=$2 AND device_unique_id != $3 + AND (authorization_status = 'AUTHORIZED' OR authorization_status = 'PENDING' OR + authorization_status = 'USER_VERIFIED_PENDING_ADMIN_CHECK')`, + [userId, bankIds.internalId, safeBody.deviceId], + ); + const isAdditionalDevice = + Number.parseInt(otherActiveDevicesRes.rows[0].device_count, 10) >= 1; + const requiresAdminCheck = + isAdditionalDevice && + !!userRes.rows[0].bank_settings?.REQUIRE_ADMIN_CHECK_FOR_SECOND_DEVICE; + const nextAuthorizationStatus = requiresAdminCheck + ? 'USER_VERIFIED_PENDING_ADMIN_CHECK' + : 'AUTHORIZED'; + if (!deviceInDb) { // CREATE AUTHORIZED DEVICE await db.query( @@ -147,7 +166,7 @@ export const requestDeviceAccess2 = async (req: Request, res: Response) => { safeBody.appVersion, safeBody.deviceId, safeBody.devicePublicKey, - 'AUTHORIZED', + nextAuthorizationStatus, bankIds.internalId, ], ); @@ -163,13 +182,21 @@ export const requestDeviceAccess2 = async (req: Request, res: Response) => { safeBody.osNameAndVersion, safeBody.appVersion, safeBody.deviceId, - 'AUTHORIZED', + nextAuthorizationStatus, deviceInDb.id, ], ); } - logInfo(safeBody.userEmail, 'requestDeviceAccess2 authorized with openid session'); - return res.status(200).json({ authorizationStatus: 'AUTHORIZED' }); + if (requiresAdminCheck) { + await sendDeviceRequestAdminEmail(safeBody.userEmail, bankIds.internalId); + logInfo( + safeBody.userEmail, + 'requestDeviceAccess2 authorized with openid session (waiting for admin check)', + ); + } else { + logInfo(safeBody.userEmail, 'requestDeviceAccess2 authorized with openid session'); + } + return res.status(200).json({ authorizationStatus: nextAuthorizationStatus }); } else { // RESEND EMAIL IF REQUEST IS STILL PENDING if ( diff --git a/src/api2/routes/deviceAuthorization/sendPasswordBackupPublicKey.ts b/src/api2/routes/deviceAuthorization/sendPasswordBackupPublicKey.ts new file mode 100644 index 00000000..28724a5a --- /dev/null +++ b/src/api2/routes/deviceAuthorization/sendPasswordBackupPublicKey.ts @@ -0,0 +1,54 @@ +import { Request, Response } from 'express'; +import Joi from 'joi'; +import { db } from '../../../helpers/db'; +import { logError, logInfo } from '../../../helpers/logger'; +import { checkDeviceAuth } from '../../helpers/authorizationChecks'; +import { usesPasswordlessUnlockForEmail } from '../authentication/usesPasswordlessUnlock'; + +// Lets an already-AUTHORIZED device (re)send its password backup public key once proven +// authentic via a deviceOnlySession. This is needed for SSO device pairing. +export const sendPasswordBackupPublicKey = async (req: Request, res: Response): Promise => { + try { + const deviceAuth = await checkDeviceAuth(req); + if (!deviceAuth.granted) { + logInfo(req.body?.userEmail, 'sendPasswordBackupPublicKey fail: device auth not granted'); + res.status(401).json({ error: 'badDeviceSession' }); + return; + } + + const joiRes = Joi.object({ + devicePasswordBackupPublicKey: Joi.string().required(), + }) + .unknown(true) + .validate(req.body); + if (joiRes.error) { + res.status(400).json({ error: joiRes.error.details }); + return; + } + const { devicePasswordBackupPublicKey } = joiRes.value; + + const usesPasswordless = await usesPasswordlessUnlockForEmail( + deviceAuth.vaultEmail, + deviceAuth.bankIds.internalId, + ); + if (!usesPasswordless) { + logInfo(req.body?.userEmail, 'sendPasswordBackupPublicKey OK (not passwordless, ignored)'); + res.status(200).end(); + return; + } + + // No-op if a key is already stored or a backup was already approved for this device: this + // only ever fills in a key that was withheld while pending an admin check. + await db.query( + `UPDATE user_devices SET password_backup_public_key=$1 + WHERE id=$2 AND authorization_status='AUTHORIZED' AND password_backup_public_key IS NULL + AND (encrypted_password_backup_2 IS NULL OR encrypted_password_backup_2 = '')`, + [devicePasswordBackupPublicKey, deviceAuth.deviceId], + ); + logInfo(req.body?.userEmail, 'sendPasswordBackupPublicKey OK'); + res.status(200).end(); + } catch (e) { + logError(req.body?.userEmail, 'sendPasswordBackupPublicKey', e); + res.status(400).end(); + } +}; diff --git a/src/api2/routes/passwordReset/backupPassword.ts b/src/api2/routes/passwordReset/backupPassword.ts index 707d6fdc..0f8f391b 100644 --- a/src/api2/routes/passwordReset/backupPassword.ts +++ b/src/api2/routes/passwordReset/backupPassword.ts @@ -18,22 +18,29 @@ export const backupPassword2 = async (req: any, res: any) => { return res.status(401).end(); } - await Promise.all( - backups.map((backup) => - db.query( - "UPDATE user_devices SET encrypted_password_backup_2=$1 WHERE device_unique_id=$2 AND user_id=$3 AND authorization_status='AUTHORIZED' AND bank_id=$4", + const results = await Promise.all( + backups.map(async (backup) => { + // Also clears the target device's pending public key (SSO device pairing flow). + const result = await db.query( + `UPDATE user_devices SET encrypted_password_backup_2=$1, password_backup_public_key=NULL + WHERE device_unique_id=$2 AND user_id=$3 AND bank_id=$4 AND authorization_status='AUTHORIZED'`, [ backup.encryptedPassword, backup.deviceId, basicAuth.userId, basicAuth.bankIds.internalId, ], - ), - ), + ); + return { deviceId: backup.deviceId, applied: (result.rowCount ?? 0) > 0 }; + }), ); + // A backup can fail to apply if the target device was concurrently revoked/rejected by + // another authorized device in the meantime (see rejectSsoDevice) — the caller needs to know + // which ones, so it doesn't record a device it never actually managed to back up. + const unappliedDeviceIds = results.filter((r) => !r.applied).map((r) => r.deviceId); logInfo(req.body?.userEmail, 'backupPassword2 OK'); // Return res - return res.status(204).end(); + return res.status(200).json({ unappliedDeviceIds }); } catch (e) { logError(req.body?.userEmail, 'backupPassword2', e); return res.status(400).end(); diff --git a/src/api2/routes/passwordReset/getPasswordBackup.ts b/src/api2/routes/passwordReset/getPasswordBackup.ts index 93ea3c89..a6dda80f 100644 --- a/src/api2/routes/passwordReset/getPasswordBackup.ts +++ b/src/api2/routes/passwordReset/getPasswordBackup.ts @@ -9,6 +9,7 @@ import libsodium from 'libsodium-wrappers'; import { getBankIds } from '../../helpers/bankUUID'; import Joi from 'joi'; import { SessionStore } from '../../../helpers/sessionStore'; +import { usesPasswordlessUnlockForEmail } from '../authentication/usesPasswordlessUnlock'; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any export const getPasswordBackup2 = async (req: any, res: any) => { @@ -133,19 +134,25 @@ export const getPasswordBackup2 = async (req: any, res: any) => { return res.status(403).json({ error: 'backup_not_setup' }); } - if (safeBody.openidSession) { - // create password reset request for the record - await db.query( - `INSERT INTO password_reset_request (device_id, status, bank_id, granted_by) + const isPasswordlessSSOUnlock = await usesPasswordlessUnlockForEmail( + safeBody.userEmail, + bankIds.internalId, + ); + if (!isPasswordlessSSOUnlock) { + if (safeBody.openidSession) { + // create password reset request for the record + await db.query( + `INSERT INTO password_reset_request (device_id, status, bank_id, granted_by) VALUES ($1,'COMPLETED',$2, 'SSO authentication')`, - [deviceRes.rows[0].id, bankIds.internalId], - ); - } else { - // update status for reset request - await db.query( - `UPDATE password_reset_request SET status='COMPLETED', reset_token=null WHERE id=$1 AND bank_id=$2`, - [resetRequest.reset_request_id, bankIds.internalId], - ); + [deviceRes.rows[0].id, bankIds.internalId], + ); + } else { + // update status for reset request + await db.query( + `UPDATE password_reset_request SET status='COMPLETED', reset_token=null WHERE id=$1 AND bank_id=$2`, + [resetRequest.reset_request_id, bankIds.internalId], + ); + } } await db.query( 'UPDATE user_devices SET password_challenge_error_count=0, last_password_challenge_submission_date=null WHERE device_unique_id=$1 AND bank_id=$2', diff --git a/src/server.ts b/src/server.ts index 90d1b3bd..ad631fff 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,6 +16,8 @@ import { getBankConfig } from './api2/routes/bank/getBankConfig'; import { getUrlList2 } from './api2/routes/bank/getUrlList'; import { requestDeviceAccess2 } from './api2/routes/deviceAuthorization/requestDeviceAccess'; import { checkDevice2 } from './api2/routes/deviceAuthorization/checkDevice'; +import { rejectSsoDevice } from './api2/routes/deviceAuthorization/rejectSsoDevice'; +import { sendPasswordBackupPublicKey } from './api2/routes/deviceAuthorization/sendPasswordBackupPublicKey'; import { getAuthenticationChallenges2 } from './api2/routes/authentication/getAuthenticationChallenges'; import { authenticate2 } from './api2/routes/authentication/authenticate'; import { updateVaultData } from './api2/routes/data/updateVaultData'; @@ -72,6 +74,7 @@ import { shamirSecurityAlert } from './api2/routes/shamirRecovery/shamirSecurity import { getShamirRecoveryChallenge } from './api2/routes/shamirRecovery/getShamirRecoveryChallenge'; import { getRecoveryKeyPair } from './api2/routes/shamirRecovery/getRecoveryKeyPair'; import helmet from 'helmet'; +import { usesPasswordlessUnlock } from './api2/routes/authentication/usesPasswordlessUnlock'; export const app = express(); @@ -160,6 +163,11 @@ app.post( requestDeviceAccess2, ); app.post(['/:bankUUID/api2/check-device', '/api2/check-device'], checkDevice2); +app.post(['/:bankUUID/api2/reject-sso-device', '/api2/reject-sso-device'], rejectSsoDevice); +app.post( + ['/:bankUUID/api2/send-password-backup-public-key', '/api2/send-password-backup-public-key'], + sendPasswordBackupPublicKey, +); // AUTHENTICATION app.post( @@ -169,6 +177,10 @@ app.post( app.post(['/:bankUUID/api2/authenticate', '/api2/authenticate'], authenticate2); app.post(['/:bankUUID/api2/disconnect', '/api2/disconnect'], disconnect2); app.post(['/:bankUUID/api2/authenticate-device-only'], authenticateDeviceOnly); +app.post( + ['/:bankUUID/api2/uses-passwordless-unlock', '/api2/uses-passwordless-unlock'], + usesPasswordlessUnlock, +); // OPENID app.post(['/:bankUUID/api2/authenticate-with-openid-auth-code'], authenticateWithOpenidAuthCode);