SaaS-Modernisierung mit Amazon EKS, Teil 2 SaaS-Identität mittels Open ID Connect

Von Markus Kokott *

Bei der Entwicklung von SaaS-Lösungen ist die Mandantenfähigkeit eine zentrale Anforderung. Erst mit an den Mandanten gekoppelten SaaS-Identitäten ist der Zugriffsschutz gewährleistet. Dieser Beitrag zeigt, wie sich das umsetzen lässt.

Firmen zum Thema

Die Pool ID wrd benötigt, um User programmatisch über das AWS CLI anzulegen.
Die Pool ID wrd benötigt, um User programmatisch über das AWS CLI anzulegen.
(Bild: AWS Germany)

Im ersten Teil dieser SaaS-Serie haben wir unser Szenario eingeführt: Ein Softwarehersteller möchte sein erfolgreiches Lizenzprodukt zusätzlich als SaaS-Lösung anbieten. Dazu haben wir ein Kubernetes Cluster in Amazon Elastic Kubernetes Service (EKS) aufgesetzt und gezeigt wie Amazon Elastic Container Registry (ECR) für das Hosting der Container Images verwendet werden kann.

Für den Eigenbetrieb entwickelte Produkte sind oft nicht mandantenfähig ausgelegt. Zwar wird die Verwendung durch mehrere Nutzer unterstützt; der Betrieb eines Systems für mehrere Mandanten, die wiederum eigene Nutzer besitzen, ist oft jedoch eine neue Anforderung.

Gibt es im Datenmodell interne, für alle authentifizierten Nutzer zugängliche Daten, reicht eine reine Authentifizierung eines Nutzers in einer Multi-Tenant-Umgebung nicht mehr aus. Die Mandantenzugehörigkeit muss nun vielmehr geprüft werden, damit interne Daten nicht versehentlich fremden Mandanten zugänglich gemacht werden.

Wir sprechen deshalb auch von einer SaaS-Identität: die gewöhnliche Nutzeridentität wird verknüpft mit einer Mandanten-Identität. Ein Nutzer gehört dabei fest zu einem Mandanten und alle Zugriffsberechtigungen dieses Nutzers werden nur im Kontext des jeweiligen Mandanten gültig. Im Kontext eines fremden Mandanten gilt dieser Nutzer als nicht authentifiziert.

Grundsätzlich gibt es zwei Optionen, die Mandantenidentität für einen authentifizierten Nutzer im System zu ermitteln. Wir können das System die Überprüfung aktiv und auf Nachfrage durch den Aufruf eines Tenant Service durchführen lassen, der diese Information unabhängig vom Identity Provider verwaltet.

Eigenständiger Service zur Verwaltung der Mandantenzugehörigkeit.
Eigenständiger Service zur Verwaltung der Mandantenzugehörigkeit.
(Bild: AWS Germany)

Die erste Option sieht schematisch aus, wie vorangestellt. Ein eigenständiger Service zur Verwaltung der Mandantenzugehörigkeit bietet den Vorteil, dass nahezu beliebig komplexe und damit flexible Logik zur Bestimmung des Mandanten verwendet werden kann. Bei wachsendem Erfolg der Lösung überwiegen jedoch sehr schnell die Nachteile: Latenzen im System steigen, da das System an vielen Stellen den dedizierten Service aufrufen muss. Die Last auf diesem Service wird darüber hinaus zu nehmen, was dazu führt, dass der Service mit steigender Nutzerzahl skaliert werden muss.

Die Mandanten-Identität lässt sich auch an die Nutzerinformation binden.
Die Mandanten-Identität lässt sich auch an die Nutzerinformation binden.
(Bild: AWS Germany)

Bei der zweiten Option, hier im Bild, wird die Mandanten-Identität an die Nutzerinformation gebunden. Wenn der Nutzer sich einloggt, wird seine Session um die Information, zu welchem Mandanten er gehört, erweitert. Dies kann an zentraler Stelle im eigenen System erfolgen, oder aber bereits durch einen externen Identity Provider (IdP) durchgeführt werden.

An jeder Stelle im Code ist die Information dann im Request enthalten. Umgesetzt wird dies beispielsweise in Form eines Tokens als HTTP Header. In dieser Architektur wird kein dedizierter Service benötigt. Dadurch entstehen auch keine Latenzen durch die Bestimmung des zugehörigen Mandanten. Die Header-Informationen und damit der Tenant-Kontext sind an jeder Stelle des Systems verfügbar.

Wird die Mandantenzugehörigkeit benötigt, liest der jeweilige Service diese einfach aus dem Token des Nutzers aus. Darüber hinaus können viele externe Systeme wie zum Beispiel Load Balancer HTTP Header Informationen auslesen oder zum Filtern verwenden.

OpenID Connect & JSON Web Token

Diese zweite Option lässt sich sehr einfach durch den OpenID-Connect-, kurz OIDC-Standard implementieren. OIDC setzt auf dem OAuth 2.0 Framework auf und erweitert dieses um die Möglichkeit, Identifizierungsmerkmale eines Nutzers in einem IdP an externe Systeme zu übergeben.

OIDC verwendet JSON Web Token (JWT), um Zugriff auf Ressourcen zu erlauben (dies ist der OAuth 2.0 Teil von OIDC) und den Nutzer zu authentifizieren und gegebenenfalls zusätzliche Metadaten wie beispielsweise den Namen zu übermitteln. Für die SaaS-Identität sind besonders diese Metadaten interessant.

OIDC definiert ein Set von Standard-Informationen, den sogenannten Claims innerhalb eines JWT. Diese bilden im Wesentlichen das Profil des Nutzers (zum Beispiel Name, Email-Adresse, Geschlecht, Telefonnummer). Ein JWT kann darüber hinaus noch beliebige weitere Key-Value-Paare als Custom Claims enthalten.

Ein JWT Token setzt sich aus einem ID Token und einem Access Token zusammen:

{
  "token_use": "id",
  "sub": "7cfef578-a3a5-4283-bfa5-a4a38412eac4",
  "iss": "https://cognito-idp.eu-central-1.amazonaws.com/eu-west-1_LDx4Eg2rK",
  "auth_time": 1632255679,
  "exp": 1632259279,
  "iat": 1632255679,
  "name": "Time Test",
  "email": "tim.test@example.com",
  "email_verified": true,
  "custom:service-tier": "basic",
  "custom:status": "inactive",
  "custom:tenant-id": "test-tenant",
  "custom:tenant-role": "admin"
}
{
  "token_use": "access",
  "sub": "9da08b9c-0c57-4526-a81a-1410709e9332",
  "iss": "https://cognito-idp.eu-central-1.amazonaws.com/eu-west-1_LDx4Eg2rK",
  "aud": "htps://example.com/v1/"
  "scope": "read write",
  "auth_time": 1534061060,
  "exp": 1632259279,
  "iat": 1632255679
}

Letzteres wird genutzt, um eine Autorisierung durch den IdP zu realisieren. Es enthält den Scope (die gewährten Zugriffsrechte) auf eine Liste von Ressourcen. Im Beispiel oben erhält der Nutzer lesenden und schreibenden Zugriff auf Ressourcen unter https://example.com/v1/. In unserem Beispiel werden wir Access Token nicht verwenden. Wir möchten keine Autorisierung durch den IdP durchführen, sondern das ID Token um Mandanteninformationen anreichern.

Neben den Nutzerinformationen (Standard wie Custom Claims) enthält ein ID Token Metainformationen, darunter die ID des ausstellenden IdPs oder Ablaufinformationen des Tokens. Die Custom Claims können wir im ID Token dazu nutzen, Nutzeridentitäten mit ihrer Mandantenidentität zu verbinden und außerhalb der Anwendung zu verwalten. In unserem Beispiel verwenden wir vier Custom Claims:

  • Die tenant-id enthält die im Onboarding-Prozess festgelegte eindeutige ID des Mandanten, dem ein Nutzer zugeordnet ist.
  • Das service-tier beinhaltet die Informationen, welches Service-Level durch den zugeordneten Mandanten eines Nutzers aktuell gebucht ist.
  • Über den status können wir dem System mitteilen, ob der Mandant, zu dem ein Nutzer gehört, aktiv ist und die Nutzung im Rahmen seines gebuchten Service-Levels ist, oder aber Aktionen beispielsweise gedrosselt/abgelehnt werden müssen.
  • Die tenant-role enthält die Gruppenzugehörigkeit des Nutzers im System. Sie wird zum Beispiel verwendet, um Administratoren zu kennzeichnen, die selbst weitere Nutzer innerhalb eines Mandanten anlegen können.

Amazon Cognito

Zunächst benötigen wir einen IdP, der es ermöglicht, Custom Claims für registrierte Nutzer zu verwalten. Wir werden mit Amazon Cognito einen von AWS als Managed Service bereitgestellten IdP verwenden. Der Service ermöglicht eine einfache Integration in Web- und Mobile-Apps und bietet vollständige Registrierungs- und Login-Prozesse für die Anwendungsnutzer. Es unterstützt unter anderem OIDC und erlaubt es uns, Custom Claims mit den Nutzerprofilen zu verwalten.

Anlegen eines User Pools in Amazon Cognito.
Anlegen eines User Pools in Amazon Cognito.
(Bild: AWS Germany)

Amazon Cognito ist darüber hinaus ein Managed Service, für den der Nutzer keine Infrastruktur betrieben muss. Für unsere Zwecke legen wir einen User Pool in Amazon Cognito an. Ein User Pool ist eine eigenständige Nutzerdatenbank innerhalb von Cognito. Wir wählen einen passenden Namen für die Nutzerverwaltung unserer Anwendung.

Wir vergeben die Standard-Attribute für die Anwenderinnen und Anwender im User Pool.
Wir vergeben die Standard-Attribute für die Anwenderinnen und Anwender im User Pool.
(Bild: AWS Germany)

In der anschließenden Maske können wir unten nun die gewünschten Custom Claims eintragen. Da wir nicht vorsehen, dass Nutzer den Mandanten wechseln können, deaktivieren wir den Haken in der Checkbox „Mutable“ für die tenant-id. Alle weiteren Einstellungen können wir für unser Beispiel bei den voreingestellten Werten belassen. Dies gilt auch für die weiteren Masken.

Die Pool ID wrd benötigt, um User programmatisch über das AWS CLI anzulegen.
Die Pool ID wrd benötigt, um User programmatisch über das AWS CLI anzulegen.
(Bild: AWS Germany)

Nach kurzer Zeit ist der neue User Pool einsatzbereit. Für den Moment benötigen wir nur die Pool Id, die auf der Übersichtsseite des User Pools zu finden ist. Wir können nun programmatisch Nutzer erzeugen oder aber Metadaten von bestehenden Nutzern verändern. Dabei erhalten Custom Claims immer automatisch von Cognito den Präfix „custom:“.

Bei der Einrichtung von Cognito haben wir lediglich die E-Mail-Adresse als verpflichtend ausgewählt. Standard Claims wie den Namen lassen sich natürlich trotzdem hinzufügen. So lässt sich beispielsweise ein Nutzer über die AWS CLI im Amazon Cognito User Pool anlegen:

aws cognito-idp admin-create-user \
  --user-pool-id <pool-id> \
  --username tim.test@example.com \
  --user-attributes \
    Name="name",Value="Tim Test" \
    Name="email",Value="tim.test@example.com" \
    Name="custom:service-tier",Value="premium" \
    Name="custom:tenant-id",Value="test-tenant" \
    Name="custom:status",Value="inactive" \
    Name="custom:tenant-role",Value="admin"

Die Ausgabe erhält das neue User-Objekt:

User:
  Attributes:
  - Name: sub
    Value: <sub-value>
  - Name: custom:tenant-role
    Value: admin
  - Name: name
    Value: Tim Test
  - Name: custom:status
    Value: inactive
  - Name: custom:service-tier
    Value: premium
  - Name: email
    Value: tim.test@example.com
  - Name: custom:tenant-id
    Value: test-tenant
  Enabled: true
  UserCreateDate: '2021-09-03T14:21:06.646000+02:00'
  UserLastModifiedDate: '2021-09-03T14:21:06.646000+02:00'
  UserStatus: FORCE_CHANGE_PASSWORD
  Username: tim.test@example.com

Dabei ist zu beachten, dass nun der Nutzer eine Nachricht von Amazon Cognito mit einem Bestätigungs-Link an seine Email-Adresse gesendet bekommt und aufgefordert wird, sein Passwort zu setzen. Dieses Verhalten von Cognito kann nach den Anforderungen der Anwendung angepasst werden: so ist es möglich, die Verifizierung zu deaktivieren, statt der E-Mail die Telefonnummer als Pflichtfeld zu definieren und Passwörter initial durch den Administrator zu vergeben.

Markus Kokott
Markus Kokott
(Bild: AWS Deutschland)

Wir werden den User Pool und die SaaS-Identität in den folgenden Teilen dieser Reihe verwenden. Im nächsten Artikel betrachten wir zunächst, wie GitOps in Kubernetes eingesetzt werden kann, um Mandanten bereitzustellen, und wie Anwendungen sich in Multi-Tenant-Umgebungen kontrollieren sowie mit minimalem Aufwand verwalten lassen.

* Markus Kokott arbeitet als Solutions Architect bei Amazon Web Services. Er hilft Softwareherstellern dabei ihre Prozesse und Produkte zu modernisieren und damit fit für die Cloud zu machen. Technologisch interessiert sich Markus insbesondere für die Bereiche DevOps und Container.

(ID:47762427)