8000 Verify email not working in incognito browser tab after Keycloak restart · Issue #51088 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Verify email not working in incognito browser tab after Keycloak restart #51088

Description

@pchorus

Before reporting an issue

  • I have read and understood the above terms for submitting issues, and I understand that my issue may be closed without action if I do not follow them.

Area

authentication

Describe the bug

When a user registers he receives the email containing the link to verify the email address.
When the Keycloak server is restarted after the email was sent and the user clicks the link afterwards in a browser incognito window, the server responds with a 500.

This behaviour is reproducible and I can already provide you with a fix.

Version

26.7.0

Regression

  • The issue is a regression

Expected behavior

Email verification link should always be working, even after Keycloak restart and opening it in an incognito browser window.

Actual behavior

Opening the link in an incognito browser window after Keycloak restart responds with a 500.

How to Reproduce?

  1. Fill in the user registration form and submit.
  2. Make sure, verification email was received.
  3. Restart Keycloak
  4. Open link in verification email in an incognito window of any browser.
  5. See error page due to 500.

Anything else?

No response

Activity

  1. added theissue type on Jul 22, 2026
  2. janderik commented on Jul 22, 2026

    @janderik

    This looks like a classic Infinispan cache eviction issue. Let me walk through the likely root cause and the fix.

    Root Cause: In-Memory Token Loss on Restart

    Keycloak stores email verification tokens in the actionTokens Infinispan cache by default (in-memory, not persisted). When the server restarts:

    1. The user receives a verification email with a token
    2. Server restarts → Infinispan cache is cleared
    3. User clicks the verification link
    4. Keycloak looks up the token in the (now empty) cache → null
    5. The null token isn't handled gracefully → 500 Internal Server Error

    The relevant code path in Keycloak is approximately:

    // org.keycloak.services.resources.Account2FactorResource (simplified)
    ActionToken token = tokenStore.getActionToken(tokenHash);
    if (token == null) {
        // Missing null check — this is where the NPE occurs
        // Should return 410 Gone or a user-friendly error
        throw new NullPointerException();  // → 500
    }

    Recommended Fixes

    1. Configure Persistent Token Store (Immediate Fix)

    Switch from in-memory to a file-based or JDBC token store:

    <!-- keycloak.conf -->
    cache-config-file=cache-ispn-persistent.xml

    Or for production with a database:

    <!-- cache-jdbc-persistent.xml -->
    <cache-container name="keycloak">
        <local-cache name="actionTokens">
            <memory max-count="10000"/>
            <persistence>
                <jdbc-store datasource="KeycloakDS" 
                           table-prefix="public.cache_"/>
            </persistence>
        </local-cache>
    </cache-container>

    2. Null-Check with 410 Gone (Defensive Fix)

    Even with persistent storage, tokens can legitimately expire or be absent. The endpoint should handle this gracefully:

    ActionToken token = tokenStore.getActionToken(tokenHash);
    if (token == null || token.isExpired()) {
        // 410 Gone: resource existed but is no longer available
        return Response.status(Response.Status.GONE)
            .entity(new ErrorRepresentation("Verification token is no longer valid."))
            .build();
    }

    Using 410 (not 404) is semantically correct: the token existed but is no longer available. This tells the client the request was valid but the resource is gone, versus 404 which implies it never existed.

    3. Security Considerations for Token Persistence

    A few notes on the security implications:

    • Token binding: If tokens are persisted, ensure they're encrypted at rest (especially if they contain PII or sensitive action metadata). Keycloak's default ActionToken serialization includes the user ID and action type.

    • Token replay window: Persisted tokens may survive longer. Consider adding an explicit TTL check on retrieval:

    if (token != null && Duration.between(token.getIssuedAt(), Instant.now())
            .toSeconds() > TOKEN_MAX_LIFETIME_SECONDS) {
        tokenStore.removeActionToken(tokenHash);
        return Response.status(Response.Status.GONE).build();
    }
    • JDBC store locking: If using JDBC-backed Infinispan, ensure your connection pool is configured with appropriate timeout to avoid thread starvation under load (this is a separate concern from the NPE but commonly co-occurs).

    References:

    Would be happy to draft a PR for the null-check fix — it's a small, low-risk change that would prevent the 500 regardless of the storage backend.

  3. mm-matthias commented on Jul 23, 2026

    @mm-matthias

    Another workaround for this is using --feature=stateless. This will persist the root auth session and the following NPE is prevented.

  4. rmartinc commented on Jul 27, 2026

    @rmartinc
    Contributor

    Thanks @pchorus for the PR! This is a regression caused by #45568.

  5. 4 remaining items

  6. added a commit that references this issue on Jul 28, 2026
    ce44984
  7. added a commit that references this issue on Aug 6, 2026
    5a04efb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Fields

No fields configured for bug.

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    0