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

2. Realm erstellen
-
Navigiere zu http://localhost:8080
-
Logge dich mit
admin
/admin
ein -
Klicke auf Create realm

-
Gib z. B.
myrealm
als Namen ein

-
Klicke auf Create
3. Benutzer anlegen
-
Gehe links zu Users → Add user

-
Vergib einen Benutzernamen, z. B.
testuser

-
Nach dem Erstellen → Tab Credentials

-
Passwort z. B.
test123
, Temporary:OFF
, Set password

4. Flow duplizieren
-
Gehe zu Authentication > Flows

-
Klicke bei
browser
auf Duplicate

-
Name:
browser-secret-question
→ Duplicate

5. Secret Question Authenticator einfügen
-
Klicke den neuen Flow
browser-secret-question
an

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




-
Add step nach Username/Password → Secret Question Authenticator und Add

-
Reihenfolge anpassen

-
Requirement auf
REQUIRED
setzen
6. Flow aktivieren
-
Gehe zu Authentication > Bindings
-
Setze Browser Flow auf
browser-secret-question



-
Save drücken
7. Login-Theme aktivieren
Falls du ein eigenes FTL-Template (secret-question.ftl
) nutzt:
-
Gehe zu Realm Settings → Themes
-
Setze Login Theme auf
mytheme


-
Und Save drücken
8. Client erstellen
-
Gehe zu Clients → Create client

-
Gib
my-client
ein

-
Client authentication auf Off und Standard Flow auf On

-
Setze folgende Felder:
Feld | Wert |
---|---|
Root URL |
|
Valid Redirect URIs |
-
Speichern
9. Test: Login mit Secret Question
-
Starte Quarkus-App
-
Gehe zu http://localhost:8081
-
Du wirst weitergeleitet zum Keycloak Login


-
Nach dem Passwort kommt die Sicherheitsfrage:

-
Eingabe:
correctAnswer

-
Erfolg:

10. Struktur - Quarkus

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();
}
}
11. 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>