Files
Virtnet.bond/extensions/Others/Authentik/Http/Controllers/AuthentikController.php
2025-11-30 11:57:22 +01:00

283 lines
10 KiB
PHP

<?php
namespace Paymenter\Extensions\Others\Authentik\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Extension;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Laravel\Socialite\Facades\Socialite;
class AuthentikController extends Controller
{
protected $authentik_driver;
public function __construct()
{
// Initialiser le driver Authentik avec la configuration de l'extension
$this->initializeAuthentikDriver();
}
/**
* Initialiser le driver Authentik avec la configuration de l'extension
*/
private function initializeAuthentikDriver()
{
$extension = Extension::where('extension', 'Authentik')->first();
if (!$extension || !$extension->settings) {
return;
}
$settings = $extension->settings->pluck('value', 'key')->toArray();
if (empty($settings['enabled']) || empty($settings['client_id']) || empty($settings['client_secret']) || empty($settings['base_url'])) {
return;
}
$this->authentik_driver = Socialite::buildProvider(\Paymenter\Extensions\Others\Authentik\Providers\AuthentikProvider::class, [
'client_id' => $settings['client_id'],
'client_secret' => $settings['client_secret'],
'redirect' => '/oauth/authentik/callback',
'base_url' => $settings['base_url'],
'authorize_url' => $settings['base_url'] . '/application/o/authorize/',
'token_url' => $settings['base_url'] . '/application/o/token/',
'userinfo_url' => $settings['base_url'] . '/application/o/userinfo/',
]);
}
/**
* Rediriger vers Authentik pour l'authentification
*/
public function redirect()
{
if (!$this->authentik_driver) {
return redirect()->route('login')->with('error', 'Authentik n\'est pas configuré correctement.');
}
return $this->authentik_driver
->scopes(['openid', 'email', 'profile'])
->with(['prompt' => 'consent'])
->redirect();
}
/**
* Gérer le callback d'authentification d'Authentik
*/
public function handle(Request $request)
{
if (!$this->authentik_driver) {
return redirect()->route('login')->with('error', 'Authentik n\'est pas configuré correctement.');
}
if ($request->input('error')) {
return redirect()->route('login')->with('error', 'Erreur d\'authentification: ' . $request->input('error_description'));
}
try {
// Obtenir le token d'accès
$token = $this->authentik_driver->getAccessTokenResponse($request->get('code'));
// Obtenir les données utilisateur
try {
$oauth_user_data = $this->authentik_driver->getUserByToken($token['access_token']);
} catch (\Exception $e) {
Log::error('Erreur lors de la récupération des données utilisateur Authentik: ' . $e->getMessage());
// Fallback: décoder le token ID
$id_token = $token['id_token'];
$oauth_user_data = $this->decodeIdToken($id_token);
// Essayer d'obtenir l'email depuis l'API Authentik
try {
$user_email = $this->getUserEmailFromAuthentik($token['access_token'], $oauth_user_data['sub']);
if ($user_email) {
$oauth_user_data['email'] = $user_email;
}
} catch (\Exception $emailError) {
Log::error('Erreur lors de la récupération de l\'email Authentik: ' . $emailError->getMessage());
}
}
$oauth_user = $this->authentik_driver->mapUserToObject($oauth_user_data);
Log::info('Utilisateur Authentik connecté', [
'email' => $oauth_user->email,
'name' => $oauth_user->name,
'id' => $oauth_user->id,
'raw_data' => $oauth_user_data
]);
// Vérifier si l'utilisateur existe
$user = null;
if (!empty($oauth_user->email)) {
$user = User::where('email', $oauth_user->email)->first();
}
// Récupérer les paramètres de l'extension
$extension = Extension::where('extension', 'Authentik')->first();
$settings = $extension ? $extension->settings->pluck('value', 'key')->toArray() : [];
if (!$user) {
// Vérifier si la création automatique d'utilisateurs est activée
if (empty($settings['auto_create_users'])) {
Log::warning('Utilisateur Authentik non trouvé et création automatique désactivée', [
'searched_email' => $oauth_user->email,
'searched_authentik_id' => $oauth_user->id ?? null,
'user_data' => $oauth_user_data
]);
return redirect()->route('login')->with('error', 'Votre compte n\'est pas enregistré. Veuillez contacter un administrateur.');
}
// Créer un nouvel utilisateur
$user = $this->createUserFromAuthentikData($oauth_user, $oauth_user_data);
} else {
// Vérifier si l'utilisateur existant est requis
if (!empty($settings['require_existing_user']) && !$user) {
return redirect()->route('login')->with('error', 'Votre compte n\'est pas enregistré. Veuillez contacter un administrateur.');
}
}
// Connecter l'utilisateur
Auth::login($user, true);
return redirect()->route('home');
} catch (\Exception $e) {
Log::error('Erreur lors du callback Authentik: ' . $e->getMessage());
return redirect()->route('login')->with('error', 'Échec de l\'authentification: ' . $e->getMessage());
}
}
/**
* Créer un utilisateur à partir des données Authentik
*/
private function createUserFromAuthentikData($oauth_user, $oauth_user_data)
{
// Dériver nom et prénom
$rawGivenName = $oauth_user_data['given_name'] ?? ($oauth_user_data['name'] ?? null);
$rawFamilyName = $oauth_user_data['family_name'] ?? null;
$fullName = $oauth_user_data['name'] ?? ($oauth_user->name ?? '');
$firstName = '';
$lastName = '';
if (!empty($rawGivenName) && !empty($rawFamilyName)) {
$firstName = $rawGivenName;
$lastName = $rawFamilyName;
} elseif (!empty($fullName)) {
$parts = preg_split('/\s+/', trim($fullName));
$firstName = $parts[0] ?? '';
$lastName = trim(implode(' ', array_slice($parts, 1)));
}
$email = $oauth_user->email ?? ($oauth_user_data['email'] ?? null);
if (empty($email)) {
throw new \Exception('Authentik n\'a pas fourni d\'email pour l\'utilisateur.');
}
return User::create([
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email,
'password' => Str::random(40),
'email_verified_at' => Carbon::now(),
'provider' => 'authentik',
'provider_id' => $oauth_user->id,
]);
}
/**
* Décoder le token ID JWT d'Authentik
*/
private function decodeIdToken($idToken)
{
// Les tokens JWT ont 3 parties séparées par des points
$parts = explode('.', $idToken);
if (count($parts) !== 3) {
throw new \Exception('Format de token ID invalide');
}
// Décoder le payload (deuxième partie)
$payload = $parts[1];
// Ajouter le padding si nécessaire
$payload .= str_repeat('=', (4 - strlen($payload) % 4) % 4);
// Décoder base64
$decoded = base64_decode($payload);
if ($decoded === false) {
throw new \Exception('Échec du décodage du payload du token ID');
}
$userData = json_decode($decoded, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Échec de l\'analyse du payload du token ID: ' . json_last_error_msg());
}
return $userData;
}
/**
* Essayer d'obtenir l'email de l'utilisateur depuis l'API Authentik
*/
private function getUserEmailFromAuthentik($accessToken, $userId)
{
try {
$extension = Extension::where('extension', 'Authentik')->first();
$baseUrl = $extension ? $extension->settings->where('key', 'base_url')->first()->value : null;
if (!$baseUrl) {
return null;
}
// Endpoint 1: API des utilisateurs Authentik
$response = $this->getHttpClient()->get(
$baseUrl . '/api/v3/core/users/' . $userId . '/',
['headers' => ['Authorization' => 'Bearer ' . $accessToken]]
);
$userData = json_decode($response->getBody(), true);
if (isset($userData['email']) && !empty($userData['email'])) {
return $userData['email'];
}
// Endpoint 2: Informations de l'utilisateur actuel
$response = $this->getHttpClient()->get(
$baseUrl . '/api/v3/core/users/me/',
['headers' => ['Authorization' => 'Bearer ' . $accessToken]]
);
$userData = json_decode($response->getBody(), true);
if (isset($userData['email']) && !empty($userData['email'])) {
return $userData['email'];
}
} catch (\Exception $e) {
Log::error('Erreur API Authentik: ' . $e->getMessage());
}
return null;
}
/**
* Obtenir le client HTTP pour faire des appels aux APIs externes
*/
private function getHttpClient()
{
return new \GuzzleHttp\Client([
'timeout' => 10,
'verify' => false, // Seulement pour le développement, en production devrait être true
]);
}
}