Neuigkeiten von trion.
Immer gut informiert.

Keycloak SAML Integration Test mit Testcontainers und Spring Boot

OpenID Connect und OAuth2 sind vielleicht etwas moderner und hipper als das bereits etwas in die Jahre gekommene SAML 2 Protokoll. Soll jedoch im Enterpriseumfeld eine Anwendung in bestehende Landschaften integriert werden, führt selten ein Weg an SAML 2 vorbei. Das gilt um so stärker, wenn es sich um eine Branche mit hohem Sicherheitsbedarf wie Luftfahrt, Banken oder Versicherungen handelt.

Keycloak hat sich als zuverlässige und gleichzeitig sehr leicht zugängliche Plattform zur Umsetzung von OpenID Connect oder SAML erwiesen. Dabei kann Keycloak sowohl produktiv eingesetzt werden, als auch sehr komfortabel als lokale Testumgebung für Entwicklung und Test verwendet werden. Zur Integration von Keycloak in Spring Boot existieren neben einem Keycloak Modul auch Spring-Security-SAML bzw. OAuth2 Client und Resourceserver.

Gerade wenn es um das Thema Security geht, bringen automatisierte Tests ein wichtiges Sicherheitsnetz für die Software. Wir wollen Keycloak als SAML 2 IdP verwenden und mit einer Spring Boot Anwendung Authentifizierung mit SAML 2 als Beispielanwendung für automatisierte Integrationstests verwenden. Ein - relativ einfacher - Weg wäre, Keycloak als Docker Container im Rahmen der CI-Pipeline mit Jenkins, Bamboo oder GitLab-CI bereitzustellen, so dass die zu testende Software darauf zugreifen kann. Allerding verliert man damit die Möglichkeit, die Tests lokal genauso zu entwickeln und zu validieren, wie sie nachher in der Buildserver Umgebung laufen.

Eine Alternative stellen Testcontainers dar. Die allgemeine Verwendung von Testcontainers wurde bereits in Testcontainers mit JUnit 5 erläutert. Nun schauen wir uns an, wie Keycloak, Testcontainers und Spring Boot SAML 2 zusammen eingesetzt werden kann.

Als erstes stellt sich die Frage, ob Keycloak als GenericContainer durch Testcontainers bereitgestellt werden sollte, oder ob es bereits eine höherwertige Implementierung gibt. Die Wahl fiel dabei auf das testcontainers-keycloak Projekt von Niko Köbler. Die Library ist via Maven verfügbar, so dass sie leicht einzubinden ist.

Maven Dependency für testcontainers-keycloak
<dependency>
    <groupId>com.github.dasniko</groupId>
    <artifactId>testcontainers-keycloak</artifactId>
    <version>1.3.1</version>
    <scope>test</scope>
</dependency>

Nachdem die Library eingebunden wurde, steht der neue Typ KeycloakContainer zur Verfügung.

Zur Konfiguration einer Keycloak Instanz gibt es prinzipiell zwei Varianten:

  1. Programmatische Konfiguration durch den Keycloak Admin Client

  2. Manuelle Konfiguration und Export mit anschließendem Import der Konfiguration

Beide Varianten werden durch die testcontainers-keycloak Library unterstützt, indem eine Instanz des Admin-Clients bereitgestellt wird bzw. Pfad zum Import einer Konfiguration spezifiziert werden kann. Im Beispiel wird ein Realm-Export verwendet.
Damit die SAML Konfiguration auf einfache Weise an verschiedene Umgebungen angepasst werden kann, wurde die Anwendung mit entsprechendem Support für eigene Properties implementiert. Dank @DynamicPropertySource können die Properties entsprechend der Testcontainerumgebung für Spring Boot bereitgestellt werden.

Beispiel SAML 2 Integrationstest mit Spring Boot und Testcontainers
@Testcontainers
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
class SamlAuthenticationIT
{
    @Container
    private static final KeycloakContainer keycloak =
       new KeycloakContainer()
       .withRealmImportFile("keycloak/realm-export.json")
       .waitingFor(Wait.forLogMessage(".*Keycloak .* started in \\d+ms - Started.*", 1));

    @DynamicPropertySource
    static void dataSourceProperties(DynamicPropertyRegistry registry)
    {
        registry.add("app.example.saml.enabled",  () -> true);
        registry.add("app.example.saml.idp-metadata-location",  () -> keycloak.getAuthServerUrl() + "/realms/sample/protocol/saml/descriptor");
        registry.add("app.example.saml.keyStorePassword",  () -> "abc123");
        registry.add("app.example.saml.keyStoreLocation",  () -> "classpath:/keycloak/keystore-local.jks");
        registry.add("app.example.saml.spKeyName",  () -> "login.local.sample");
    }
...
}

Jetzt fehlt nur noch ein Testcase. Dazu wird MockMvc verwendet, um Requests gegen die Anwendung zu senden, und die Reaktionen zu prüfen. So kann sichergestellt werden, dass auf keiner Ressource die Security Konfiguration vergessen wurde, oder fehlerhaft ist. Um auch authentifizierte Requests durchführen zu können, kann auch ein SAML Login als Teil des Testfalles durchgeführt werden. Dazu bietet es sich an von MockMvc die MockHttpSession zu verwenden, mit der die SAML Anmeldung gegen Keycloak durchgeführt wird, und die für weitere Requests genutzt wird.
Empfehlenswert ist dann mit einer eigenen Abstraktion zu arbeiten, um einen erfolgreichen Login, ggf. noch mit passenden Rollen, im Test einfach durchführen zu können. Im Beispiel unten ist das die doLogin() Methode.

Testcase für SAML 2 mit Keycloak und Spring Boot
@Test
void authenticatedRequest() throws Exception
{
    MockHttpSession session = doLogin();

    mockMvc.perform(
       post("/api/data")
          .contentType(APPLICATION_JSON)
          .content(READ_REQUEST_SAMPLE)
          .session(session))
       .andExpect(status().isOk());
}

Dabei kann man entweder einen richtigen Browser verwenden, hier bietet Testcontainers Selenium Webdriver Container an. Alternativ kann man auch programmatisch mit einem Http-Client vorgehen und die jeweiligen Antworten der Spring Boot Anwendung und Keycloak auswerten. Ohne Cookie bzw. Session kommt man jedoch bei SAML 2 nicht aus.




Zu den Themen Security, Docker und Spring Boot bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels