Render serverside msg in ftl forms
Render Multiple field errors in ftl forms
Render laravel dynamic msg in FTL forms
Render serverside msg in ftl forms
STEP 1️⃣ Add message display in FTL (you already have this)
You already wrote this (correct):
<#if message?has_content>
<div class="alert alert-error">
${message.summary}
</div>
</#if>
This displays server-side errors automatically.
STEP 2️⃣ Do validation in Java action() method
Example: PhoneInputAuthenticator.action()
@Override
public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String country = formData.getFirst("country");
String phone = formData.getFirst("phone");
// ❌ SERVER-SIDE VALIDATION
if (country == null || country.isBlank()) {
context.failureChallenge(
AuthenticationFlowError.INVALID_CREDENTIALS,
context.form()
.setError("Country is required")
.createForm("phone-login.ftl")
);
return;
}
if (phone == null || !phone.matches("\\d{10}")) {
context.failureChallenge(
AuthenticationFlowError.INVALID_CREDENTIALS,
context.form()
.setError("Phone number must be 10 digits")
.createForm("phone-login.ftl")
);
return;
}
// ✅ VALID → continue
context.success();
}
STEP 3️⃣ What actually happens internally
When you call:
context.form()
.setError("Phone number must be 10 digits")
.createForm("phone-login.ftl");
Keycloak:
Stores the error message
Re-renders the same FTL
Injects message.summary
Your <#if message?has_content> block displays it
STEP 4️⃣ Important: DON’T use context.success() on error
❌ Wrong:
context.success(); // even on error
✅ Correct:
context.failureChallenge(...)
Render Multiple field errors in ftl forms
Multiple field errors (optional, advanced)
You can show multiple errors:
context.challenge(
context.form()
.setErrors(List.of(
new FormMessage("phone", "Invalid phone number"),
new FormMessage("country", "Country required")
))
.createForm("phone-login.ftl")
);
public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData =
context.getHttpRequest().getDecodedFormParameters();
String country = trimToNull(formData.getFirst("country"));
String phone = trimToNull(formData.getFirst("phone"));
// Collect multiple field errors
List<FormMessage> errors = new ArrayList<>();
// ----- VALIDATIONS -----
if (country == null) {
errors.add(new FormMessage("country", "Country is required"));
}
if (phone == null) {
errors.add(new FormMessage("phone", "Phone number is required"));
} else {
// Example: only digits and length 10 (adjust as per your rules)
if (!phone.matches("\\d+")) {
errors.add(new FormMessage("phone", "Phone must contain only digits"));
} else if (phone.length() != 10) {
errors.add(new FormMessage("phone", "Phone number must be 10 digits"));
}
}
// If there are any errors -> re-render same FTL with errors
if (!errors.isEmpty()) {
context.failureChallenge(
AuthenticationFlowError.INVALID_CREDENTIALS,
context.form()
// preserve typed values
.setAttribute("countryValue", country)
.setAttribute("phoneValue", phone)
// attach multiple errors
.setErrors(errors)
.createForm("phone-login.ftl")
);
return;
}
// ✅ If valid -> continue flow
context.success();
}
And in FTL:
<#list messagesPerField?keys as field>
<div class="error">${messagesPerField[field]?first}</div>
</#list>
<form id="kc-phone-form" action="${url.loginAction}" method="post">
<!-- COUNTRY -->
<div class="form-group">
<label>Country Code</label>
<select name="country" class="form-control" required>
<option value="">Select Country</option>
<option value="+91" <#if (countryValue!"") == "+91">selected</#if>>India (+91)</option>
<option value="+1" <#if (countryValue!"") == "+1">selected</#if>>USA (+1)</option>
<option value="+81" <#if (countryValue!"") == "+81">selected</#if>>Japan (+81)</option>
</select>
<!-- Field error under country -->
<#if messagesPerField?? && messagesPerField.exists('country')>
<div class="error" style="color:#d32f2f; margin-top:6px;">
${messagesPerField.get('country')?first}
</div>
</#if>
</div>
<!-- PHONE -->
<div class="form-group">
<label>Phone</label>
<input name="phone"
class="form-control"
placeholder="Enter phone number"
value="${phoneValue!""}"
required />
<!-- Field error under phone -->
<#if messagesPerField?? && messagesPerField.exists('phone')>
<div class="error" style="color:#d32f2f; margin-top:6px;">
${messagesPerField.get('phone')?first}
</div>
</#if>
</div>
<button class="btn btn-primary" type="submit">Send OTP</button>
</form>
ONE-SENTENCE RULE (remember this)
Server-side validation = context.failureChallenge() + form().setError() + re-render FTL
ONE-LINE GOLDEN RULE
If anything fails → failureChallenge() + re-render FTL
If everything succeeds → context.success()
Render laravel dynamic msg in FTL forms
Change Laravel to always return message
You already do this:
{ "success": false, "message": "Failed to send WhatsApp message." }
Good.
2) In Java, parse JSON and return message
Create a small response class
static class LaravelOtpResponse {
final boolean success;
final String message;
LaravelOtpResponse(boolean success, String message) {
this.success = success;
this.message = message;
}
}
Update your API call method to return this object
(Using Keycloak’s built-in JSON utility:
org.keycloak.util.JsonSerialization)
import java.util.Map;
import org.keycloak.util.JsonSerialization;
private LaravelOtpResponse sendOtpViaLaravel(String phone) throws Exception {
URL url = new URL("https://api.motoshare.in/auth/send-whatsapp-otp");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", "Bearer INTERNAL_SECRET");
conn.setDoOutput(true);
String json = "{\"phone\":\"" + phone.replace("\"", "") + "\"}";
try (OutputStream os = conn.getOutputStream()) {
os.write(json.getBytes(StandardCharsets.UTF_8));
}
int status = conn.getResponseCode();
InputStream is = (status >= 200 && status < 300)
? conn.getInputStream()
: conn.getErrorStream();
String responseBody;
try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
responseBody = br.lines().collect(Collectors.joining());
}
// Parse JSON like: {"success":true,"message":"..."}
Map<String, Object> map = JsonSerialization.readValue(responseBody, Map.class);
boolean success = Boolean.TRUE.equals(map.get("success"));
String message = (map.get("message") != null) ? String.valueOf(map.get("message")) : null;
// fallback message if Laravel didn't send one
if (message == null || message.isBlank()) {
message = success ? "OTP sent." : "Failed to send OTP.";
}
return new LaravelOtpResponse(success, message);
}
3) Use Laravel message in action() and render in FTL
@Override
public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData =
context.getHttpRequest().getDecodedFormParameters();
String phone = formData.getFirst("phone");
if (phone == null || phone.isBlank()) {
context.failureChallenge(
AuthenticationFlowError.INVALID_CREDENTIALS,
context.form()
.setError("Phone number is required")
.createForm("phone-login.ftl")
);
return;
}
try {
LaravelOtpResponse laravelResp = sendOtpViaLaravel(phone);
if (!laravelResp.success) {
// ✅ SHOW DYNAMIC MESSAGE FROM LARAVEL IN FTL
context.failureChallenge(
AuthenticationFlowError.INVALID_CREDENTIALS,
context.form()
.setError(laravelResp.message) // <-- dynamic
.createForm("phone-login.ftl")
);
return;
}
// Optional: show success message too (only if you want)
// context.form().setSuccess(laravelResp.message); // if you want banner
} catch (Exception e) {
context.failureChallenge(
AuthenticationFlowError.INTERNAL_ERROR,
context.form()
.setError("OTP service is temporarily unavailable.")
.createForm("phone-login.ftl")
);
return;
}
context.success();
}
Now your existing FTL block shows it:
<#if message?has_content>
<div class="alert alert-error">${message.summary}</div>
</#if>
4) If you want “success message” also on same page
Instead of moving forward immediately, you can re-render with a green banner.
In Java:
context.challenge(
context.form()
.setSuccess(laravelResp.message)
.createForm("phone-login.ftl")
);
return;
In FTL:
<#if message?has_content>
<div class="alert ${message.type}">
${message.summary}
</div>
</#if>
Top comments (0)