Vue + Zitadel — Lernprojekt
Kleine Vue-App, die Login/Logout über Zitadel (OIDC) zeigt — ohne eigenes Passwort-Formular.
Stack: Vue 3, TypeScript, Vite
Zitadel-Instanz: https://auth.schild.work
Kernkomponente: src/components/auth-area.vue
pnpm install
pnpm dev
Wie funktioniert der Login?
Die App nutzt OpenID Connect (OIDC) mit Authorization Code + PKCE — der Standard für SPAs.
Vue App Zitadel
│ 1. Sign in/up │
│ ─────────────────────►│ Login- oder Register-Seite
│ 2. Redirect mit code │
│ ◄─────────────────────│ ?code=... auf localhost:5173
│ 3. POST /oauth/v2/token│
│ ─────────────────────►│ code + code_verifier (PKCE)
│ 4. access_token │
│ ◄─────────────────────│ (+ id_token, optional refresh_token)
Sign in → normale Login-Seite von Zitadel.
Sign up → gleicher Flow, aber mit prompt=create (direkt Registrierung).
Credentials (E-Mail/Passwort) werden nur auf Zitadel eingegeben, nie in der Vue-App.
Logout → Redirect zu Zitadel end_session, danach zurück auf die App.
Wie ist das implementiert?
Alles in auth-area.vue:
| Schritt | Was passiert |
|---|---|
| Sign in / Sign up | redirectToAuthorize() — PKCE-Verifier erzeugen, Redirect zu /oauth/v2/authorize |
| Zurück auf der App | handleCallback() liest ?code= aus der URL |
| Token holen | exchangeCodeForTokens() — POST an /oauth/v2/token |
| Session speichern | access_token und id_token in sessionStorage |
| App-Start | loadStoredSession() — Token aus sessionStorage laden |
Konfiguration oben in der Datei:
const ZITADEL_ISSUER = "https://auth.schild.work";
const CLIENT_ID = "…"; // aus Zitadel Console
const REDIRECT_URI = "http://localhost:5173/";
const POST_LOGOUT_REDIRECT_URI = "http://localhost:5173/";
const SCOPES = "openid profile email offline_access";
In Zitadel: Application (User Agent / SPA, PKCE), Redirect URIs und Post Logout Redirect URI eintragen, Register allowed für Sign up aktivieren.
Woran erkennt man, dass man eingeloggt ist?
In der App: isAuthenticated — ein computed, das prüft ob accessToken gesetzt ist.
const isAuthenticated = computed(() => accessToken.value !== null);
Im Template: eingeloggt → Name/E-Mail + Logout; nicht eingeloggt → Sign in / Sign up.
User-Anzeige: Der id_token ist ein JWT. Payload decodieren (Mitte zwischen den Punkten) liefert z. B. email, name — siehe userLabel in der Komponente.
Technisch: Ein gültiger access_token in sessionStorage (zitadel_access_token) bedeutet: Session vorhanden. Nach Tab-Schließen weg (sessionStorage), nach erneutem Öffnen neu einloggen.
API-Aufrufe mit Login
Geschützte APIs erwarten den Token im Header:
const token = sessionStorage.getItem("zitadel_access_token");
const response = await fetch("https://api.example.com/me", {
headers: {
Authorization: `Bearer ${token}`,
},
});
Die API (nicht Vue) muss das JWT prüfen — Signatur gegen Zitadel JWKS (/oauth/v2/keys), Issuer https://auth.schild.work, Ablaufzeit (exp).
| Token | Zweck |
|---|---|
access_token |
API-Aufrufe (Authorization: Bearer …) |
id_token |
User-Infos in der UI (JWT decodieren) |
refresh_token |
Neues access_token ohne erneuten Login (Scope offline_access) |
Wichtig: Token nur an eigene, vertrauenswürdige APIs senden. Die API validiert das Token serverseitig — die Vue-App allein „macht“ keine Sicherheit.
Was dieses Projekt nicht macht
- Kein eigenes Registrierungsformular (E-Mail/Passwort direkt an Zitadel) — dafür bräuchte man ein Backend mit Service-User-Token.
- Kein Refresh-Token-Handling — könnte später ergänzt werden.