Files
neonail-database/resources/views/admin/sso/show.blade.php
T
HousemannandClaude Sonnet 4.6 60259a3655 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>
2026-06-13 15:13:30 +02:00

223 lines
9.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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