Anmeldung wahlweise ueber OpenID Connect (Authentik)
Neben dem Passwort-Login gibt es jetzt einen zweiten, gleichberechtigten Weg: OpenID Connect gegen einen selbst gehosteten Anmeldedienst. Ohne Konfiguration aendert sich nichts - keine SSO-Knoepfe, Passwort-Login wie bisher. Der Anmeldedienst ist NUR fuer die Anmeldung zustaendig. Danach stellt Vorrania sein eigenes Token aus wie beim Passwort-Login, deshalb bleiben deps.py, die Rollenpruefung und die langlebigen API-Tokens unveraendert. Backend - Anbieter kommen aus der Umgebung (OIDC_PROVIDERS + OIDC_<KUERZEL>_*). Ein zweiter Anbieter ist damit reine Konfiguration, ohne Codeaenderung. - Browser-Weg (vertraulicher Client) und App-Weg (oeffentlicher Client, PKCE) enden beide im selben Token-Schema. - state/nonce/PKCE-Verifier liegen in einer kurzlebigen Tabelle statt in einem Cookie: Starlettes CORS-Schicht spiegelt bei allow_origins=["*"] zusammen mit allow_credentials die Herkunft zurueck, sobald ein Request ein Cookie traegt. Ohne Cookie entsteht diese Wechselwirkung erst gar nicht - und der Ablauf bleibt mit der db-Fixture pruefbar. - Zuordnung ueber die sub des Anbieters; die Erstverknuepfung laeuft ueber die BESTAETIGTE E-Mail-Adresse. Rollen kommen nie aus dem Anbieter, und ein gleichlautender preferred_username uebernimmt nie ein bestehendes Konto. - users.password_hash darf jetzt leer sein (reine SSO-Konten). verify_password faengt das ab, sonst antwortete der Login mit 500 statt 401. Verknuepfen bestehender Konten - Neue Seite "Mein Konto" (fuer alle Benutzer, nicht nur Admins): verknuepfen und wieder loesen. Die Berechtigung stammt aus der laufenden Sitzung, es braucht also weder eine hinterlegte Adresse noch Admin-Rechte. - Der Start laeuft ueber POST, weil eine Seitennavigation das Bearer-Token nicht mitschicken kann. - Die letzte Anmeldemoeglichkeit eines Kontos laesst sich nicht loesen. iOS - AppAuth (erste Paketabhaengigkeit) mit PKCE im System-Browser, nie in einem eingebetteten WebView, und ohne Client-Secret. - offline_access nur im App-Weg: der Browser-Weg braucht kein Refresh-Token und bekaeme sonst einen ueberfluessigen Zustimmungsdialog. - Anmeldezustand je Serverprofil im Keychain; abgelaufene Token erneuert die App still im Hintergrund. Gleichzeitige 401er teilen sich eine Erneuerung. Deployment - docker-compose reicht die ganze .env durch. Einzeln aufzuzaehlen ginge nur mit fest verdrahteten Kuerzeln - genau das soll ein weiterer Anbieter nicht erfordern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8d4954610b
commit
3d0ff54bf5
+11
-2
@@ -6,6 +6,7 @@ import Icon from "./components/Icon";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import BrandMark from "./components/BrandMark";
|
||||
import Login from "./pages/Login";
|
||||
import OidcCallback from "./pages/OidcCallback";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import Products from "./pages/Products";
|
||||
import ItemList from "./pages/ItemList";
|
||||
@@ -27,6 +28,7 @@ import ShoppingList from "./pages/ShoppingList";
|
||||
import History from "./pages/History";
|
||||
import Transfer from "./pages/Transfer";
|
||||
import Settings from "./pages/Settings";
|
||||
import Konto from "./pages/Konto";
|
||||
|
||||
const navClass = ({ isActive }) => (isActive ? "nav-link active" : "nav-link");
|
||||
|
||||
@@ -104,10 +106,11 @@ function Sidebar() {
|
||||
)}
|
||||
</nav>
|
||||
<div className="sidebar-foot">
|
||||
<div className="sidebar-user">
|
||||
{/* Der eigene Name ist der naheliegendste Weg zum eigenen Konto. */}
|
||||
<NavLink to="/konto" className="sidebar-user" title="Mein Konto">
|
||||
<span className="name">{user?.username}</span>
|
||||
<span className="role">{isAdmin ? "Administrator" : "Benutzer"}</span>
|
||||
</div>
|
||||
</NavLink>
|
||||
<button className="btn-icon" onClick={handleLogout} title="Abmelden">
|
||||
<Icon name="logout" size={18} />
|
||||
</button>
|
||||
@@ -187,6 +190,8 @@ export default function App() {
|
||||
<Route path="/shopping" element={<Protected><ShoppingList /></Protected>} />
|
||||
<Route path="/mindestbestaende" element={<Protected wide><MinStock /></Protected>} />
|
||||
<Route path="/history" element={<Protected><History /></Protected>} />
|
||||
{/* Bewusst ohne adminOnly: die SSO-Verknuepfung braucht jeder selbst. */}
|
||||
<Route path="/konto" element={<Protected><Konto /></Protected>} />
|
||||
<Route path="/shops" element={<Protected adminOnly><Shops /></Protected>} />
|
||||
<Route path="/locations" element={<Protected adminOnly><Locations /></Protected>} />
|
||||
<Route path="/units" element={<Protected adminOnly wide><Units /></Protected>} />
|
||||
@@ -194,6 +199,10 @@ export default function App() {
|
||||
<Route path="/users" element={<Protected adminOnly><Users /></Protected>} />
|
||||
<Route path="/transfer" element={<Protected adminOnly><Transfer /></Protected>} />
|
||||
<Route path="/settings" element={<Protected adminOnly><Settings /></Protected>} />
|
||||
{/* Rueckweg vom Single-Sign-on: MUSS oeffentlich sein (der Nutzer ist in
|
||||
diesem Moment noch nicht angemeldet) und MUSS vor dem Auffangmuster
|
||||
unten stehen – sonst schluckt "*" die Weiterleitung samt Token. */}
|
||||
<Route path="/auth/callback" element={<OidcCallback />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user