diff --git a/app/Http/Controllers/Admin/SsoSettingController.php b/app/Http/Controllers/Admin/SsoSettingController.php new file mode 100644 index 0000000..43b5ec1 --- /dev/null +++ b/app/Http/Controllers/Admin/SsoSettingController.php @@ -0,0 +1,37 @@ +validate([ + 'base_url' => 'nullable|url|max:255', + 'client_id' => 'nullable|string|max:255', + 'client_secret' => 'nullable|string|max:255', + 'slug' => 'nullable|string|max:255', + ]); + + $sso = SsoSetting::get(); + + $sso->base_url = rtrim($request->base_url ?? '', '/') ?: null; + $sso->client_id = $request->client_id ?: null; + $sso->client_secret = $request->client_secret ?: null; + $sso->slug = $request->slug ?: null; + $sso->enabled = $request->boolean('enabled'); + $sso->save(); + + return redirect()->route('admin.sso.show')->with('success', 'SSO-Einstellungen gespeichert.'); + } +} diff --git a/app/Http/Controllers/Auth/SsoController.php b/app/Http/Controllers/Auth/SsoController.php new file mode 100644 index 0000000..5cffaaf --- /dev/null +++ b/app/Http/Controllers/Auth/SsoController.php @@ -0,0 +1,109 @@ +isConfigured()) { + return redirect()->route('login')->with('error', 'SSO ist nicht konfiguriert.'); + } + + $state = Str::random(40); + session(['sso_state' => $state]); + + $query = http_build_query([ + 'client_id' => $sso->client_id, + 'redirect_uri' => route('sso.callback'), + 'response_type' => 'code', + 'scope' => 'openid email profile', + 'state' => $state, + ]); + + return redirect($sso->authorizeUrl() . '?' . $query); + } + + public function callback(Request $request) + { + $sso = SsoSetting::get(); + + if (!$sso->isConfigured()) { + return redirect()->route('login')->with('error', 'SSO ist nicht konfiguriert.'); + } + + // State prüfen + if ($request->get('state') !== session('sso_state')) { + return redirect()->route('login')->with('error', 'Ungültige SSO-Anfrage (state mismatch).'); + } + session()->forget('sso_state'); + + if ($request->has('error')) { + return redirect()->route('login')->with('error', 'SSO-Anmeldung abgebrochen: ' . $request->get('error_description', $request->get('error'))); + } + + // Code gegen Token tauschen + $tokenResponse = Http::asForm()->post($sso->tokenUrl(), [ + 'grant_type' => 'authorization_code', + 'client_id' => $sso->client_id, + 'client_secret' => $sso->client_secret, + 'redirect_uri' => route('sso.callback'), + 'code' => $request->get('code'), + ]); + + if ($tokenResponse->failed()) { + return redirect()->route('login')->with('error', 'SSO-Token konnte nicht abgerufen werden.'); + } + + $accessToken = $tokenResponse->json('access_token'); + + // Userinfo abrufen + $userInfo = Http::withToken($accessToken)->get($sso->userInfoUrl()); + + if ($userInfo->failed()) { + return redirect()->route('login')->with('error', 'SSO-Benutzerinformationen konnten nicht abgerufen werden.'); + } + + $info = $userInfo->json(); + $providerId = $info['sub'] ?? null; + $email = $info['email'] ?? null; + $name = $info['name'] ?? $info['preferred_username'] ?? $email; + + if (!$providerId || !$email) { + return redirect()->route('login')->with('error', 'Unvollständige Benutzerinformationen vom SSO-Provider.'); + } + + // User suchen oder anlegen + $user = User::where('sso_provider_id', $providerId)->first() + ?? User::where('email', $email)->first(); + + if (!$user) { + $user = User::create([ + 'name' => $name, + 'email' => $email, + 'password' => bcrypt(Str::random(32)), + 'sso_provider_id' => $providerId, + 'is_sso_user' => true, + ]); + } elseif (!$user->sso_provider_id) { + // Bestehenden Account mit SSO verknüpfen + $user->sso_provider_id = $providerId; + $user->is_sso_user = true; + $user->save(); + } + + Auth::login($user, true); + + return redirect()->intended(route('user-nail-polishes.index')); + } +} diff --git a/app/Models/SsoSetting.php b/app/Models/SsoSetting.php new file mode 100644 index 0000000..d80e249 --- /dev/null +++ b/app/Models/SsoSetting.php @@ -0,0 +1,53 @@ + 'boolean', + ]; + } + + public static function get(): self + { + return static::firstOrCreate([]); + } + + public function isConfigured(): bool + { + return $this->enabled + && !empty($this->base_url) + && !empty($this->client_id) + && !empty($this->client_secret); + } + + public function authorizeUrl(): string + { + return rtrim($this->base_url, '/') . '/application/o/authorize/'; + } + + public function tokenUrl(): string + { + return rtrim($this->base_url, '/') . '/application/o/token/'; + } + + public function userInfoUrl(): string + { + return rtrim($this->base_url, '/') . '/application/o/userinfo/'; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 385a0f6..8458ce0 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -22,6 +22,8 @@ class User extends Authenticatable 'email', 'password', 'is_admin', + 'sso_provider_id', + 'is_sso_user', ]; /** @@ -43,7 +45,8 @@ class User extends Authenticatable { return [ 'email_verified_at' => 'datetime', - 'password' => 'hashed', + 'password' => 'hashed', + 'is_sso_user' => 'boolean', ]; } diff --git a/database/migrations/2026_06_13_000001_create_sso_settings_table.php b/database/migrations/2026_06_13_000001_create_sso_settings_table.php new file mode 100644 index 0000000..a26c806 --- /dev/null +++ b/database/migrations/2026_06_13_000001_create_sso_settings_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('base_url')->nullable(); + $table->string('client_id')->nullable(); + $table->string('client_secret')->nullable(); + $table->string('slug')->nullable()->comment('Authentik Application Slug'); + $table->boolean('enabled')->default(false); + $table->timestamps(); + }); + + // Leere Zeile anlegen, damit immer genau eine Konfiguration existiert + DB::table('sso_settings')->insert([ + 'enabled' => false, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + public function down(): void + { + Schema::dropIfExists('sso_settings'); + } +}; diff --git a/database/migrations/2026_06_13_000002_add_sso_fields_to_users_table.php b/database/migrations/2026_06_13_000002_add_sso_fields_to_users_table.php new file mode 100644 index 0000000..c102943 --- /dev/null +++ b/database/migrations/2026_06_13_000002_add_sso_fields_to_users_table.php @@ -0,0 +1,23 @@ +string('sso_provider_id')->nullable()->unique()->after('email'); + $table->boolean('is_sso_user')->default(false)->after('sso_provider_id'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['sso_provider_id', 'is_sso_user']); + }); + } +}; diff --git a/resources/views/admin/sso/show.blade.php b/resources/views/admin/sso/show.blade.php new file mode 100644 index 0000000..bb4c026 --- /dev/null +++ b/resources/views/admin/sso/show.blade.php @@ -0,0 +1,222 @@ +@extends('layouts.app') + +@section('title', 'SSO-Konfiguration – NeoNail DB') + +@section('content') + +
+
+

Single Sign-On

+

Authentik OAuth2/OIDC Konfiguration

+
+ + @if($sso->isConfigured()) + Aktiv + @else + Nicht konfiguriert + @endif + +
+ +{{-- Status-Banner --}} +@if($sso->isConfigured()) +
+ + SSO ist aktiv. Auf der Login-Seite wird der „Mit Authentik anmelden"-Button angezeigt. +
+@elseif($sso->enabled && !$sso->isConfigured()) +
+ + SSO ist aktiviert, aber die Konfiguration ist unvollständig. Bitte alle Felder ausfüllen. +
+@endif + +
+ + {{-- Konfigurationsformular --}} +
+
+
+ Einstellungen +
+
+
+ @csrf + @method('PUT') + + {{-- Aktivieren --}} +
+
+ enabled ? 'checked' : '' }}> + +
+
+ Nur wenn aktiviert und alle Felder ausgefüllt sind, erscheint der SSO-Button auf der Login-Seite. +
+
+ +
+ + {{-- Base URL --}} +
+ + + @error('base_url') +
{{ $message }}
+ @enderror +
Die URL deiner Authentik-Instanz, ohne abschließenden Slash.
+
+ + {{-- Client ID --}} +
+ + + @error('client_id') +
{{ $message }}
+ @enderror +
+ + {{-- Client Secret --}} +
+ +
+ + +
+ @error('client_secret') +
{{ $message }}
+ @enderror +
+ + {{-- Slug --}} +
+ + + @error('slug') +
{{ $message }}
+ @enderror +
Nur zur Dokumentation – wird nicht im OAuth2-Flow verwendet.
+
+ +
+ + @if($sso->isConfigured()) + + SSO testen + + @endif +
+
+
+
+
+ + {{-- Info-Sidebar --}} +
+ + {{-- Callback URL --}} +
+
+ Redirect URI +
+
+

+ Diese URL musst du in Authentik als Redirect URI eintragen: +

+
+ + {{ route('sso.callback') }} + + +
+
+
+ + {{-- Setup-Anleitung --}} +
+
+ Setup in Authentik +
+
+
    +
  1. In Authentik: Applications → Create
  2. +
  3. Provider-Typ: OAuth2/OpenID Provider
  4. +
  5. Authorization flow wählen
  6. +
  7. Redirect URI eintragen (siehe links)
  8. +
  9. Client ID und Client Secret kopieren
  10. +
  11. Hier eintragen und speichern
  12. +
+ +
+ +

+ + Scopes: openid email profile +

+
+
+ +
+
+ +@endsection + +@section('scripts') + +@endsection diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 1610e5a..ab80114 100755 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -74,6 +74,21 @@ + @php $sso = \App\Models\SsoSetting::get(); @endphp + @if($sso->isConfigured()) +
+
+ oder +
+
+ + + + + Mit Authentik anmelden + + @endif + diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 3834fc7..6458acd 100755 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -471,6 +471,9 @@ Nagellacke
  • Statistiken
  • +
  • +
  • + SSO-Konfiguration
  • @endif diff --git a/routes/web.php b/routes/web.php index 5217390..cb6990a 100755 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,8 @@ use App\Http\Controllers\NailPolishController; use App\Http\Controllers\UserNailPolishController; use App\Http\Controllers\AdminController; use App\Http\Controllers\Auth\LoginController; +use App\Http\Controllers\Auth\SsoController; +use App\Http\Controllers\Admin\SsoSettingController; use App\Http\Controllers\ManufacturerController; // Startseite @@ -16,8 +18,12 @@ Route::get('/', function () { Route::middleware('guest')->group(function () { Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login'); Route::post('/login', [LoginController::class, 'login']); + Route::get('/sso/redirect', [SsoController::class, 'redirect'])->name('sso.redirect'); }); +// SSO Callback (kein guest-Middleware, da Authentik zurückleitet) +Route::get('/sso/callback', [SsoController::class, 'callback'])->name('sso.callback'); + // Logout (nur für eingeloggte Benutzer) Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth'); @@ -42,6 +48,8 @@ Route::middleware(['auth'])->group(function () { Route::delete('/users/{user}', [AdminController::class, 'destroyUser'])->name('users.destroy'); Route::get('/statistics', [AdminController::class, 'statistics'])->name('statistics'); Route::get('/users/{user}/collection', [UserNailPolishController::class, 'showUserCollection'])->name('users.collection'); + Route::get('/sso', [SsoSettingController::class, 'show'])->name('sso.show'); + Route::put('/sso', [SsoSettingController::class, 'update'])->name('sso.update'); }); // Nagellack-Verwaltung (nur für Admin)