> For the complete documentation index, see [llms.txt](https://docs.lightningrails.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.lightningrails.com/web-3-blockchain/metamask-authentication.md).

# MetaMask Authentication

<figure><img src="/files/6UQhQNbNM3kaMtBOFnI3" alt=""><figcaption></figcaption></figure>

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

```ruby
# Gemfile
gem "omniauth"
gem "omniauth-rails_csrf_protection"
gem "omniauth_openid_connect", ">= 0.8.0"
```

```bash
bundle install
```

***

### 2) Add User fields

Generate and run the migration to store the wallet address and a timestamp:

```bash
rails g migration AddWeb3ToUsers wallet_address:string last_siwe_at:datetime
bin/rails db:migrate
```

If you prefer the exact file from the PR:

```ruby
# 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

```ruby
# 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).

```ruby
# 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
```

```ruby
# config/initializers/omniauth_protection.rb
OmniAuth.config.allowed_request_methods = %i[post get]
```

***

### 5) Devise routes

```ruby
# 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:

```bash
rails g controller users/omniauth_callbacks
```

Replace its content with:

```ruby
# 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):

```erb
# 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:

```ruby
# 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:

```erb
# 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)

```bash
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:

```bash
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 change `SIWE_ISSUER` and the registration URL.

***

### 11) Sanity checks

After you’ve set everything:

```bash
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/...`**&#x59;our 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` and `omniauth_providers: [:openid_connect]` are in `User`.
* **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.
