<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>disposable-email on Mr. Buch</title><link>https://mrbu.ch/tags/disposable-email/</link><description>Recent content in disposable-email on Mr. Buch</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>c@mrbu.ch (Chintan Buch)</managingEditor><webMaster>c@mrbu.ch (Chintan Buch)</webMaster><copyright>© 2026 Chintan Buch</copyright><lastBuildDate>Sun, 17 May 2026 04:30:00 +0530</lastBuildDate><atom:link href="https://mrbu.ch/tags/disposable-email/index.xml" rel="self" type="application/rss+xml"/><item><title>Every Disposable Email Is A Hole In Your Funnel</title><link>https://mrbu.ch/articles/keycloak-block-disposable-email-extension/</link><pubDate>Sun, 17 May 2026 04:30:00 +0530</pubDate><author>c@mrbu.ch (Chintan Buch)</author><guid>https://mrbu.ch/articles/keycloak-block-disposable-email-extension/</guid><description>&lt;p&gt;Here&amp;rsquo;s something I noticed while looking at user signups for a product we were running on Keycloak.&lt;/p&gt;
&lt;p&gt;Healthy registration numbers. Decent activation rate. But a chunk of users just&amp;hellip; never came back. Not after day one, not after the welcome email, not after the follow-up. Just gone. When I started digging into the email addresses, it was obvious — &lt;code&gt;temp-mail.org&lt;/code&gt;, &lt;code&gt;guerrillamail.com&lt;/code&gt;, &lt;code&gt;tempmailo.com&lt;/code&gt;. Throwaway addresses. The accounts existed but the people never did.&lt;/p&gt;
&lt;p&gt;Blocking them isn&amp;rsquo;t hard in theory. You just need a list of known disposable domains and a place to check against it during registration. The problem is &lt;em&gt;where&lt;/em&gt; in Keycloak to put that check.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s no built-in setting. There&amp;rsquo;s no toggle in the Admin Console. You can&amp;rsquo;t just paste a regex somewhere. If you want to validate anything custom during registration, you&amp;rsquo;re building a Keycloak SPI extension — a Java plugin that hooks into the authentication flow.&lt;/p&gt;
&lt;p&gt;So I built one.&lt;/p&gt;
&lt;hr&gt;

&lt;h2 class="relative group"&gt;What it does
 &lt;div id="what-it-does" class="anchor"&gt;&lt;/div&gt;
 
 &lt;span
 class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
 &lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#what-it-does" aria-label="Anchor"&gt;#&lt;/a&gt;
 &lt;/span&gt;
 
&lt;/h2&gt;
&lt;p&gt;Two things.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blocks registration with disposable email addresses.&lt;/strong&gt; If someone tries to sign up with &lt;code&gt;throwaway123@tempmail.com&lt;/code&gt;, they get an error on the registration form. Same UX as any other validation error — inline, next to the email field, no page reload. The account never gets created.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exposes a REST endpoint to refresh the domain blocklist.&lt;/strong&gt; The list of known disposable domains comes from the &lt;a href="https://github.com/ZliIO/zliio-disposable" target="_blank" rel="noreferrer"&gt;zliio/disposable&lt;/a&gt; library. It&amp;rsquo;s loaded on startup, but you can hit an endpoint to pull fresh data without restarting Keycloak.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s it. No database schema changes. No custom theme files. No event listeners to configure separately. Drop the JAR in, wire up the flow, and it works.&lt;/p&gt;
&lt;hr&gt;

&lt;h2 class="relative group"&gt;How it actually works
 &lt;div id="how-it-actually-works" class="anchor"&gt;&lt;/div&gt;
 
 &lt;span
 class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
 &lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#how-it-actually-works" aria-label="Anchor"&gt;#&lt;/a&gt;
 &lt;/span&gt;
 
&lt;/h2&gt;
&lt;p&gt;Keycloak&amp;rsquo;s authentication system is built around flows — chains of steps that run during login, registration, or any other interaction. Each step is a &lt;code&gt;FormAction&lt;/code&gt;. If you want to inject custom logic, you implement that interface and register it as an SPI.&lt;/p&gt;
&lt;p&gt;This extension does exactly that. &lt;code&gt;EmailDomainValidationFormAction&lt;/code&gt; plugs into the registration flow. When someone submits the form, Keycloak calls &lt;code&gt;validate()&lt;/code&gt; — it pulls the email field, checks it against the blocklist, and either passes or fails the validation. If it&amp;rsquo;s a disposable domain, Keycloak shows the error inline. Registration stops. The user&amp;rsquo;s not in the system.&lt;/p&gt;
&lt;p&gt;The actual domain check lives in &lt;code&gt;DisposableEmailManager&lt;/code&gt; — a singleton wrapping the zliio &lt;code&gt;Disposable&lt;/code&gt; instance. One object, shared across all requests. Worth noting: zliio&amp;rsquo;s &lt;code&gt;validate()&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; for &lt;em&gt;valid&lt;/em&gt; (non-disposable) emails, so &lt;code&gt;isDisposableEmail&lt;/code&gt; flips that — &lt;code&gt;true&lt;/code&gt; means keep out. Small thing, but it tripped me up the first time I read the code.&lt;/p&gt;
&lt;p&gt;The REST endpoint is a separate SPI — a &lt;code&gt;RealmResourceProvider&lt;/code&gt; — registered at:&lt;/p&gt;
&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;POST /realms/{realm}/brew-disposable-email-resource-provider/refresh-domain-list&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;It&amp;rsquo;s locked to service accounts with &lt;code&gt;realm-admin&lt;/code&gt; role in the &lt;code&gt;realm-management&lt;/code&gt; client. Regular user tokens, even admin ones, get a 403. The auth check manually parses the JWT instead of using Keycloak&amp;rsquo;s built-in role enforcement — it&amp;rsquo;s checking &lt;code&gt;resourceAccess[&amp;quot;realm-management&amp;quot;].roles&lt;/code&gt; contains &lt;code&gt;&amp;quot;realm-admin&amp;quot;&lt;/code&gt; directly in the token claims.&lt;/p&gt;
&lt;hr&gt;

&lt;h2 class="relative group"&gt;Setting it up
 &lt;div id="setting-it-up" class="anchor"&gt;&lt;/div&gt;
 
 &lt;span
 class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
 &lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#setting-it-up" aria-label="Anchor"&gt;#&lt;/a&gt;
 &lt;/span&gt;
 
&lt;/h2&gt;
&lt;p&gt;Build the JAR, drop it in Keycloak&amp;rsquo;s &lt;code&gt;providers/&lt;/code&gt; directory, restart. Full steps are in the &lt;a href="https://gitlab.com/mrbuch/keycloak/keycloak-block-disposable-email/-/blob/main/README.md" target="_blank" rel="noreferrer"&gt;README&lt;/a&gt;. Requires Java 17, tested on Keycloak 26.2.5.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The step that&amp;rsquo;s easy to miss:&lt;/strong&gt; installing the JAR does nothing on its own. You have to wire it into a registration flow.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Admin Console → your realm → &lt;strong&gt;Authentication → Flows&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Find &lt;code&gt;registration&lt;/code&gt; and duplicate it — name it something like &lt;code&gt;registration-with-email-check&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;In the new flow, click &lt;strong&gt;Add execution&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Find &lt;strong&gt;&amp;ldquo;Email Domain Validation&amp;rdquo;&lt;/strong&gt; and add it&lt;/li&gt;
&lt;li&gt;Set its requirement to &lt;strong&gt;Required&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Authentication → Bindings&lt;/strong&gt; → set &lt;strong&gt;Registration flow&lt;/strong&gt; to your new flow&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now every registration attempt goes through the check. Your existing flows aren&amp;rsquo;t touched, and you can always swap back by changing the binding.&lt;/p&gt;
&lt;hr&gt;

&lt;h2 class="relative group"&gt;Refreshing the domain list
 &lt;div id="refreshing-the-domain-list" class="anchor"&gt;&lt;/div&gt;
 
 &lt;span
 class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
 &lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#refreshing-the-domain-list" aria-label="Anchor"&gt;#&lt;/a&gt;
 &lt;/span&gt;
 
&lt;/h2&gt;
&lt;p&gt;The blocklist loads on startup. To pull an update without restarting, POST to:&lt;/p&gt;
&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/realms/{realm}/brew-disposable-email-resource-provider/refresh-domain-list&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;You need a service account token with &lt;code&gt;realm-admin&lt;/code&gt; role in &lt;code&gt;realm-management&lt;/code&gt;. Regular user tokens get a 403 — even admin ones. Full curl example in the README.&lt;/p&gt;
&lt;p&gt;Set this up as a weekly cron job. The zliio upstream list grows as new throwaway services pop up — you want to stay current.&lt;/p&gt;
&lt;hr&gt;

&lt;h2 class="relative group"&gt;Things worth knowing before you deploy
 &lt;div id="things-worth-knowing-before-you-deploy" class="anchor"&gt;&lt;/div&gt;
 
 &lt;span
 class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
 &lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#things-worth-knowing-before-you-deploy" aria-label="Anchor"&gt;#&lt;/a&gt;
 &lt;/span&gt;
 
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The blocklist is in-memory.&lt;/strong&gt; It lives in the JVM heap, not a database. Keycloak restart clears it back to the startup defaults. If you&amp;rsquo;re scheduling refreshes, also schedule one on startup — or just accept that you&amp;rsquo;ll be on the bundled list until the next refresh fires.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One manager instance per JVM.&lt;/strong&gt; &lt;code&gt;DisposableEmailManager&lt;/code&gt; is a singleton. In a multi-realm Keycloak setup, a refresh on one realm&amp;rsquo;s endpoint refreshes the list for all of them. That&amp;rsquo;s usually fine, but worth knowing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;minimizeJar=true&lt;/code&gt; is active.&lt;/strong&gt; The shade plugin strips unused classes to keep the JAR small. If you&amp;rsquo;re extending this and adding dependencies, verify they don&amp;rsquo;t get pruned. The &lt;code&gt;minimizeJar&lt;/code&gt; flag can be aggressive and it fails silently at runtime.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keycloak SPI classes stay out of the JAR.&lt;/strong&gt; &lt;code&gt;keycloak-server-spi&lt;/code&gt;, &lt;code&gt;keycloak-server-spi-private&lt;/code&gt;, &lt;code&gt;keycloak-services&lt;/code&gt; — all &lt;code&gt;provided&lt;/code&gt; scope. They come from Keycloak&amp;rsquo;s own classpath at runtime. Never shade them in. If you do, you&amp;rsquo;ll get classloader conflicts that are annoying to debug.&lt;/p&gt;
&lt;hr&gt;

&lt;h2 class="relative group"&gt;What this doesn&amp;rsquo;t do
 &lt;div id="what-this-doesnt-do" class="anchor"&gt;&lt;/div&gt;
 
 &lt;span
 class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
 &lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#what-this-doesnt-do" aria-label="Anchor"&gt;#&lt;/a&gt;
 &lt;/span&gt;
 
&lt;/h2&gt;
&lt;p&gt;No fuzzy matching, no MX record lookups, no custom domain allowlists, no per-realm config. If someone signs up with a real Gmail account they don&amp;rsquo;t care about, they get through. This only blocks known disposable domains from the zliio blocklist.&lt;/p&gt;
&lt;p&gt;If you need something more sophisticated — like blocking domains with no valid MX records, or maintaining your own allowlist — that&amp;rsquo;s a fork, not a config option.&lt;/p&gt;
&lt;p&gt;For most use cases though, the zliio list covers what you need. It&amp;rsquo;s thousands of domains wide and gets updated regularly.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a perfect solution — nothing is. But five minutes of setup to cut out a whole category of junk registrations is a pretty good trade.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Found a bug or want a feature? &lt;a href="https://gitlab.com/mrbuch/keycloak/keycloak-block-disposable-email" target="_blank" rel="noreferrer"&gt;Open an issue on GitLab&lt;/a&gt;&lt;/p&gt;</description><media:content xmlns:media="http://search.yahoo.com/mrss/" url="https://mrbu.ch/articles/keycloak-block-disposable-email-extension/cover.jpg"/></item></channel></rss>