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>
223 lines
9.7 KiB
PHP
223 lines
9.7 KiB
PHP
@extends('layouts.app')
|
||
|
||
@section('title', 'SSO-Konfiguration – NeoNail DB')
|
||
|
||
@section('content')
|
||
|
||
<div class="page-hero d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4">
|
||
<div>
|
||
<h1><i class="fas fa-shield-alt me-2"></i>Single Sign-On</h1>
|
||
<p>Authentik OAuth2/OIDC Konfiguration</p>
|
||
</div>
|
||
<span class="count-chip">
|
||
@if($sso->isConfigured())
|
||
<i class="fas fa-circle text-success" style="font-size:.6rem"></i> Aktiv
|
||
@else
|
||
<i class="fas fa-circle text-danger" style="font-size:.6rem"></i> Nicht konfiguriert
|
||
@endif
|
||
</span>
|
||
</div>
|
||
|
||
{{-- Status-Banner --}}
|
||
@if($sso->isConfigured())
|
||
<div class="alert alert-success mb-4">
|
||
<i class="fas fa-check-circle me-2"></i>
|
||
SSO ist aktiv. Auf der Login-Seite wird der <strong>„Mit Authentik anmelden"</strong>-Button angezeigt.
|
||
</div>
|
||
@elseif($sso->enabled && !$sso->isConfigured())
|
||
<div class="alert alert-warning mb-4">
|
||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||
SSO ist aktiviert, aber die Konfiguration ist unvollständig. Bitte alle Felder ausfüllen.
|
||
</div>
|
||
@endif
|
||
|
||
<div class="row g-4">
|
||
|
||
{{-- Konfigurationsformular --}}
|
||
<div class="col-lg-8">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="fas fa-cog me-2"></i>Einstellungen
|
||
</div>
|
||
<div class="card-body p-4">
|
||
<form method="POST" action="{{ route('admin.sso.update') }}">
|
||
@csrf
|
||
@method('PUT')
|
||
|
||
{{-- Aktivieren --}}
|
||
<div class="mb-4">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" role="switch"
|
||
name="enabled" id="enabled" value="1"
|
||
{{ $sso->enabled ? 'checked' : '' }}>
|
||
<label class="form-check-label fw-semibold" for="enabled">
|
||
SSO aktivieren
|
||
</label>
|
||
</div>
|
||
<div class="form-text mt-1">
|
||
Nur wenn aktiviert <em>und</em> alle Felder ausgefüllt sind, erscheint der SSO-Button auf der Login-Seite.
|
||
</div>
|
||
</div>
|
||
|
||
<hr class="my-4">
|
||
|
||
{{-- Base URL --}}
|
||
<div class="mb-3">
|
||
<label class="form-label" for="base_url">
|
||
Authentik Base URL <span class="text-danger">*</span>
|
||
</label>
|
||
<input type="url" class="form-control @error('base_url') is-invalid @enderror"
|
||
id="base_url" name="base_url"
|
||
value="{{ old('base_url', $sso->base_url) }}"
|
||
placeholder="https://authentik.example.com">
|
||
@error('base_url')
|
||
<div class="invalid-feedback">{{ $message }}</div>
|
||
@enderror
|
||
<div class="form-text">Die URL deiner Authentik-Instanz, ohne abschließenden Slash.</div>
|
||
</div>
|
||
|
||
{{-- Client ID --}}
|
||
<div class="mb-3">
|
||
<label class="form-label" for="client_id">
|
||
Client ID <span class="text-danger">*</span>
|
||
</label>
|
||
<input type="text" class="form-control @error('client_id') is-invalid @enderror"
|
||
id="client_id" name="client_id"
|
||
value="{{ old('client_id', $sso->client_id) }}"
|
||
placeholder="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||
autocomplete="off">
|
||
@error('client_id')
|
||
<div class="invalid-feedback">{{ $message }}</div>
|
||
@enderror
|
||
</div>
|
||
|
||
{{-- Client Secret --}}
|
||
<div class="mb-3">
|
||
<label class="form-label" for="client_secret">
|
||
Client Secret <span class="text-danger">*</span>
|
||
</label>
|
||
<div class="input-group">
|
||
<input type="password" class="form-control @error('client_secret') is-invalid @enderror"
|
||
id="client_secret" name="client_secret"
|
||
value="{{ old('client_secret', $sso->client_secret) }}"
|
||
placeholder="{{ $sso->client_secret ? '••••••••••••••••' : 'Client Secret eingeben' }}"
|
||
autocomplete="off">
|
||
<button class="btn btn-outline-secondary" type="button"
|
||
onclick="toggleSecret()" id="toggleSecretBtn">
|
||
<i class="fas fa-eye" id="toggleSecretIcon"></i>
|
||
</button>
|
||
</div>
|
||
@error('client_secret')
|
||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||
@enderror
|
||
</div>
|
||
|
||
{{-- Slug --}}
|
||
<div class="mb-4">
|
||
<label class="form-label" for="slug">
|
||
Application Slug
|
||
<span class="badge bg-secondary ms-1" style="font-size:.65rem">optional</span>
|
||
</label>
|
||
<input type="text" class="form-control @error('slug') is-invalid @enderror"
|
||
id="slug" name="slug"
|
||
value="{{ old('slug', $sso->slug) }}"
|
||
placeholder="neonail-db">
|
||
@error('slug')
|
||
<div class="invalid-feedback">{{ $message }}</div>
|
||
@enderror
|
||
<div class="form-text">Nur zur Dokumentation – wird nicht im OAuth2-Flow verwendet.</div>
|
||
</div>
|
||
|
||
<div class="d-flex gap-2">
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-save me-2"></i>Speichern
|
||
</button>
|
||
@if($sso->isConfigured())
|
||
<a href="{{ route('sso.redirect') }}" target="_blank" class="btn btn-outline-secondary">
|
||
<i class="fas fa-external-link-alt me-2"></i>SSO testen
|
||
</a>
|
||
@endif
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Info-Sidebar --}}
|
||
<div class="col-lg-4">
|
||
|
||
{{-- Callback URL --}}
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
<i class="fas fa-link me-2"></i>Redirect URI
|
||
</div>
|
||
<div class="card-body p-3">
|
||
<p style="font-size:.82rem; color:#6b7280; margin-bottom:8px;">
|
||
Diese URL musst du in Authentik als <strong>Redirect URI</strong> eintragen:
|
||
</p>
|
||
<div class="d-flex align-items-center gap-2">
|
||
<code style="font-size:.78rem; background:#f3f4f6; padding:8px 10px; border-radius:8px; flex:1; word-break:break-all;">
|
||
{{ route('sso.callback') }}
|
||
</code>
|
||
<button class="btn btn-sm btn-outline-secondary flex-shrink-0"
|
||
onclick="copyRedirectUri()" title="Kopieren">
|
||
<i class="fas fa-copy" id="copyIcon"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Setup-Anleitung --}}
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="fas fa-book me-2"></i>Setup in Authentik
|
||
</div>
|
||
<div class="card-body p-3">
|
||
<ol style="font-size:.83rem; color:#374151; padding-left:1.2rem; margin:0; line-height:1.8;">
|
||
<li>In Authentik: <strong>Applications → Create</strong></li>
|
||
<li>Provider-Typ: <strong>OAuth2/OpenID Provider</strong></li>
|
||
<li>Authorization flow wählen</li>
|
||
<li>Redirect URI eintragen (siehe links)</li>
|
||
<li><strong>Client ID</strong> und <strong>Client Secret</strong> kopieren</li>
|
||
<li>Hier eintragen und speichern</li>
|
||
</ol>
|
||
|
||
<hr style="margin: 12px 0;">
|
||
|
||
<p style="font-size:.78rem; color:#9ca3af; margin:0;">
|
||
<i class="fas fa-info-circle me-1"></i>
|
||
Scopes: <code>openid email profile</code>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
@endsection
|
||
|
||
@section('scripts')
|
||
<script>
|
||
function toggleSecret() {
|
||
const input = document.getElementById('client_secret');
|
||
const icon = document.getElementById('toggleSecretIcon');
|
||
if (input.type === 'password') {
|
||
input.type = 'text';
|
||
icon.classList.replace('fa-eye', 'fa-eye-slash');
|
||
} else {
|
||
input.type = 'password';
|
||
icon.classList.replace('fa-eye-slash', 'fa-eye');
|
||
}
|
||
}
|
||
|
||
function copyRedirectUri() {
|
||
const uri = @json(route('sso.callback'));
|
||
const icon = document.getElementById('copyIcon');
|
||
navigator.clipboard.writeText(uri).then(() => {
|
||
icon.classList.replace('fa-copy', 'fa-check');
|
||
setTimeout(() => icon.classList.replace('fa-check', 'fa-copy'), 2000);
|
||
});
|
||
}
|
||
</script>
|
||
@endsection
|