![](https://m.primal.net/JXux.jpg)
@ Razvan
2025-02-02 09:00:46
In this blog post, I will walk you through the process of integrating Nostr login functionality into my Rails application using the Nos2x extension. This involved modifying the Devise User model, adding a new route to handle Nostr users, and integrating JavaScript to enhance the login experience.
## Overview
Nostr is a decentralized protocol that allows users to authenticate without relying on traditional email/password combinations. By leveraging the Nos2x extension, I was able to implement Nostr login seamlessly. Here’s how I did it.
## Step 1: Adding Asynchronous Nostr client in Ruby
```bash
bundle add nostr
```
## Step 2: Modifying the Devise User Model
The first step was to modify the Devise User model to allow empty email validation. This is crucial because Nostr users may not have an email address. I updated the `User` model as follows:
```ruby
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable, :registerable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable # Removed :validatable
# Validations
validates :nostr_public_key, presence: true, uniqueness: true, allow_blank: true
validates :email, uniqueness: true, allow_blank: true # Make email optional
validates :password, length: { minimum: 6 }, allow_blank: true # Make password optional but enforce length if provided
# Conditional validation for email and password
validates :email, presence: true, unless: :nostr_public_key_present?
validates :password, presence: true, unless: :nostr_public_key_present?
private
def nostr_public_key_present?
nostr_public_key.present?
end
end
```
By adding this validation, I ensured that users logging in via Nostr would not be required to provide an email address.
## Step 3: Adding a New Route for Nostr Users
Next, I needed to create a new route to handle the login process for Nostr users. I added the following route to my `config/routes.rb` file:
```ruby
Rails.application.routes.draw do
# Other existing routes...
post 'users/nostr_login', to: 'users#nostr_login', as: 'nostr_login'
end
```
This route points to a new action in the `UsersController` that will manage the authentication process for Nostr users.
## Step 4: Implementing the Nostr Login Action
In the `UsersController`, I implemented the `nostr_signup` action to handle the login logic:
```ruby
# app/controllers/users_controller.rb
class UsersController < ApplicationController
# Custom error handling for CSRF token verification failure
rescue_from ActionController::InvalidAuthenticityToken do |exception|
render json: { error: "Can't verify CSRF token authenticity." }, status: :unprocessable_entity
end
def nostr_signup
nostr_signed_event = params[:signed_event]
if not nostr_signed_event
render json: { success: false, error: "No signed event provided." }, status: :unprocessable_entity
return
end
public_key = nostr_signed_event[:pubkey]
content = nostr_signed_event[:content]
kind = nostr_signed_event[:kind]
csrf_token = request.headers['X-CSRF-Token']
tags_hash = tags.to_h
challenge_value = tags_hash["challenge"]
relay_value = tags_hash["relay"]
expected_relay_value = ENV.fetch('NOSTR_AUTH_RELAY_URL', 'wss://relay.primal.net')
# Check if the challenge_value and relay_value are matching or if kind is valid
if challenge_value != csrf_token || kind != 22242 || relay_value != expected_relay_value
return render json: { success: false, error: "Invalid event provided." }, status: :unprocessable_entity
end
nostr_event = Nostr::Event.new(
content: content,
id: nostr_signed_event[:id],
created_at: nostr_signed_event[:created_at],
kind: kind,
pubkey: public_key,
sig: nostr_signed_event[:sig],
tags: nostr_signed_event[:tags]
)
# Validate the event's signature
unless nostr_event.verify_signature
render json: { success: false, error: "Invalid event signature." }, status: :unprocessable_entity
return
end
user = User.find_or_initialize_by(nostr_public_key: public_key)
...
end
private
def nostr_signup_params
params.permit(signed_event: [:id, :kind, :content, :tags, :created_at, :pubkey, :sig])
end
end
```
This action will handle the authentication process and create a new user if they do not already exist.
## Step 5: Integrating JavaScript for Nostr Login
To enhance the user experience, I added a link for Nostr login on the Devise login page. I included the following JavaScript code to handle the Nostr login process:
```javascript
document.addEventListener("turbo:load", function () {
if (isNostrAvailable() && !document.getElementById('nostr-signup')) {
createNostrSignupButton();
}
});
// Function to check if Nostr is available
function isNostrAvailable() {
return window.nostr && typeof window.nostr.getPublicKey === 'function';
}
// Function to create and append the Nostr signup button
function createNostrSignupButton() {
event.preventDefault();
const button = document.createElement('button');
button.id = "nostr-signup";
button.onclick = function(event) {
event.preventDefault();
nostrSignupHandler();
};
button.type = "submit";
button.textContent = "<%= t('nostr.with_extension') %>";
const actionsDiv = document.querySelector('.actions');
if (actionsDiv) {
actionsDiv.appendChild(button);
}
}
async function nostrSignupHandler() {
try {
const publicKey = await window.nostr.getPublicKey();
if (!publicKey) {
alert("Sign-up failed. No public key.");
}
token = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
const event = {
"created_at": Math.floor(Date.now() / 1000),
"content": "",
"tags": [
[
"relay",
"<%= ENV['NOSTR_AUTH_RELAY_URL'] || 'wss://relay.primal.net' %>"
],
[
"challenge",
token
]
],
"kind": 22242,
"pubkey": publicKey
};
// Sign the event
const signedEvent = await window.nostr.signEvent(event);
if (!signedEvent) {
alert("Sign-up failed. Event not signed.");
}
const response = await fetch("/nostr_signup", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": token,
},
body: JSON.stringify({ "signed_event": signedEvent }),
});
...
```
## Conclusion
By following these steps, I successfully integrated Nostr login functionality into my Rails application using the Nos2x extension. This not only enhances the user experience by providing a decentralized login option but also aligns with modern authentication practices.
Feel free to reach out if you have any questions or need further assistance with implementing Nostr login in your own applications!
If you want to see it in action, go ahead to [Medical Dictionary - Login Page](https://medical.inarchives.com/users/sign_in) for example. You will need to be on a desktop browser with a Nostr Extension installed.
Happy coding!