Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@
"OTP verification failed": "OTP verification failed",
"Two-Factor Authentication": "Two-Factor Authentication",
"Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code.": "Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code."
}
},
"Continue": "Continue",
"Sign in with SSO": "Sign in with SSO",
"SSO login failed": "Single Sign-On failed, try again",
"SSO user not enabled": "You signed in on the identity provider, but this user is not enabled to use the CTI. Contact your administrator."
},
"SplashScreen": {
"Description": "Welcome to NethLink, a desktop solution for seamless communication. Make and receive calls, save contacts to you phonebook and much more.",
Expand Down
6 changes: 5 additions & 1 deletion public/locales/it/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@
"OTP verification failed": "Verifica OTP fallita",
"Two-Factor Authentication": "Autenticazione a Due Fattori",
"Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code.": "Inserisci il codice a 6 cifre (codice OTP) dalla tua app di autenticazione. Se non puoi accedere all'app, puoi utilizzare un codice OTP di recupero."
}
},
"Continue": "Continua",
"Sign in with SSO": "Accedi con SSO",
"SSO login failed": "Accesso Single Sign-On non riuscito, riprova",
"SSO user not enabled": "Ti sei autenticato sull'identity provider, ma questo utente non è abilitato all'uso del CTI. Contatta l'amministratore."
},
"SplashScreen": {
"Description": "Benvenuti in NethLink, la soluzione desktop per comunicazioni senza confini. Effettua e ricevi chiamate, salva i contatti nella tua rubrica e molto altro ancora.",
Expand Down
9 changes: 8 additions & 1 deletion src/main/classes/controllers/AccountController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Account, AuthAppData, AvailableDevices, ConfigFile, PhoneIslandPosition } from '@shared/types'
import { Account, AuthAppData, AvailableDevices, ConfigFile, PhoneIslandPosition, isSsoMethod } from '@shared/types'
import { Log } from '@shared/utils/logger'
import { safeStorage } from 'electron'
import { store } from '@/lib/mainStore'
Expand Down Expand Up @@ -161,6 +161,13 @@ export class AccountController {
}
}

// SSO accounts have no password: when the token expires the user
// must go through the interactive SSO flow again
if (isSsoMethod(lastLoggedAccount.authenticationMethod)) {
Log.info('auto login failed: SSO account token expired, user interaction needed')
return false
}

// Token is expired or doesn't exist, do a new login
const tempLoggedAccount = await this.NethVoiceAPI.Authentication.login(lastLoggedAccount.host, lastLoggedAccount.username, password)

Expand Down
78 changes: 77 additions & 1 deletion src/main/lib/ipcEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PhoneIslandController } from '@/classes/controllers/PhoneIslandControll
import { CommandBarController } from '@/classes/controllers/CommandBarController'
import { IPC_EVENTS } from '@shared/constants'
import { Account, OnDraggingWindow, PAGES } from '@shared/types'
import { BrowserWindow, app, ipcMain, screen, shell, desktopCapturer, globalShortcut, clipboard, Notification } from 'electron'
import { BrowserWindow, app, ipcMain, net, screen, shell, desktopCapturer, globalShortcut, clipboard, Notification } from 'electron'
import { Log } from '@shared/utils/logger'
import { NethLinkController } from '@/classes/controllers/NethLinkController'
import { AppController } from '@/classes/controllers/AppController'
Expand Down Expand Up @@ -457,6 +457,82 @@ export function registerIpcEvents() {
e.reply(IPC_EVENTS.SET_NETHVOICE_CONFIG, account)
})

ipcMain.on(IPC_EVENTS.GET_HOST_CONFIG, async (e, host: string) => {
// read the authentication capabilities of the host before asking credentials
const { parseHostConfig } = useLogin()
try {
const config: string = await NetworkController.instance.get(`https://${host}/config/config.production.js`)
e.reply(IPC_EVENTS.SET_HOST_CONFIG, { hostConfig: parseHostConfig(config) })
} catch (error: any) {
e.reply(IPC_EVENTS.SET_HOST_CONFIG, { error: error?.message || 'unreachable host' })
}
})

ipcMain.on(IPC_EVENTS.SSO_LOGIN, (event, payload: { host: string, url: string }) => {
// Single Sign-On: run the SAML dance in a dedicated browser window, then
// mint the JWT on the forwardAuth-guarded endpoint using the window session
// cookies. The persistent partition keeps the IdP session across logins.
const { host, url } = payload
const win = new BrowserWindow({
width: 520,
height: 660,
autoHideMenuBar: true,
webPreferences: { partition: 'persist:sso', nodeIntegration: false, contextIsolation: true, sandbox: true }
})
let settled = false
const settle = (token?: string, error?: string) => {
if (settled) return
settled = true
event.reply(IPC_EVENTS.SSO_LOGIN_RESULT, { token, error })
if (!win.isDestroyed()) win.destroy()
}
const mint = () => {
const request = net.request({
url: `https://${host}/api/sso-login`,
method: 'POST',
session: win.webContents.session,
useSessionCookies: true
})
request.setHeader('Content-Type', 'application/json')
request.on('response', (response) => {
let body = ''
response.on('data', (chunk) => (body += chunk))
response.on('end', () => {
try {
const token = JSON.parse(body).token
if (response.statusCode === 200 && token) settle(token)
// authenticated on the IdP but the mint was rejected: a 401/403
// means the user is not enabled on this CTI
else if (response.statusCode === 401 || response.statusCode === 403)
settle(undefined, 'SSO_USER_NOT_ENABLED')
else settle(undefined, `SSO login failed with status ${response.statusCode}`)
} catch {
settle(undefined, 'SSO login failed: invalid response')
}
})
})
request.on('error', (error) => settle(undefined, error.message))
request.end('{}')
}
// the SSO flow ends with a redirect to https://<host>/?ssologin=1: the
// session cookie is already set, mint the token instead of loading the app
const checkUrl = (e: Electron.Event, newUrl: string) => {
try {
const u = new URL(newUrl)
if (u.hostname === host && u.searchParams.has('ssologin')) {
e.preventDefault()
mint()
}
} catch (err) {
Log.warning('SSO login: unparsable navigation url', newUrl)
}
}
win.webContents.on('will-redirect', checkUrl)
win.webContents.on('will-navigate', checkUrl)
win.on('closed', () => settle(undefined, 'SSO window closed'))
win.loadURL(url)
})

ipcMain.on(IPC_EVENTS.EMIT_QUEUE_UPDATE, (_, queue) => {
try {
NethLinkController.instance.window.emit(IPC_EVENTS.EMIT_QUEUE_UPDATE, queue)
Expand Down
2 changes: 1 addition & 1 deletion src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ function startup() {

ipcMain.on(IPC_EVENTS.LOGIN, async (e, props?: { account?: Account, password?: string, showNethlink: boolean, }) => {
const { password, showNethlink, account } = props || { showNethlink: true }
if (LoginController.instance && LoginController.instance.window.isOpen() && password && account) {
if (LoginController.instance && LoginController.instance.window.isOpen() && password !== undefined && account) {
Log.info("LOGIN SUCCESS")
await LoginController.instance.quit()
await AccountController.instance.saveLoggedAccount(account, password)
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@
"Delete account": "Are you sure you want to delete {{username}}?",
"Back": "Back",
"User not authorized for NethLink": "User not authorized for NethLink",
"Sign in with SSO": "Sign in with SSO",
"SSO login failed": "Single Sign-On failed, try again",
"SSO user not enabled": "You signed in on the identity provider, but this user is not enabled to use the CTI. Contact your administrator.",
"Generic error": "Generic error",
"2FA": {
"OTP code": "OTP code",
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/public/locales/it/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@
"Delete account": "Sei sicuro di voler eliminare {{username}}?",
"Back": "Indietro",
"User not authorized for NethLink": "Utente non autorizzato per NethLink",
"Sign in with SSO": "Accedi con SSO",
"SSO login failed": "Single Sign-On non riuscito, riprova",
"SSO user not enabled": "Ti sei autenticato sull'identity provider, ma questo utente non è abilitato all'uso del CTI. Contatta l'amministratore.",
"Generic error": "Errore generico",
"2FA": {
"OTP code": "Codice OTP",
Expand Down
Loading
Loading