Search by

ithilbert / userauth

ithilbert

User Auth system with Roles, Permissions, 2FA, Audit-Log, Teams.

Package info

github.com/ITHilbert/userauth

pkg:composer/ithilbert/userauth

Statistics

Installs: 106

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

3.2.1 2026-11-05 20:22 UTC

README

Beschreibung: User Auth System mit integrierter Rollenverwaltung, Impersonate-Funktion, erweiterten Passwort-Regeln, Passwort-Reset, Zwei-Faktor-Anmeldung, Social Login (Google) und Mandanten- / Teams-Fähigkeit.

Dokumentation

Die ausführliche Dokumentation befindet sich im Ordner .project/doku/:

Installation

composer require ithilbert/userauth

Voraussetzungen und Abhängigkeiten

Abhängigkeit Anforderung Wofür
PHP 8.5 Paket-Code
Laravel (illuminate/*) 12 oder 13 Framework
ithilbert/laravel-kit ^1.2, für die Verwaltungsseiten ein Stand mit Kit-Components (siehe Upgrade-Hinweise, Punkt 8) Trait VueComboBox, das UserAuth verwendet; Kit-Components der Verwaltungsseiten
laravel/socialite ^5.0 (automatisch installiert) Social Login (Google)
yajra/laravel-datatables-oracle ^12.0 oder ^13.0 (automatisch installiert) Nutzerliste (AJAX)
nesbot/carbon ^2.0 oder ^3.0 (automatisch installiert) Datumsberechnungen
Mailserver in config/mail.php konfiguriert und erreichbar Versand der 2FA-Codes (synchron) und der Reset-Links
Cache-Store nicht array (z. B. file, database, redis) Zähler der 2FA-Fehlversuche, Sperre „Code erneut senden“, Drosselungen
Session-Treiber speichert zwischen Requests (nicht array) Offene Zwei-Faktor-Anmeldung liegt in der Session

Ein dev-main des Pakets ist nicht mehr nötig: ithilbert/laravel-kit ist ab Version 1.2 als Release verfügbar.

Eigene Testumgebung (Paket-Entwicklung)

Das Paket hat eine eigene Testbench-App (tests/TestCase.php) und ist unabhängig vom Host testbar:

cd packages/userauth
composer install
vendor/bin/phpunit
  • phpunit.xml.dist: SQLite :memory:, Cache und Session array, Mail array, Queue sync.
  • Fixture-User mit Trait UserAuth: tests/Fixtures/User.php; Paket-Migrationen laufen automatisch.
  • Die Laravel-Version kommt aus dem vendor/ des Pakets. Ein Lauf gegen Laravel 12 und einer gegen Laravel 13 erfolgen je mit eigener vendor/.
  • vendor/ und composer.lock sind in .gitignore des Pakets eingetragen.
  • Der Host führt die Paket-Tests über phpunit.xml mit aus (Suite packages/userauth/tests/Feature).

Konfiguration

.env Optionen prüfen

Um bestimmte Features global in einem Projekt zu aktivieren, füge folgende Zeilen zu deiner .env Datei hinzu und passe sie nach deinen Bedürfnissen an:

# Logging
USERAUTH_AUDIT_LOG_ENABLED=false

# Sicherheits-Features
USERAUTH_2FA_ENABLED=false
USERAUTH_PASSWORD_POLICY_ENABLED=true

# Mandantenfähigkeit (Teams)
USERAUTH_TEAMS_ENABLED=true

# E-Mail eines gelöschten Benutzers bei einer Neuanlage: sperren (Default) | reaktivieren | neu
USERAUTH_GELOESCHTE_EMAIL=sperren
# Selbstregistrierung (siehe Abschnitt „Registrierung“)
USERAUTH_REGISTRATION=false
USERAUTH_REGISTRATION_ROLE=

# Theme der Auth-Views (Login, Passwort-Reset): 'tailwind' (Default) oder 'bootstrap'
# Nur in ÄLTEREN Bootstrap-/Phoenix-Projekten setzen:
USERAUTH_THEME=bootstrap

Admin-Konto per Seeder

Der UsersTableSeeder löscht keine Benutzer. Er legt admin@admin.com (Rolle mit dem Schlüssel admin, egal mit welcher ID) nur an, wenn die Tabelle users komplett leer ist (auch ohne soft-gelöschte Zeilen) und der Host von APP_URL auf .lan endet oder localhost, 127.0.0.1 oder ::1 ist (lokale Hosts zählen wie eine .lan-Domain). Sonst gibt er Kein Admin-Konto angelegt mit Grund aus. Das Passwort kommt aus USERAUTH_ADMIN_PASSWORT; ist es leer oder nicht gesetzt, erzeugt der Seeder ein Zufallspasswort und gibt es einmal auf der Konsole aus.

Pflichten des Hosts

Diese Punkte muss der Host erfüllen, sonst schlagen Login, Passwort-Reset oder Rechteprüfung fehl:

  1. User-Model: verwendet den Trait ITHilbert\UserAuth\Traits\UserAuth und implementiert Illuminate\Contracts\Auth\CanResetPassword (z. B. durch Erweiterung von Illuminate\Foundation\Auth\User). Ohne CanResetPassword wirft der Passwort-Reset UserModelMisconfiguredException.
  2. Model-Zuordnung: auth.providers.users.model zeigt auf das User-Model. Fehlt der Eintrag, wirft das Paket UserModelMisconfiguredException; es fällt nicht auf App\Models\User zurück.
  3. Standard-Guard: auth.defaults.guard ist ein Session-Guard (Treiber session). Sonst wirft das Paket AuthGuardMisconfiguredException.
  4. Cache-Store: CACHE_STORE ist nicht array. Mit array gelten die Zähler (2FA-Fehlversuche, 60-Sekunden-Sperre, Reset-Drosselung) nur für einen Request.
  5. Absender: mail.from ist gesetzt. Reset-Link und 2FA-Code gehen an diesen Absender.
  6. Eigene Routen auf den Passwort-Reset: Die Route password.sendtocken trägt die Middleware InvokeDeferredCallbacks selbst. Eigene Routen, die den Reset-UseCase aufrufen, brauchen sie ebenfalls, sonst wird die Reset-Mail nie verschickt.
  7. Rechte vergeben: user_read für die Nutzerliste, user_create/user_edit/role_create/role_edit nur an Vertrauenspersonen (siehe Rollen und Rechte).
  8. APP_KEY: Der Schlüssel für die 2FA-Codes ist app.key. Ein Wechsel des APP_KEY macht alle offenen Zwei-Faktor-Anmeldungen ungültig.
  9. Veröffentlichte Views und Sprachdateien: Hosts mit veröffentlichten Dateien übernehmen die Änderungen (siehe Upgrade-Hinweise).
  10. Migrationen: php artisan migrate ausführen.
  11. Layout und Assets der Verwaltungsseiten: siehe Verwaltungsseiten.

Verwaltungsseiten (Benutzer, Rollen, Rechte, Passwort, Benutzerbild)

Die Verwaltungsseiten erweitern userauth::layouts.afterLogin. Dieses Layout erweitert layouts.app des Hosts und reicht die Skripte der Seiten weiter (siehe unten). Der Host muss layouts.app bereitstellen und diese Sections ausgeben:

Section Ausgabe im Host-Layout Genutzt von
title im <title> alle Verwaltungsseiten
content Seiteninhalt alle Verwaltungsseiten
css im <head> aktuell von keiner aktiven Verwaltungsseite
js nach den Skripten für jQuery und DataTables, am Ende von <body> Listen (Benutzer, Rollen, Rechte), Rollen- und Rechteformulare
Stack scripts am Ende von <body> (@stack('scripts')) dieselben Skripte wie js

content_header wird von keiner aktiven View genutzt (nur auskommentiert) und ist nicht nötig.

Skripte: Der Host gibt @stack('scripts') oder @yield('js') aus, beides ist möglich: Das Paket füllt Stack scripts und Section js mit denselben Skripten und lässt bei einem Layout mit beiden jedes Skript nur einmal stehen. Ein Nachstarter im Host-Layout (Skript, das die Skripte der Views nachträglich ausführt) ist nicht mehr nötig. Die Inline-Initialisierungen laufen erst bei DOMContentLoaded, role.js und permission.js mit defer; ein Host, der jQuery und DataTables als Modul-Bundle (Vite) lädt, braucht nichts weiter. Gibt das Layout weder scripts noch js aus, laufen keine Skripte.

Weitere Anforderungen an den Host:

  • jQuery und DataTables müssen vor dem Ausführen der Skripte geladen sein (klassisch vor js/scripts, als Modul-Bundle spätestens vor DOMContentLoaded). Die Listen rufen $('.data-table').DataTable(…) auf. Fehlen beide, bleiben die Listen leer.
  • Sprachdatei der Listen: Die Listen laden vendor/laravelkit/DataTable_DE.json. Die Datei kommt aus ithilbert/laravel-kit.
  • Kit-Components und Skripte: Die Views bestehen aus Blade-Components von laravel-kit (<x-kit::…>), das Theme bestimmt laravelkit.theme. Der Host bindet initTabs(), initLoeschen() und mountInseln() mit der Insel dialog-delete ein (siehe Upgrade-Hinweise, Punkt 9). Ein Whole-DOM-Mount ist für diese Seiten nicht nötig.
  • Lösch-Buttons: HButton::delete aus laravel-kit gibt data-kit-delete="<id>" für initLoeschen() aus und ruft weiter OpenDeleteForm(id) per onclick auf. Hosts, die OpenDeleteForm global definieren (im Host packages_dev in resources/js/forms.js), öffnen den Dialog weiter auf diesem Weg.
  • Assets: php artisan vendor:publish --provider="ITHilbert\UserAuth\UserAuthServiceProvider" kopiert Resources/assets nach public/vendor/userauth, u. a. js/role.js, js/permission.js und img/default-user.jpg. Der Befehl veröffentlicht auch Views, Config und Sprachdateien. Vorhandene Dateien überschreibt er nur mit --force.
  • Views include.message und include.breadcrumb: Die Verwaltungsviews binden beide selbst per @include ein. Das Paket liefert sie nicht, der Host muss sie bereitstellen.
    • include.message zeigt die Erfolgsmeldung aus session('message') und alle Validierungsmeldungen aus $errors. Ohne diese View kehrt ein abgelehnter Wert kommentarlos ins Formular zurück.
    • include.breadcrumb rendert die Variable $breadcrumb (Klasse ITHilbert\LaravelKit\Helpers\Breadcrumb). Ist sie nicht gesetzt, gibt die View nichts aus.
    • Beide Views dürfen keine Vue-Tags enthalten (z. B. <breadcrumb>), da die Verwaltungsseiten ohne Whole-DOM-Mount laufen.
    • Das Layout darf $errors nicht zusätzlich ausgeben, sonst erscheinen Meldungen doppelt.

Auth-View-Theme (Login & Passwort-Reset)

Die Auth-Views (Login, Passwort-vergessen/-zurücksetzen) liegen in zwei Theme-Varianten vor und werden über config('userauth.theme') gewählt. Ist der Wert null oder leer (z. B. env('USERAUTH_THEME') ohne Vorgabe in einer veröffentlichten Config), gilt der Paket-Standard tailwind:

  • tailwind (Default) — Tailwind CSS. Das Package liefert eine vorkompilierte CSS mit (Route userauth.assets.css, erreichbar unter /userauth/assets/userauth.css?v=<hash>). Der Host-Browser cached diese mit Cache-Control: immutable. Neue Projekte brauchen nichts zu konfigurieren — weder Tailwind, noch @source, noch vendor:publish.
  • bootstrap — das ältere Phoenix/Bootstrap-Markup. In bestehenden Bootstrap-Projekten per USERAUTH_THEME=bootstrap in der .env aktivieren. Wichtig: Das Package liefert kein Bootstrap oder Phoenix mit. Daher ist eine Konfiguration erforderlich (siehe unten unter Auth-Assets-Konfiguration).

Head-Erweiterung des Hosts (Favicon, Meta-Tags)

Die Auth-Seiten (Login, Passwort-Reset) haben einen eigenen <head> und nutzen das Root-Layout des Hosts nicht. Mit head_view / USERAUTH_HEAD_VIEW nennst du eine View des Hosts, deren Inhalt in diesen <head> eingebunden wird – ohne dass Package-Views veröffentlicht werden müssen. Standard: nicht gesetzt (nichts wird ergänzt). Eine nicht vorhandene View wird ignoriert.

USERAUTH_HEAD_VIEW=partials.auth-head

resources/views/partials/auth-head.blade.php:

<link rel="icon" href="/favicon.svg">

Auth-Assets-Konfiguration

Beide Layouts (Tailwind und Bootstrap) laden zusätzliche CSS und JavaScript über die Config auth_assets. Dies wird v.a. beim Bootstrap-Theme relevant, da das Package selbst kein Bootstrap oder Phoenix mitbringt.

Konfiguriere in deiner veröffentlichten config/userauth.php:

'auth_assets' => [
    'css' => [
        'assets/css/theme.min.css',  // z.B. Phoenix Theme
        'assets/css/user.min.css',   // z.B. projektspezifische Styles
    ],
    'js' => [
        // optional: projektspezifische Scripts
    ],
],

Pfade: Alle Pfade sind relativ zu public/ — z.B. 'assets/css/theme.min.css' lädt /public/assets/css/theme.min.css.

Tailwind-Theme: Das Package-eigene Tailwind wird bereits geladen (Route userauth.assets.css). Die auth_assets sind hier optional und können für zusätzliche projektspezifische Styles genutzt werden.

Bootstrap-Theme: Die auth_assets sind erforderlich, um Phoenix/Bootstrap und weitere Styles zu laden. Ohne auth_assets bleibt die Login-Seite ungestylt.

Projektspezifisches Design: Die Tailwind-Views erweitern das Layout userauth::layouts.auth. Um das Login-Design an das jeweilige Projekt anzupassen, überschreibe dieses Layout im Projekt:

resources/views/vendor/userauth/layouts/auth.blade.php

Das Package liefert eine neutrale Default-Hülle; das überschriebene Layout bestimmt Branding, Farben und Hintergrund. So bleibt die Login-Struktur im Package, das Aussehen pro Projekt.

Globale Auth-Übersetzungen: Die Standard-Laravel-Auth-Keys (auth.failed, auth.password, auth.throttle) werden vom Package namespace-los registriert und sind damit in jedem Projekt übersetzt (de/en), ohne dass eine lang/-Datei im Projekt nötig ist.

Für Package-Entwickler

Nach Änderungen an den Tailwind-Views im Package (z.B. neue Klassen hinzufügen) musst du die Package-CSS neu bauen:

cd packages/userauth
npm install
npm run build:css

Dies kompiliert resources-src/userauth.css zu src/Resources/assets/css/userauth.css. Wichtig: Die gebaute src/Resources/assets/css/userauth.css muss zusammen mit deinen View-Änderungen committed werden. Clients erhalten die neue CSS dann automatisch via Asset-Route mit aktualisiertem Versions-Hash.

Config-Optionen

Nach der Paket-Installation können die Details auch in der veröffentlichten Config-Datei unter config/userauth.php angepasst werden:

Password Policy / Ablauf

Zwingender Wechsel von Passwörtern bei Bedarf aktivieren. Verhindert auch, dass die X letzten Passwörter einfach wiederverwendet werden. Die Passwort-Historie wird nur geführt, solange enabled aktiv ist; das gilt auch beim Zurücksetzen per Reset-Link. Der Ablauf (require_change_every_days) greift erst nach der Zwei-Faktor-Anmeldung.

    'password_policy' => [
        'enabled' => env('USERAUTH_PASSWORD_POLICY_ENABLED', false),
        'require_change_every_days' => 90,
        'prevent_reuse_last_passwords' => 3,
    ],

Impersonate

System-Admins können temporär ("Impersonate") die Perspektive eines beliebigen Endnutzers einnehmen. Ein Button kann mit @if(auth()->user()->isImpersonated()) ... @endif überwacht werden. Der "Zurück"-Button ist ein POST-Formular mit @csrf auf userauth.impersonate.leave (Pfad impersonate-leave); ein GET-Link funktioniert nicht (siehe Upgrade-Hinweise). Impersonation durchläuft keine Zwei-Faktor-Anmeldung. Die Rechte prüft das Paket mit der Rolle des Handelnden (siehe Rollen und Rechte).

    'impersonate_enabled' => true,

Multi-Tenancy (Teams)

Erlaubt eine Mandanten-Trennung, bei der User zu Teams gehören (inklusive Team-eigenen Rollen). Wird das Flag USERAUTH_TEAMS_ENABLED=true gesetzt, liest das System Rechte (hasRole, hasPermission) dynamisch vom aktuell aktiven current_team_id des Users aus, statt nur von seiner globalen Rolle. Hat der Nutzer im aktiven Team keine Teamrolle, gilt seine globale role_id.

    'teams' => [
        'enabled' => env('USERAUTH_TEAMS_ENABLED', false),
    ],

E-Mail eines gelöschten Benutzers

Benutzer werden weich gelöscht (deleted_at), ihre E-Mail-Adresse bleibt in der Tabelle users. Was passiert, wenn ein Admin (Route user.store) oder eine Social-Registrierung (social.allow_registration) einen Benutzer mit dieser E-Mail anlegen will, regelt USERAUTH_GELOESCHTE_EMAIL. Groß-/Kleinschreibung der E-Mail spielt keine Rolle.

    'geloeschte_email' => env('USERAUTH_GELOESCHTE_EMAIL', 'sperren'),
Wert Admin-Anlage Social-Registrierung
sperren (Default) Validierungsfehler am Feld email: „Diese E-Mail-Adresse gehört zu einem gelöschten Benutzer.“ Es wird nichts angelegt. Fehler „Dieses Konto ist deaktiviert oder gelöscht.“ (wie bisher)
reaktivieren Die gelöschte Zeile wird wiederhergestellt (gleiche id). Name, Profilfelder, Rolle, Bild und Passwort kommen aus dem Formular wie bei einer Neuanlage, die Passwortregeln gelten. Meldung: „Der gelöschte Benutzer wurde wiederhergestellt.“ Die gelöschte Zeile wird wiederhergestellt, Rolle aus social.default_role, E-Mail gilt als bestätigt, der Passwort-Hash bleibt. Danach wird das Anbieter-Konto verknüpft und der Benutzer angemeldet.
neu Die gelöschte Zeile wird endgültig entfernt, der Benutzer normal neu angelegt (neue id). Meldung: „Ein gelöschtes Konto mit dieser E-Mail-Adresse wurde endgültig entfernt und der Benutzer neu angelegt.“ Die gelöschte Zeile wird endgültig entfernt, danach normale Social-Registrierung. Die Login-Seite zeigt bei den Social-Schaltflächen einen Hinweis darauf (ohne konkrete E-Mail).

Jeder andere Wert (auch leer oder anders geschrieben, z. B. Neu) wirkt wie sperren. Ist social.allow_registration aus, bleibt die Social-Anmeldung mit der E-Mail eines gelöschten Kontos in jeder Option abgelehnt. Ein aktiver Benutzer mit derselben E-Mail blockiert die Admin-Anlage immer (Fehler „bereits vergeben“).

Beim Reaktivieren bereinigt (in derselben Transaktion wie die Anlage – scheitert ein Schritt, z. B. weil das Passwort abgelehnt wird, bleibt alles wie vorher):

  • alle Social-Konten (user_social_accounts) und offenen Verknüpfungen (user_pending_social_links) des Benutzers gelöscht,
  • alle Team-Mitgliedschaften (team_user) entfernt, current_team_id geleert; die Teams selbst bleiben bestehen,
  • remember_token neu erzeugt (alte „Angemeldet bleiben“-Cookies gelten nicht mehr),
  • Passwort-Reset-Tokens zur E-Mail (password_reset_tokens) gelöscht,
  • two_factor_code und two_factor_expires_at geleert.

Passwort-Historie (user_password_histories) und Audit-Log (user_auth_audit_logs) bleiben erhalten. Bei der Social-Reaktivierung bleibt das Bild der Zeile erhalten. Gibt es mehrere gelöschte Zeilen mit derselben E-Mail (Altbestand), wird die zuletzt gelöschte wiederhergestellt, die übrigen bleiben gelöscht.

Beim endgültigen Löschen entfernt (in derselben Transaktion wie die Neuanlage – scheitert sie, bleibt die alte Zeile erhalten): alle gelöschten Zeilen mit dieser E-Mail samt Social-Konten, offenen Verknüpfungen, Team-Mitgliedschaften, Passwort-Historie und Passwort-Reset-Tokens zur E-Mail. Die Einträge im Audit-Log bleiben unverändert stehen.

Bilddatei der alten Zeile: Verweist nach dem Vorgang keine Zeile in users mehr auf die Bilddatei des gelöschten Benutzers (neu; reaktivieren über die Admin-Anlage mit neuem oder ohne Bild), wird sie von der Disk public gelöscht – erst nach erfolgreichem Abschluss; scheitert der Vorgang, bleibt sie. Dieselbe Regel gilt beim Ersetzen eines Bildes (Bearbeiten, Benutzerbild): Die alte Datei wird nur gelöscht, wenn keine andere Zeile (auch keine gelöschte) mehr auf sie verweist.

Audit-Log (falls USERAUTH_AUDIT_LOG_ENABLED=true): user_reactivated beim Wiederherstellen, user_purged je endgültig gelöschter Zeile, jeweils mit user_id und E-Mail der alten Zeile.

Passwort erzeugen (Benutzerverwaltung)

Optional zeigt die Benutzerverwaltung (Anlegen und Bearbeiten) beim Feld „Passwort“ die Buttons „Passwort erzeugen“ und „Kopieren“. Dafür stellt der Host einen Generator bereit und trägt seine Adresse (absolut oder ab /) in die .env ein:

USERAUTH_PASSWORT_GENERATOR_URL=/passwort/erzeugen

Config-Schlüssel: userauth.passwort_generator_url (Standard leer = kein Button, kein Skript). Der Browser ruft die Adresse per GET (gleiche Herkunft, Cookies, Accept: application/json) auf und erwartet die Antwort {"passwort": "…"} (weitere Felder werden ignoriert). Das Passwort wird in beide Felder eingetragen und im Klartext gezeigt; gesendet wird es erst mit dem Formular, Validierung und Passwortrichtlinie gelten unverändert. Schlägt der Abruf fehl, erscheint „Passwort konnte nicht erzeugt werden.“ und die Felder bleiben unverändert. Beispiel: deploy bietet /passwort/erzeugen. Der Generator sollte die Passwortrichtlinie des Pakets erfüllen, sonst lehnt das Formular das Passwort ab.

Rollen und Rechte

Privilegierte Rollen

Privilegiert (hat jedes Recht, erfüllt jede Rolle und jede Gate-Prüfung) ist nur eine Rolle (Entscheidung 0011, Projekt packages_dev):

Rolle Bedingung Bedeutung
Admin Rolle mit dem Schlüssel admin (roles.role, exakter Vergleich), egal mit welcher ID Erfüllt jedes Recht und jede Rolle, auch hasRole:dev
  • Jede andere Rolle hat nur ihre eigenen Rechte und erfüllt nur ihren eigenen Rollennamen, auch eine Rolle mit dem Schlüssel dev und auch eine Rolle mit der ID 1 oder 2.
  • Andere Schreibweisen (Admin, admin) sind nicht privilegiert. Gibt es mehrere Rollen mit dem Schlüssel admin, ist jede nicht gelöschte privilegiert.
  • Die Rolle admin hat in der Rollenverwaltung keine Schaltflächen zum Ändern und Löschen, und das Löschen ist serverseitig gesperrt; alle anderen Rollen lassen sich ändern und löschen.
  • Mit Teams gilt die Rolle des aktiven Teams für alle Prüfungen (Gate, Middleware, isAdmin/isDev, Impersonation). Ohne Teamrolle gilt die globale role_id.
  • Fehlt die Rolle (role_id ist null), ist ungültig (0, negativ) oder zeigt auf eine gelöschte oder nicht vorhandene Rolle, ist der Nutzer nicht privilegiert und hat keine Rechte (fail-closed).

Vergabe von Rollen und Rechten ohne Hierarchie

Entscheidung 0006 (Projekt packages_dev, Ordner .project/doku/decisions/):

  • Wer user_create oder user_edit hat, kann jede existierende Rolle vergeben, auch Admin, auch an sich selbst.
  • Wer role_create oder role_edit hat, kann einer Rolle beliebige Rechte geben, auch solche, die er selbst nicht hat.
  • Es gibt keine Hierarchie-Prüfung.
  • Ausgewählt werden können nur existierende, nicht gelöschte Rollen. Rechte-IDs müssen existieren. Eine Rolle wird mit ihren Rechten in einer Transaktion gespeichert: alles oder nichts.

Konsequenz: Ein Inhaber von user_edit oder role_edit kann sich selbst zum Admin machen und das Passwort jedes Nutzers setzen. Diese Rechte wie Admin-Rechte behandeln und nur Vertrauenspersonen geben.

Rechte in Routen und Views

Prüfung Verhalten
Route hasPermission:a Recht a oder Admin
Route hasPermissionAnd:a,b Rechte a und b (Admin: immer erlaubt)
Route hasPermissionOr:a,b Recht a oder b
Route hasRole:x Rollenname x (Admin erfüllt jede Rolle, auch dev)
Route isAdmin nur Admin
Route isDev wie isAdmin (Übergang, keine eigene Dev-Prüfung)
Blade @hasPermissionAnd('a,b') / @hasPermissionOr(['a','b']) Komma-Liste und Array sind gleichwertig
Gate @can('a'), Gate::allows('a') wie hasPermission

Leere, null oder fehlende Rechtenamen gelten als nicht vorhandenes Recht. Sie führen zur Ablehnung, nie zu einem Serverfehler.

Die Route /users verlangt user_read an der Route selbst. Das gilt unabhängig von routes.users_middleware in der veröffentlichten Config. Ohne Recht erhält der Nutzer dieselbe Ablehnung wie bei jeder anderen fehlenden Berechtigung (abhängig von userauth.redirect_on_no_permission: Anmeldung oder Seite no-permission).

Die Rechte-Middlewares (hasPermission, hasPermissionAnd, hasPermissionOr, hasRole, isAdmin, isDev) laufen nach der Anmeldung und vor dem Route-Model-Binding (SubstituteBindings). Das Paket trägt sie selbst in die Middleware-Priorität von Router und HTTP-Kernel ein. Sonst verriete ein 404 bei Routen mit Model-Parameter, welche IDs existieren. Hosts können eine eigene Übergangslösung dafür (z. B. in bootstrap/app.php) entfernen; bleibt sie bestehen, entsteht kein doppelter Eintrag, und auch ein falsch platzierter Eintrag (z. B. hinter SubstituteBindings) wird an die richtige Stelle gesetzt. Vor den Rechte-Middlewares laufen weiterhin die Vorprüfungen, die das Paket in die Gruppe web einträgt (EnforcePasswordPolicy, TwoFactorMiddleware): Ein Nutzer mit abgelaufenem Passwort wird auf die Passwortänderung umgeleitet, eine alte Sitzung ohne 2FA-Markierung abgemeldet, auch wenn ihm das Recht fehlt. Einträge anderer Pakete in der Priorität bleiben erhalten. Steht can: vor einer Rechte-Middleware an derselben Route, läuft die Rechte-Middleware zuerst: Ohne Recht antwortet dann die Ablehnung von userauth statt 403 von can: (Laravel ordnet Authorize hinter SubstituteBindings ein).

Nutzerliste (JSON)

Die AJAX-Liste liefert pro Zeile nur: id, name, email, RoleName und die Spalte action (HTML der Aktionen). Passwort-Hash, Reset-Token, 2FA-Code und 2FA-Ablaufzeit gehören nicht zur Ausgabe. Eigene Erweiterungen der Liste müssen die Felder explizit auswählen, nicht das ganze Model serialisieren.

Passwort zurücksetzen

Entscheidung 0008 (Projekt packages_dev, Ordner .project/doku/decisions/). Der Ablauf läuft über den Laravel-Password-Broker.

  1. Anfordern: POST password/sendtocken (Name password.sendtocken). Der Nutzer gibt seine E-Mail ein.
  2. Antwort: Bekannte Adresse, unbekannte Adresse und gelöschtes Konto erhalten dieselbe Weiterleitung und dieselbe Meldung. Es entsteht kein Serverfehler.
  3. Versand: Die Mail geht an die in der Datenbank gespeicherte Adresse, unabhängig von der Schreibweise der Eingabe. Der Versand erfolgt nach der HTTP-Antwort (defer()). Ein Queue-Worker ist dafür nicht nötig. Die Route trägt InvokeDeferredCallbacks (siehe Pflichten des Hosts). Läuft PHP ohne Server, der defer() nach der Antwort ausführt (z. B. CLI, Tests), sendet Laravel beim Beenden der Anwendung.
  4. Token: Der Broker erzeugt das Token und speichert nur den Hash in der Tabelle password_reset_tokens. Tabellenname aus auth.passwords.<broker>.table. Die Migration legt die Tabelle nur an, wenn sie fehlt.
  5. Ablauf: auth.passwords.<broker>.expire (Default 60 Minuten). Der Reset-Link ist danach ungültig.
  6. Zurücksetzen: POST password/updatewithtoken (Name password.updatewithtoken) mit Token, E-Mail und neuem Passwort. Bei Erfolg:
    • das Passwort ist geändert, password_changed_at ist gesetzt,
    • die Passwort-Historie wird fortgeschrieben (nur bei aktiver userauth.password_policy.enabled),
    • das Token ist verbraucht; eine zweite Nutzung wird abgelehnt. Falsches, abgelaufenes, fremdes oder nicht-textuelles Token: Hinweis, Passwort bleibt unverändert.

Drosselung:

Grenze Wert Schlüssel
Reset-Link je E-Mail höchstens 1 pro 60 Sekunden eigene Sperre im Paket, unabhängig von auth.passwords.*.throttle
Anfordern je IP 5 pro Minute Limiter userauth-passwort-anfordern
Zurücksetzen je IP 5 pro Minute Limiter userauth-passwort-zuruecksetzen

Mail: ITHilbert\UserAuth\Mail\PasswortZuruecksetzenLink, Absender aus mail.from.

Links in Mails: Der Reset-Link und der Bestätigungslink der Kontoverknüpfung (Social Login) entstehen aus APP_URL (app.url), nicht aus Host und Schema der Anfrage. So kann ein gefälschter X-Forwarded-Host den Token nicht auf einen fremden Host lenken. APP_URL muss deshalb auf die öffentliche Adresse zeigen (auch mit Unterpfad, z. B. https://example.com/app). Ist APP_URL leer, entsteht der Link wie bisher aus der Anfrage und das Paket schreibt eine Warnung ins Log.

Registrierung

Selbstregistrierung per E-Mail und Passwort (Dashboard #255). Standard: aus. Ist sie aus, antworten alle Registrierungs-Routen mit 404, und die Login-Seite zeigt keinen Link „Registrieren“.

Konfiguration

# Registrierung einschalten (Default: false)
USERAUTH_REGISTRATION=true
# Rollen-Key (roles.role) für neue Konten; leer = keine Rolle (role_id null)
USERAUTH_REGISTRATION_ROLE=user

In config/userauth.php (Gruppe registration):

Schlüssel Default Wirkung
enabled env('USERAUTH_REGISTRATION', false) Registrierung an/aus (aus: jede Registrierungs-Route antwortet mit 404, die Login-Seite zeigt keinen Link)
role env('USERAUTH_REGISTRATION_ROLE') Rolle neuer Konten; leer = keine Rolle
verify_ttl_minutes 60 Gültigkeit des Bestätigungslinks in Minuten; leer oder ungültig = 60

Hosts mit veröffentlichter Config ergänzen die Gruppe registration; fehlende Schlüssel bekommen die Werte aus der Tabelle.

Rolle: Die Rolle darf keine privilegierte sein. Steht in USERAUTH_REGISTRATION_ROLE admin oder dev (früher privilegiert) oder ein Schlüssel, zu dem es keine Rolle gibt, legt die Registrierung kein Konto an und wirft RegistrierungFalschKonfiguriertException (Text nennt userauth.registration.role).

Pflichten des Hosts

  1. Mail-Versand konfiguriert (MAIL_*, mail.from): Ohne Mail kann niemand sein Konto bestätigen.
  2. APP_URL richtig: Der Bestätigungslink entsteht aus APP_URL und ist für genau diese Adresse signiert. Zeigt APP_URL auf einen anderen Host als den, unter dem die Seite aufgerufen wird, gilt jeder Link als ungültig.
  3. php artisan migrate: Die Migration 2026_11_03_000000_add_self_registered_to_users_table ergänzt die Spalte users.self_registered.
  4. Cache-Store nicht array (Drosselung, siehe Abschnitt „Passwort zurücksetzen“, Unterpunkt „Drosselung“).

Die Routen der Registrierung sind immer registriert; ob sie antworten, entscheidet EnsureRegistrationEnabled bei jeder Anfrage. Ein- und Ausschalten wirkt deshalb ohne neuen Routen-Cache (bei config:cache wie jede Config-Änderung erst nach neuem Config-Cache). Ausgeschaltet antworten GET und POST auf alle Pfade unter register mit 404 – auch in einem Host mit GET-Fallback-Route (Route::fallback, z. B. Paket pages) und vor der CSRF-Prüfung (die Middleware steht in der Middleware-Priorität vor ShareErrorsFromSession).

Ablauf

  1. Formular: GET register (Name register), Felder Name, E-Mail, Passwort, Passwort-Wiederholung; Passwortregeln wie beim Passwort ändern. Angemeldete Nutzer werden wie beim Login weitergeleitet.
  2. Anlegen: POST register (Name register.store). Das Konto entsteht mit „Bestätigung offen“ (self_registered = 1, email_verified_at leer) und der Rolle aus der Config. Der Nutzer ist nicht angemeldet und landet auf register/bestaetigung mit der Meldung „Wir haben Ihnen eine E-Mail geschickt …“. Bei aktiver Passwort-Richtlinie gilt password_changed_at = jetzt.
  3. Mail: ITHilbert\UserAuth\Mail\RegistrierungBestaetigen mit einem signierten Link (register/bestaetigen/{id}/{hash}, Name register.bestaetigen), gültig verify_ttl_minutes, gebunden an Konto-ID und E-Mail. Versand nach der HTTP-Antwort (defer(), wie beim Passwort-Reset); die Routen tragen InvokeDeferredCallbacks.
  4. Bestätigen: Gültiger Link → email_verified_at gesetzt, Weiterleitung auf die Login-Seite mit Erfolgsmeldung. Ein zweiter Aufruf ändert nichts und zeigt dieselbe Meldung. Abgelaufener, veränderter Link, gelöschtes Konto oder geänderte E-Mail → Hinweis „Link ungültig oder abgelaufen“ mit Formular für einen neuen Link.
  5. Vor der Bestätigung: Anmeldung mit richtigem Passwort wird abgelehnt, mit Hinweis und Link „Bestätigungslink erneut anfordern“; auch kein 2FA-Code. Konten, die nicht über die Registrierung entstanden sind (Admin, Seeder, Social Login), melden sich an wie bisher – auch mit leerem email_verified_at.
  6. Link erneut anfordern: GET register/bestaetigung (Name register.bestaetigung) zeigt das Formular, POST register/bestaetigung (Name register.erneut) verschickt den Link – nur, wenn zu der E-Mail ein Konto mit offener Bestätigung existiert. Die Antwort ist für jede Eingabe dieselbe.
  7. Bestätigung über den Social Login: Meldet sich jemand per Social Login mit derselben (beim Anbieter bestätigten) Adresse an und bestätigt die Verknüpfung per Mail-Link, ist die Bestätigung erledigt. Zugleich verfällt das bei der Registrierung gesetzte Passwort (Anmeldung damit scheitert wie mit falschem Passwort) und alle Angemeldet-bleiben-Tokens des Kontos – sonst könnte, wer eine fremde Adresse vorab registriert hat, das Konto nach der Verknüpfung durch den echten Inhaber mitbenutzen. Anmelden per Social Login und ein neues Passwort über „Passwort vergessen“ funktionieren. Konten ohne offene Bestätigung behalten ihr Passwort wie bisher.

Doppelte E-Mail: Ist die Adresse bereits vergeben (auch soft-gelöscht, Groß-/Kleinschreibung egal), entsteht kein zweites Konto, das bestehende bleibt unverändert, und es geht keine Mail raus. Die Antwort ist dieselbe wie bei einer neuen Adresse – so lässt sich nicht erfragen, welche Adressen ein Konto haben.

Drosselung:

Grenze Wert Schlüssel
Registrieren je IP 5 pro Minute Limiter userauth-registrieren
Link erneut anfordern je IP 5 pro Minute Limiter userauth-registrierung-link
Neuer Link je E-Mail höchstens 1 pro 60 Sekunden eigene Sperre im Paket

Audit-Log (falls USERAUTH_AUDIT_LOG_ENABLED=true): registered beim Anlegen (nicht bei doppelter E-Mail), email_verified beim ersten gültigen Aufruf des Links. Das Laravel-Event Illuminate\Auth\Events\Registered wird bewusst nicht ausgelöst (Laravel würde sonst bei MustVerifyEmail-Models eine zweite Mail schicken).

Upgrade: Die Registrierung bringt die neue Migration 2026_11_03_000000_add_self_registered_to_users_table mit – nach dem Update php artisan migrate ausführen. Bestehende Konten bekommen self_registered = 0 und sind nicht betroffen. Veröffentlichte Login-Views übernehmen den Link „Registrieren“ aus login/tailwind.blade.php bzw. login/bootstrap.blade.php (Variable $registrierungAktiv).

Zwei-Faktor-Anmeldung

Entscheidung 0009 (Projekt packages_dev, Ordner .project/doku/decisions/). Aktivierung: USERAUTH_2FA_ENABLED=true. Die Zwei-Faktor-Anmeldung gilt für Passwort-Login und Social Login gleichermaßen.

Ablauf

  1. Passwort oder Google korrekt: Der Nutzer ist noch nicht angemeldet. Das Paket legt eine offene Anmeldung in der Session (Schlüssel userauth.2fa) an und regeneriert die Session-ID. Es erzeugt einen 6-stelligen Code aus random_int und versendet ihn synchron per Mail. Die Antwort wartet also auf den Mailserver.
  2. Code-Eingabe: GET verify (Name verify.index) zeigt das Formular, POST verify (Name verify.store) prüft den Code.
    • Gültig: 10 Minuten ab Versand.
    • Abgelaufen oder falsch: Ablehnung mit Hinweis. Ein abgelaufener Code zählt als Fehlversuch.
    • Nach 5 Fehlversuchen ist der Code ungültig, die offene Anmeldung wird verworfen, der Nutzer landet auf der Anmeldung.
  3. Richtiger Code: Der Nutzer ist angemeldet (neue Session-ID). Er wird zum ursprünglich gewünschten Ziel geleitet. Der Code ist verbraucht. Das Cookie „Angemeldet bleiben“ wird erst jetzt gesetzt. Das Audit-Ereignis login wird erst jetzt protokolliert.
  4. Code erneut senden: Nur POST verify/resend (Name verify.resend), höchstens einmal pro 60 Sekunden. Der neue Code ersetzt den alten.
  5. Abbrechen oder Abmelden (auch als Gast): Route logout. Die offene Anmeldung ist verworfen.
  6. Versandfehler (Mailserver nicht erreichbar): kein Serverfehler. Der Nutzer landet auf der Anmeldung mit dem Hinweis, dass der Code nicht versendet werden konnte. Er ist nicht angemeldet.

Vor bestandener 2FA sind Seiten hinter auth nicht erreichbar. Die Routen verify.* sind nur für Gäste erreichbar.

Speicherung der Codes

  • Der Code liegt nirgends im Klartext: nicht in der Datenbank, nicht in der Session.
  • Gespeichert wird ein HMAC-SHA256 über die zufällige Kennung der offenen Anmeldung und den Code, mit dem Schlüssel app.key.
  • Der Vergleich ist zeitkonstant (hash_equals).
  • Ein Code gilt nur in der Sitzung, für die er erzeugt wurde. Eine zweite Sitzung mit offener Anmeldung bleibt unangemeldet, wenn die erste bestätigt.
  • Fehlversuche und die 60-Sekunden-Sperre zählt der RateLimiter im Cache je offener Anmeldung. Der Zähler übersteht „Code erneut senden“.

Audit-Log (falls USERAUTH_AUDIT_LOG_ENABLED=true)

  • login: erst nach bestandener 2FA.
  • two_factor_failed: abgelehnter Code. Eigenes Ereignis, kein login.

Bekannte Grenzen

  • Fehlversuche zählen je offener Anmeldung. Wer das Passwort kennt, kann pro Anmeldung 5 Codes raten, jeweils mit einer Mail an das Konto. Eine Drosselung je Nutzer über mehrere Anmeldungen hinweg ist nicht umgesetzt.
  • Der Versand ist synchron. Ein langsamer Mailserver verzögert die Antwort nach dem Passwort.

Migrationen

Migration Wirkung down()
2019_10_12_000000_create_my_users_table legt users an, wenn sie fehlt; sonst ergänzt sie nur fehlende Spalten. Bestehende Nutzer bleiben erhalten. ohne Wirkung
2026_10_23_000000_make_role_id_nullable_on_users_table role_id nullable, ohne Standardwert Admin. Nutzer ohne Rolle sind nicht privilegiert. Bestehende Zuordnungen bleiben. ohne Wirkung
2026_10_23_100000_create_password_reset_tokens_table legt password_reset_tokens an, wenn sie fehlt ohne Wirkung
2026_10_23_200000_clear_remember_tokens_on_users_table leert einmalig remember_token aller Nutzer, auch gelöschter ohne Wirkung
2026_11_03_000000_add_self_registered_to_users_table ergänzt users.self_registered (Konto aus der Selbstregistrierung), wenn die Spalte fehlt ohne Wirkung
2026_11_05_000000_move_dev_users_to_admin_role stellt einmalig alle Nutzer (auch gelöschte) und team_user.role_id von der Rolle dev auf die nicht gelöschte Rolle admin mit der kleinsten ID um; ohne Rolle admin ändert sie nichts. Die Rolle dev bleibt stehen. ohne Wirkung

Keine Rückbau-Migrationen: Ob das Paket eine Tabelle angelegt oder nur ergänzt hat, ist beim Zurückrollen nicht feststellbar. Ein Löschen würde die Nutzer des Hosts vernichten. Der alte Zustand (role_id mit Admin als Default) wäre die geschlossene Sicherheitslücke. Die geleerten Angemeldet-bleiben-Tokens lassen sich nicht wiederherstellen (Entscheidungen 0007 und 0009, Projekt packages_dev).

Abhängigkeiten im Überblick

Siehe Voraussetzungen und Abhängigkeiten. Zusätzlich:

Abhängigkeit Dev / Laufzeit Wofür
orchestra/testbench ^10.0 oder ^11.0 Dev eigene Testumgebung
phpunit/phpunit ^11.5 oder ^12.0 Dev Tests
Google OAuth extern, nur bei USERAUTH_SOCIAL_ENABLED=true Social Login

Upgrade-Hinweise (nach 3.0.0, unveröffentlicht)

Diese Punkte gelten für Hosts, die den aktuellen Stand des Pakets nutzen. Eine Version nach 3.0.0 gibt es noch nicht.

Nur noch admin ist privilegiert (neue Migration, Entscheidung 0011)

  1. php artisan migrate ausführen: 2026_11_05_000000_move_dev_users_to_admin_role stellt alle Nutzer mit der Rolle dev (auch gelöschte) und alle Team-Mitgliedschaften mit dev auf die Rolle admin um. Gibt es im Host keine (nicht gelöschte) Rolle admin, ändert die Migration nichts: dann die Rolle admin anlegen und den Nutzern von Hand zuweisen. Bis zur Umstellung haben Nutzer mit dev keine Privilegien mehr.
  2. Die Rolle dev bleibt als gewöhnliche Rolle stehen (nur ihre eigenen Rechte, nur ihr eigener Rollenname) und kann in der Rollenverwaltung gelöscht werden. Privilegiert ist allein die Rolle mit dem Schlüssel admin, unabhängig von ihrer ID.
  3. Der Middleware-Alias isDev bleibt als Übergang registriert und wirkt genau wie isAdmin. Routen des Hosts mit isDev können auf isAdmin umgestellt werden.
  4. Veröffentlichte Host-Config: Steht in routes.permissions_middleware noch hasRole:dev, auf hasRole:admin umstellen (neue Paket-Vorgabe). Funktional bleibt es auch ohne Umstellung, denn admin erfüllt jede Rolle.
  5. Die Seeder legen nur noch die Rollen admin und user an; UsersTableSeeder gibt admin@admin.com die Rolle admin.
  6. Hosts mit veröffentlichten Sprachdateien (resources/lang/vendor/userauth/de/role.php bzw. lang/vendor/userauth/de/role.php) übernehmen MsgDeleteRoot = „Die Rolle Admin kann nicht gelöscht werden.“ (bisher „Die Rollen Developer und Admin können nicht gelöscht werden.“).

Selbstregistrierung (neue Migration)

  1. php artisan migrate ausführen: 2026_11_03_000000_add_self_registered_to_users_table ergänzt users.self_registered. Die Registrierung ist standardmäßig aus; Einschalten und Pflichten siehe Registrierung.
  2. Die ungenutzten Laravel-UI-Reste (Views register/verify unter views/auth, Schemas dazu) sind entfernt. Veröffentlichte Kopien im Host können gelöscht werden.

Logout und Impersonate nur per POST

  1. logout, userauth.impersonate und userauth.impersonate.leave nehmen nur POST mit gültigem CSRF-Token an. Die Routennamen sind unverändert. Ein GET ergibt 405, in Hosts mit einer Catch-all-Fallback-Route (z. B. Paket pages) 404. Ohne gültiges Token antwortet die Anwendung mit 419 und ändert nichts.
  2. GET-Links auf diese Routen schlagen damit fehl. Umstellen auf Formulare:
<form method="POST" action="{{ route('logout') }}">
    @csrf
    <button type="submit">Abmelden</button>
</form>

<form method="POST" action="{{ route('userauth.impersonate.leave') }}">
    @csrf
    <button type="submit">Zurück zum eigenen Account</button>
</form>

Übernahme starten: <form method="POST" action="{{ route('userauth.impersonate', $user->id) }}"> mit @csrf.

Stundensatz und Geburtstag 3. Ein leer gesendetes hourly_rate speichert 0, ein leer gesendetes birthday speichert null. Gilt für Anlegen (user.store) und Bearbeiten (user.update). 4. Ist das Feld per Config ausgeschaltet (userauth.user.hourly_rate bzw. userauth.user.birthday, Default false), sendet das Formular es nicht. Beim Bearbeiten bleibt dann der gespeicherte Wert erhalten. Beim Anlegen gilt 0 bzw. null.

Validierung 5. hourly_rate muss eine Zahl ≥ 0 sein, birthday das Format Y-m-d haben. Sonst gibt es einen Validierungsfehler für das Feld, und der Benutzer wird nicht gespeichert. Der Host muss die Fehler anzeigen (siehe Verwaltungsseiten).

Lösch-Dialog 6. Die Listen übergeben dialog-delete das Prop titel statt title. Veröffentlichte Views unter resources/views/vendor/userauth/ müssen angepasst werden, wenn sie title= am Dialog setzen.

Verwaltungsseiten ohne Vue-Tags (Kit-Components) 7. Die Verwaltungsviews (user/index|create|edit|icon, role/index|create|edit, permission/index|create|edit) bestehen aus Blade-Components von laravel-kit (<x-kit::…>) statt aus Vue-Tags. Ein Ausdruck wie {{ 7*7 }} in Namen oder Anzeigen erscheint als Text. Das Markup wählt laravel-kit über laravelkit.theme (tailwind oder bootstrap5). 8. Mindeststand von ithilbert/laravel-kit: der erste Stand nach 1.2.0 mit den Kit-Components (#142) und der Component combobox (#115). Eine Versionsnummer dafür gibt es noch nicht. ^1.2 aus composer.json genügt erst, wenn dieser Stand als Release vorliegt. 9. Der Host bindet die Skripte von laravel-kit einmal pro Seite ein, z. B. in resources/js/app.js:

import { initTabs } from '../../vendor/ithilbert/laravel-kit/js/tabs.js';
import { initLoeschen } from '../../vendor/ithilbert/laravel-kit/js/loeschen.js';
import { mountInseln } from '../../vendor/ithilbert/laravel-kit/js/mount-inseln.js';

initTabs();
initLoeschen();
mountInseln({
    'dialog-delete': () => import('../../vendor/ithilbert/laravel-kit/vue/tailwind/dialog/delete.vue'),
});

initTabs() schaltet die Reiter im Rollenformular. initLoeschen() nimmt den Klick auf die Lösch-Buttons der Listen entgegen (data-kit-delete aus HButton::delete). mountInseln() mountet die Insel dialog-delete. Der Dialog sendet das Formular #formDelete, das die Listen selbst mitbringen (Methode DELETE, CSRF-Token). Die Insel dialog-delete gibt es nur für das Theme tailwind. 10. Ein Whole-DOM-Mount (vueapp_TW.js, vueapp_B5.js, vueapp_B4.js aus laravel-kit) ist für diese Seiten nicht mehr nötig. 11. Veröffentlichte Verwaltungs-Views unter resources/views/vendor/userauth/ neu veröffentlichen: php artisan vendor:publish --provider="ITHilbert\UserAuth\UserAuthServiceProvider" --force. Ohne das behält der Host die alten Views mit Vue-Tags. --force überschreibt auch Config und Sprachdateien; eigene Änderungen dort vorher sichern. Neu sind die Teil-Views user/formularfelder, role/formularfelder, role/recht, permission/rechtezeilen und partials/loeschen. 12. Die Views include.breadcrumb und include.message des Hosts dürfen keine Vue-Tags enthalten (z. B. <breadcrumb>). Ohne Whole-DOM-Mount rendert der Browser sie sonst nicht.

Rechtegruppen: Gruppenname 13. group_name einer Rechtegruppe wird beim Bearbeiten (permission.update) formatiert gespeichert, wie beim Anlegen. Bisher blieb er roh, während die CRUD-Rechte den formatierten Namen erhielten. Gruppen, die nicht bearbeitet werden, behalten ihren gespeicherten group_name. 14. Ein Gruppenname ist eindeutig, geprüft wird der formatierte Name. Kollisionen ergeben beim Anlegen und Bearbeiten einen Validierungsfehler am Feld group_name (Meldung userauth::permission.NameVergeben), keinen 500er: - der formatierte Name ist bereits als Gruppe vergeben (auch soft-gelöscht), - bei einer CRUD-Gruppe ist einer der Rechtenamen <name>_create, _read, _edit oder _delete bereits vergeben (aktiv oder soft-gelöscht), außer er gehört zur bearbeiteten Gruppe selbst. Dann wird weder die Gruppe noch ein Recht angelegt oder umbenannt.

Skripte der Verwaltungsseiten 15. Die Verwaltungsseiten legen ihre Skripte in den Stack userauth-scripts; userauth::layouts.afterLogin bringt sie in Stack scripts und Section js des Host-Layouts (siehe Verwaltungsseiten). Ein Nachstarter im Host-Layout für diese Skripte kann entfallen. Das Host-Layout gibt den Stack bzw. die Section nach dem Bundle mit jQuery und DataTables aus. 16. Hosts mit veröffentlichtem layouts/afterLogin.blade.php oder veröffentlichten Verwaltungs-Views unter resources/views/vendor/userauth/ übernehmen die neue Fassung (neu veröffentlichen, siehe Punkt 11). Eine alte afterLogin ohne Brücke gibt die Skripte der neuen Views nicht aus; alte Views mit @section('js') laufen weiter wie bisher.

Interne Namen: eine Regel 17. Server und Browser bilden den internen Namen (Rolle, Rechtegruppe, Einzelrecht) aus der Anzeige nach derselben Regel, in dieser Reihenfolge: (1) Kleinbuchstaben, auch Umlaute; (2) ä→ae, ö→oe, ü→ue, ß→ss; (3) die Zeichen ; : " ' ` ~ < > | entfallen; (4) Leerraum am Anfang und Ende entfällt; (5) jede Folge von Leerraum im Innern wird zu genau einem _. Andere Zeichen (-, ., Ziffern, é) bleiben stehen. Beispiele: Bericht Lesen → bericht_lesen, Export Ä → export_ae, Größe : Ändern → groesse_aendern, kunde_read bleibt kunde_read. 18. Ist der interne Name nach dieser Regel leer (z. B. Anzeige : ;), gibt es einen Validierungsfehler am Feld des internen Namens (group_name, permission_new_<n>, permission_<id>), gespeichert wird nichts. 19. Assets neu veröffentlichen: php artisan vendor:publish --provider="ITHilbert\UserAuth\UserAuthServiceProvider" --force (neue gemeinsame Datei public/vendor/userauth/js/rechtename.js, permission.js und role.js nutzen sie). --force überschreibt auch Config, Views und Sprachdateien (siehe Punkt 11). 20. Gespeicherte Namen mit großen Umlauten (z. B. exportÄ) bleiben unverändert, bis zum nächsten Speichern der Gruppe; dann werden sie nach der Regel neu gebildet (export_ae).

Englische Erfolgsmeldungen 21. Die englischen Erfolgsmeldungen MsgAddSuccess, MsgEditSuccess und MsgDeleteSuccess in lang/en/user.php sind korrigiert. Hosts mit veröffentlichten Sprachdateien (resources/lang/vendor/userauth/en/user.php bzw. lang/vendor/userauth/en/user.php) übernehmen die drei Zeilen.

E-Mail eines gelöschten Benutzers 22. Neue Option USERAUTH_GELOESCHTE_EMAIL (siehe E-Mail eines gelöschten Benutzers). Der Standard sperren ändert am Verhalten nichts: Die E-Mail eines gelöschten Benutzers bleibt belegt. Neu ist nur die Meldung bei der Admin-Anlage – statt des allgemeinen Fehlers „bereits vergeben“ heißt es „Diese E-Mail-Adresse gehört zu einem gelöschten Benutzer.“ (Sprachschlüssel userauth::user.email_geloeschter_benutzer). 23. Hosts mit veröffentlichten Views oder Sprachdateien übernehmen die neuen Stellen (neu veröffentlichen, siehe Punkt 11): login/tailwind und login/bootstrap den Hinweis bei Option neu; neue Sprachschlüssel in user.php (email_geloeschter_benutzer, MsgReaktiviert, MsgEntferntUndAngelegt) und social.php (hinweis_geloeschtes_konto). 24. Wer SocialUserRepository selbst implementiert: existsDeletedWithEmail() entfällt (die Prüfung liegt im Anwendungsfall GeloeschteEmailAufloesen), neu ist adoptReactivated().

  1. Neue Option USERAUTH_PASSWORT_GENERATOR_URL (siehe Passwort erzeugen). Assets neu veröffentlichen (php artisan vendor:publish --provider="ITHilbert\UserAuth\UserAuthServiceProvider" --force): neue Datei public/vendor/userauth/js/passwort-erzeugen.js. Hosts mit veröffentlichten Views übernehmen die neuen Stellen in user/formularfelder, user/create und user/edit sowie die Sprachschlüssel password_generate, password_copy, password_copied, password_generate_failed. Ohne die Option ändert sich nichts.

Upgrade-Hinweise (3.0.0)

Diese Punkte gelten für Hosts, die auf 3.0.0 aktualisieren. Sie sind nach dem Code geprüft.

Unterstützt werden Laravel 12 und 13. ithilbert/laravel-kit wird ab ^1.2 benötigt.

Berechtigungen und Daten

  1. /users verlangt user_read (an der Route). Rollen ohne dieses Recht verlieren den Zugriff auf die Nutzerliste.
  2. Die JSON-Nutzerliste enthält nur noch id, name, email, RoleName und action.
  3. Model-Serialisierung (toArray, toJson) blendet password, remember_token, two_factor_code, two_factor_expires_at, edit_pw_token und edit_pw_token_end aus. Ein eigenes $hidden des Hosts bleibt erhalten.
  4. Dev und Admin gelten nur noch mit ID 1 + Schlüssel dev bzw. ID 2 + Schlüssel admin. Umbenannte Rollen 1 oder 2 verlieren ihre Privilegien. Mit Teams gilt die Rolle des aktiven Teams. (Überholt durch #357: privilegiert ist nur noch die Rolle mit dem Schlüssel admin, unabhängig von der ID – siehe „Nur noch admin ist privilegiert“.)
  5. role_id ist nullable, ohne Default Admin. Neue Nutzer ohne Rolle haben keine Rechte.
  6. Trait-Methoden hasPermissionAnd und hasPermissionOr erwarten string|array. Mehrere Argumente (('a', 'b')) sind weiterhin möglich.

Migrationen 7. Die vier Migrationen aus Migrationen haben eine leere down(). migrate:rollback entfernt durch sie keine Spalten und Tabellen. Ältere Paket-Migrationen (2FA-Spalten, Passwort-Richtlinie, Teams, Audit-Log, Social Login) behalten ihren bisherigen Rückbau. 8. Neu hinzugekommen: 2026_10_23_000000_make_role_id_nullable_on_users_table, 2026_10_23_100000_create_password_reset_tokens_table (nur, wenn die Tabelle fehlt) und 2026_10_23_200000_clear_remember_tokens_on_users_table. 9. Die Migration clear_remember_tokens leert remember_token aller Nutzer einmalig. Alle Nutzer, die „Angemeldet bleiben“ genutzt hatten, sind danach einmal abgemeldet. Alte Cookies melden niemanden mehr an. 10. Sitzungen, die vor dem Update über den alten 2FA-Ablauf entstanden sind, werden einmal abgemeldet. Voraussetzung: 2FA ist aktiv und der Nutzer hat noch einen offenen Code in users.two_factor_code. Der Nutzer meldet sich neu an.

Passwort-Reset 11. Die Mail-Klasse App\Mail\ForgottenPassword und ihre View sind entfernt. Den Reset-Link versendet ITHilbert\UserAuth\Mail\PasswortZuruecksetzenLink. 12. Alte Reset-Links (aus edit_pw_token) sind ungültig. Betroffene fordern einen neuen Link an. 13. Der Absender kommt aus mail.from. 14. Das User-Model braucht CanResetPassword (siehe Pflichten des Hosts).

Zwei-Faktor-Anmeldung und Routen 15. verify.resend ist nur noch per POST erreichbar. Die veröffentlichte View resources/views/vendor/userauth/auth/twoFactor.blade.php muss "Code erneut senden" als POST-Formular haben. Ein GET-Link bricht. 16. verify.* ist nur für Gäste erreichbar. logout ist ohne auth-Middleware erreichbar. 17. Das Mailable TwoFactorCode nimmt (code, gueltigMinuten). Host-Overrides des Mailables anpassen. 18. Das Event Login feuert erst nach bestandener 2FA. Listener auf Login werden bei offener 2FA nicht mehr ausgelöst. 19. Der Versand des 2FA-Codes ist synchron. Ein Versandfehler zeigt einen Hinweis auf der Anmeldung statt eines Serverfehlers.

Entfernt 20. Trait TwoFactorAuthenticatable sowie die Methoden generateTwoFactorCode() und resetTwoFactorCode() von UserAuth. 21. Contract AuthSession und Klasse SessionAuth. 22. Hook authenticated() im LoginController. Die Anmeldung läuft über den Controller und den UseCase CompleteAuthentication.

Konfiguration, Views und Sprache 23. Das User-Model wird über auth.providers.users.model aufgelöst. Fehlt der Eintrag, wirft das Paket UserModelMisconfiguredException. 24. Der Standard-Guard muss ein Session-Guard sein (siehe Pflichten des Hosts). 25. Der Cache-Store darf nicht array sein. 26. Neue Sprachdateien zwei_faktor.php (de und en) und Schlüssel in password.php (de und en). Hosts mit veröffentlichten Sprachdateien unter lang/vendor/userauth/{de,en}/ übernehmen sie. 27. Nach dem Update php artisan view:clear ausführen.

Social Login (Google)

Das Package unterstützt Social Login über Google mit sicherer Account-Verknüpfung für bestehende Nutzer.

Installation & Vorbereitung

Abhängigkeiten sind bereits in der composer.json des Packages enthalten:

composer require laravel/socialite

Datenbank: Führe folgende Migration aus, um die neuen Tabellen zu erstellen:

php artisan migrate

Dies erzeugt zwei neue Tabellen:

  • user_social_accounts — verknüpfte Social-Konten pro User
  • user_pending_social_links — Bestätigungstokens für unverifiziierte Accounts

Betrieb — Abgelaufene Bestätigungstokens löschen:

Das Package registriert automatisch einen täglich geplanten Job (userauth:prune-social-links), der abgelaufene Einträge aus user_pending_social_links löscht. Damit dieser Job ausgeführt wird, benötigst du einen laufenden Laravel Scheduler:

* * * * * php artisan schedule:run >> /dev/null 2>&1

Diese Cron-Zeile sollte in dein System-Crontab eingetragen werden (typischerweise crontab -e oder per Hoster-Control-Panel). Sie ruft den Scheduler jede Minute auf; der Scheduler bestimmt selbst, welche Jobs fällig sind.

Ohne laufenden Scheduler können alte Bestätigungstokens in der Datenbank verbleiben. Die Tokens selbst sind mit konfigurierbarer TTL (Default: 60 Minuten) weiterhin ungültig; das Löschen ist eine Datenbankwartung.

Google Cloud Console Setup

  1. Gehe zu Google Cloud Console und erstelle einen neuen OAuth 2.0 Client (Typ: Web Application).
  2. Konfiguriere die Redirect-URI exakt als:
    {APP_URL}/auth/google/callback
    
  3. Aktiviere die Scopes (minimal erforderlich):
    • openid
    • email
    • profile
  4. Konfiguriere den OAuth Consent Screen mit deinen Datenschutzerklärung- und Impressum-Links (erforderlich für Production).

Host-App Konfiguration

Füge deine Google OAuth-Credentials in die .env Datei ein:

GOOGLE_CLIENT_ID=your-client-id-here
GOOGLE_CLIENT_SECRET=your-client-secret-here

Registriere diese in deiner config/services.php:

'google' => [
    'client_id' => env('GOOGLE_CLIENT_ID'),
    'client_secret' => env('GOOGLE_CLIENT_SECRET'),
],

Wichtig: Die Callback-URL (redirect-Key) ist nicht erforderlich. Das Package registriert die Route userauth.social.callback selbstständig und verwendet sie als Callback-URL gegenüber Google.

Feature-Konfiguration

Aktiviere Social Login in .env:

# Social Login Master-Schalter
USERAUTH_SOCIAL_ENABLED=true

# Registrierung von neuen Usern per Social Login erlauben (Default: aus)
USERAUTH_SOCIAL_REGISTRATION=false

# Standard-Rolle für neue Social-Nutzer (Rollen-Key, z.B. 'user')
USERAUTH_SOCIAL_DEFAULT_ROLE=user

# Optionale Domain-Beschränkung (Komma-getrennt, leer = alle erlaubt)
# USERAUTH_SOCIAL_ALLOWED_DOMAINS=company.com,partner.com

# Gültigkeitsdauer von Bestätigungstokens (Minuten)
# USERAUTH_SOCIAL_PENDING_LINK_TTL=60

Oder verwende die veröffentlichte config/userauth.php:

'social' => [
    'enabled' => env('USERAUTH_SOCIAL_ENABLED', false),
    'allow_registration' => env('USERAUTH_SOCIAL_REGISTRATION', false),
    'default_role' => env('USERAUTH_SOCIAL_DEFAULT_ROLE', 'user'),
    'allowed_domains' => explode(',', env('USERAUTH_SOCIAL_ALLOWED_DOMAINS', '')),
    'pending_link_ttl_minutes' => env('USERAUTH_SOCIAL_PENDING_LINK_TTL', 60),
],

Verfügbarkeit von Social-Login Providern

Der Google-Login-Button (auf der Login-Seite) und der Verbinden-Link (auf der Profil-/Kontenseite) erscheinen nur wenn folgende Bedingungen erfüllt sind:

  1. USERAUTH_SOCIAL_ENABLED=true ist aktiviert UND
  2. GOOGLE_CLIENT_ID und GOOGLE_CLIENT_SECRET in .env gesetzt sind

Fehlen die Credentials, werden folgende Fehler geloggt (ohne Werte, nur Feldnamen):

Missing configuration keys: google.client_id, google.client_secret

Dies hilft bei der Fehlersuche. Die UI blendet den Button/Link aus und zeigt keinen visuellen Fehler.

Technische Fehler (z.B. Google nicht erreichbar) während des Login/Verbinden führen den User zurück zur Login-Seite (oder Profil-Seite) mit der Meldung userauth::social.errors.provider_unavailable (benutzersichtbar). Debug-Details landen nur im Log (nicht in der UI).

Views müssen Variable availableSocialProviders prüfen: Die Views (Login, Profil) erhalten vom View-Composer AvailableSocialProvidersComposer die Variable availableSocialProviders (Array von Provider-Keys). Nutze diese, um zu prüfen, ob ein Provider verfügbar ist:

@if(in_array('google', $availableSocialProviders))
    <a href="{{ route('userauth.social.redirect', 'google') }}">
        Mit Google anmelden
    </a>
@endif

Ablauf & Sicherheit (für Integratoren)

Gast-Flow (Neuer Login per Google)

  1. Domain-Prüfung: Falls allowed_domains gesetzt, wird die E-Mail-Domain + Google hd-Claim geprüft.
  2. E-Mail-Verifikation: Google muss email_verified = true melden, sonst wird der Login mit Begründung abgelehnt.
  3. Erkennung: System prüft über (provider, provider_user_id) (Google sub), nicht über E-Mail.
  4. Bestehendes Konto:
    • Bereits verknüpft → Login, danach 2FA, falls aktiv (Nutzer ist vor dem Code nicht angemeldet).
    • Nicht verknüpft, aber lokale E-Mail vorhanden:
      • E-Mail ist lokal bestätigt (email_verified_at gesetzt) → sofort verknüpfen + Login (danach 2FA, falls aktiv).
      • E-Mail ist NOT bestätigt → Bestätigungsmail an die lokale E-Mail. User klickt Link, wird verknüpft, email_verified_at wird gesetzt. Kein automatischer Login nach Bestätigung.
    • Keine lokale E-Mail:
      • allow_registration = true → neuer User (zufälliges Passwort-Hash, Standard-Rolle, Login).
      • allow_registration = false → Fehler mit verständlicher Meldung.
  5. Gelöschter User: Gibt es zur E-Mail nur ein gelöschtes Konto, entscheidet USERAUTH_GELOESCHTE_EMAIL (Default sperren: Anmeldung abgelehnt; reaktivieren: Konto wiederhergestellt und bereinigt, dann verknüpft; neu: altes Konto endgültig entfernt, dann normale Registrierung), siehe E-Mail eines gelöschten Benutzers. Ohne allow_registration bleibt die Anmeldung abgelehnt.

Bestätigungsablauf (Unverifiziierte E-Mail)

  • GET auth/social/confirm/{token} — zeigt Bestätigungsseite (keine Login).
  • POST auth/social/confirm/{token} — verknüpft das Konto, setzt email_verified_at, loggt NICHT ein. Bei einem Konto mit offener Bestätigung aus der Selbstregistrierung verfallen dabei Passwort und Angemeldet-bleiben-Tokens (siehe „Registrierung“, Ablauf 7).
  • Token ist einmalig gültig, mit konfigurierbarer Gültigkeitsdauer (Default: 60 Minuten).

2FA nach Social Login

Wenn der User two_factor_enabled hat, wird nach dem Google-Login der Code-Flow ausgelöst. Das ist dieselbe Logik wie beim Passwort-Login (siehe Zwei-Faktor-Anmeldung).

Konto-Verwaltung

  • GET social-accounts — listet alle verknüpften Social-Konten (nur für eingeloggte User).
  • DELETE social-accounts/{provider} — entfernt eine Verknüpfung. Passwort bleibt erhalten als Fallback-Methode.

Benachrichtigungen & Audit

E-Mails (queued):

  • SocialLinkConfirmation — Bestätigung für unverifiziierte Konten (GET-Link mit Token).
  • SocialAccountLinked — Info-Mail bei jeder neuen Verknüpfung (auch per Bestätigungsmail).

Audit-Log (falls USERAUTH_AUDIT_LOG_ENABLED=true):

  • Event social_linked — neue Verknüpfung.
  • Event social_unlinked — Verknüpfung entfernt.
  • Event user_reactivated / user_purged — gelöschtes Konto bei der Registrierung wiederhergestellt bzw. endgültig entfernt.

Wichtig: Queue-Worker & Scheduler

Queue-Worker für Social-Login-Mails: Die Bestätigungsmails und Info-Mails sind als ShouldQueue definiert. Damit sie versendet werden, muss ein Queue-Worker laufen:

php artisan queue:work

Ohne laufenden Worker werden die Mails in der Queue verbleiben und nicht versendet. Passwort-Reset und 2FA-Codes brauchen keinen Worker (siehe oben).

Scheduler für Wartung: Abgelaufene Bestätigungstokens werden durch den eingebauten Job userauth:prune-social-links täglich gelöscht (siehe Abschnitt Installation & Vorbereitung oben).

DSGVO / Datenschutz

  • Externe Service: Google wird als Authentifizierungsprovider genutzt. Dies muss in deiner Datenschutzerklärung dokumentiert sein (Zweck: Authentifizierung, Datenempfänger: Google LLC).
  • User-Kontrolle: Verknüpfungen können jederzeit im Profil unter social-accounts entfernt werden.
  • Audit-Trail: Alle Verknüpfungen und Entfernungen werden im Audit-Log protokolliert (wenn aktiv).

Routen-Referenz

Methode Route Name Beschreibung
GET auth/{provider}/redirect userauth.social.redirect Weiterleitung zu Google OAuth
GET auth/{provider}/callback userauth.social.callback Callback nach Google OAuth
GET auth/social/confirm/{token} userauth.social.confirm Bestätigungsseite anzeigen
POST auth/social/confirm/{token} userauth.social.confirm.store Bestätigung verarbeiten
GET social-accounts userauth.social.accounts Verknüpfte Konten auflisten
DELETE social-accounts/{provider} userauth.social.disconnect Verknüpfung entfernen

Alle Routen sind unter throttle-Middleware geschützt. Falls USERAUTH_SOCIAL_ENABLED=false, geben alle Social-Routen einen 404-Fehler zurück.

Zusätzlich für Anmeldung und Passwort:

Methode Route Name Beschreibung
GET verify verify.index Code-Eingabe (nur Gäste mit offener Anmeldung)
POST verify verify.store Code prüfen
POST verify/resend verify.resend Code erneut senden (1× je 60 Sekunden)
POST logout logout Abmelden bzw. offene 2FA abbrechen (Formular mit @csrf)
POST password/sendtocken password.sendtocken Reset-Link anfordern (5/Min. je IP)
POST password/updatewithtoken password.updatewithtoken Passwort mit Token setzen (5/Min. je IP)

Namespace

ITHilbert\UserAuth