SismaFramework fornisce un sistema di sicurezza completo che copre due aspetti fondamentali:
Questo documento descrive come implementare funzionalità di sicurezza enterprise-grade nella tua applicazione.
Il sistema di autenticazione è fondato su una classe astratta base:
BaseAuthentication (Security/BaseClasses/) — classe @internal che centralizza le dipendenze comuni a tutti i flussi (request, filter, session, authenticable interface).Authentication (Security/HttpClasses/) — flusso form-based classico; usa SubmittableTrait per la gestione di submission e degli errori di validazione.OAuthAuthentication (Security/HttpClasses/) — flusso OAuth 2.0 Authorization Code; non usa SubmittableTrait perché non esiste un form da sottomettere.Authentication)
La classe Authentication può essere iniettata direttamente in un controller e fornisce i metodi per validare le credenziali utente in modo sicuro (inclusa la protezione CSRF).
Nota sulla gestione della sessione: La classeAuthenticationsi occupa esclusivamente della validazione delle credenziali. La persistenza dello stato di autenticazione (login/logout) va gestita manualmente tramite la classeSession.
namespace MyModule\Application\Controllers;
use SismaFramework\Core\BaseClasses\BaseController;
use SismaFramework\Core\HelperClasses\Encryptor;
use SismaFramework\Core\HelperClasses\Session;
use SismaFramework\Core\HttpClasses\Request;
use SismaFramework\Core\HttpClasses\Response;
use SismaFramework\Security\HttpClasses\Authentication;
use MyModule\Application\Models\UserModel;
use MyModule\Application\Models\PasswordModel;
class SecurityController extends BaseController
{
public function login(Request $request, Authentication $auth): Response
{
// Se l'utente è già loggato (sessione attiva), reindirizzalo
if (Session::hasItem('userId')) {
return $this->router->redirect('dashboard/index');
}
// Se il form è stato inviato (metodo POST)
if ($request->server['REQUEST_METHOD'] === 'POST') {
// 1. Inietta i modelli necessari
$auth->setAuthenticableModelInterface(new UserModel($this->dataMapper));
$auth->setPasswordModelInterface(new PasswordModel($this->dataMapper));
// 2. checkAuthenticable() verifica in sequenza: CSRF token,
// identificatore utente e password. Restituisce true solo
// se tutti i controlli passano.
if ($auth->checkAuthenticable()) {
// 3. Recupera l'entità autenticata e persisti l'ID in sessione
$user = $auth->getAuthenticableInterface();
Session::setItem('userId', $user->getId());
return $this->router->redirect('dashboard/index');
}
// Se i controlli falliscono, gli errori sono disponibili tramite getFilterErrors()
$this->vars['errors'] = $auth->getFilterErrors();
}
// Genera e salva il token CSRF per il form: checkCsrfToken()/checkAuthenticable()
// lo verificano soltanto, non lo generano — tocca all'applicazione farlo prima
// di renderizzare il form.
if (Session::hasItem('csrfToken') === false) {
Session::setItem('csrfToken', Encryptor::getSimpleRandomToken());
}
$this->vars['csrfToken'] = Session::getItem('csrfToken');
$this->vars['pageTitle'] = 'Login';
return $this->render->generateView('security/login', $this->vars);
}
public function logout(): Response
{
Session::end();
return $this->router->redirect('security/login');
}
}
La classeAuthenticationgestisce automaticamente la protezione da attacchi CSRF tramitecheckCsrfToken(), che viene chiamato internamente dacheckAuthenticable().
OAuthAuthentication)
La classe OAuthAuthentication implementa il flusso Authorization Code OAuth 2.0 ed è progettata per provider come Google, GitHub, ecc. Non usa SubmittableTrait perché non esiste un form da sottomettere: gli errori arrivano come parametri URL dal provider e vengono gestiti tramite valori di ritorno.
OAuthWrapperInterface
Per astrarre la comunicazione con il provider OAuth, il framework fornisce l'interfaccia OAuthWrapperInterface (Security/Interfaces/Wrappers/). Ogni provider deve implementare due metodi:
namespace SismaFramework\Security\Interfaces\Wrappers;
interface OAuthWrapperInterface
{
// Costruisce l'URL di autorizzazione con il parametro state anti-CSRF
public function getAuthorizationUrl(string $state): string;
// Scambia il codice di autorizzazione per un identificatore utente (es. email).
// In caso di errore (token invalido, errore di rete), propaga un'eccezione.
public function getAuthenticableIdentifier(string $code): string;
}
namespace MyModule\Application\Wrappers;
use SismaFramework\Security\Interfaces\Wrappers\OAuthWrapperInterface;
class GoogleOAuthWrapper implements OAuthWrapperInterface
{
private string $clientId;
private string $clientSecret;
private string $redirectUri;
public function __construct(string $clientId, string $clientSecret, string $redirectUri)
{
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
$this->redirectUri = $redirectUri;
}
public function getAuthorizationUrl(string $state): string
{
$params = http_build_query([
'client_id' => $this->clientId,
'redirect_uri' => $this->redirectUri,
'response_type' => 'code',
'scope' => 'openid email profile',
'state' => $state,
]);
return 'https://accounts.google.com/o/oauth2/v2/auth?' . $params;
}
public function getAuthenticableIdentifier(string $code): string
{
// Scambia il code per un access token, poi recupera l'email
// (implementazione specifica del provider)
$tokenResponse = $this->exchangeCodeForToken($code);
$userInfo = $this->getUserInfo($tokenResponse['access_token']);
return $userInfo['email'];
}
// ... metodi privati di supporto ...
}
Il flusso OAuth si articola in due action: una che avvia il redirect verso il provider e una che gestisce il callback.
namespace MyModule\Application\Controllers;
use SismaFramework\Core\BaseClasses\BaseController;
use SismaFramework\Core\HelperClasses\Session;
use SismaFramework\Core\HttpClasses\Request;
use SismaFramework\Core\HttpClasses\Response;
use SismaFramework\Security\HttpClasses\OAuthAuthentication;
use MyModule\Application\Models\UserModel;
use MyModule\Application\Wrappers\GoogleOAuthWrapper;
class OAuthController extends BaseController
{
// Fase 1: Reindirizza l'utente al provider OAuth
public function redirectToProvider(OAuthAuthentication $auth): Response
{
$wrapper = new GoogleOAuthWrapper(
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
redirectUri: 'https://myapp.com/oauth/callback',
);
$auth->setOAuthWrapperInterface($wrapper);
// getAuthorizationUrl() genera lo state anti-CSRF, lo salva in sessione
// e restituisce l'URL completo del provider
$url = $auth->getAuthorizationUrl();
return $this->router->redirectToUrl($url);
}
// Fase 2: Gestisce il callback del provider
public function callback(Request $request, OAuthAuthentication $auth): Response
{
$wrapper = new GoogleOAuthWrapper(
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
redirectUri: 'https://myapp.com/oauth/callback',
);
$auth->setOAuthWrapperInterface($wrapper);
$auth->setAuthenticableModelInterface(new UserModel($this->dataMapper));
// checkCallback() verifica lo state anti-CSRF, scambia il code per
// un identificatore e recupera l'utente dal modello
if ($auth->checkCallback()) {
$user = $auth->getAuthenticableInterface();
Session::setItem('userId', $user->getId());
return $this->router->redirect('dashboard/index');
}
$this->vars['error'] = 'Autenticazione OAuth fallita.';
return $this->render->generateView('security/oauth-error', $this->vars);
}
}
Protezione CSRF in OAuth:getAuthorizationUrl()genera automaticamente unostatecasuale conrandom_bytes(16)e lo persiste in sessione.checkCallback()lo verifica in modo timing-safe tramitehash_equals(), seguendo lo stesso pattern difensivo dicheckCsrfToken()nella classeAuthentication.
Il sistema di autorizzazione si basa su due concetti: Voters e Permissions.
AccessDeniedException), bloccando l'esecuzione.Questo disaccoppia la logica di sicurezza (nel Voter) dal suo utilizzo (nella Permission e nel Controller).
Scenario: Solo l'autore di un Post può modificarlo.
##### 1. Creare il Voter
Crea un PostVoter nella cartella Voters del tuo modulo.
MyBlog/Application/Voters/PostVoter.php
namespace MyBlog\Application\Voters;
use SismaFramework\Security\BaseClasses\BaseVoter;
use MyBlog\Application\Entities\Post;
use MyModule\Application\Entities\User; // La tua entità utente
class PostVoter extends BaseVoter
{
// Specifica che questo Voter agisce solo su oggetti di tipo Post
protected function isInstancePermitted(): bool
{
return $this->subject instanceof Post;
}
// Contiene la logica di autorizzazione vera e propria
protected function checkVote(): bool
{
$post = $this->subject;
$user = $this->authenticable;
// Se l'utente non è loggato o non è un'istanza di User, nega l'accesso
if (!$user instanceof User) {
return false;
}
// L'utente è l'autore del post?
return $post->getAuthor()->getId() === $user->getId();
}
}
##### 2. Creare la Permission
Crea una PostPermission nella cartella Permissions che utilizzi il PostVoter.
MyBlog/Application/Permissions/PostPermission.php
namespace MyBlog\Application\Permissions;
use SismaFramework\Security\BaseClasses\BasePermission;
use MyBlog\Application\Voters\PostVoter;
class PostPermission extends BasePermission
{
// Non ci sono altre permission da chiamare prima
protected function callParentPermissions(): void {}
// Specifica quale Voter deve essere usato
protected function getVoter(): string
{
return PostVoter::class;
}
}
##### 3. Usare la Permission nel Controller
Ora, all'inizio dell'action edit del tuo PostController, invoca la Permission.
use MyBlog\Application\Permissions\PostPermission;
use SismaFramework\Security\Enumerations\AccessControlEntry;
class PostController extends BaseController
{
public function edit(Request $request, Post $post, Authentication $auth): Response
{
// 1. Controlla il permesso. Se fallisce, lancia un'eccezione 403.
PostPermission::isAllowed(
$post, // Il soggetto su cui decidere
AccessControlEntry::check, // Il tipo di controllo
$auth->getAuthenticableInterface() // L'utente attualmente loggato
);
// 2. Se il controllo passa, prosegui con la logica del form...
$form = new PostForm($post);
// ...
}
}
Encryptor
La classe Encryptor fornisce un'API unificata per tutte le operazioni crittografiche, dalla generazione di token all'encryption completa dei dati.
Per creare token sicuri da usare in sessioni, CSRF protection o API keys:
use SismaFramework\Core\HelperClasses\Encryptor;
// Genera un token esadecimale di 32 caratteri (16 bytes)
$token = Encryptor::getSimpleRandomToken();
// Esempio output: "a1b2c3d4e5f6789012345678901234567890abcd"
// Uso tipico per CSRF token
$_SESSION['csrf_token'] = Encryptor::getSimpleRandomToken();
Gli hash semplici sono ideali per verifiche di integrità, checksum e confronti rapidi:
// Hash con algoritmo di default (configurabile)
$hash = Encryptor::getSimpleHash('testo da hashare');
// Verifica hash
$isValid = Encryptor::verifySimpleHash('testo da hashare', $hash);
// Con configurazione personalizzata
$customConfig = Config::getInstance();
$hash = Encryptor::getSimpleHash('testo', $customConfig);
Configurazione algoritmo in Config/config.php:
const SIMPLE_HASH_ALGORITHM = 'sha256'; // o 'md5', 'sha1', 'sha512'
Per l'hashing sicuro delle password utilizza sempre Blowfish/BCrypt:
// Hash password (cost configurabile)
$hashedPassword = Encryptor::getBlowfishHash('password_utente');
// Verifica password
$isCorrect = Encryptor::verifyBlowfishHash('password_utente', $hashedPassword);
Configurazione workload in Config/config.php:
const BLOWFISH_HASH_WORKLOAD = 12; // Range: 4-31 (default: 10)
Note di sicurezza:
Per crittografare dati sensibili con chiave simmetrica:
// 1. Genera Initialization Vector (IV)
$iv = Encryptor::createInitializationVector();
// 2. Cifra il testo
$plaintext = 'Dati sensibili da proteggere';
$ciphertext = Encryptor::encryptString($plaintext, $iv);
// 3. Decifra il testo
$decrypted = Encryptor::decryptString($ciphertext, $iv);
// IMPORTANTE: Salva sempre IV insieme ai dati cifrati
$dataToStore = base64_encode($iv) . '|' . $ciphertext;
Configurazione crittografia in Config/config.php:
const ENCRYPTION_ALGORITHM = 'AES-256-CBC';
const ENCRYPTION_PASSPHRASE = 'chiave-molto-lunga-e-sicura';
const INITIALIZATION_VECTOR_BYTES = 16;
Per casi d'uso che richiedono chiavi pubbliche/private reali — firma digitale di documenti, identità verificabili, catene di certificati, dati leggibili solo da un destinatario specifico — Encryptor fornisce un'API basata su OpenSSL completa: generazione di chiavi e certificati, verifica della catena di fiducia, firma/verifica di dati e cifratura a busta.
Tutte le operazioni funzionano senza alcuna configurazione: se OPENSSL_CONFIG_PATH non è valorizzata, Encryptor genera e usa autonomamente una configurazione OpenSSL minimale autosufficiente (un file temporaneo per processo, ripulito automaticamente a fine richiesta) — non serve un openssl.cnf di sistema risolvibile, nemmeno su ambienti (tipicamente Windows) che ne sono privi.
Generare una coppia di chiavi:
$keyPair = Encryptor::generateAsymmetricKeyPair();
// ['privateKey' => '-----BEGIN PRIVATE KEY-----...', 'publicKey' => '-----BEGIN PUBLIC KEY-----...']
Certificato self-signed (il soggetto è al contempo titolare e garante — es. un fondatore/root of trust iniziale). Il secondo parametro booleano (default true) controlla l'estensione X.509v3 basicConstraints: true produce un certificato abilitato a firmare altri certificati, false un certificato foglia non abilitato a farlo:
$certificate = Encryptor::generateSelfSignedCertificate(
$keyPair['privateKey'],
['CN' => 'Mario Rossi', 'O' => 'La Mia Organizzazione']
// true di default: root of trust abilitata a firmare altri certificati
);
Certificato emesso da una CA (il soggetto genera la propria chiave e una CSR; la CA firma la CSR con il proprio certificato/chiave, producendo un certificato con issuer diverso dal subject). Anche qui un parametro booleano (default false) decide se il certificato emesso è a sua volta una CA intermedia o un certificato foglia:
// Lato CA: una CA self-signed già esistente
$caKeyPair = Encryptor::generateAsymmetricKeyPair();
$caCertificate = Encryptor::generateSelfSignedCertificate($caKeyPair['privateKey'], ['CN' => 'La Mia CA']);
// Lato soggetto: genera la propria chiave e la CSR
$subjectKeyPair = Encryptor::generateAsymmetricKeyPair();
$csr = Encryptor::generateCertificateSigningRequest($subjectKeyPair['privateKey'], ['CN' => 'Mario Rossi']);
// Lato CA: firma la CSR, emette il certificato del soggetto (foglia, non CA)
$subjectCertificate = Encryptor::signCertificateSigningRequest($csr, $caCertificate, $caKeyPair['privateKey']);
Verificare la catena di fiducia ("questo certificato è stato davvero emesso da questa CA?" — diverso dal verificare la firma su un documento):
$isTrusted = Encryptor::verifyCertificateSignedByIssuer($subjectCertificate, $caCertificate); // true
$isTrusted = Encryptor::verifyCertificateSignedByIssuer($subjectCertificate, $unrelatedCertificate); // false
Firma e verifica di dati:
$signature = Encryptor::signData('testo del documento', $subjectKeyPair['privateKey']);
// La verifica accetta sia un certificato che una chiave pubblica
$isValid = Encryptor::verifySignature('testo del documento', $signature, $subjectCertificate); // true
$isValid = Encryptor::verifySignature('testo manomesso', $signature, $subjectCertificate); // false
Cifratura a busta (envelope encryption) — a differenza della cifratura RSA diretta, non ha limiti di dimensione sul dato cifrato: la chiave pubblica cifra una chiave simmetrica generata al volo, che a sua volta cifra i dati (stesso algoritmo di ENCRYPTION_ALGORITHM):
$envelope = Encryptor::encryptWithPublicKey('dati riservati per il destinatario', $subjectCertificate);
// ['data' => ..., 'envelopeKey' => ..., 'initializationVector' => ...] — tutti in base64
$decrypted = Encryptor::decryptWithPrivateKey($envelope, $subjectKeyPair['privateKey']);
// 'dati riservati per il destinatario'
// Con la chiave privata sbagliata: false, non un'eccezione (stesso contratto di decryptString())
$failed = Encryptor::decryptWithPrivateKey($envelope, $altraChiavePrivata); // false
Configurazione in Config/config.php:
const ASYMMETRIC_KEY_TYPE = OPENSSL_KEYTYPE_RSA;
const ASYMMETRIC_KEY_BITS = 2048;
const ASYMMETRIC_DIGEST_ALGORITHM = 'sha256';
const CERTIFICATE_VALIDITY_DAYS = 3650;
// Opzionale: necessaria solo per esigenze avanzate (es. un provider/engine OpenSSL
// specifico). Se il file indicato non contiene le sezioni [v3_ca]/[v3_leaf] usate da
// generateSelfSignedCertificate()/signCertificateSigningRequest(), l'estensione
// basicConstraints non verrà applicata ai certificati generati.
define(__NAMESPACE__ . '\OPENSSL_CONFIG_PATH', getenv('OPENSSL_CONFIG_PATH') ?: '');
Nota per progetti già installati:Config/configFramework.phpviene copiato daConfig/config.phpuna sola volta, in fase di installazione, e non si aggiorna automaticamente con gli upgrade del framework. Se il tuo progetto esisteva prima dell'introduzione di questa funzionalità, aggiungi manualmente le costanti sopra al tuoconfigFramework.phpprima di usare questi metodi — altrimenti la prima chiamata sollevaError: Undefined constant.
AuthenticableInterfaceDefinisce il contratto per entità che possono essere autenticate:
use SismaFramework\Security\Interfaces\Entities\AuthenticableInterface;
class User implements AuthenticableInterface
{
public function getAuthIdentifier(): string
{
return $this->email; // o $this->username
}
public function getAuthPassword(): string
{
return $this->passwordHash;
}
}
PasswordInterfacePer entità che gestiscono reset password:
use SismaFramework\Security\Interfaces\Entities\PasswordInterface;
class User implements PasswordInterface
{
public function getEmailForPasswordReset(): string
{
return $this->email;
}
public function getPasswordResetToken(): ?string
{
return $this->resetToken;
}
public function setPasswordResetToken(?string $token): void
{
$this->resetToken = $token;
$this->resetTokenExpiry = $token ? (new DateTime())->add(new DateInterval('P1D')) : null;
}
}
MultiFactorInterfacePer autenticazione a due fattori:
use SismaFramework\Security\Interfaces\Entities\MultiFactorInterface;
class User implements MultiFactorInterface
{
public function isMfaEnabled(): bool
{
return $this->mfaSecret !== null;
}
public function getMfaSecret(): ?string
{
return $this->mfaSecret;
}
public function setMfaSecret(?string $secret): void
{
$this->mfaSecret = $secret;
}
}
class AuthController extends BaseController
{
public function login(Request $request): Response
{
$email = $request->input['email'] ?? '';
$password = $request->input['password'] ?? '';
$user = $this->userModel->getByEmail($email);
if ($user && Encryptor::verifyBlowfishHash($password, $user->getAuthPassword())) {
// Login riuscito
Session::setItem('userId', $user->getId());
Session::setItem('sessionToken', Encryptor::getSimpleRandomToken());
return $this->router->redirect('dashboard');
}
$this->vars['error'] = 'Credenziali non valide';
return $this->render->generateView('auth/login', $this->vars);
}
}
class UserProfile
{
public function encryptSensitiveData(string $data): string
{
$iv = Encryptor::createInitializationVector();
$encrypted = Encryptor::encryptString($data, $iv);
// Combina IV e dati cifrati per storage
return base64_encode($iv) . '|' . $encrypted;
}
public function decryptSensitiveData(string $encryptedData): string|false
{
[$ivBase64, $ciphertext] = explode('|', $encryptedData, 2);
$iv = base64_decode($ivBase64);
return Encryptor::decryptString($ciphertext, $iv);
}
}
class PasswordResetController extends BaseController
{
public function requestReset(Request $request): Response
{
$email = $request->input['email'] ?? '';
$user = $this->userModel->getByEmail($email);
if ($user) {
// Genera token sicuro
$token = Encryptor::getSimpleRandomToken();
$user->setPasswordResetToken($token);
$this->dataMapper->save($user);
// Invia email con link di reset
$resetLink = "https://mysite.com/reset-password?token={$token}";
// ... codice invio email
}
return $this->render->generateView('auth/reset-sent', $this->vars);
}
public function resetPassword(Request $request): Response
{
$token = $request->query['token'] ?? '';
$newPassword = $request->input['password'] ?? '';
$user = $this->userModel->getByResetToken($token);
if ($user && $this->isTokenValid($user)) {
// Hash nuova password
$hashedPassword = Encryptor::getBlowfishHash($newPassword);
$user->setPassword($hashedPassword);
$user->setPasswordResetToken(null); // Invalida token
$this->dataMapper->save($user);
return $this->router->redirect('login');
}
$this->vars['error'] = 'Token non valido o scaduto';
return $this->render->generateView('auth/reset-error', $this->vars);
}
}
In Config/config.php per ambiente di produzione:
// Hash e Encryption
const BLOWFISH_HASH_WORKLOAD = 12;
const ENCRYPTION_ALGORITHM = 'AES-256-CBC';
const SIMPLE_HASH_ALGORITHM = 'sha256';
I parametri del cookie di sessione non sono configurabili tramite costanti: Session::start() li imposta direttamente via session_set_cookie_params():
[
"lifetime" => 3600,
"path" => "/",
"domain" => $request->server["HTTP_HOST"],
"secure" => Communication::getCommunicationProtocol($request) === CommunicationProtocol::https,
"httponly" => true,
"samesite" => "Lax",
]
secure viene determinato automaticamente in base al protocollo della richiesta corrente (coerente con HTTPS_IS_FORCED, non un flag separato); httponly è sempre attivo; samesite è sempre Lax (non configurabile, e non Strict). Per personalizzare questi valori è necessario un override della classe Session, non una costante.
Anche qui la chiave di sessione (csrfToken) è fissa e non configurabile, e il framework non applica alcuna scadenza al token. Authentication::checkCsrfToken() (richiamato internamente da checkAuthenticable()) verifica soltanto che $_SESSION['csrfToken'] corrisponda al campo csrfToken inviato dal form — non lo genera. Genera e salva il token nella action che mostra il form, prima di renderizzarlo:
use SismaFramework\Core\HelperClasses\Encryptor;
use SismaFramework\Core\HelperClasses\Session;
if (Session::hasItem('csrfToken') === false) {
Session::setItem('csrfToken', Encryptor::getSimpleRandomToken());
}
$this->vars['csrfToken'] = Session::getItem('csrfToken');
e includilo nel form come campo nascosto:
<input type="hidden" name="csrfToken" value="<?= htmlspecialchars($csrfToken) ?>">
Indice | Precedente: Enumerazioni | Successivo: Barra di Debug
Se hai trovato errori o vuoi suggerire miglioramenti, apri una issue su GitHub.
Report Issue Edit on GitHub