E2E-Tests mit Cypress und OAuth 2.0

E2E-Tests mit Cypress und OAuth 2.0
E2E-Tests mit Cypress und OAuth 2.0

In fast jedem Projekt, dessen Lifecycle dem CI/CD-Prinzip folgt, zählen automatische Tests mit zum Repertoire. Unit- und Integration-Tests sind sinnvoll und decken eine große Anzahl der Fragestellungen ab, jedoch ist oftmals meisten von Interesse, ob die Anwendung aus Sicht des Users auch wirklich lauffähig ist. Hier kommen die End-to-End- oder kurz E2E-Tests in's Spiel. Wenn man ein stark begrenztes Projektbudget hat, empfehle ich mit E2E-Tests anzufangen. Von dort aus können die Tests nach dem Top-Down-Prinzip verfeinert werden.

Wenn öffentliche API-Endpunkte getestet werden, stellt sich die Frage der Authentifizierung nicht. Was aber, wenn geschützte API-Endpunkte getestet werden sollen. Sofern API-Keys zum Einsatz kommen, kann dieser der Testumgebung hinzugefügt werden. In Anwendungen, die von Benutzern und nicht wiederum von APIs aufgerufen werden, sind OAuth 2.0 beziehungsweise OpenID-Connect sehr häufig anzutreffen.

Das Ziel bei E2E-Tests ist es so nah wie möglich den Prozess nachzumimen, dem der Benutzer folgt. Nur so handelt es sich um einen wirklich E2E-Test. Da Authentifizierung und auch Authorization nun ein elementarer Bestandteil sind, muss dies mit in den Test integriert werden. Das Mocken eines JWT-Tokens und andere Behelfsmethoden stufen den Test zu einem Integration-Test hinab.

Nun kommt eine Schwierigkeit ins Spiel: Sinnvollerweise ist die Authentifizierung von Benutzern mit einem zweiten Faktor geschützt. Dieser Prozess ist für eine automatische Testsuite schwer oder gar nicht automatisierbar und fällt daher raus. Der Ansatz hier ist es das Authentifikationssystem anzuweisen, bestimmte Test-Tokens ohne MFA auszugeben. Wichtig an dieser Stelle ist, den Einsatzbereich dieser Test-Tokens zu limitieren, da ansonsten das MFA-Prinzip stark geschwächt wird.

In diesem Post wird das Konzept am Beispiel eines E2E-Tests mit Cypress und Keycloak als IAM-Systems gezeigt. Beide Implementationen sind sehr verbreitet und eignen sich daher gut als Beispiel.

Anlegen eines eigenen Clients für das automatisierte Testen
Für den neuen Client wird ein Client Secret festgelegt

Der Trick ist nun, dass dieser Client gar keine Benutzer authentifizieren darf. Ein missbräuchliches Verwenden im Namen der User ist selbst bei einem Leak des Secrets nicht gegeben. Stattdessen hat der Client selbst die Attribute eines Testbenutzers. Beim Anlegen dieses Testbenutzers ist darauf zu achten, dass dieser dem gleichen Prinzip wie ein echter Benutzer folgt, ansonsten wäre auch hier wieder das E2E-Prinzip gebrochen.

Dies geschieht unter "Service account roles" in den Eigenschaften des Clients. Dieser Service-Account hat dann die Attribute analog der Benutzer:

Attribute des Serviceusers

Falls die Anwendung dies unterstützt, kann der Testtoken weiterhin nur für Staging- und Development-Umgebungen freigegeben werden. Dies erhöht die Sicherheit noch einmal, verletzt aber das E2E-Prinzip. An diesem Punkt gilt es abzuwägen.

Nun muss Cypress dazu angewiesen werden sich diesen Token beim Testen automatisch zu besorgen. Hierzu wird eine neue Methode getToken() eingefügt, die das erledigt:

Cypress.Commands.add('getToken', () => {
    return cy.request({
      method: "POST",
      url: "https://auth.oliverlott.net/realms/Oliverlott/protocol/openid-connect/token",
      form: true,
      body: {
        "grant_type": "client_credentials",
        "client_id": Cypress.env('CLIENT_ID'),
        "client_secret": Cypress.env('CLIENT_SECRET'),
      },
    })
      .its("body");
  });

Diese Methode kann nun in den eigentlichen Tests vor dem Call aufgerufen werden:

context("POST /api/v1/generate-offer", () => {
  it("Test generate-offer without electro components", () => {
    cy.getToken().then(returned_value => {
      cy.log(returned_value);
      cy.request(
        {
          method: "POST",
          url: "/api/v1/generate-offer",
          headers: {
            "Content-Type": 'application/json',
            "Authorization": `Bearer ${returned_value.access_token}`,
          },
          body: JSON.stringify({
            "dealId":9861,"general":{"annualPowerDemand":2000,"consumptionPeople":2,"desiredInstallationDate":"2024-09-25T11:19:33.501Z","estimatedBudget":20000}
          }),
        }
      ).then((response) => {
        expect(response.status).to.eq(200)
      })
    })
  })
})

Die Methode liefert nun einen Token zurück. Dieser wird nun im Header Authorization hinter dem Keyword Bearer eingefügt. Der API-Call erfolgt authentifiziert.

Dieser Beispiel zeigt, dass automatische E2E-Tests und Authentifizierung sicher möglich sind. Dies erfordert zwar etwas Arbeit, allerdings ist das Ergebnis eine bessere Testabdeckung und gleichzeitig eine sichere Umgebung für den Betreiber der API und dessen Endkunden.

Oliver Lott

Oliver Lott

Vielen Dank fürs Lesen! Benötigen Sie Hilfe? Ich helfe Ihnen gerne. Schreiben Sie mir einfach eine Mail oder benutzen Sie das Kontaktformular.