Add Authentik SSO via OAuth2/OIDC
Implements Single Sign-On with Authentik alongside the existing password login. Admins configure Client ID, Client Secret and Base URL directly in the app; the SSO button on the login page only appears when the configuration is complete and enabled. - SsoSetting model + migration (one-row config table) - sso_provider_id / is_sso_user fields on users (migration) - SsoController: redirect to Authentik + callback (token exchange, userinfo fetch, auto-create unknown users) - Admin\SsoSettingController + admin/sso/show view with setup guide and one-click Redirect URI copy - Admin dropdown: SSO-Konfiguration entry - Login page: Authentik button rendered conditionally No additional Composer packages required. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c5fe3ad808
commit
60259a3655
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SsoSetting;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SsoSettingController extends Controller
|
||||
{
|
||||
public function show()
|
||||
{
|
||||
$sso = SsoSetting::get();
|
||||
return view('admin.sso.show', compact('sso'));
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$request->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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SsoSetting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SsoController extends Controller
|
||||
{
|
||||
public function redirect(Request $request)
|
||||
{
|
||||
$sso = SsoSetting::get();
|
||||
|
||||
if (!$sso->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'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SsoSetting extends Model
|
||||
{
|
||||
protected $table = 'sso_settings';
|
||||
|
||||
protected $fillable = [
|
||||
'base_url',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'slug',
|
||||
'enabled',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => '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/';
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user