1. Keycloak mit Docker starten

Erstelle die folgende docker-compose.yml im Projektverzeichnis:

services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.1.3
    command: start-dev
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    ports:
      - 8080:8080
    volumes:
      - ./providers:/opt/keycloak/providers
      - ./themes:/opt/keycloak/themes

Starte Keycloak mit:

docker compose up
1

2. Realm erstellen

  1. Navigiere zu http://localhost:8080

  2. Logge dich mit admin / admin ein

  3. Klicke auf Create realm

2
  1. Gib z. B. myrealm als Namen ein

3
  1. Klicke auf Create

3. Benutzer anlegen

  1. Gehe links zu UsersAdd user

4
  1. Vergib einen Benutzernamen, z. B. testuser

5
  1. Nach dem Erstellen → Tab Credentials

6
  1. Passwort z. B. test123, Temporary: OFF, Set password

7

4. Flow duplizieren

  1. Gehe zu Authentication > Flows

8
  1. Klicke bei browser auf Duplicate

9
  1. Name: browser-secret-questionDuplicate

10

5. Secret Question Authenticator einfügen

  1. Klicke den neuen Flow browser-secret-question an

11
  1. So sieht das dann aus. Man sucht dann nach browser-secret-question Browser - Conditional und drückt auf das plus

12
13
14
15
  1. Add step nach Username/Password → Secret Question Authenticator und Add

16
  1. Reihenfolge anpassen

17
  1. Requirement auf REQUIRED setzen

6. Flow aktivieren

  1. Gehe zu Authentication > Bindings

  2. Setze Browser Flow auf browser-secret-question

18
19
20
  1. Save drücken

7. Login-Theme aktivieren

Falls du ein eigenes FTL-Template (secret-question.ftl) nutzt:

  1. Gehe zu Realm Settings → Themes

  2. Setze Login Theme auf mytheme

21
22
  1. Und Save drücken

8. Client erstellen

  1. Gehe zu ClientsCreate client

23
  1. Gib my-client ein

change
  1. Client authentication auf Off und Standard Flow auf On

26
  1. Setze folgende Felder:

Feld Wert

Root URL

http://localhost:8081

Valid Redirect URIs

http://localhost:8081/*

  1. Speichern

9. Test: Login mit Secret Question

  1. Starte Quarkus-App

  2. Gehe zu http://localhost:8081

  3. Du wirst weitergeleitet zum Keycloak Login

27
28
  1. Nach dem Passwort kommt die Sicherheitsfrage:

29
  1. Eingabe: correctAnswer

30
  1. Erfolg:

31

10. Struktur - Quarkus

struktur quarkus

10.1. Secret Question Code

10.2. SecretQuestionAuthenticator.java

package at.htlleonding.keycloak;

import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

public class SecretQuestionAuthenticator implements Authenticator {

    @Override
    public void authenticate(AuthenticationFlowContext context) {
        if (hasCookie(context)) {
            context.success();
            return;
        }

        Response challenge = context.form()
                .setAttribute("theme", "mytheme")
                .createForm("secret-question.ftl");
        context.challenge(challenge);
    }

    @Override
    public void action(AuthenticationFlowContext context) {
        boolean validated = validateAnswer(context);
        if (!validated) {
            Response challenge = context.form()
                    .setError("badSecret")
                    .createForm("secret-question.ftl");
            context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
        }

        setCookie(context);
        context.success();
    }

    private boolean validateAnswer(AuthenticationFlowContext context) {
        String userInput = context.getHttpRequest().getDecodedFormParameters().getFirst("secret_answer");
        return "correctAnswer".equalsIgnoreCase(userInput);
    }

    private boolean hasCookie(AuthenticationFlowContext context) {
        Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED");
        return cookie != null;
    }

    private void setCookie(AuthenticationFlowContext context) {
        NewCookie newCookie = new NewCookie(
                "SECRET_QUESTION_ANSWERED",
                "true",
                "/", null, "Secret Question Cookie", 86400, false, true
        );

        context.getAuthenticationSession().setAuthNote("SECRET_QUESTION_ANSWERED", "true");
        context.success();
    }

    @Override public void close() {}
    @Override public boolean requiresUser() { return true; }
    @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return true; }
    @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {}
}

10.3. SecretQuestionAuthenticatorFactory.java

package at.htlleonding.keycloak;

import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;

import java.util.List;

public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {

    public static final String PROVIDER_ID = "secret-question-authenticator";

    @Override
    public String getId() {
        return PROVIDER_ID;
    }

    @Override
    public String getDisplayType() {
        return "Secret Question Authenticator";
    }

    @Override
    public String getHelpText() {
        return "Benutzer muss eine geheime Frage korrekt beantworten.";
    }

    @Override
    public Authenticator create(KeycloakSession session) {
        return new SecretQuestionAuthenticator();
    }

    @Override
    public void init(Config.Scope config) {}

    @Override
    public void postInit(KeycloakSessionFactory factory) {}

    @Override
    public void close() {}

    @Override
    public boolean isConfigurable() {
        return false;
    }

    @Override
    public Requirement[] getRequirementChoices() {
        return new Requirement[]{
                Requirement.REQUIRED,
                Requirement.ALTERNATIVE,
                Requirement.DISABLED
        };
    }

    @Override
    public boolean isUserSetupAllowed() {
        return false;
    }

    @Override
    public String getReferenceCategory() {
        return null;
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return List.of();
    }
}

10.4. HelloResource.java

@Path("/hello")
public class HelloResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello, authenticated user!";
    }
}

10.5. Projekt bauen

mvn clean package

10.6. .jar in Keycloak einbinden

mkdir -p providers
cp target/keycloak-secret-question.jar providers/

Danach nicht vergessen, Keycloak neu zu starten:

docker compose restart

11. Struktur - Keycloak

struktur keycloak

11.1. Template

Pfad: themes/mytheme/login/secret-question.ftl

<form id="kc-totp-login-form" action="${url.loginAction}" method="post">
    <div class="form-group">
        <label for="secret_answer">${msg("loginSecretQuestion")}</label>
        <input id="secret_answer" name="secret_answer" type="text" class="form-control" />
        <#if message?has_content>
            <div class="error">${message.summary}</div>
        </#if>
    </div>
    <div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>

12. Ergebnis

Die Sicherheitsfrage erscheint nach dem Login – nur bei richtiger Antwort ist der Zugriff erlaubt.

Die richtige Antwort für die Sicherheitsfrage in diesem Beispiel lautet: correctAnswer

31