Files
desk-moloni/modules/desk_moloni/libraries/TokenManager.php
Emanuel Almeida c19f6fd9ee fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module
- Normalize hooks to after_client_* and instantiate PerfexHooks safely
- Fix OAuthController view path and API client class name
- Add missing admin views for webhook config/logs; adjust view loading
- Harden client portal routes and admin routes mapping
- Make Dashboard/Logs/Queue tolerant to optional model methods
- Align log details query with existing schema; avoid broken joins

This makes the module operational in Perfex (admin + client), reduces 404s,
and avoids fatal errors due to inconsistent tables/methods.
2025-09-11 17:38:45 +01:00

392 lines
11 KiB
PHP

<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Token Manager Library
*
* Handles secure token storage and management with AES-256 encryption
*
* @package DeskMoloni
* @author Descomplicar®
* @copyright 2025 Descomplicar
* @version 3.0.0
*/
class TokenManager
{
private $CI;
// Encryption configuration
private $cipher = 'AES-256-CBC';
private $key_size = 32; // 256 bits
private $iv_size = 16; // 128 bits
// Token storage keys
private $access_token_key = 'desk_moloni_access_token_encrypted';
private $refresh_token_key = 'desk_moloni_refresh_token_encrypted';
private $token_expires_key = 'desk_moloni_token_expires';
private $token_scope_key = 'desk_moloni_token_scope';
private $encryption_key_option = 'desk_moloni_encryption_key';
public function __construct()
{
$this->CI = &get_instance();
// Ensure encryption key exists
$this->ensure_encryption_key();
}
/**
* Save OAuth tokens securely
*
* @param array $token_data Token response from OAuth
* @return bool Success status
*/
public function save_tokens($token_data)
{
try {
// Validate required fields
if (!isset($token_data['access_token'])) {
throw new Exception('Access token is required');
}
// Calculate expiration time with 60-second buffer
$expires_in = isset($token_data['expires_in']) ? (int)$token_data['expires_in'] : 3600;
$expires_at = time() + $expires_in - 60;
// Encrypt and save access token
$encrypted_access = $this->encrypt($token_data['access_token']);
update_option($this->access_token_key, $encrypted_access);
// Encrypt and save refresh token if provided
if (isset($token_data['refresh_token'])) {
$encrypted_refresh = $this->encrypt($token_data['refresh_token']);
update_option($this->refresh_token_key, $encrypted_refresh);
}
// Save expiration and scope
update_option($this->token_expires_key, $expires_at);
update_option($this->token_scope_key, $token_data['scope'] ?? '');
// Log successful token save
log_activity('Desk-Moloni: OAuth tokens saved securely');
return true;
} catch (Exception $e) {
log_activity('Desk-Moloni: Token save failed - ' . $e->getMessage());
return false;
}
}
/**
* Get decrypted access token
*
* @return string|null Access token or null if not available
*/
public function get_access_token()
{
try {
$encrypted_token = get_option($this->access_token_key);
if (empty($encrypted_token)) {
return null;
}
return $this->decrypt($encrypted_token);
} catch (Exception $e) {
log_activity('Desk-Moloni: Access token decryption failed - ' . $e->getMessage());
return null;
}
}
/**
* Get decrypted refresh token
*
* @return string|null Refresh token or null if not available
*/
public function get_refresh_token()
{
try {
$encrypted_token = get_option($this->refresh_token_key);
if (empty($encrypted_token)) {
return null;
}
return $this->decrypt($encrypted_token);
} catch (Exception $e) {
log_activity('Desk-Moloni: Refresh token decryption failed - ' . $e->getMessage());
return null;
}
}
/**
* Check if tokens are valid and not expired
*
* @return bool Token validity status
*/
public function are_tokens_valid()
{
// Check if access token exists
if (empty($this->get_access_token())) {
return false;
}
// Check expiration
$expires_at = get_option($this->token_expires_key);
if ($expires_at && time() >= $expires_at) {
return false;
}
return true;
}
/**
* Check if tokens are close to expiring (within 5 minutes)
*
* @return bool True if tokens expire soon
*/
public function tokens_expire_soon()
{
$expires_at = get_option($this->token_expires_key);
if (!$expires_at) {
return false;
}
return (time() + 300) >= $expires_at; // 5 minutes
}
/**
* Get token expiration timestamp
*
* @return int|null Expiration timestamp or null
*/
public function get_token_expiration()
{
return get_option($this->token_expires_key) ?: null;
}
/**
* Get token scope
*
* @return string Token scope
*/
public function get_token_scope()
{
return get_option($this->token_scope_key) ?: '';
}
/**
* Clear all stored tokens
*
* @return bool Success status
*/
public function clear_tokens()
{
try {
update_option($this->access_token_key, '');
update_option($this->refresh_token_key, '');
update_option($this->token_expires_key, '');
update_option($this->token_scope_key, '');
log_activity('Desk-Moloni: OAuth tokens cleared');
return true;
} catch (Exception $e) {
log_activity('Desk-Moloni: Token clear failed - ' . $e->getMessage());
return false;
}
}
/**
* Get comprehensive token status
*
* @return array Token status information
*/
public function get_token_status()
{
$expires_at = $this->get_token_expiration();
return [
'has_access_token' => !empty($this->get_access_token()),
'has_refresh_token' => !empty($this->get_refresh_token()),
'is_valid' => $this->are_tokens_valid(),
'expires_soon' => $this->tokens_expire_soon(),
'expires_at' => $expires_at,
'expires_in' => $expires_at ? max(0, $expires_at - time()) : 0,
'scope' => $this->get_token_scope(),
'formatted_expiry' => $expires_at ? date('Y-m-d H:i:s', $expires_at) : null
];
}
/**
* Encrypt data using AES-256-CBC
*
* @param string $data Data to encrypt
* @return string Base64 encoded encrypted data with IV
*/
private function encrypt($data)
{
if (empty($data)) {
return '';
}
$key = $this->get_encryption_key();
$iv = random_bytes($this->iv_size);
$encrypted = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
if ($encrypted === false) {
throw new Exception('Encryption failed');
}
// Prepend IV to encrypted data and encode
return base64_encode($iv . $encrypted);
}
/**
* Decrypt data using AES-256-CBC
*
* @param string $encrypted_data Base64 encoded encrypted data with IV
* @return string Decrypted data
*/
private function decrypt($encrypted_data)
{
if (empty($encrypted_data)) {
return '';
}
$data = base64_decode($encrypted_data);
if ($data === false || strlen($data) < $this->iv_size) {
throw new Exception('Invalid encrypted data');
}
$key = $this->get_encryption_key();
$iv = substr($data, 0, $this->iv_size);
$encrypted = substr($data, $this->iv_size);
$decrypted = openssl_decrypt($encrypted, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
if ($decrypted === false) {
throw new Exception('Decryption failed');
}
return $decrypted;
}
/**
* Get or generate encryption key
*
* @return string Encryption key
*/
private function get_encryption_key()
{
$key = get_option($this->encryption_key_option);
if (empty($key)) {
throw new Exception('Encryption key not found');
}
return base64_decode($key);
}
/**
* Ensure encryption key exists
*/
private function ensure_encryption_key()
{
$existing_key = get_option($this->encryption_key_option);
if (empty($existing_key)) {
// Generate new random key
$key = random_bytes($this->key_size);
$encoded_key = base64_encode($key);
update_option($this->encryption_key_option, $encoded_key);
log_activity('Desk-Moloni: New encryption key generated');
}
}
/**
* Rotate encryption key (for security maintenance)
* WARNING: This will invalidate all existing tokens
*
* @return bool Success status
*/
public function rotate_encryption_key()
{
try {
// Clear existing tokens first
$this->clear_tokens();
// Generate new key
$new_key = random_bytes($this->key_size);
$encoded_key = base64_encode($new_key);
update_option($this->encryption_key_option, $encoded_key);
log_activity('Desk-Moloni: Encryption key rotated - all tokens cleared');
return true;
} catch (Exception $e) {
log_activity('Desk-Moloni: Key rotation failed - ' . $e->getMessage());
return false;
}
}
/**
* Validate encryption setup
*
* @return array Validation results
*/
public function validate_encryption()
{
$issues = [];
// Check if OpenSSL is available
if (!extension_loaded('openssl')) {
$issues[] = 'OpenSSL extension not loaded';
}
// Check if cipher is supported
if (!in_array($this->cipher, openssl_get_cipher_methods())) {
$issues[] = 'AES-256-CBC cipher not supported';
}
// Check if encryption key exists
try {
$this->get_encryption_key();
} catch (Exception $e) {
$issues[] = 'Encryption key not available: ' . $e->getMessage();
}
// Test encryption/decryption
try {
$test_data = 'test_token_' . time();
$encrypted = $this->encrypt($test_data);
$decrypted = $this->decrypt($encrypted);
if ($decrypted !== $test_data) {
$issues[] = 'Encryption/decryption test failed';
}
} catch (Exception $e) {
$issues[] = 'Encryption test failed: ' . $e->getMessage();
}
return [
'is_valid' => empty($issues),
'issues' => $issues,
'cipher' => $this->cipher,
'openssl_loaded' => extension_loaded('openssl'),
'supported_ciphers' => openssl_get_cipher_methods()
];
}
}