Introduzione procedura di autenticazione OAuth2 per account email

This commit is contained in:
Dasc3er 2021-07-27 10:16:10 +02:00
parent 5f51c4af16
commit 5135a2c04c
9 changed files with 408 additions and 35 deletions

View File

@ -39,6 +39,8 @@
"illuminate/database": "~5.4.0",
"intervention/image": "^2.3",
"league/csv": "^8.2",
"league/oauth2-client": "^2.6",
"league/oauth2-google": "^3.0",
"maximebf/debugbar": "^1.15",
"monolog/monolog": "^1.22",
"mpdf/mpdf": "^v8.0.7",
@ -55,6 +57,7 @@
"symfony/polyfill-php70": "^1.8",
"symfony/translation": "^3.3",
"symfony/var-dumper": "^3.3",
"thenetworg/oauth2-azure": "^2.0",
"willdurand/geocoder": "^3.3"
},
"require-dev": {

View File

@ -55,7 +55,7 @@ $rootdir = ROOTDIR;
$baseurl = BASEURL;
// Sicurezza della sessioni
ini_set('session.cookie_samesite', 'strict');
//ini_set('session.cookie_samesite', 'strict');
ini_set('session.use_trans_sid', '0');
ini_set('session.use_only_cookies', '1');

View File

@ -34,6 +34,9 @@ class Account extends Model
protected $table = 'em_accounts';
/** @var OAuth2 */
protected $gestoreOAuth2;
public function testConnection()
{
// Impostazione di connected_at a NULL
@ -54,6 +57,17 @@ class Account extends Model
return $result;
}
public function getGestoreOAuth2()
{
if (isset($this->gestoreOAuth2)) {
return $this->gestoreOAuth2;
}
$this->gestoreOAuth2 = new OAuth2($this);
return $this->gestoreOAuth2;
}
/* Relazioni Eloquent */
public function templates()

View File

@ -0,0 +1,190 @@
<?php
namespace Modules\Emails;
use InvalidArgumentException;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\Google;
use League\OAuth2\Client\Token\AccessToken;
use TheNetworg\OAuth2\Client\Provider\Azure;
class OAuth2
{
public static $providers = [
'microsoft' => [
'name' => 'Microsoft',
'class' => Azure::class,
'options' => [
'scope' => [
'offline_access',
'https://graph.microsoft.com/SMTP.Send',
//'https://outlook.office.com/IMAP.AccessAsUser.All'
],
],
'help' => 'https://docs.openstamanager.com/faq/configurazione-oauth2#microsoft',
],
'google' => [
'name' => 'Google',
'class' => Google::class,
'options' => [
'scope' => ['https://mail.google.com/'],
'accessType' => 'offline',
],
'help' => 'https://docs.openstamanager.com/faq/configurazione-oauth2#google',
],
];
protected $provider;
protected $account;
public function __construct(Account $account)
{
$this->account = $account;
$this->init();
}
/**
* Inizializza il ->inprovider per l'autenticazione OAuth2.
*/
public function init()
{
$redirect_uri = base_url().'/oauth2.php';
$class = $this->getProviderConfiguration()['class'];
// Authorization
$this->provider = new $class([
'clientId' => $this->account->client_id,
'clientSecret' => $this->account->client_secret,
'redirectUri' => $redirect_uri,
]);
// Configurazioni specifiche per il provider di Microsoft Azure
if ($this->provider instanceof Azure) {
$this->provider->defaultEndPointVersion = Azure::ENDPOINT_VERSION_2_0;
$this->provider->tenant = 'consumers';
}
}
public function getProvider()
{
return $this->provider;
}
public function getProviderConfiguration()
{
return self::$providers[$this->account->provider];
}
public function needsConfiguration()
{
$access_token = $this->getAccessToken();
return empty($access_token);
}
/**
* Gestisce le operazioni di configurazione per l'autenticazione OAuth2.
* Restituisce l'URL di redirect per le operazioni di aggiornamento dei dati, lancia un eccezione in caso di errori e restituisce null in caso di completamento della configurazione.
*
* Nota: l'autenticazione OAuth2 richiede una serie di richieste su una singola pagina
* - Richiesta di autenticazione al server remoto (code, state vuoti)
* - Conferma di autenticazione alla pagina di redirect (code, state impostati)
* - Richiesta del token di accesso dalla pagina di redirect al server remoto
*
* @param string|null $code
* @param string|null $state
*
* @throws IdentityProviderException
* @throws InvalidArgumentException
*
* @return string|null
*/
public function configure($code, $state)
{
if (!$this->needsConfiguration()) {
return null;
}
$provider = $this->getProvider();
$options = $this->getProviderConfiguration()['options'];
if (empty($code)) {
// Fetch the authorization URL from the provider; this returns the
// urlAuthorize option and generates and applies any necessary parameters
// (e.g. state).
$authorizationUrl = $provider->getAuthorizationUrl($options);
// Get the state generated for you and store it to the session.
$this->account->oauth2_state = $provider->getState();
$this->account->save();
// Redirect the user to the authorization URL.
return $authorizationUrl;
} elseif (!empty($this->account->oauth2_state) && $this->account->oauth2_state !== $state) {
$this->account->oauth2_state = null;
$this->account->save();
throw new InvalidArgumentException();
} else {
$this->account->oauth2_state = null;
$this->account->save();
// Try to get an access token using the authorization code grant.
$accessToken = $provider->getAccessToken('authorization_code', [
'code' => $code,
]);
//dd($accessToken);
$this->setAccessToken($accessToken);
}
return null;
}
/**
* Imposta l'access token per l'autenticazione OAuth2.
*
* @param AccessToken|null
*/
public function setAccessToken($value)
{
$this->account->access_token = serialize($value);
$this->account->save();
}
/**
* Restituisce l'access token per l'autenticazione OAuth2.
*
* @return AccessToken|null
*/
public function getAccessToken()
{
$access_token = unserialize($this->account->access_token);
if (!empty($access_token) && $access_token->hasExpired()) {
// Tentativo di refresh del token di accessp
if (!empty($access_token->getRefreshToken())) {
$access_token = $this->getProvider()->getAccessToken('refresh_token', [
'refresh_token' => $access_token->getRefreshToken(),
]);
} else {
$access_token = null;
}
$this->setAccessToken($access_token);
}
return $access_token;
}
public function getRefreshToken()
{
$access_token = unserialize($this->account->access_token);
if (!empty($access_token)) {
return $access_token->getRefreshToken();
}
return null;
}
}

View File

@ -19,7 +19,7 @@
include_once __DIR__.'/../../core.php';
switch (post('op')) {
switch (filter('op')) {
case 'add':
$dbo->insert('em_accounts', [
'name' => post('name'),
@ -39,6 +39,8 @@ switch (post('op')) {
$dbo->query('UPDATE em_accounts SET predefined = 0');
}
$abilita_oauth2 = post('abilita_oauth2');
$dbo->update('em_accounts', [
'name' => post('name'),
'note' => post('note'),
@ -53,6 +55,9 @@ switch (post('op')) {
'timeout' => post('timeout'),
'ssl_no_verify' => post('ssl_no_verify'),
'predefined' => $predefined,
'provider' => $abilita_oauth2 ? post('provider') : null,
'client_id' => $abilita_oauth2 ? post('client_id') : null,
'client_secret' => $abilita_oauth2 ? post('client_secret') : null,
], ['id' => $id_record]);
flash()->info(tr('Informazioni salvate correttamente!'));
@ -96,5 +101,11 @@ switch (post('op')) {
flash()->info(tr('Account email eliminato!'));
break;
case 'oauth2':
$redirect = base_path().'/oauth2.php?id_account='.$account->id;
redirect($redirect);
break;
}

View File

@ -17,9 +17,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use Modules\Emails\OAuth2;
include_once __DIR__.'/../../core.php';
?>
echo '
<form action="" method="post" id="edit-form">
<input type="hidden" name="op" value="update">
<input type="hidden" name="backto" value="record-edit">
@ -27,35 +29,35 @@ include_once __DIR__.'/../../core.php';
<!-- DATI -->
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title"><?php echo tr('Dati'); ?></h3>
<h3 class="panel-title">'.tr('Dati').'</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
{[ "type": "text", "label": "<?php echo tr('Nome account'); ?>", "name": "name", "value": "$name$", "required": 1 ]}
{[ "type": "text", "label": "'.tr('Nome account').'", "name": "name", "value": "$name$", "required": 1 ]}
</div>
<div class="col-md-3">
{[ "type": "checkbox", "label": "<?php echo tr('Indirizzo PEC'); ?>", "name": "pec", "value": "$pec$" ]}
{[ "type": "checkbox", "label": "'.tr('Indirizzo PEC').'", "name": "pec", "value": "$pec$" ]}
</div>
<div class="col-md-3">
{[ "type": "checkbox", "label": "<?php echo tr('Indirizzo predefinito'); ?>", "name": "predefined", "value": "$predefined$", "help": "<?php echo tr('Account da utilizzare per l\'invio di tutte le email dal gestionale.'); ?>" ]}
{[ "type": "checkbox", "label": "'.tr('Indirizzo predefinito').'", "name": "predefined", "value": "$predefined$", "help": "'.tr('Account da utilizzare per l\'invio di tutte le email dal gestionale.').'" ]}
</div>
</div>
<div class="row">
<div class="col-md-6">
{[ "type": "text", "label": "<?php echo tr('Nome visualizzato'); ?>", "name": "from_name", "value": "$from_name$" ]}
{[ "type": "text", "label": "'.tr('Nome visualizzato').'", "name": "from_name", "value": "$from_name$" ]}
</div>
<div class="col-md-3">
{[ "type": "email", "label": "<?php echo tr('Email mittente'); ?>", "name": "from_address", "value": "$from_address$", "required": 1 ]}
{[ "type": "email", "label": "'.tr('Email mittente').'", "name": "from_address", "value": "$from_address$", "required": 1 ]}
</div>
<div class="col-md-3">
{[ "type": "checkbox", "label": "<?php echo tr('Non verificare il certificato SSL'); ?>", "name": "ssl_no_verify", "value": "$ssl_no_verify$" ]}
{[ "type": "checkbox", "label": "'.tr('Non verificare il certificato SSL').'", "name": "ssl_no_verify", "value": "$ssl_no_verify$" ]}
</div>
@ -63,44 +65,119 @@ include_once __DIR__.'/../../core.php';
<div class="row">
<div class="col-md-6">
{[ "type": "text", "label": "<?php echo tr('Server SMTP'); ?>", "name": "server", "required": 1, "value": "$server$" ]}
{[ "type": "text", "label": "'.tr('Server SMTP').'", "name": "server", "required": 1, "value": "$server$" ]}
</div>
<div class="col-md-3">
{[ "type": "number", "label": "<?php echo tr('Porta SMTP'); ?>", "name": "port", "required": 1, "class": "text-center", "decimals":"0", "max-value":"65535", "value": "$port$" ]}
{[ "type": "number", "label": "'.tr('Porta SMTP').'", "name": "port", "required": 1, "class": "text-center", "decimals":"0", "max-value":"65535", "value": "$port$" ]}
</div>
<div class="col-md-3">
{[ "type": "select", "label": "<?php echo tr('Sicurezza SMTP'); ?>", "name": "encryption", "values": "list=\"\": \"<?php echo tr('Nessuna'); ?>\", \"tls\": \"TLS\", \"ssl\": \"SSL\"", "value": "$encryption$" ]}
{[ "type": "select", "label": "'.tr('Sicurezza SMTP').'", "name": "encryption", "values": "list=\"\": \"'.tr('Nessuna').'\", \"tls\": \"TLS\", \"ssl\": \"SSL\"", "value": "$encryption$" ]}
</div>
</div>
<div class="row">
<div class="col-md-6">
{[ "type": "text", "label": "<?php echo tr('Username SMTP'); ?>", "name": "username", "value": "$username$" ]}
{[ "type": "text", "label": "'.tr('Username SMTP').'", "name": "username", "value": "$username$" ]}
</div>
<div class="col-md-3">
{[ "type": "password", "label": "<?php echo tr('Password SMTP'); ?>", "name": "password", "value": "$password$" ]}
{[ "type": "password", "label": "'.tr('Password SMTP').'", "name": "password", "value": "$password$" ]}
</div>
<div class="col-md-3">
{[ "type": "number", "label": "<?php echo tr('Timeout coda di invio (millisecondi)'); ?>", "name": "timeout", "value": "$timeout$", "decimals": 1, "min-value": 100 ]}
{[ "type": "number", "label": "'.tr('Timeout coda di invio (millisecondi)').'", "name": "timeout", "value": "$timeout$", "decimals": 1, "min-value": 100 ]}
</div>
</div>
<div class="row">
<div class="col-md-12">
{[ "type": "textarea", "label": "<?php echo tr('Note'); ?>", "name": "note", "value": "$note$" ]}
{[ "type": "textarea", "label": "'.tr('Note').'", "name": "note", "value": "$note$" ]}
</div>
</div>
</div>
</div>
</div>';
// Elenco provider disponibili
$providers = OAuth2::$providers;
$elenco_provider = [];
foreach ($providers as $key => $provider) {
$elenco_provider[] = [
'id' => $key,
'text' => $provider['name'],
'help' => $provider['help'],
];
}
echo '
<!-- OAuth2 -->
<div class="box box-info">
<div class="box-header">
<h3 class="box-title">'.tr('OAuth2').'</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-md-6">
<span class="label label-warning pull-right hidden" id="guida-configurazione"></span>
{[ "type": "select", "label": "'.tr('Provider account').'", "name": "provider", "value": "$provider$", "values": '.json_encode($elenco_provider).', "disabled": "'.intval(empty($account->provider)).'" ]}
</div>
<div class="col-md-3">
{[ "type": "checkbox", "label": "'.tr('Abilita OAuth2').'", "name": "abilita_oauth2", "value": "'.intval(!empty($account->provider)).'" ]}
</div>
<div class="col-md-3">
<a type="button" class="btn btn-success btn-block '.(empty($account->provider) || empty($account->client_id) || empty($account->client_secret) ? 'disabled' : '').'" style="margin-top: 25px" href="'.base_url().'/editor.php?id_module='.$id_module.'&id_record='.$id_record.'&op=oauth2">
<i class="fa fa-refresh"></i> '.(empty($account->access_token) ? tr('Completa configurazione') : tr('Ripeti configurazione')).'
</a>
</div>
<div class="col-md-6">
{[ "type": "text", "label": "'.tr('Client ID').'", "name": "client_id", "value": "$client_id$", "disabled": "'.intval(empty($account->provider)).'" ]}
</div>
<div class="col-md-6">
{[ "type": "text", "label": "'.tr('Client Secret').'", "name": "client_secret", "value": "$client_secret$", "disabled": "'.intval(empty($account->provider)).'" ]}
</div>
</div>
</div>
</div>
</form>
<?php
<script>
var abilita_oauth2 = input("abilita_oauth2");
var provider = input("provider");
var client_id = input("client_id");
var client_secret = input("client_secret");
var guida = $("#guida-configurazione");
abilita_oauth2.change(function() {
const disable = !abilita_oauth2.get();
provider.setDisabled(disable);
client_id.setDisabled(disable);
client_secret.setDisabled(disable);
});
provider.change(function() {
const data = provider.getData();
if (data.id) {
guida.removeClass("hidden");
guida.html(`<a href="${data.help}">'.tr('Istruzioni di configurazione').' <i class="fa fa-external-link"></i></a>`);
} else {
guida.addClass("hidden");
}
})
$(document).ready(function() {
provider.trigger("change");
})
</script>';
// Collegamenti diretti
// Template email collegati a questo account
$elementi = $dbo->fetchArray('SELECT `id`, `name` FROM `em_templates` WHERE `id_account` = '.prepare($id_record));
@ -134,4 +211,3 @@ if (!empty($elementi)) {
<i class="fa fa-trash"></i> '.tr('Elimina').'
</a>';
}
?>

52
oauth2.php Normal file
View File

@ -0,0 +1,52 @@
<?php
use Models\Module;
use Modules\Emails\Account;
use Modules\Emails\OAuth2;
$skip_permissions = true;
include_once __DIR__.'/core.php';
session_write_close();
// Authorization information
$state = $_GET['state'];
$code = $_GET['code'];
// Account individuato via oauth2_state
if (!empty($state)) {
$account = Account::where('oauth2_state', '=', $state)
->first();
} else {
$account = Account::find(get('id_account'));
// Impostazione access token a null per reimpostare la configurazione
$account->access_token = null;
$account->save();
}
if (empty($account)) {
echo tr('Errore durante il completamento della configurazione: account non trovato');
return;
}
// Inizializzazione
$oauth = new OAuth2($account);
// Redirect all'URL di autorizzazione del servizio esterno
$redirect = $oauth->configure($code, $state);
// Redirect automatico al record
if (empty($redirect)) {
$modulo_account_email = Module::pool('Account email');
$redirect = base_path().'/editor.php?id_module='.$modulo_account_email->id.'&id_record='.$account->id;
}
if (empty($_GET['error'])) {
redirect($redirect);
exit();
} else {
echo $_GET['error'].'<br>'.$_GET['error_description'].'
<br><br>
<a href="'.$redirect.'">'.tr('Riprova').'</a>';
}

View File

@ -23,6 +23,8 @@ use Modules\Emails\Account;
use Modules\Emails\Mail;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\OAuth;
use PHPMailer\PHPMailer\SMTP;
use Prints;
use Uploads;
@ -40,16 +42,16 @@ class EmailNotification extends PHPMailer implements NotificationInterface
$this->CharSet = 'UTF-8';
// Configurazione di base
$config = Account::find($account);
if (empty($config)) {
$config = Account::where('predefined', true)->first();
$account = $account instanceof Account ? $account : Account::find($account);
if (empty($account)) {
$account = Account::where('predefined', true)->first();
}
// Preparazione email
$this->IsHTML(true);
if (!empty($config['server'])) {
$this->IsSMTP(true);
if (!empty($account['server'])) {
$this->IsSMTP();
// Impostazioni di debug
$this->SMTPDebug = \App::debug() ? 2 : 0;
@ -58,22 +60,40 @@ class EmailNotification extends PHPMailer implements NotificationInterface
};
// Impostazioni dell'host
$this->Host = $config['server'];
$this->Port = $config['port'];
$this->Host = $account['server'];
$this->Port = $account['port'];
// Impostazioni di autenticazione
if (!empty($config['username'])) {
if (!empty($account['username'])) {
$this->SMTPAuth = true;
$this->Username = $config['username'];
$this->Password = $config['password'];
$this->Username = $account['username'];
// Configurazione OAuth2
if (!empty($account['access_token'])) {
$oauth2 = $account->getGestoreOAuth2();
$this->AuthType = 'XOAUTH2';
$this->setOAuth(
new OAuth([
'provider' => $oauth2->getProvider(),
'refreshToken' => $oauth2->getRefreshToken(),
'clientId' => $account->client_id,
'clientSecret' => $account->client_secret,
'userName' => $account->username,
])
);
} else {
$this->Password = $account['password'];
}
}
// Impostazioni di sicurezza
if (in_array(strtolower($config['encryption']), ['ssl', 'tls'])) {
$this->SMTPSecure = strtolower($config['encryption']);
if (in_array(strtolower($account['encryption']), ['ssl', 'tls'])) {
$this->SMTPSecure = strtolower($account['encryption']);
}
if (!empty($config['ssl_no_verify'])) {
// Disabilitazione verifica host
if (!empty($account['ssl_no_verify'])) {
$this->SMTPOptions = [
'ssl' => [
'verify_peer' => false,
@ -84,8 +104,8 @@ class EmailNotification extends PHPMailer implements NotificationInterface
}
}
$this->From = $config['from_address'];
$this->FromName = $config['from_name'];
$this->From = $account['from_address'];
$this->FromName = $account['from_name'];
$this->WordWrap = 78;
}

View File

@ -142,3 +142,10 @@ ALTER TABLE `dt_ddt` ADD `id_ddt_trasporto_interno` INT(11) NULL, ADD FOREIGN KE
-- Aggiunto ragruppamento referenti per sede
UPDATE `zz_plugins` SET `options` = ' { \"main_query\": [ { \"type\": \"table\", \"fields\": \"Nome, Indirizzo, Città, CAP, Provincia, Referente\", \"query\": \"SELECT an_sedi.id, an_sedi.nomesede AS Nome, an_sedi.indirizzo AS Indirizzo, an_sedi.citta AS Città, an_sedi.cap AS CAP, an_sedi.provincia AS Provincia, GROUP_CONCAT(an_referenti.nome SEPARATOR \\\", \\\") AS Referente FROM an_sedi LEFT OUTER JOIN an_referenti ON idsede = an_sedi.id WHERE 1=1 AND an_sedi.idanagrafica=|id_parent| GROUP BY an_sedi.id HAVING 2=2 ORDER BY an_sedi.id DESC\"} ]}' WHERE `zz_plugins`.`name` = 'Sedi';
UPDATE `zz_group_module` SET `clause` = 'in_interventi.id IN (SELECT idintervento FROM in_interventi_tecnici WHERE idintervento=in_interventi.id AND idtecnico=|id_anagrafica| UNION SELECT id_intervento FROM in_interventi_tecnici_assegnati WHERE id_intervento=in_interventi.id AND id_tecnico=|id_anagrafica|)' WHERE `zz_group_module`.`name` = 'Mostra interventi ai tecnici coinvolti';
-- Aggiunto supporto autenticazione OAuth 2
ALTER TABLE `em_accounts` ADD `provider` varchar(255),
ADD `client_id` TEXT,
ADD `client_secret` TEXT,
ADD `oauth2_state` TEXT,
ADD `access_token` TEXT;