All checks were successful
Deploy netdiag / deploy (push) Successful in 15s
GET meldet, ob die Anmeldung ueber die awlauth-Seite eingerichtet ist, und legt NetDiag dort beim ersten Aufruf selbst an. POST loest den Einmal-Code mit PKCE-Geheimnis ein und antwortet wie auth.php - eingeloest wird im Modul, weil eine App auf dem Handy das App-Geheimnis fuer awlauth/sso/token.php nicht verwahren kann. Braucht awlauth >= 1.6.0, sonst available:false. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
172 lines
6.8 KiB
PHP
172 lines
6.8 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* \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'),
|
|
),
|
|
));
|