* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file netdiag/api/sso.php * \ingroup netdiag * \brief API-Endpunkt: Anmeldung der App ueber die awlauth-Anmeldeseite (SSO). * * GET -> {available, clientId, redirectUri, authorizePath} * POST {code, code_verifier} -> {token, expiresIn, user} (wie auth.php) * * Ablauf: Die App oeffnet `authorizePath` im Chrome-Tab. Ist der Benutzer im * Handy-Browser schon angemeldet (awl_sso-Cookie, z. B. aus einer der PWAs), * springt awlauth ohne Rueckfrage mit einem Einmal-Code zurueck in die App * (`de.data-it-solution.netdiag:/sso?code=…&state=…`), sonst erst nach * Passwort oder Fingerabdruck auf der awlauth-Seite. Die App loest den Code * hier ein. * * Warum hier und nicht ueber awlauth/sso/token.php: token.php verlangt das * App-Geheimnis, und eine App auf dem Handy kann keines verwahren — es steckte * lesbar in der APK. Dieses Modul laeuft im selben Dolibarr wie awlauth und * loest den Code direkt ein. Gegen einen abgefangenen Code (jede App kann sich * das Schema zusaetzlich registrieren) schuetzt PKCE: eingeloest wird nur mit * dem Geheimnis, dessen Pruefwert die App beim Start mitgeschickt hat. * * Die App wird in awlauth beim ersten GET selbst als „NetDiag" angelegt. Ist * sie dort abgeschaltet oder fehlt die Ruecksprungadresse, bleibt sie es — * dann antwortet GET mit available=false und die App zeigt nur das Passwortfeld. */ require_once __DIR__.'/netdiag_api.lib.php'; netdiag_api_bootstrap(); /** @var DoliDB $db */ /** @var Translate $langs */ /** Kennung der App in awlauth und ihre einzige Ruecksprungadresse */ const NETDIAG_SSO_CLIENT_ID = 'netdiag'; const NETDIAG_SSO_REDIRECT = 'de.data-it-solution.netdiag:/sso'; /** * Prueft, ob die Anmeldung ueber awlauth moeglich ist, und legt die App dort * bei Bedarf an. * * @return array{0: array|null, 1: string} App-Zeile (oder null) und Grund */ function netdiag_sso_client() { if (!netdiag_awlauth_available()) { return array(null, 'Das Modul AWL-Auth ist nicht aktiv'); } dol_include_once('/awlauth/lib/awlauth_sso.lib.php'); if (!function_exists('awlauth_sso_pkce_verify')) { return array(null, 'AWL-Auth ist zu alt für die Anmeldung per Weiterleitung (ab Version 1.6.0)'); } $client = awlauth_sso_client_fetch(NETDIAG_SSO_CLIENT_ID, false); if ($client === null) { $neu = awlauth_sso_client_create('NetDiag', array(NETDIAG_SSO_REDIRECT)); if ($neu === null || $neu['client_id'] !== NETDIAG_SSO_CLIENT_ID) { return array(null, 'NetDiag konnte in AWL-Auth nicht angelegt werden'); } dol_syslog('netdiag sso: App in awlauth angelegt', LOG_INFO); $client = awlauth_sso_client_fetch(NETDIAG_SSO_CLIENT_ID, false); } if ($client === null || empty($client['active'])) { return array(null, 'NetDiag ist in AWL-Auth abgeschaltet'); } if (!awlauth_sso_redirect_allowed($client, NETDIAG_SSO_REDIRECT)) { return array(null, 'In AWL-Auth fehlt die Rücksprungadresse '.NETDIAG_SSO_REDIRECT); } return array($client, ''); } if ($_SERVER['REQUEST_METHOD'] === 'GET') { list($client, $grund) = netdiag_sso_client(); if ($client === null) { netdiag_api_respond(array('available' => false, 'reason' => $grund)); } netdiag_api_respond(array( 'available' => true, 'clientId' => (string) $client['client_id'], 'redirectUri' => NETDIAG_SSO_REDIRECT, 'authorizePath' => dol_buildpath('/awlauth/sso/authorize.php', 1), )); } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { netdiag_api_error('Nur GET oder POST erlaubt', 405); } list($client, $grund) = netdiag_sso_client(); if ($client === null) { netdiag_api_error('Anmeldung per Weiterleitung nicht möglich: '.$grund, 503); } // Dieselbe Bremse wie bei awlauth/sso/token.php: Fehlversuche je Absender-IP $rlKey = 'netdiag-sso:'.awlauth_client_ip(); $rate = awlauth_rl_check($rlKey, 10, 900, true); if (!$rate['allowed']) { netdiag_api_error('Zu viele Fehlversuche — bitte später erneut versuchen', 429); } $body = netdiag_api_read_body(); $code = isset($body['code']) ? (string) $body['code'] : ''; $verifier = isset($body['code_verifier']) ? (string) $body['code_verifier'] : ''; if ($code === '' || $verifier === '') { netdiag_api_error('code und code_verifier erforderlich', 400); } $row = awlauth_sso_code_redeem($code, $client, NETDIAG_SSO_REDIRECT, $verifier); // Zweite Sicherung: ohne hinterlegten Pruefwert wird hier nie eingeloest, // auch wenn awlauth das irgendwann anders handhaben sollte. if ($row === null || (string) $row['code_challenge'] === '') { awlauth_rl_fail($rlKey, 900); netdiag_api_error('Anmeldung abgelaufen oder ungültig — bitte erneut anmelden', 400); } awlauth_rl_clear($rlKey); require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; $user = new User($db); // Zwischen Anmeldung und Einloesen liegen Sekunden — massgeblich ist das Konto JETZT if ($user->fetch((int) $row['fk_user']) <= 0 || $user->statut != 1 || !empty($user->socid)) { netdiag_api_error('Benutzer nicht aktiv oder nicht für die App freigegeben', 403); } $user->getrights(); if (!$user->hasRight('netdiag', 'protocol', 'read')) { netdiag_api_error('Keine Berechtigung für NetDiag', 403); } // Gleiche Sitzungsart und gleiche Beschriftung wie bei auth.php — in der // awlauth-Geraeteliste steht das Handy so oder so als „NetDiag-App · Android". $ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : ''; $label = 'NetDiag-App'; if ($ua !== '' && function_exists('awlauth_device_label')) { $parts = explode(' · ', awlauth_device_label($ua)); if (!empty($parts[0])) { $label .= ' · '.$parts[0]; } } $issued = awlauth_issue_bearer($user, 0, (string) $row['login_type'], $label); if ($issued === null) { netdiag_api_error('Anmeldung konnte nicht abgeschlossen werden', 500); } netdiag_api_respond(array( 'token' => $issued['token'], 'expiresIn' => (int) $issued['expiresIn'], 'user' => array( 'id' => (int) $user->id, 'login' => $user->login, 'name' => $user->getFullName($langs), 'email' => $user->email, 'canWrite' => (bool) $user->hasRight('netdiag', 'protocol', 'write'), ), ));