Debug School

rakesh kumar
rakesh kumar

Posted on

Managing Session Flags and Channel State Across Keycloak authenticate() and action()

Why your flags become true/false randomly

In Keycloak custom authenticators:

1) authenticate() runs on GET (page render)

You should read session notes and render UI attributes

This is where you show flags like:


emailOtpSent

emailVerified

phoneOtpSent

phoneVerified

phoneChannel

phoneDialCode

phoneE164
Enter fullscreen mode Exit fullscreen mode

2) action() runs on POST (form submit)

This is where you process buttons (send, verify, reset, send_otp, verify_otp, register)

You should update session notes first, then re-read from session, then re-render
Enter fullscreen mode Exit fullscreen mode

✅ Key rule: FTL should never trust form input for state.
Always trust AuthenticationSessionModel notes (s.getAuthNote(...)).

The real reason “phoneChannel” or flags are inconsistent

Because your UI shows variables passed via .setAttribute(...), and those are built from:

sometimes form values (channel/dialCode)

sometimes notes (NOTE_PHONE_CHANNEL / NOTE_DEFAULT_CHANNEL)

sometimes wrong note keys (example: NOTE_PHONE_DIAL vs NOTE_PHONE_DIAL_CODE)
Enter fullscreen mode Exit fullscreen mode

So your “true/false + channel” must have ONE single source of truth:

✅ Session Notes.

Step-by-step coding pattern (ONLY flags + phoneChannel persistence)

Step 1: Define a single source of truth (note keys)

Use these keys consistently everywhere:

private static final String NOTE_EMAIL_OTP_SENT = "EMAIL_OTP_SENT";     // "1" / "0"
private static final String NOTE_EMAIL_VERIFIED = "EMAIL_VERIFIED";     // "1" / "0"
private static final String NOTE_EMAIL_VALUE = "EMAIL_OTP_EMAIL";

private static final String NOTE_PHONE_OTP_SENT = "PHONE_OTP_SENT";     // "1" / "0"
private static final String NOTE_PHONE_VERIFIED = "PHONE_VERIFIED";     // "1" / "0"

private static final String NOTE_PHONE_CHANNEL = "PHONE_CHANNEL";       // "sms"|"whatsapp"
private static final String NOTE_PHONE_DIAL_CODE = "PHONE_DIAL_CODE";   // "+91"
private static final String NOTE_PHONE_RAW = "PHONE_RAW";
private static final String NOTE_PHONE_E164 = "PHONE_E164";

private static final String NOTE_DEFAULT_CHANNEL = "DEFAULT_CHANNEL";   // fallback if PHONE_CHANNEL empty
Enter fullscreen mode Exit fullscreen mode

Important: Don’t mix NOTE_PHONE_DIAL and NOTE_PHONE_DIAL_CODE. Pick one.
In your class, you already have both → that causes mismatch.

Step 2: In authenticate() always read notes and render flags

Pattern:

@Override
public void authenticate(AuthenticationFlowContext context) {
    AuthenticationSessionModel s = context.getAuthenticationSession();

    boolean emailOtpSent  = "1".equals(safe(s.getAuthNote(NOTE_EMAIL_OTP_SENT)));
    boolean emailVerified = "1".equals(safe(s.getAuthNote(NOTE_EMAIL_VERIFIED)));
    String emailValue     = safe(s.getAuthNote(NOTE_EMAIL_VALUE));

    boolean phoneOtpSent  = "1".equals(safe(s.getAuthNote(NOTE_PHONE_OTP_SENT)));
    boolean phoneVerified = "1".equals(safe(s.getAuthNote(NOTE_PHONE_VERIFIED)));

    String phoneDialCode  = safe(s.getAuthNote(NOTE_PHONE_DIAL_CODE));
    String phoneE164      = safe(s.getAuthNote(NOTE_PHONE_E164));
    String phoneChannel   = safe(s.getAuthNote(NOTE_PHONE_CHANNEL));

    if (phoneChannel.isBlank()) {
        phoneChannel = safe(s.getAuthNote(NOTE_DEFAULT_CHANNEL)); // fallback
    }

    Response ch = context.form()
        .setAttribute("emailOtpSent", emailOtpSent)
        .setAttribute("emailVerified", emailVerified)
        .setAttribute("emailValue", emailValue)
        .setAttribute("phoneOtpSent", phoneOtpSent)
        .setAttribute("phoneVerified", phoneVerified)
        .setAttribute("phoneDialCode", phoneDialCode)
        .setAttribute("phoneE164", phoneE164)
        .setAttribute("phoneChannel", phoneChannel)
        .createForm("register-start.ftl");

    context.challenge(ch);
}

Enter fullscreen mode Exit fullscreen mode

✅ Now your debug panel will always reflect real state.

Step 3: In action() update notes FIRST, then re-render from notes
Example: user selects WhatsApp + dial code and clicks “Send OTP”

Do:

if ("send_otp".equalsIgnoreCase(phoneAction)) {

    // 1) Save the selection FIRST
    s.setAuthNote(NOTE_PHONE_CHANNEL, channel);        // whatsapp|sms
    s.setAuthNote(NOTE_PHONE_DIAL_CODE, dialCode);     // +91
    s.setAuthNote(NOTE_PHONE_RAW, phoneRaw);           // raw input

    // 2) When OTP successfully sent:
    s.setAuthNote(NOTE_PHONE_OTP_SENT, "1");
    s.setAuthNote(NOTE_PHONE_VERIFIED, "0");           // reset verified
    s.setAuthNote(NOTE_PHONE_E164, finalPhone);        // store computed E164

    // 3) Now always re-read from notes (do NOT trust local vars)
    boolean phoneOtpSentNow  = "1".equals(safe(s.getAuthNote(NOTE_PHONE_OTP_SENT)));
    boolean phoneVerifiedNow = "1".equals(safe(s.getAuthNote(NOTE_PHONE_VERIFIED)));

    String dialNow    = safe(s.getAuthNote(NOTE_PHONE_DIAL_CODE));
    String e164Now    = safe(s.getAuthNote(NOTE_PHONE_E164));
    String chanNow    = safe(s.getAuthNote(NOTE_PHONE_CHANNEL));

    Response ch = context.form()
        .setAttribute("phoneOtpSent", phoneOtpSentNow)
        .setAttribute("phoneVerified", phoneVerifiedNow)
        .setAttribute("phoneDialCode", dialNow)
        .setAttribute("phoneE164", e164Now)
        .setAttribute("phoneChannel", chanNow)
        .createForm("register-start.ftl");

    context.challenge(ch);
    return;
}

Enter fullscreen mode Exit fullscreen mode

✅ This guarantees your FTL shows

 phoneOtpSent=true and correct
 phoneChannel.
Enter fullscreen mode Exit fullscreen mode

Step 4: When verifying OTP, flip only verified flag

if ("verify_otp".equalsIgnoreCase(phoneAction)) {

    // ...validate otp...

    s.setAuthNote(NOTE_PHONE_VERIFIED, "1");

    // Optional: keep NOTE_PHONE_OTP_SENT as "1" (fine)
    // Or remove it (also fine), but UI should use phoneVerified to decide.

    // Re-render from notes
    boolean phoneOtpSentNow  = "1".equals(safe(s.getAuthNote(NOTE_PHONE_OTP_SENT)));
    boolean phoneVerifiedNow = "1".equals(safe(s.getAuthNote(NOTE_PHONE_VERIFIED)));

    String dialNow  = safe(s.getAuthNote(NOTE_PHONE_DIAL_CODE));
    String e164Now  = safe(s.getAuthNote(NOTE_PHONE_E164));
    String chanNow  = safe(s.getAuthNote(NOTE_PHONE_CHANNEL));

    Response ch = context.form()
        .setAttribute("phoneOtpSent", phoneOtpSentNow)
        .setAttribute("phoneVerified", phoneVerifiedNow)
        .setAttribute("phoneDialCode", dialNow)
        .setAttribute("phoneE164", e164Now)
        .setAttribute("phoneChannel", chanNow)
        .createForm("register-start.ftl");

    context.challenge(ch);
    return;
}

Enter fullscreen mode Exit fullscreen mode

Step 5: FTL debug block (correct, stable)

Your FTL is already right. Just ensure you use the exact case:

phoneOtpSent = <b>${phoneOtpSent?c}</b><br/>
phoneVerified = <b>${phoneVerified?c}</b><br/>
phoneDialCode = <b>${phoneDialCode!""}</b><br/>
phoneE164 = <b>${phoneE164!""}</b><br/>
phoneChannel = <b>${phoneChannel!""}</b><br/>
Enter fullscreen mode Exit fullscreen mode

==================OR==================

          <form id="kc-register-start-form" action="${url.loginAction}" method="post" autocomplete="off">
<input type="hidden" id="dial_code" name="dial_code" value="${phoneDialCode!""}" />
<input type="hidden" id="phone_e164" name="phone_e164" value="${phoneE164!""}" />
            <label class="ms-rs-label">
            <#assign phoneOtpSent  = (phoneOtpSent!false) />
<#assign phoneVerified = (phoneVerified!false) />
<#assign phoneRaw      = (phoneRaw!"") />
<#assign phoneDialCode = (phoneDialCode!"") />
<#assign phoneChannel  = (phoneChannel!"") />
              <#if isPartnerResolved>Name of Partner<#else>Name</#if>
            </label>
            <input class="ms-rs-input" type="text" name="name"   value="${nameValue!""}"  placeholder="Enter your company or full name" />
<label class="ms-rs-label">E-Mail Address</label>
<#-- server flags -->

<div style="
  background:#111827;
  color:#e5e7eb;
  padding:10px;
  margin-bottom:10px;
  border-radius:6px;
  font-size:12px;
">
  <b>DEBUG – Flags</b><br/><br/>

  <b>Email</b><br/>
  emailOtpSent = <b>${emailOtpSent?c}</b><br/>
  emailVerified = <b>${emailVerified?c}</b><br/>
  emailValue = <b>${emailValue}</b><br/><br/>

  <b>Phone</b><br/>
  phoneOtpSent = <b>${phoneOtpSent?c}</b><br/>
  phoneVerified = <b>${phoneVerified?c}</b><br/>
  phoneDialCode = <b>${phoneDialCode}</b><br/>
  phoneE164 = <b>${phoneE164}</b><br/>
  phoneChannel = <b>${phoneChannel}</b><br/>



</div>
Enter fullscreen mode Exit fullscreen mode

One critical correction in your current code

In different places you use:

NOTE_PHONE_DIAL

NOTE_PHONE_DIAL_CODE
Enter fullscreen mode Exit fullscreen mode

This will break UI values.
✅ Fix: use only NOTE_PHONE_DIAL_CODE everywhere (recommended).

So replace reads like:

safe(s.getAuthNote(NOTE_PHONE_DIAL))
Enter fullscreen mode Exit fullscreen mode

with:

safe(s.getAuthNote(NOTE_PHONE_DIAL_CODE))
Enter fullscreen mode Exit fullscreen mode

Top comments (0)