Sponsored Content
Skip to content

Repository files navigation

Keycloak Wallet Authenticator

Wallet login for Keycloak. Sign-In With Ethereum (SIWE) and Solana (SIWS) β€” EOA and smart-contract wallets (EIP-1271, EIP-6492) β€” as a native Keycloak Authenticator. No wallet SDK, no third-party auth service, your existing passwords/MFA/social logins untouched.

CI Release License Keycloak

Wallet login demo β€” Keycloak login β†’ Sign with MetaMask β†’ logged in

Try it in 2 minutes

Docker is the only requirement β€” no Java, no build:

git clone https://github.com/aliIsazadeh/keycloak-wallet-authenticator.git
cd keycloak-wallet-authenticator
docker compose -f demo/docker-compose.yml up

Then open http://localhost:8080/realms/wallet-demo/account and sign in with MetaMask (Ethereum) or Phantom (Solana). Details, admin credentials, and teardown: demo/README.md.


Why this exists

Every existing "Sign-In With Ethereum + Keycloak" project I could find was an abandoned proof-of-concept β€” pinned to dead testnets, EOA-only, no smart-contract wallet support, and untested against a real Keycloak. Teams that already run Keycloak for their identity and want to add wallet login are left wiring up a second OIDC server or a fragile reverse-proxy hack.

Keycloak Wallet Authenticator is the version that isn't a toy: a real Authenticator SPI plugin, built on a framework-free verification core, tested end-to-end against a live Keycloak in Testcontainers.

What it does

  • Adds wallet login to your existing Keycloak as one Authenticator in a browser flow. Your passwords, MFA, and social logins stay exactly as they are β€” this is additive, not a replacement.
  • Verifies the signature, not just the address. It recovers the signer, checks it matches the address claimed inside the signed message, and validates every server-authoritative field (domain, URI, nonce, issued-at / expiration) before anyone is logged in.
  • Provisions a stable Keycloak user keyed on wallet identity, so switching chains never splits one wallet owner into multiple users.

Feature matrix

Capability Status
SIWE β€” Sign-In With Ethereum (EIP-4361) βœ…
SIWS β€” Sign-In With Solana βœ…
EVM chains (eip155) βœ…
Solana (Ed25519) βœ…
EOA (externally-owned) wallets βœ…
EIP-1271 deployed smart-contract wallets βœ…
EIP-6492 counterfactual (pre-deploy) smart-contract wallets βœ…
Native (non-browser) direct-grant flow for mobile/native apps βœ…
Single-use nonce (replay-protected) βœ…
Domain / URI binding (anti-phishing) βœ…
Tested against real Keycloak (Testcontainers) βœ…
Zero wallet-SDK dependency βœ…
Zero external auth service βœ…

How it works

Two round trips, and the server is authoritative on domain, nonce, and expiry:

  1. Challenge. When the Web3 Wallet Authenticator runs, it generates a 128-bit CSPRNG nonce, stores it in the Keycloak authentication-session note (no Redis, no extra infrastructure), and renders a login page that talks to the browser wallet (window.ethereum / Phantom window.solana).
  2. Verify. On postback it parses the SIWE/SIWS message, requires the message nonce to equal the stored note (single-use β€” the note is removed on success), validates domain / URI / timestamps (Β±5 min skew), verifies the signature, and confirms the recovered signer equals the claimed address. Only then is a Keycloak user provisioned or logged in.

The Keycloak username is the canonical identity key namespace:address, so the same wallet is always the same user regardless of chain. Address, namespace, and chain ID are stored as user attributes (w3auth_address, w3auth_namespace, w3auth_chainId).

For EVM smart-contract wallets, the plugin talks to an Ethereum node over Java 21's native HttpClient (HttpChainClient) β€” deliberately avoiding web3j-core so Keycloak's server classpath stays clean.

Native and mobile apps that can't drive a browser flow can use the direct-grant wire contract instead: see docs/native-api.md.

Install into your own Keycloak

Requirements: Keycloak 25+, Java 21, an EVM RPC endpoint only if you need smart-contract wallet support.

1. Get the plugin JAR

Grab the prebuilt fat JAR from the latest release (SHA-256 checksums included), or build it yourself:

./gradlew :w3auth-keycloak-plugin:jar
# produces w3auth-keycloak-plugin/build/libs/w3auth-keycloak-plugin-<version>.jar

2. Install it into Keycloak

cp w3auth-keycloak-plugin-*.jar /opt/keycloak/providers/
/opt/keycloak/bin/kc.sh build
/opt/keycloak/bin/kc.sh start

3. Add it to a browser flow

In the admin console: Authentication β†’ Flows, duplicate the browser flow, add an execution, choose Web3 Wallet Authenticator, and bind the flow to your realm (or an application) as the browser flow.

4. Configure it

On the authenticator's config, set:

Setting Key Default Notes
Expected Domain expected-domain localhost:8080 Must match the SIWE/SIWS domain. This is the anti-phishing control β€” set it to your real domain in production.
Expected URI expected-uri http://localhost:8080 Must match the message uri.
Ethereum RPC URL ethereum-rpc-url (empty) JSON-RPC endpoint for EIP-1271 / EIP-6492 verification. Leave empty and smart-contract wallets are disabled β€” EVM falls back to EOA-only.

That's it. Users can now log in with a wallet.

Configuration: expected-domain must be host:port, exactly. EIP-4361 requires the SIWE/SIWS domain field to equal the requesting page's origin authority β€” the host, plus the port when it's non-default β€” exactly as it appears in the browser's address bar. expected-domain must match that, not just the bare hostname. If it's wrong, the wallet doesn't error clearly; you'll see something like "the domain in the sign-in message does not match the requesting app's origin" with no mention of Keycloak or this plugin. Example: serving on http://localhost:8080 needs expected-domain = localhost:8080, not localhost.

Wallet-user profile data

Users provisioned through wallet login get placeholder profile data: first name "Wallet", last name set to the wallet's address, and a non-deliverable email (<address>@wallet.invalid) with emailVerified set to true. This is deliberate β€” Keycloak's default declarative user profile requires first name/last name/email, and a bare user without them gets diverted to an "Update Account Information" screen after a valid wallet signature, defeating the point of wallet-only login. The placeholders keep login to one round trip on any realm, with no extra realm configuration required.

If you want to collect a real email or name from wallet users instead β€” for example to send them account notifications β€” the plugin only sets these placeholders once, at first provisioning; it never overwrites them on later logins. Update the user's profile yourself afterward (admin console, Admin REST API, or your own post-login flow), or adjust your realm's User profile configuration to require the fields you actually want collected.

Security model

  • Domain binding is enforced β€” a signature valid for another site's domain is rejected. This is the anti-phishing / cross-site-replay control.
  • Single-use nonces β€” the challenge nonce is consumed on verify and cannot be replayed.
  • Claim validation, not address recovery β€” the recovered signer must equal the address inside the signed message; recovery alone authenticates nobody.
  • Server-authoritative fields β€” domain, URI, and expiry are checked against server config and policy, with a Β±5 minute clock-skew tolerance.

Found a vulnerability? See SECURITY.md. Please do not open a public issue for security reports.

Repository layout

This repo is a multi-module Gradle build (Java 21, Kotlin DSL, centralized version catalog):

Module Role
w3auth-core Framework-free verification engine: identity, challenge, SIWE/SIWS parsing, signature verification, sessions. No Spring / JPA / Redis on its classpath.
w3auth-keycloak-plugin The Keycloak Authenticator SPI plugin β€” the product, and this README's focus. Fat JAR, BouncyCastle excluded (Keycloak provides it).
demo The 2-minute Docker quickstart above.
examples/self-hosted-rest-api Reference example, not the product. A standalone Spring Boot REST auth API built on the same core, showing how to self-host wallet login without Keycloak. See its README.

The same verification engine powers both the plugin and the reference REST API β€” the architecture is protocol-driven, so no wallet vendor's code ever leaks into it.

Design notes

The deeper reasoning β€” why identity is modeled as CAIP-10 namespace:address, why nonces are consumed atomically, the EIP-6492 dispatch ordering, why HS256 for short-lived tokens β€” lives in docs/ARCHITECTURE.md.

Self-hosting without Keycloak (reference example)

Not running Keycloak? The repo ships a small reference example that demonstrates the same verification core (w3auth-core) self-hosted as a standalone Spring Boot REST API, with its own Postgres/Redis adapters and challenge/verify/refresh/logout endpoints. It is a demonstration, not the product β€” the Keycloak plugin above remains the headline. If you already run Keycloak, ignore this; use the plugin.

See examples/self-hosted-rest-api to run it.

License

Apache-2.0. See LICENSE.

Who built this

I build production-grade wallet and crypto-identity authentication infrastructure. If your team needs wallet login in Keycloak (or elsewhere), or custom web3 identity integration, I'm available for consulting and integration work.

About

Wallet login for Keycloak β€” Sign-In with Ethereum (SIWE) & Solana (SIWS), EOA + smart-contract wallets (EIP-1271/6492), as a native Authenticator plugin

Topics

Resources

Security policy

Stars

10 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages