<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ali Isazadeh</title>
    <description>The latest articles on DEV Community by Ali Isazadeh (@aliisazadeh).</description>
    <link>https://dev.to/aliisazadeh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F856028%2F578debb4-3c60-4d86-9d5b-bbecfcfa62c3.jpg</url>
      <title>DEV Community: Ali Isazadeh</title>
      <link>https://dev.to/aliisazadeh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aliisazadeh"/>
    <language>en</language>
    <item>
      <title>Building a production-grade wallet-login authenticator for Keycloak</title>
      <dc:creator>Ali Isazadeh</dc:creator>
      <pubDate>Mon, 13 Jul 2026 16:42:25 +0000</pubDate>
      <link>https://dev.to/aliisazadeh/building-a-production-grade-wallet-login-authenticator-for-keycloak-ai</link>
      <guid>https://dev.to/aliisazadeh/building-a-production-grade-wallet-login-authenticator-for-keycloak-ai</guid>
      <description>&lt;p&gt;&lt;em&gt;SIWE, Solana, and smart-contract wallets — as a native Keycloak plugin, with no wallet SDK and no third-party auth service.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you already run Keycloak and you want users to log in with a crypto wallet, your options today are bad. You can stand up a &lt;em&gt;second&lt;/em&gt; OIDC server just for Sign-In-With-Ethereum and federate it in. You can reverse-proxy your way through a mismatch of endpoint paths and pray. Or you can find one of the handful of open-source "Keycloak + Ethereum" projects on GitHub — every one of which, when I went looking, turned out to be a proof-of-concept pinned to a dead testnet, EOA-only, and never tested against a real Keycloak.&lt;/p&gt;

&lt;p&gt;So I built the version that isn't a toy. This post is what it does, and the engineering decisions I think are worth sharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-sentence version
&lt;/h2&gt;

&lt;p&gt;Keycloak Wallet Authenticator is an Authenticator SPI plugin that lets users sign in with a wallet — Ethereum (SIWE / EIP-4361) or Solana (SIWS), externally-owned or smart-contract wallets — as one authenticator inside your existing browser flow. It doesn't replace your passwords, MFA, or social logins. It sits next to them.&lt;/p&gt;

&lt;p&gt;It supports EOA wallets, EIP-1271 deployed smart-contract wallets, and EIP-6492 counterfactual (not-yet-deployed) wallets, across EVM chains and Solana. It has zero dependency on any wallet SDK, and it's tested end-to-end against a real Keycloak 25 running in Testcontainers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: protocol-driven, not SDK-driven
&lt;/h2&gt;

&lt;p&gt;The single most important design choice is what the backend refuses to know. It doesn't know about Reown, WalletConnect, MetaMask, or Phantom. Every client, no matter the vendor, collapses to the same three inputs: a &lt;code&gt;message&lt;/code&gt;, a &lt;code&gt;signature&lt;/code&gt;, and a claimed &lt;code&gt;accountId&lt;/code&gt;. No wallet vendor's concepts leak past the front door.&lt;/p&gt;

&lt;p&gt;That's what lets the same verification engine power both the Keycloak plugin and a standalone REST API without change. The engine is a framework-free Java library with no Spring, JPA, or Redis on its classpath — a boundary enforced at compile time by the module split and re-checked by an ArchUnit test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: verification is claim validation, not address recovery
&lt;/h2&gt;

&lt;p&gt;This is the mistake that turns a wallet login into a vulnerability. Recovering the address that signed a message tells you &lt;em&gt;someone&lt;/em&gt; signed &lt;em&gt;something&lt;/em&gt;. It does not tell you they meant to log into &lt;em&gt;your&lt;/em&gt; site, right &lt;em&gt;now&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So the plugin does all of this before anyone is authenticated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It recovers the signer &lt;strong&gt;and&lt;/strong&gt; checks it equals the address claimed &lt;em&gt;inside&lt;/em&gt; the signed message.&lt;/li&gt;
&lt;li&gt;It enforces &lt;strong&gt;domain binding&lt;/strong&gt; — the SIWE &lt;code&gt;domain&lt;/code&gt; must match your configured domain. This is the anti-phishing / cross-site-replay control. Skip it and a signature farmed on a phishing page replays against you.&lt;/li&gt;
&lt;li&gt;It validates the &lt;code&gt;uri&lt;/code&gt;, and the &lt;code&gt;issuedAt&lt;/code&gt; / &lt;code&gt;expiration&lt;/code&gt; timestamps with a ±5 minute clock-skew tolerance.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;nonce is single-use.&lt;/strong&gt; It's generated server-side (128-bit CSPRNG), stored in Keycloak's own authentication-session note, required to match on postback, and removed on success. No replay.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notably, the nonce lives in Keycloak's session storage — not Redis. The plugin adds &lt;em&gt;zero&lt;/em&gt; new infrastructure. That was a hard requirement: a Keycloak operator should be able to drop in a JAR, not stand up a datastore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: smart-contract wallets without dragging web3j into Keycloak
&lt;/h2&gt;

&lt;p&gt;EOA verification is just &lt;code&gt;ecrecover&lt;/code&gt;. Smart-contract wallets are where it gets interesting, because the wallet is a contract and "is this signature valid?" is a question only the chain can answer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EIP-1271&lt;/strong&gt; (deployed contract wallets): call the wallet's &lt;code&gt;isValidSignature&lt;/code&gt; via an Ethereum node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EIP-6492&lt;/strong&gt; (counterfactual wallets that aren't deployed yet): the signature is wrapped in an envelope that deploys the wallet counterfactually inside a single &lt;code&gt;eth_call&lt;/code&gt; frame, checks the signature, and unwinds. The dispatch order matters — you have to detect the 6492 wrapper &lt;em&gt;first&lt;/em&gt;, because it's a property of the signature, not the address, and can appear even on an already-deployed contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catch: doing this normally means pulling in web3j-core, which drags RxJava and OkHttp onto Keycloak's server classpath. So instead the plugin implements the chain client over &lt;strong&gt;Java 21's native &lt;code&gt;HttpClient&lt;/code&gt;&lt;/strong&gt; with hand-rolled ABI encoding. If you don't configure an RPC URL, EVM verification cleanly degrades to EOA-only. Keycloak's classpath stays clean either way.&lt;/p&gt;

&lt;p&gt;(One more packaging detail that costs everyone an afternoon the first time: the plugin ships as a fat JAR but &lt;strong&gt;excludes BouncyCastle&lt;/strong&gt;, because Keycloak already provides it on the boot classpath and bundling a second copy causes runtime linkage clashes.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: identity is the address, not the chain
&lt;/h2&gt;

&lt;p&gt;A wallet identity is modeled as a CAIP-10 account — &lt;code&gt;namespace:address&lt;/code&gt; — and keyed on exactly that. Not &lt;code&gt;(address, provider, chainId)&lt;/code&gt;. The reason: for EVM, the same private key controls every chain, so putting &lt;code&gt;chainId&lt;/code&gt; in the identity key would split one human into many accounts the moment they switch networks. Chain ID is session context, not identity. The Keycloak username is the canonical &lt;code&gt;namespace:address&lt;/code&gt;, so a wallet owner is always the same Keycloak user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;Grab &lt;code&gt;w3auth-keycloak-plugin-1.0.2.jar&lt;/code&gt; from the &lt;a href="https://github.com/aliIsazadeh/keycloak-wallet-authenticator/releases/latest" rel="noopener noreferrer"&gt;latest release&lt;/a&gt; — CI-built, with a SHA-256 checksum — or build from source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# build&lt;/span&gt;
./gradlew :w3auth-keycloak-plugin:jar

&lt;span class="c"&gt;# install&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;w3auth-keycloak-plugin/build/libs/w3auth-keycloak-plugin-&lt;span class="k"&gt;*&lt;/span&gt;.jar /opt/keycloak/providers/
/opt/keycloak/bin/kc.sh build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; /opt/keycloak/bin/kc.sh start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the admin console, add the &lt;strong&gt;Web3 Wallet Authenticator&lt;/strong&gt; to a copy of the browser flow, set your expected domain and URI, optionally add an RPC URL for smart-contract wallets, and you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is, and where it isn't
&lt;/h2&gt;

&lt;p&gt;It's open source under Apache-2.0. It's a real plugin, not a demo — but it's early, and I'd genuinely like eyes on the security-sensitive paths. If you run Keycloak and have wanted wallet login, try it and tell me where it breaks.&lt;/p&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/aliIsazadeh/keycloak-wallet-authenticator" rel="noopener noreferrer"&gt;github.com/aliIsazadeh/keycloak-wallet-authenticator&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if your team needs wallet auth or web3 identity wired into your stack — Keycloak or otherwise — this is the kind of thing I build. I'm reachable at &lt;a href="mailto:isazadhali@gmail.com"&gt;isazadhali@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>web3</category>
      <category>ethereum</category>
      <category>security</category>
    </item>
  </channel>
</rss>
