Zum Hauptinhalt springen

Baue eine Multi-Tenant-SaaS-Anwendung: Ein vollständiger Leitfaden von Design bis Implementierung

Wie werden Apps wie Notion, Slack oder Figma gebaut? Diese Multi-Tenant-SaaS-Anwendungen wirken einfach zu bedienen, aber selbst eine zu bauen? Das ist eine andere Geschichte.

Als ich zum ersten Mal darüber nachdachte, so ein komplexes Biest zu bauen, explodierte mein Kopf:

  • Benutzer benötigen mehrere Anmeldeoptionen (E-Mail, Google, GitHub)
  • Jeder Benutzer kann mehrere Organisationen erstellen und zu mehreren Organisationen gehören
  • Unterschiedliche Berechtigungsstufen innerhalb jeder Organisation
  • Enterprise-Organisationen, die Auto-Join für bestimmte E-Mail-Domains erfordern
  • MFA-Anforderungen für sensible Operationen
  • Und mehr...

"Chef, lass uns in zwei Wochen über das Produktdesign sprechen. Ich stecke gerade fest."

Aber als ich tatsächlich damit anfing, stellte ich fest, dass es gar nicht so entmutigend ist, wie es scheint.

Ich habe ein System mit all diesen Funktionen mit überraschend wenig Aufwand gebaut!

documind-home-page.png

Documind DashboardDocumind Organisationsseite

Ich zeige dir genau, wie du ein solches System von Grund auf entwirfst und implementierst – und du wirst erstaunt sein, wie einfach es im Jahr 2025 mit modernen Tools und dem richtigen Architekturansatz wirklich ist.

Der vollständige Quellcode ist in diesem Github-Repo verfügbar. Los geht's!

Wir starten mit einem KI-Dokumentations-SaaS-Produkt namens DocuMind.

DocuMind ist ein KI-Dokumentations-SaaS-Produkt, das mit einem Multi-Tenant-Modell entwickelt wurde, um Einzelpersonen, kleine Unternehmen und Konzerne zu unterstützen.

Die Plattform bietet leistungsstarke KI-Funktionen für das Dokumentenmanagement, darunter automatische Zusammenfassungen, Schlüsselpunktextraktion und intelligente Inhaltsvorschläge innerhalb von Organisationen.

Welche Funktionen sind für SaaS-Authentifizierung und Autorisierung erforderlich?

Zuerst schauen wir uns die notwendigen Anforderungen an. Welche Funktionen brauchst du?

Multi-Tenant-Architektur

Um eine Multi-Tenant-Architektur zu ermöglichen, benötigst du eine Entitätsschicht namens Organisation. Stell dir einen einzigen Pool von Benutzern vor, die auf mehrere Arbeitsbereiche zugreifen können. Jede Organisation repräsentiert einen Arbeitsbereich, und Benutzer behalten eine einzige Identität, während sie je nach zugewiesener Rolle auf verschiedene Arbeitsbereiche (Organisationen) zugreifen.

multi-tenant-app-architecture.svg

Dies ist eine weit verbreitete Funktion bei Authentifizierungsanbietern. Eine Organisation in einem Identitätsmanagementsystem entspricht dem Workspace, Projekt oder Tenant deiner SaaS-App.

organization-examples.png

Mitgliedschaft

Ein Mitglied ist ein temporäres Konzept, das den Mitgliedsstatus einer Identität innerhalb einer Organisation anzeigt.

Beispiel: Sarah registriert sich mit ihrer E-Mail [email protected] in deiner App. Sie kann zu verschiedenen Arbeitsbereichen gehören. Wenn Sarah Teil von Workspace A ist, aber nicht von Workspace B, gilt sie als Mitglied von Workspace A, aber nicht von Workspace B.

Rollen- und Berechtigungsdesign

In einer Multi-Tenant-Architektur benötigen Benutzer Rollen mit bestimmten Berechtigungen, um auf ihre Tenant-Ressourcen zuzugreifen. Berechtigungen sind detaillierte Zugangskontrollen, die bestimmte Aktionen definieren, wie read: order oder write: order. Sie bestimmen, welche Aktionen auf bestimmten Ressourcen ausgeführt werden dürfen.

Rollen sind eine Sammlung von Berechtigungen, die Mitgliedern in einer Multi-Tenant-Umgebung zugewiesen werden.

Du musst diese Rollen und Berechtigungen definieren, dann Rollen Benutzern zuweisen, manchmal auch automatisiert. Zum Beispiel:

  1. Benutzer, die einer Organisation beitreten, erhalten automatisch die Mitglied-Rolle.
  2. Der erste Benutzer, der einen Arbeitsbereich erstellt, erhält automatisch die Admin-Rolle.

Registrierungs- und Login-Flow

Stelle einen benutzerfreundlichen und sicheren Registrierungs- und Authentifizierungsprozess sicher, einschließlich grundlegender Anmelde- und Registrierungsoptionen:

  1. E-Mail- und Passwort-Anmeldung: Traditionelle Anmeldemethode mit E-Mail und Passwort.
  2. Passwortlose Anmeldung: Verwende E-Mail-Bestätigungscodes für einfachen und sicheren Zugang.
  3. Kontoverwaltung: Ein Account Center, in dem Benutzer ihre E-Mail, ihr Passwort und weitere Details aktualisieren können.
  4. Soziale Anmeldung: Optionen wie Google und GitHub für schnelles Login.
  5. Multi-Faktor-Authentifizierung (MFA): Erhöhe die Sicherheit, indem du die Anmeldung über Authenticator-Apps wie Duo ermöglichst.

Tenant-Erstellung und Einladung

In einer Multi-Tenant-SaaS-App ist ein wichtiger Unterschied im Benutzerfluss die Unterstützung der Tenant-Erstellung und der Einladung von Mitgliedern. Dieser Prozess erfordert sorgfältige Planung und Umsetzung, da er eine Schlüsselrolle bei der Produktaktivierung und beim Wachstum spielt.

Hier sind einige typische Nutzungsflüsse, die du berücksichtigen solltest:

BenutzertypEinstiegspunkt
Neues KontoEinstieg über Anmelde- und Registrierungsseite zur Erstellung eines neuen Tenants
Bestehendes KontoErstelle einen weiteren Tenant innerhalb des Produkts
Bestehendes Konto erhält eine neue Tenant-EinladungEinstieg über Anmelde- und Registrierungsseite
Bestehendes Konto erhält eine neue Tenant-EinladungEinstieg über die Einladungsemail
Neues Konto erhält eine neue Tenant-EinladungEinstieg über Anmelde- und Registrierungsseite
Neues Konto erhält eine neue Tenant-EinladungEinstieg über die Einladungsemail

Dies sind gängige Szenarien, die in fast jeder SaaS-App zu finden sind. Nutze sie als Referenz, um dein Produkt- und Designteam zu inspirieren, und erstelle bei Bedarf eigene Flows.

Ein neues Konto erstellt einen TenantEin bestehender Benutzer erstellt einen weiteren Tenant
Ein bestehender Benutzer meldet sich anEin bestehender Benutzer tritt per E-Mail bei
Ein neuer Benutzer meldet sich anEin neuer Benutzer tritt per E-Mail bei

Technische Architektur und Systemdesign

Sobald wir alle Produktanforderungen verstanden haben, gehen wir zur Umsetzung über.

Authentifizierungsstrategie festlegen

Authentifizierung wirkt abschreckend. Benutzer benötigen:

  • E-Mail & Passwort Registrierung / Login
  • Ein-Klick-Anmeldung mit Google / Github
  • Passwort zurücksetzen, wenn sie es vergessen
  • Teamweite Anmeldung für Unternehmenskunden
  • ...

Allein diese Grundfunktionen zu implementieren, könnte Wochen dauern.

Aber jetzt müssen wir das alles NICHT mehr selbst bauen!

Moderne Auth-Anbieter (ich wähle diesmal Logto) haben all diese Funktionen für uns gebündelt. Der Authentifizierungs-Flow ist unkompliziert:

Von Wochen Entwicklungszeit zu 15 Minuten Einrichtung – Logto übernimmt alle komplexen Abläufe für uns! Die Integrationsschritte behandeln wir später im Implementierungsteil. Jetzt können wir uns auf die Kernfunktionen von DocuMind konzentrieren!

Multi-Tenant-Architektur etablieren

Das Organisationssystem ermöglicht es Benutzern, mehrere Organisationen zu erstellen und ihnen beizutreten. Schauen wir uns die Kernbeziehungen an:

In diesem System kann jeder Benutzer mehreren Organisationen angehören, und jede Organisation kann mehrere Mitglieder haben.

Zugangskontrolle in der Multi-Tenant-App aktivieren

Rollenbasierte Zugangskontrolle (RBAC) ist wichtig, um Sicherheit und Skalierbarkeit in Multi-Tenant-SaaS-Anwendungen zu gewährleisten.

In einer Multi-Tenant-App ist das Design von Berechtigungen und Rollen meist konsistent, da es aus dem Produktdesign hervorgeht. Beispielsweise gibt es in mehreren Arbeitsbereichen typischerweise eine Admin-Rolle und eine Mitgliederrolle. Logto als Auth-Anbieter hat folgendes organisationsbasiertes rollenbasiertes Zugangskontroll-Design:

  1. Einheitliche Berechtigungsdefinitionen: Berechtigungen werden auf Systemebene definiert und gelten konsistent für alle Organisationen, was eine wartbare und konsistente Berechtigungsverwaltung gewährleistet.
  2. Organisationstemplates: Vorgefertigte Rollen- und Berechtigungskombinationen durch Organisationstemplates, die die Initialisierung von Organisationen vereinfachen.

Die Berechtigungsbeziehung sieht so aus:

Da jeder Benutzer innerhalb jeder Organisation eigene Rolle(n) benötigt, muss die Beziehung zwischen Rollen und Organisationen die zugewiesenen Rollen jedes Benutzers widerspiegeln:

Wir haben das Organisationssystem und das Zugangskontrollsystem entworfen, jetzt können wir mit dem Bau unseres Produkts beginnen!

Tech-Stack

Ich habe einen einsteigerfreundlichen, portablen Stack gewählt:

  1. Frontend: React (leicht auf Vue / Angular / Svelte übertragbar)
  2. Backend: Express (einfaches, intuitives API)

Warum Frontend und Backend trennen? Weil es eine klare Architektur bietet, leicht zu erlernen und einfach den Stack zu wechseln ist. Und als Auth-Anbieter verwende ich Logto als Beispiel.

Und für die folgenden Anleitungen gilt: Das Muster funktioniert mit jedem Frontend, jedem Backend und jedem Auth-System.

Füge deiner App einen grundlegenden Authentifizierungs-Flow hinzu

Das ist der einfachste Schritt. Wir müssen Logto nur in unser Projekt integrieren. Dann können wir im Logto Console die Methoden für Benutzerlogin / Registrierung nach unseren Bedürfnissen konfigurieren.

Logto in deiner App installieren

Melde dich zuerst bei Logto Cloud an. Du kannst ein kostenloses Konto erstellen, falls du noch keines hast. Erstelle einen Entwicklungstenant zum Testen.

Im Tenant Console klicke auf die Schaltfläche "Application" auf der linken Seite. Wähle dann React, um mit dem Aufbau unserer Anwendung zu beginnen.

Folge der Anleitung auf der Seite. Die Logto-Integration ist in etwa 5 Minuten erledigt!

Hier ist mein Integrationscode:

const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
};

function App() {
return (
<LogtoProvider config={config}>
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100">
<Routes>
{/* Dieser Callback verarbeitet die Benutzer-Login-Weiterleitung von Logto */}
<Route path="/callback" element={<Callback />} />
<Route path="/*" element={<AppContent />} />
</Routes>
</div>
</LogtoProvider>
);
}

function AppContent() {
const { isAuthenticated } = useLogto();

if (!isAuthenticated) {
// Zeige Landingpage für nicht authentifizierte Benutzer
return <Landing />;
}

// Zeige Haupt-App für authentifizierte Benutzer
return (
<Routes>
{/* Dashboard zeigt alle verfügbaren Organisationen */}
<Route path="/" element={<Dashboard />} />

{/* Organisationsseite nach Klick auf eine Organisation im Dashboard */}
<Route path="/:orgId" element={<Organization />} />
</Routes>
);
}

documind-home-page.png

Ein nützlicher Trick: Unsere Login-Seite hat sowohl Anmelden- als auch Registrieren-Buttons. Der Registrieren-Button führt direkt zur Registrierungsseite von Logto. Das funktioniert über Logtos First Screen-Funktion. Sie bestimmt, welchen Schritt des Auth-Flows Benutzer zuerst sehen.

Du kannst die Registrierungsseite als Standard festlegen, wenn dein Produkt viele neue Benutzer erwartet.

function LandingPage() {
const { signIn } = useLogto();

return (
<div className="landing-container">
<div className="auth-buttons">
<button
className="sign-in-button"
onClick={() => {
signIn({
redirectUri: '<YOUR_APP_CALLBACK_URL>',
});
}}
>
Anmelden
</button>

<button
className="register-button"
onClick={() => {
signIn({
redirectUri: '<YOUR_APP_CALLBACK_URL>',
firstScreen: 'register',
});
}}
>
Registrieren
</button>
</div>
</div>
);
}

Nach dem Klick auf Anmelden gelangst du zur Logto-Login-Seite. Nach erfolgreichem Login (oder Registrierung) – Glückwunsch! Deine App hat ihren ersten Benutzer (dich)!

Und rufe die Funktion signOut aus dem useLogto-Hook auf, um den Benutzer abzumelden, wann immer du möchtest.

function SignOutButton() {
const { signOut } = useLogto();

return <button onClick={() => signOut('<YOUR_POST_LOGOUT_REDIRECT_URL>')}>Abmelden</button>;
}

Anmelde- und Registrierungsarten anpassen

Im Logto Console klicke im linken Menü auf "Sign-in Experience". Dann auf den Tab "Sign-up and sign-in". Auf dieser Seite folge den Anweisungen, um Logtos Login- / Registrierungsarten zu konfigurieren.

sign-in-experience-settings.png

Und der Anmelde-Flow sieht dann so aus:

Logto Anmeldeseite

Multi-Faktor-Authentifizierung aktivieren

Mit Logto ist das Aktivieren von MFA einfach. Klicke einfach auf die Schaltfläche "Multi-factor auth" im Logto Console. Aktiviere es dann auf der Multi-Faktor-Authentifizierungsseite.

mfa-settings.png

Und der MFA-Flow sieht dann so aus:

MFA-VerifizierungsschrittQR-Code in Authenticator-App scannen

Alles ist so einfach! Wir haben in wenigen Minuten ein komplexes Benutzer-Authentifizierungssystem eingerichtet!

Multi-Tenant-Organisationserlebnis hinzufügen

Jetzt haben wir unseren ersten Benutzer! Allerdings gehört dieser Benutzer noch keiner Organisation an, und wir haben noch keine Organisationen erstellt.

Logto bietet integrierte Unterstützung für Multi-Tenancy. Du kannst beliebig viele Organisationen in Logto erstellen. Jede Organisation kann mehrere Mitglieder haben.

Jeder Benutzer kann seine Organisationsinformationen von Logto abrufen. Das ermöglicht Multi-Tenancy-Unterstützung.

Organisationsinformationen eines Benutzers abrufen

Um die Organisationsinformationen eines Benutzers von Logto zu erhalten, befolge diese zwei Schritte:

Deklariere den Zugriff auf Organisationsinformationen im Logto Config. Das geschieht durch Setzen der entsprechenden scopes und resources.

import { UserScope, ReservedResource } from "@logto/react";
const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
scopes: [UserScope.Organizations], // Wert: "urn:logto:scope:organizations"
resources: [ReservedResource.Organization], // Wert: "urn:logto:resource:organizations"
};

Nutze Logtos fetchUserInfo-Methode, um Benutzerinformationen einschließlich Organisationsdaten zu erhalten.

function Dashboard() {
// Benutzerinfo abrufen
const { fetchUserInfo } = useLogto();
const [organizations, setOrganizations] = useState<OrganizationData[]>([]);
const [loading, setLoading] = useState(false);

useEffect(() => {
const loadOrganizations = async () => {
try {
setLoading(true);
// Benutzerinfo abrufen
const userInfo = await fetchUserInfo();
// Organisationsinfo des Benutzers abrufen
const organizationData = userInfo?.organization_data || [];
setOrganizations(organizationData);
} catch (error) {
console.error('Fehler beim Abrufen der Organisationen:', error);
} finally {
setLoading(false);
}
};

loadOrganizations();
}, [fetchUserInfo]);

if (loading) {
return <div>Lädt...</div>;
}

if (organizations.length === 0) {
return <div>Du bist noch kein Mitglied einer Organisation</div>;
}

return <div>Organisationen: {organizations.map(org => org.name).join(', ')}</div>;
}

Nach Abschluss dieser Schritte musst du dich ab- und wieder anmelden. Das ist notwendig, weil wir den angeforderten Scope und die Resource geändert haben.

Im Moment hast du noch keine Organisationen erstellt. Der Benutzer ist auch noch keiner Organisation beigetreten. Das Dashboard zeigt "Du hast noch keine Organisation".

dashboard-no-orgs.png

Als Nächstes erstellen wir eine Organisation für unsere Benutzer und fügen sie hinzu.

Dank Logto müssen wir keine komplexen Organisationsbeziehungen bauen. Wir müssen nur eine Organisation in Logto erstellen und Benutzer hinzufügen. Logto übernimmt die gesamte Komplexität für uns. Es gibt zwei Möglichkeiten, Organisationen zu erstellen:

  1. Organisationen manuell über das Logto Console erstellen
  2. Die Logto Management API verwenden, um Organisationen zu erstellen, insbesondere wenn du einen SaaS-Flow entwirfst, bei dem Benutzer ihre eigenen Organisationen (Workspaces) erstellen können.

Organisation im Logto Console erstellen

Klicke im Logto Console auf die Schaltfläche "Organizations" im linken Menü. Erstelle eine Organisation.

Jetzt hast du deine erste Organisation.

console-organizations.png

Als Nächstes fügen wir den Benutzer zu dieser Organisation hinzu.

Gehe zur Organisationsdetailseite. Wechsle zum Tab Mitglieder. Klicke auf die Schaltfläche "+ Mitglied hinzufügen". Wähle deinen Login-Benutzer aus der linken Liste. Klicke auf die Schaltfläche "Mitglieder hinzufügen" unten rechts. Jetzt hast du den Benutzer erfolgreich zur Organisation hinzugefügt.

console-add-member-to-orgs.png

Aktualisiere deine APP-Seite. Du siehst, dass der Benutzer jetzt zu einer Organisation gehört!

dashboard-has-orgs.png

Self-Service-Organisationserstellung implementieren

Eine Organisation im Console zu erstellen reicht nicht aus. Deine SaaS-App benötigt einen Flow, der Endbenutzern das einfache Erstellen und Verwalten eigener Workspaces ermöglicht. Um diese Funktion zu implementieren, verwende die Logto Management API.

Für Anleitungen siehe die Dokumentation Mit Management API interagieren, um die API-Kommunikation mit Logto einzurichten.

Auth-Interaktionsfluss für Organisationen verstehen

Nehmen wir den Organisations-Erstellungs-Flow als Beispiel. So funktioniert der Prozess:

Dieser Flow hat zwei wichtige Authentifizierungsanforderungen:

  1. Backend-Service-API schützen:
    • Frontend-Zugriff auf unsere Backend-Service-API erfordert Authentifizierung
    • API-Endpunkte werden durch Validierung des Logto Zugangstokens des Benutzers geschützt
    • Stellt sicher, dass nur authentifizierte Benutzer auf unsere Dienste zugreifen können
  2. Zugriff auf Logto Management API:
    • Backend-Service muss sicher die Logto Management API aufrufen können
    • Folge dem Leitfaden Mit Management API interagieren für die Einrichtung
    • Verwende Maschine-zu-Maschine-Authentifizierung, um Zugangsdaten zu erhalten

Dein Backend-API schützen

Erstelle zunächst einen API-Endpunkt in unserem Backend-Service zur Organisationserstellung.

app.post('/organizations', async (req, res) => {
// Implementierung mit Logto Management API
// ...
});

Unsere Backend-Service-API erlaubt nur authentifizierten Benutzern den Zugriff. Wir müssen Logto verwenden, um unsere API zu schützen. Außerdem benötigen wir die aktuellen Benutzerinformationen (wie Benutzer-ID).

Im Logto-Konzept (und OAuth 2.0) agiert unser Backend-Service als Ressourcensserver. Benutzer greifen mit einem Zugangstoken vom Frontend auf den DocuMind-Ressourcenserver zu. Der Ressourcenserver überprüft dieses Token. Ist es gültig, gibt er die angeforderten Ressourcen zurück.

Erstelle eine API-Ressource, um unseren Backend-Service darzustellen.

Gehe zum Logto Console.

  1. Klicke auf die Schaltfläche "API resources" rechts.
  2. Klicke auf "Create API resource". Wähle Express im Popup.
  3. Gib als API-Name "DocuMind API" ein. Verwende "https://api.documind.com" als API-Identifier.
  4. Klicke auf Erstellen.

Keine Sorge wegen dieser API-Identifier-URL. Sie ist nur ein eindeutiger Bezeichner für deine API in Logto. Sie ist nicht mit deiner tatsächlichen Backend-Service-URL verbunden.

Du siehst ein Tutorial zur Verwendung der API-Ressource. Du kannst diesem Tutorial oder unseren Schritten unten folgen.

Erstelle ein requireAuth-Middleware, um unseren POST /organizations-Endpunkt zu schützen.

const { createRemoteJWKSet, jwtVerify } = require('jose');

const getTokenFromHeader = (headers) => {
const { authorization } = headers;
const bearerTokenIdentifier = 'Bearer';

if (!authorization) {
throw new Error('Authorization header fehlt');
}

if (!authorization.startsWith(bearerTokenIdentifier)) {
throw new Error('Authorization-Token-Typ wird nicht unterstützt');
}

return authorization.slice(bearerTokenIdentifier.length + 1);
};

const requireAuth = (resource) => {
if (!resource) {
throw new Error('Resource-Parameter ist für die Authentifizierung erforderlich');
}

return async (req, res, next) => {
try {
// Token extrahieren
const token = getTokenFromHeader(req.headers);

const { payload } = await jwtVerify(
token,
createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL)),
{
issuer: process.env.LOGTO_ISSUER,
audience: resource,
}
);

// Benutzerinfo zur Anfrage hinzufügen
req.user = {
id: payload.sub,
};

next();
} catch (error) {
console.error('Auth-Fehler:', error);
res.status(401).json({ error: 'Nicht autorisiert' });
}
};
};

module.exports = {
requireAuth,
};

Um dieses Middleware zu verwenden, benötigen wir diese Umgebungsvariablen:

  • LOGTO_JWKS_URL
  • LOGTO_ISSUER

Hole diese Variablen aus dem OpenID-Konfigurationsendpunkt deines Logto-Tenants. Besuche https://<your-tenant-id>.logto.app/oidc/.well-known/openid-configuration. Die benötigten Informationen findest du im zurückgegebenen JSON:

{
"jwks_uri": "<https://tenant-id.logto.app/oidc/jwks>",
"issuer": "<https://tenant-id.logto.app/oidc>"
}

Jetzt verwende das requireAuth-Middleware in unserem POST /organizations-Endpunkt.

app.post('/organizations', requireAuth('<https://api.documind.com>'), async (req, res) => {
// Logik zur Organisationserstellung
// ...
});

Damit ist unser POST /organizations-Endpunkt geschützt. Nur Benutzer mit gültigen Logto Zugangstokens können darauf zugreifen.

Wir können das Token jetzt im Frontend von Logto erhalten. Benutzer können mit diesem Token Organisationen über unseren Backend-Service erstellen. Das Middleware gibt uns auch die Benutzer-ID. Das hilft beim Hinzufügen von Benutzern zu Organisationen.

Im Frontend-Code deklariere diese API-Ressource im Logto-Config. Füge ihren Identifier zum resources-Array hinzu.

const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
scopes: [UserScope.Organizations],
resources: [ReservedResource.Organization, "<https://api.documind.com>"], // Neu erstellter API-Ressourcen-Identifier
};

Wie zuvor müssen sich Benutzer nach der Aktualisierung des Logto-Configs erneut anmelden.

Im Dashboard hole das Logto Zugangstoken, wenn eine Organisation erstellt wird. Verwende dieses Token, um auf unsere Backend-Service-API zuzugreifen.

// Zugangstoken für "DocuMind API" holen
const token = await getAccessToken('<https://api.documind.com>');

// Zugriff auf unsere Backend-Service-API mit dem Token
const response = await fetch('<http://localhost:3000/organizations>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: 'Organisation A',
description: 'Beschreibung Organisation A',
}),
});

Jetzt können wir korrekt auf die DocuMind-Backend-Service-API zugreifen.

Logto Management API aufrufen

Implementieren wir die Organisationserstellung mit der Logto Management API.

Wie bei Frontend-Anfragen an den Backend-Service benötigen Backend-Service-Anfragen an Logto Zugangstokens.

In Logto verwenden wir Maschine-zu-Maschine-Authentifizierung für Zugangstokens. Siehe Mit Management API interagieren.

Gehe zur Anwendungsseite im Logto Console. Erstelle eine Maschine-zu-Maschine-Anwendung. Weise die Rolle "Logto Management API access" zu. Kopiere den Token-Endpunkt, App-ID und App-Secret. Wir verwenden diese für Zugangstokens.

m2m-application.png

Jetzt können wir Logto Management API Zugangstokens über diese M2M-Anwendung erhalten.

async function fetchLogtoManagementApiAccessToken() {
const response = await fetch(process.env.LOGTO_MANAGEMENT_API_TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(
`${process.env.LOGTO_MANAGEMENT_API_APPLICATION_ID}:${process.env.LOGTO_MANAGEMENT_API_APPLICATION_SECRET}`
).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
resource: process.env.LOGTO_MANAGEMENT_API_RESOURCE,
scope: 'all',
}).toString(),
});
const data = await response.json();
return data.access_token;
}

Verwende dieses Zugangstoken, um die Logto Management API aufzurufen.

Wir verwenden diese Management-APIs:

app.post('/organizations', requireAuth('<https://api.documind.com>'), async (req, res) => {
const accessToken = await fetchLogtoManagementApiAccessToken();
// Organisation in Logto erstellen und Benutzer hinzufügen
const response = await fetch(`${process.env.LOGTO_ENDPOINT}/api/organizations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
name: req.body.name,
description: req.body.description,
}),
});

const createdOrganization = await response.json();

await fetch(`${process.env.LOGTO_ENDPOINT}/api/organizations/${createdOrganization.id}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
userIds: [req.user.id],
}),
});

res.json({ data: createdOrganization });
});

Wir haben jetzt die Organisationserstellung über die Logto Management API implementiert. Wir können auch Benutzer zu Organisationen hinzufügen.

Testen wir diese Funktion im Dashboard.

dashboard-create-org.png

und klicke auf „Organisation erstellen“

dashboard-has-orgs.png

Erstellung erfolgreich!

Der nächste Schritt wäre, Benutzer zu einer Organisation einzuladen. Wir implementieren diese Funktion in unserem Tutorial noch nicht. Du weißt bereits, wie du die Management API verwendest. Du kannst dich an Tenant-Erstellung und Einladung als Produktdesign-Referenz orientieren und diese Funktion leicht mit diesem Blogpost umsetzen: How we implement user collaboration within a multi-tenant app.

Zugangskontrolle für deine Multi-Tenant-App implementieren

Kommen wir nun zur Zugangskontrolle für Organisationen.

Wir wollen erreichen:

  • Benutzer können nur auf Ressourcen ihrer eigenen Organisationen zugreifen: Das kann über Logtos Organisationstoken erfolgen
  • Benutzer haben spezifische Rollen innerhalb von Organisationen (mit unterschiedlichen Berechtigungen), um autorisierte Aktionen auszuführen: Das kann über Logtos Organisationstemplate-Funktion umgesetzt werden

Schauen wir uns an, wie diese Funktionen implementiert werden.

Verwendung des Logto Organisationstokens

Ähnlich wie beim zuvor erwähnten Logto Zugangstoken stellt Logto ein Zugangstoken aus, das einer bestimmten Ressource entspricht, und Benutzer verwenden dieses Token, um auf geschützte Ressourcen im Backend-Service zuzugreifen. Entsprechend stellt Logto ein Organisationstoken aus, das einer bestimmten Organisation entspricht, und Benutzer verwenden dieses Token, um auf geschützte Organisationsressourcen im Backend-Service zuzugreifen.

In der Frontend-Anwendung können wir mit Logtos getOrganizationToken-Methode ein Token für den Zugriff auf eine bestimmte Organisation erhalten.

const { getOrganizationToken } = useLogto();
const organizationToken = await getOrganizationToken(organizationId);

Hier ist organizationId die ID der Organisation, zu der der Benutzer gehört.

Bevor du getOrganization oder andere Organisationsfunktionen verwendest, stelle sicher, dass der Scope urn:logto:scope:organizations und die Resource urn:logto:resource:organization im Logto-Config enthalten sind. Da wir das bereits deklariert haben, wiederholen wir es nicht.

Auf unserer Organisationsseite verwenden wir das Organisationstoken, um Dokumente innerhalb der Organisation abzurufen.

function OrganizationPage() {
const { organizationId } = useParams();
const navigate = useNavigate();
const { signOut, getOrganizationToken } = useLogto();
const [error, setError] = useState<Error | null>(null);
const [documents, setDocuments] = useState([]);

const fetchDocuments = useCallback(async () => {
if (!organizationId) return;

try {
const organizationToken = await getOrganizationToken(organizationId);
const response = await fetch(`http://localhost:3000/documents`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${organizationToken}`,
},
});
const documents = await response.json();
setDocuments(documents);
} catch (error: unknown) {
if (error instanceof Error) {
setError(error);
} else {
setError(new Error(String(error)));
}
}
},[getOrganizationToken, organizationId]);

useEffect(() => {
void fetchDocuments();
}, [fetchDocuments]);

if (error) {
return <div>Fehler: {error.message}</div>;
}

return <div>
<h1>Organisationsdokumente</h1>
<ul>
{documents.map((document) => (
<li key={document.id}>{document.name}</li>
))}
</ul>
</div>
}

Zwei wichtige Punkte sind bei dieser Implementierung zu beachten:

  1. Wenn die an getOrganizationToken übergebene organizationId keine Organisation ist, zu der der aktuelle Benutzer gehört, kann diese Methode kein Token erhalten. So wird sichergestellt, dass Benutzer nur auf ihre eigenen Organisationen zugreifen können.
  2. Beim Anfordern von Organisationsressourcen verwenden wir das Organisationstoken anstelle des Zugangstokens, da wir für Ressourcen, die zu einer Organisation gehören, die Organisationsberechtigungssteuerung und nicht die Benutzerberechtigungssteuerung verwenden möchten (das wird beim Implementieren der API GET /documents später noch deutlicher).

Als Nächstes erstellen wir eine API GET /documents in unserem Backend-Service. Ähnlich wie wir die API-Ressource zum Schutz der API POST /organizations verwenden, nutzen wir organisationsspezifische Ressourcenindikatoren, um die API GET /documents zu schützen.

Erstellen wir zunächst ein requireOrganizationAccess-Middleware, um Organisationsressourcen zu schützen.

const getTokenFromHeader = (headers) => {
const { authorization } = headers;
const bearerTokenIdentifier = 'Bearer';

if (!authorization) {
throw new Error('Authorization header fehlt');
}

if (!authorization.startsWith(bearerTokenIdentifier)) {
throw new Error('Authorization-Token-Typ wird nicht unterstützt');
}

return authorization.slice(bearerTokenIdentifier.length + 1);
};

const extractOrganizationId = (aud) => {
if (!aud || typeof aud !== 'string' || !aud.startsWith('urn:logto:organization:')) {
throw new Error('Ungültiges Organisationstoken');
}
return aud.replace('urn:logto:organization:', '');
};

const decodeJwtPayload = (token) => {
try {
const [, payloadBase64] = token.split('.');
if (!payloadBase64) {
throw new Error('Ungültiges Token-Format');
}
const payloadJson = Buffer.from(payloadBase64, 'base64').toString('utf-8');
return JSON.parse(payloadJson);
} catch (error) {
throw new Error('Fehler beim Dekodieren des Token-Payloads');
}
};

const requireOrganizationAccess = () => {
return async (req, res, next) => {
try {
// Token extrahieren
const token = getTokenFromHeader(req.headers);

// Audience dynamisch aus dem Token holen
const { aud } = decodeJwtPayload(token);
if (!aud) {
throw new Error('Audience im Token fehlt');
}

// Token mit Audience verifizieren
const { payload } = await jwtVerify(
token,
createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL)),
{
issuer: process.env.LOGTO_ISSUER,
audience: aud,
}
);

// Organisations-ID aus dem Audience-Claim extrahieren
const organizationId = extractOrganizationId(payload.aud);

// Organisationsinfo zur Anfrage hinzufügen
req.user = {
id: payload.sub,
organizationId,
};

next();
} catch (error) {
console.error('Organisation Auth-Fehler:', error);
res.status(401).json({ error: 'Nicht autorisiert - Ungültiger Organisationszugriff' });
}
};
};

Dann verwenden wir das requireOrganizationAccess-Middleware, um die API GET /documents zu schützen.

app.get('/documents', requireOrganizationAccess(), async (req, res) => {
// Du kannst die aktuelle Benutzer-ID und OrganisationId über req.user erhalten
console.log('userId', req.user.id);
console.log('organizationId', req.user.organizationId);

// Dokumente aus der Datenbank nach OrganisationId abrufen
// ....
const documents = await getDocumentsByOrganizationId(req.user.organizationId);

res.json(documents);
});

So haben wir die Verwendung von Organisationstokens zum Zugriff auf Organisationsressourcen implementiert. Im Backend-Service kannst du entsprechende Ressourcen aus der Datenbank anhand der Organisations-ID abrufen.

Manche Software erfordert Datenisolation zwischen Organisationen. Für weitere Diskussionen und Implementierungen siehe den Blogpost: Multi-Tenancy-Implementierung mit PostgreSQL: Lerne anhand eines einfachen Praxisbeispiels.

Organisationsebene rollenbasierte Zugangskontrolle implementieren

Wir haben die Verwendung von Organisationstokens zum Zugriff auf Organisationsressourcen implementiert. Als Nächstes implementieren wir die Benutzerberechtigungssteuerung innerhalb von Organisationen mit RBAC.

Nehmen wir an, DocuMind hat zwei Rollen: Admin und Mitarbeiter.

Admins können Dokumente erstellen und darauf zugreifen, während Mitarbeiter nur auf Dokumente zugreifen können.

Unsere Organisation benötigt also diese beiden Rollen: Admin und Mitarbeiter.

Admin hat sowohl die Berechtigungen read:documents als auch create:documents, während Mitarbeiter nur die Berechtigung read:documents haben.

  • Admin
    • read:documents
    • create:documents
  • Mitarbeiter
    • read:documents

Hier kommt die Organisationstemplate-Funktion von Logto ins Spiel.

Ein Organisationstemplate ist eine Blaupause des Zugangskontrollmodells für jede Organisation: Es definiert die Rollen und Berechtigungen, die für alle Organisationen gelten.

Warum Organisationstemplate?

Weil Skalierbarkeit eine der wichtigsten Anforderungen für SaaS-Produkte ist. Mit anderen Worten: Was für einen Kunden funktioniert, sollte für alle Kunden funktionieren.

Gehe zu Logto Console > Organization Templates > Organization permissions und erstelle zwei Berechtigungen: read:documents und create:documents.

org-template-permission.png

Gehe dann zum Tab Organisationsrollen, um zwei Benutzerrollen zu erstellen: Admin und Mitarbeiter, und weise diesen Rollen die entsprechenden Berechtigungen zu.

organization-details.png

So haben wir ein RBAC-Berechtigungsmodell für jede Organisation erstellt.

Als Nächstes gehen wir auf unsere Organisationsdetailseite, um unseren Mitgliedern die passenden Rollen zuzuweisen.

org-template-role.png

Jetzt haben unsere Organisationsbenutzer Rollen! Du kannst diese Schritte auch über die Logto Management API durchführen:

// Weisen Sie dem Organisationsersteller die Rolle 'Admin' zu
app.post('/organizations', requireAuth('https://api.documind.com'), async (req, res) => {
const accessToken = await fetchLogtoManagementApiAccessToken();
// Organisation in Logto erstellen
// bestehender Code...

// Benutzer zur Organisation in Logto hinzufügen
await fetch(`${process.env.LOGTO_ENDPOINT}/api/organizations/${createdOrganization.id}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
userIds: [req.user.id],
}),
});

// Weisen Sie dem ersten Benutzer die Rolle `Admin` zu.
const rolesResponse = await fetch(`${process.env.LOGTO_ENDPOINT}/api/organization-roles`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
});

const roles = await rolesResponse.json();

// Finde die `Admin`-Rolle
const adminRole = roles.find((role) => role.name === 'Admin');

// Weisen Sie dem ersten Benutzer die Rolle `Admin` zu.
await fetch(
`${process.env.LOGTO_ENDPOINT}/api/organizations/${createdOrganization.id}/users/${req.user.id}/roles`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
organizationRoleIds: [adminRole.id],
}),
}
);

// bestehender Code...
});

Jetzt können wir die Benutzerberechtigungssteuerung durch Überprüfung ihrer Berechtigungen implementieren.

Im Code müssen wir dafür sorgen, dass das Organisationstoken des Benutzers Berechtigungsinformationen enthält, und diese dann im Backend überprüfen.

Im Logto-Config des Frontend-Codes müssen wir die Berechtigungen deklarieren, die Benutzer innerhalb der Organisation anfordern müssen. Fügen wir read:documents und create:documents zu den scopes hinzu.

const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
scopes: [UserScope.Organizations, "read:documents", "create:documents"],
resources: [ReservedResource.Organization, "<https://api.documind.com>"], // Neu erstellter API-Ressourcen-Identifier
};

Wie üblich, melde dich erneut mit deinem Benutzer an, damit diese Konfigurationen wirksam werden.

Dann fügen wir im Backend im requireOrganizationAccess-Middleware die Überprüfung der Benutzerberechtigungen hinzu.

const hasRequiredScopes = (tokenScopes, requiredScopes) => {
if (!requiredScopes || requiredScopes.length === 0) {
return true;
}
const scopeSet = new Set(tokenScopes);
return requiredScopes.every((scope) => scopeSet.has(scope));
};

const requireOrganizationAccess = ({ requiredScopes = [] } = {}) => {
return async (req, res, next) => {
try {
//...

// Token mit Audience verifizieren
const { payload } = await jwtVerify(
token,
createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL)),
{
issuer: process.env.LOGTO_ISSUER,
audience: aud,
}
);

//...

// Scopes aus dem Token holen
const scopes = payload.scope?.split(' ') || [];

// Erforderliche Scopes überprüfen
if (!hasRequiredScopes(scopes, requiredScopes)) {
throw new Error('Unzureichende Berechtigungen');
}

//...

next();
} catch (error) {
//...
}
};
};

Dann erstelle eine API POST /documents und verwende das requireOrganizationAccess-Middleware mit requiredScopes-Konfiguration, um diese API und die vorherige API GET /documents zu schützen.

// API zum Erstellen von Dokumenten
app.post(
'/documents',
requireOrganizationAccess({ requiredScopes: ['create:documents'] }),
async (req, res) => {
//...
}
);

// API zum Abrufen von Dokumenten
app.get(
'/documents',
requireOrganizationAccess({ requiredScopes: ['read:documents'] }),
async (req, res) => {
//...
}
);

So haben wir die Benutzerberechtigungssteuerung durch Überprüfung der Benutzerberechtigungen implementiert.

Im Frontend kannst du die Benutzerberechtigungsinformationen durch Dekodieren des Organisationstokens oder durch Aufruf von Logtos getOrganizationTokenClaims-Methode erhalten.

const [scopes, setScopes] = useState([]);
const { getOrganizationTokenClaims } = useLogto();

const loadScopes = async () => {
const claims = await getOrganizationTokenClaims(organizationId);
setScopes(claims.scope.split(' '));
};

// ...

Steuere Seitenelemente basierend auf Benutzerberechtigungen, indem du die Scopes in den Claims prüfst.

Weitere Multi-Tenant-App-Funktionen hinzufügen

Bisher haben wir die grundlegenden Benutzer- und Organisationsfunktionen in einem Multi-Tenant-SaaS-System implementiert! Es gibt jedoch noch einige Funktionen, die wir nicht behandelt haben, wie z. B. individuelles Branding der Anmeldeseite für jede Organisation, automatisches Hinzufügen von Benutzern mit bestimmten Domain-E-Mails zu bestimmten Organisationen und die Integration von Enterprise-SSO-Funktionalität.

Dies sind alles Out-of-the-Box-Funktionen, und du findest weitere Informationen dazu in der Logto-Dokumentation:

Zusammenfassung

Erinnerst du dich, wie überwältigend es am Anfang wirkte? Benutzer, Organisationen, Berechtigungen, Enterprise-Features ... es schien ein endloser Berg zu sein.

Aber schau, was wir erreicht haben:

  • Ein vollständiges Authentifizierungssystem mit mehreren Anmeldeoptionen und MFA-Unterstützung
  • Ein flexibles Organisationssystem, das mehrere Mitgliedschaften unterstützt
  • Rollenbasierte Zugangskontrolle innerhalb von Organisationen

Und das Beste daran? Wir mussten das Rad nicht neu erfinden. Durch die Nutzung moderner Tools wie Logto haben wir aus Monaten Entwicklungszeit Minuten gemacht.

Der vollständige Quellcode zu diesem Tutorial ist verfügbar unter: Multi-Tenant SaaS Sample.

Das ist die Kraft moderner Entwicklung im Jahr 2025 – wir können uns darauf konzentrieren, einzigartige Produktfunktionen zu bauen, statt mit Infrastruktur zu kämpfen. Jetzt bist du dran, etwas Großartiges zu bauen!

Entdecke alle Funktionen von Logto, von Logto Cloud bis Logto OSS, auf der Logto-Website oder registriere dich noch heute bei Logto Cloud.