Cypress - Anwendung Single Sign-on (SSO)-Autorisierung auf einer anderen Superdomain

Cypress - Anwendung Single Sign-on (SSO)-Autorisierung auf einer anderen Superdomain
17/12/2020

Diejenigen, die mit dem Testwerkzeug Cypress arbeiten, wissen, dass Cypress das perfekte moderne Tool für E2E-UI-Tests ist. Aber es hat immer noch ein paar Einschränkungen, von denen einige von den Cypress-Entwicklern festgelegt wurden. Eine der Einschränkungen ist die Beschränkung auf nur eine Superdomäne pro Test.

Hinweis: Während der Fertigstellung dieses Artikels wurde die Version 6.0 von Cypress veröffentlicht. Alle Code Snippets in diesem Artikel sind für die Versionen 5.1 - 5.6 bestimmt.

In einer der Anwendungen unseres Projekts ist die Anmeldung extern über ein 2-Faktor-SSO-Gateway (Single Sign-On) implementiert, wobei der Benutzer umgeleitet wird.

Normalerweise scheitert unser Test in Cypress, wenn wir versuchen auf diese Anwendung zuzugreifen, weil wir als Benutzer automatisch zum SSO-Gateway in einer anderen Domäne umgeleitet werden.

Cypress

Es gibt zwei Möglichkeiten, wie man eine Anmeldung in einem Test implementieren kann:

  1. UI-Anmeldung
  2. Anfragen zum Netzwerk

Eine Anmeldung mit Netzwerkanforderungen wird empfohlen und sogar vom Cypress-Team verwendet, ist aber nicht immer möglich - zum Beispiel, wenn das SSO-Gateway extern konfiguriert ist und Sie keine Kontrolle darüber haben.

UI Login

Schauen wir uns zunächst an, wie wir die SSO-Anmeldung über die UI-Anmeldung implementieren können.

Dazu müssen Sie einige Änderungen in der Cypress-Konfigurationsdatei (cypress.json) vornehmen:

{% c-block language="js" %}
{
   "baseUrl": `${applicationUrl}`,
   "experimentalNetworkStubbing": true,
   "chromeWebSecurity": false
}
{% c-block-end %}

Zuerst muss 'chromeWebSecurity' auf 'false' gesetzt werden, was Cypress dazu veranlasst, keine herkunftsübergreifende Zugriffe mehr zu blockieren.

In unserem Fall wird der Test mit dieser Änderung erfolgreich sein, aber der Browser wird die Anmeldeseite aufgrund der SSO-Gateway-Inhaltssicherheitspolitik nicht anzeigen. Die Browserkonsole zeigt dann folgende Fehlermeldung:

Verweigert '${ssoGatewayUrl}' zu framen, weil ein Ancestor gegen die folgende Richtlinie der Inhaltssicherheitsrichtlinie verstößt: "frame-ancestors 'none'".

Um diese Einschränkung zu beheben, müssen wir 'experimentalNetworkStubbing' auf true setzen. Mit dieser neuen Funktion wird Cypress alle Anwendungsanforderungen (xhr, fetch usw.) abhören, und sie ermöglicht auch die Verwendung eines neuen Cypress-Befehls "cy.route2()", mit dem wir die Anwendungsanforderungen abfangen können.

Hinweis: Für Cypress 6.0 wurde der Befehl "cy.route2()" von den experimentellen Funktionen in die Standardfunktionen verschoben. In Version 6.0 brauchen Sie 'experimentalNetworkStubbing' nicht auf true zu setzen, und der Befehl heißt "cy.intercept()". Das Änderungsprotokoll der Version 6.0 finden Sie hier.

Während des Tests fangen wir Anfragen ab, die auf das SSO-Gateway zeigen.

{% c-block language="js" %}
describe('SSO resolve', () => {
   it('login', () => {
       cy.route2(`${ssoGatewayUrl}/**`).as('sso')
       cy.visit('/')
   });
})
{% c-block-end %}

Nun wird die Anmeldeseite normal angezeigt und man kann den Anmelde-Workflow ausführen.

TIPP: Wenn Ihr Login-Workflow den 2. Schritt als OTP-Token-Login (Token, der normalerweise von einer mobilen Authentisierungsanwendung bezogen wird) vollzogen hat, kann  man das Cypress-Plugin 'cypress-otp' verwenden. Das hier zu finden ist: https://docs.cypress.io/plugins/index.html. Aber man muss sich bewusst sein, dass es kein offizielles Plugin ist - es wird von der Community erstellt. Mit diesem Plugin können wir einen OTP-Token über einen benutzerdefinierten Befehl mit einem secret_key erzeugen.

Netzwerk-Anfragen

Für diejenigen, die eine schnellere Anmeldesequenz in Ihren Tests wünschen (zum Beispiel in "before()" oder "beforeEach()"-Hooks), können wir eine SSO-Anmeldung mit Netzwerkanforderungen implementieren. Die genaue Implementierung hängt hauptsächlich vom Anmelde-Workflow der Anwendung ab, so dass Sie diesen Workflow selbst im Webbrowser debuggen müssen (schauen Sie sich z.B. die Anforderungen für jeden Anmeldeschritt im Netzwerk-Tool für Browser-Entwickler an).

In unserem Fall war die Implementierung etwas schwieriger, weil das Anmeldeformular in der Benutzeroberfläche die stateId als Teil des Action-Attributs enthält (in der URL zur Umleitung).

Man kann den normalen Cypress-Befehl cy.request() zum Senden von Netzwerkanforderungen verwenden, aber für sauberere Testdateien würde ich die Erstellung benutzerdefinierter Befehle empfehlen.

Unser Workflow Case:

  1. Anmeldung mit Benutzername und Passwort
  2. Umgeleitete URL mit stateId vom OTP Anmeldeformular Action-Attribut erhalten
  3. Anmeldung mit OTP

Benutzerdefinierte Befehle können in der Datei commands.js im Support-Ordner platziert werden, oder man kann eine eigene Datei erstellen und diese dann in die Datei support/commands.js importieren.

Erstellen wir zunächst einen benutzerdefinierten Befehl für die Anmeldung mit Benutzername/Passwort.

{% c-block language="js" %}
Cypress.Commands.add('ssoUserLogin', () => {
   return new Cypress.Promise((resolve, reject) => {
       Cypress.log()

       const ssoUserLogin = async () => {
           const params = new URLSearchParams();
           params.append('username', `${username}`);
           params.append('password', `${password}`);
           params.append('login-form-type', 'pwd');

           const response = await fetch(`${ssoUserLoginUrl}`, {
               method: 'POST',
               body: params
           }).then(res => {
               if (!res.ok) {
                   reject(res.statusText)
               }
               return res
           })
           return response
       }

       ssoUserLogin().then(resolve)
   })
})
{% c-block-end %}

Wie man sehen kann, haben wir Cypress.Promise erstellt und verwenden die async/await-Methode. Dieser Befehl löst sich erst auf, wenn wir eine Antwort auf die Anfrage erhalten. Wenn die Anfrage fehlschlägt, lehnen wir dieses Versprechen ab und geben die Details der Anfrage zu Debugging-Zwecken in die Cypress-Browserkonsole zurück.

Der Befehl Cypress.log() zeigt benutzerdefinierte Befehle in der Cypress Benutzeroberfläche an.

Cypress

Als Nächstes erstellen wir einen benutzerdefinierten Befehl zum Abrufen einer umgeleiteten URL mit einer stateId aus dem OTP-Anmeldeformular.

{% c-block language="js" %}
Cypress.Commands.add('ssoState', () => {
   return new Cypress.Promise((resolve, reject) => {
       Cypress.log()

       const ssoState = async () => {
           const response = await fetch(`${ssoOtpLoginFormUrl}`).then(res => {
               if (!res.ok) {
                   reject(res.statusText)
               }

               return res.text()
           }).then((text) => {
               const html = document.createElement('html');
               html.innerHTML = text;
               const node = html.getElementsByTagName('form');
               Cypress.env('actionUrl',node[0].attributes.action.value)
           })
           return response
       }
       ssoState().then(resolve)
   })
})
{% c-block-end %}

Wir parsen die URL aus dem HTML-Element-Attribut und speichern sie als eine Cypress-Variable.

Schließlich erstellen wir einen letzten benutzerdefinierten Befehl für die OTP-Anmeldung.

{% c-block language="js" %}
Cypress.Commands.add('ssoOTPLogin', () => {
   return new Cypress.Promise((resolve, reject) => {
       Cypress.log()

       const ssoOTPLogin = async () => {
           const otpGenerator = require("cypress-otp")
           const otp = otpGenerator(`${secret_key}`).toString()
           const params = new URLSearchParams();
           const stateId = Cypress.env('actionUrl')
           params.append('otp', otp);
           params.append('operation', 'verify');

           const response = await fetch(`${ssoOtpLoginUrl}${stateId}`, {
               method: 'POST',
               body: params
           }).then(res => {
               if (!res.ok) {
                   reject(res.statusText)
               }
               return res
           })
           return response
       }

       ssoOTPLogin().then(resolve)
   })
})
{% c-block-end %}

Für mehr Komfort kann man einen zusätzlichen benutzerdefinierten Befehl erstellen, um die zuvor erstellten benutzerdefinierten Befehle zu einem einzigen zusammenzuführen.

{% c-block language="js" %}
Cypress.Commands.add("login", () => {
   cy.ssoUserLogin()
   cy.ssoState()
   cy.ssoOTPLogin()
})
{% c-block-end %}

Jetzt haben wir einen funktionierenden Login-Workflow über Netzwerkanfragen, aber Moment ... es funktioniert nur in Electron. In Chrome wird dieser Login-Workflow fehlschlagen und der Benutzer wird wieder zum SSO-Gateway umgeleitet.

Der Grund dafür ist, dass diese Anfragen Cookies in Antwortkopfzeilen erhalten (um das Login zu bewahren), die im Browser gesetzt werden sollten, aber Chrome hat die aktivierte Funktion SameSiteByDefaultCookies (von Chrome 84).

Diese Funktion blockiert die Einrichtung von Cookies im Browser, wenn der Antwortkopf "Set-Cookie" nicht "SameSite=none" für die herkunftsübergreifende Verwendung enthält.

Glücklicherweise können wir diese Funktion für Chrome via Flag vor dem Start des Browsers deaktivieren.

Diese Einstellung wird in der Datei plugins/index.js innerhalb der Funktion module.exports vorgenommen.

{% c-block language="js" %}
module.exports = (on, config) => {
 on('before:browser:launch', (browser = {}, launchOptions) => {
   console.log(launchOptions.args) // print all current args

   if (browser.family === 'chromium' && browser.name !== 'electron') {
     launchOptions.args.push('--disable-features=SameSiteByDefaultCookies')
     // whatever you return here becomes the launchOptions
     return launchOptions
   }
 })
}
{% c-block-end %}

Wenn Sie mehr über Testverfahren erfahren möchten, können Sie meinen früheren Blog-Eintrag lesen, unsere sozialen Medien verfolgen oder mich gerne direkt kontaktieren.

Teilen:
Martin ist ein fähiger Tester, schnell lernender DevOps und ein zuverlässiger Teamkollege mit HTML und CSS, JavaScript, Cypress und Robot Framework Kenntnissen. Erfahrung mit automatisierten Tests und seit kurzem unser Guru für Performance Tests. Großartiger Rock'n'Roll-Tänzer und Liebhaber von Energy Drinks.

Weitere Artikel dieses Autors

Article collaborators

SABO Newsletter icon

SABO NEWSLETTER

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

SABO Mobile IT

Für unsere Kunden aus der Industrie entwickeln wir spezialisierte Software zur Umsetzung von Industry 4.0. IoT, Machine Learning und Künstliche Intelligenz ermöglichen uns, signifikante Effizienzsteigerungen bei unseren Kunden zu erzielen.
Über uns