🦊MetaMask Authentication
Sign in with MetaMask (SIWE) using OmniAuth OpenID Connect

This guide adds “Sign in with MetaMask” (Sign-In with Ethereum, SIWE) to your Lightning Rails app using Devise + OmniAuth OpenID Connect. It keeps your regular Devise auth and adds a wallet-based login. No private keys ever touch your server—users only sign a message.
You’ll:
Install and configure OmniAuth OIDC
Register an OIDC client for development and production
Add a Devise callback controller
Store the user’s wallet address and show it in the navbar
1) Add gems
# Gemfile
gem "omniauth"
gem "omniauth-rails_csrf_protection"
gem "omniauth_openid_connect", ">= 0.8.0"
bundle install
2) Add User fields
Generate and run the migration to store the wallet address and a timestamp:
rails g migration AddWeb3ToUsers wallet_address:string last_siwe_at:datetime
bin/rails db:migrate
If you prefer the exact file from the PR:
# db/migrate/xxxxxx_add_web3_to_users.rb
class AddWeb3ToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :wallet_address, :string
add_index :users, :wallet_address
add_column :users, :last_siwe_at, :datetime
end
end
3) Make your Devise model omniauthable
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:omniauthable, omniauth_providers: [:openid_connect]
end
We use the provider name
:openid_connect
to match routes and callback.
4) OmniAuth configuration
Create the OmniAuth initializer and allow POST/GET (GET helps during local testing; you can lock to POST later).
# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :openid_connect,
name: :openid_connect, # Devise works best with this name
scope: %i[openid profile],
response_type: :code,
issuer: ENV.fetch("SIWE_ISSUER"), # e.g. https://oidc.login.xyz/
discovery: true,
client_options: {
identifier: ENV.fetch("SIWE_CLIENT_ID"),
secret: ENV.fetch("SIWE_CLIENT_SECRET"),
redirect_uri: ENV.fetch("SIWE_REDIRECT_URI")
}
end
# config/initializers/omniauth_protection.rb
OmniAuth.config.allowed_request_methods = %i[post get]
5) Devise routes
# config/routes.rb
devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }
This generates:
Authorize:
/users/auth/openid_connect
Callback:
/users/auth/openid_connect/callback
6) OmniAuth callbacks controller
Generate the controller:
rails g controller users/omniauth_callbacks
Replace its content with:
# app/controllers/users/omniauth_callbacks_controller.rb
module Users
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def openid_connect
auth = request.env["omniauth.auth"] || {}
info = auth["info"] || {}
extra = auth["extra"] || {}
raw = (extra["raw_info"] || {}) # userinfo claims
# Common places SIWE providers put the address (varies by issuer)
addr = (info["address"] ||
raw["address"] ||
raw["wallet_address"] ||
auth["uid"]).to_s.downcase
if addr.blank?
Rails.logger.warn("SIWE: missing address. auth=#{auth.inspect}")
redirect_to new_user_session_path, alert: "Wallet address missing from SIWE response"
return
end
user = User.find_or_initialize_by(wallet_address: addr)
if user.new_record?
user.email = "eth-#{addr}@example.invalid" # or prompt later
user.password = Devise.friendly_token[0, 32]
end
user.last_siwe_at = Time.current
user.save!
sign_in_and_redirect user, event: :authentication
set_flash_message(:notice, :success, kind: "MetaMask") if is_navigational_format?
rescue => e
Rails.logger.error("SIWE callback error: #{e.class} #{e.message}")
redirect_to new_user_session_path, alert: "Sign-in failed"
end
def failure
redirect_to new_user_session_path, alert: "Sign-in canceled"
end
end
end
7) Add the “Sign in with MetaMask” button
On your Devise sessions page (Lightning Rails’ sign-in):
# app/views/devise/sessions/new.html.erb
<%= button_to user_openid_connect_omniauth_authorize_path,
method: :post,
data: { turbo: false },
class: "btn btn-primary gap-2 mt-4" do %>
<!-- MetaMask logo (optional SVG) -->
<svg ... class="w-6 h-6">...</svg>
Sign in with MetaMask
<% end %>
OmniAuth 2 prefers POST for the authorize request; using
button_to
handles that.
8) Show the wallet in the navbar (optional)
Helper:
# app/helpers/application_helper.rb
module ApplicationHelper
def truncate_address(address)
return "" unless address.present?
core = extract_core_address(address)
"0x...#{core[-4..]}"
end
def extract_core_address(address)
return "" unless address.present?
# Handle eip155:1:0xabc...
if address.include?(":")
parts = address.split(":")
return parts.last if parts.last.start_with?("0x")
end
address.start_with?("0x") ? address : "0x#{address}"
end
end
Navbar snippet:
# app/views/shared/_navbar.html.erb (or your layout’s nav)
<% if user_signed_in? && current_user.wallet_address.present? %>
<%= truncate_address(current_user.wallet_address) %>
<% else %>
<%= link_to "Get started", new_user_registration_path, class: "btn btn-primary" %>
<% end %>
9) Environment variables (.env)
Lightning Rails already loads .env
. Add these:
# Development issuer (example: login.xyz)
SIWE_ISSUER=https://oidc.login.xyz/
SIWE_CLIENT_ID=your_dev_client_id
SIWE_CLIENT_SECRET=your_dev_client_secret
SIWE_REDIRECT_URI=http://localhost:3000/users/auth/openid_connect/callback
Important: the redirect path must match your provider name
:openid_connect
. If you change the provider name to:siwe
, use/users/auth/siwe/callback
and update all references.
10) Register your OIDC client
You need a client per environment (dev and prod). Most SIWE OIDC issuers support dynamic registration.
Development registration (example)
curl -X POST 'https://oidc.login.xyz/register' \
-H 'Content-Type: application/json' \
--data '{
"redirect_uris": ["http://localhost:3000/users/auth/openid_connect/callback"]
}'
You’ll receive JSON with client_id
and client_secret
. Put them in your dev .env
.
Production registration
Use your production domain:
curl -X POST 'https://oidc.login.xyz/register' \
-H 'Content-Type: application/json' \
--data '{
"redirect_uris": ["https://YOUR_DOMAIN.com/users/auth/openid_connect/callback"]
}'
Add the prod values to your production environment (e.g., Render/Heroku/Fly secrets) as:
SIWE_ISSUER=https://oidc.login.xyz/
SIWE_CLIENT_ID=your_prod_client_id
SIWE_CLIENT_SECRET=your_prod_client_secret
SIWE_REDIRECT_URI=https://YOUR_DOMAIN.com/users/auth/openid_connect/callback
If you use another issuer (e.g.,
https://oidc.signinwithethereum.org/
), the steps are identical—just changeSIWE_ISSUER
and the registration URL.
11) Sanity checks
After you’ve set everything:
rails routes | grep openid_connect
You should see:
user_openid_connect_omniauth_authorize /users/auth/openid_connect(.:format)
user_openid_connect_omniauth_callback /users/auth/openid_connect/callback(.:format)
Common fixes:
Routing error for
/auth/...
Your Devise routes live under/users/auth/...
. Make sure the registered redirect URI matches exactly.“Mapping omniauth_callbacks on a resource that is not omniauthable”: ensure
:omniauthable
andomniauth_providers: [:openid_connect]
are inUser
.Path helper not found: ensure the provider name matches in the initializer, model, controller method, and the button helper.
12) What you have now
Users can sign in with MetaMask (SIWE) via a trusted OIDC provider.
You store their wallet address on the
users
table.You can display it in the UI and later fetch balances/holdings.
From here, you can add:
A “Link/Unlink wallet” page
ETH balance fetch on the dashboard
Parallel Solana login (Phantom) if you want multi-chain auth
That’s it—SIWE is live in your Lightning Rails app.
Last updated
Was this helpful?