# 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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.lightningrails.com/web-3-blockchain/metamask-authentication.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
