Refactor Nostr settings/connect
* Use NIP-42 auth event instead of short text note * Verify event ID and signature using the nostr gem instead of custom code
This commit is contained in:
parent
d4e67a830c
commit
22d362e1a0
@ -1,4 +1,5 @@
|
|||||||
PRIMARY_DOMAIN=kosmos.org
|
PRIMARY_DOMAIN=kosmos.org
|
||||||
|
AKKOUNTS_DOMAIN=accounts.kosmos.org
|
||||||
|
|
||||||
REDIS_URL='redis://localhost:6379/0'
|
REDIS_URL='redis://localhost:6379/0'
|
||||||
|
|
||||||
|
@ -63,4 +63,9 @@ class ApplicationController < ActionController::Base
|
|||||||
@fetch_balance_retried = true
|
@fetch_balance_retried = true
|
||||||
lndhub_fetch_balance
|
lndhub_fetch_balance
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def nostr_event_from_params
|
||||||
|
params.permit!
|
||||||
|
params[:signed_event].to_h.symbolize_keys
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -87,25 +87,27 @@ class SettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_nostr_pubkey
|
def set_nostr_pubkey
|
||||||
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
|
signed_event = Nostr::Event.new(**nostr_event_from_params)
|
||||||
|
|
||||||
is_valid_id = NostrManager::ValidateId.call(event: signed_event)
|
is_valid_sig = signed_event.verify_signature
|
||||||
is_valid_sig = NostrManager::VerifySignature.call(event: signed_event)
|
is_valid_auth = NostrManager::VerifyAuth.call(
|
||||||
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
|
event: signed_event,
|
||||||
|
challenge: session[:shared_secret]
|
||||||
|
)
|
||||||
|
|
||||||
unless is_valid_id && is_valid_sig && is_correct_content
|
unless is_valid_sig && is_valid_auth
|
||||||
flash[:alert] = "Public key could not be verified"
|
flash[:alert] = "Public key could not be verified"
|
||||||
http_status :unprocessable_entity and return
|
http_status :unprocessable_entity and return
|
||||||
end
|
end
|
||||||
|
|
||||||
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event[:pubkey])
|
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
|
||||||
|
|
||||||
if user_with_pubkey.present? && (user_with_pubkey != current_user)
|
if user_with_pubkey.present? && (user_with_pubkey != current_user)
|
||||||
flash[:alert] = "Public key already in use for a different account"
|
flash[:alert] = "Public key already in use for a different account"
|
||||||
http_status :unprocessable_entity and return
|
http_status :unprocessable_entity and return
|
||||||
end
|
end
|
||||||
|
|
||||||
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event[:pubkey])
|
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event.pubkey)
|
||||||
session[:shared_secret] = nil
|
session[:shared_secret] = nil
|
||||||
|
|
||||||
flash[:success] = "Public key verification successful"
|
flash[:success] = "Public key verification successful"
|
||||||
@ -160,12 +162,6 @@ class SettingsController < ApplicationController
|
|||||||
params.require(:user).permit(:current_password)
|
params.require(:user).permit(:current_password)
|
||||||
end
|
end
|
||||||
|
|
||||||
def nostr_event_params
|
|
||||||
params.permit(signed_event: [
|
|
||||||
:id, :pubkey, :created_at, :kind, :content, :sig, tags: []
|
|
||||||
])
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_email_password
|
def generate_email_password
|
||||||
characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
|
characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
|
||||||
SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join
|
SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join
|
||||||
|
@ -3,7 +3,12 @@ import { Controller } from "@hotwired/stimulus"
|
|||||||
// Connects to data-controller="settings--nostr-pubkey"
|
// Connects to data-controller="settings--nostr-pubkey"
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
|
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
|
||||||
static values = { userAddress: String, pubkeyHex: String, sharedSecret: String }
|
static values = {
|
||||||
|
userAddress: String,
|
||||||
|
pubkeyHex: String,
|
||||||
|
site: String,
|
||||||
|
sharedSecret: String
|
||||||
|
}
|
||||||
|
|
||||||
connect () {
|
connect () {
|
||||||
if (window.nostr) {
|
if (window.nostr) {
|
||||||
@ -19,11 +24,15 @@ export default class extends Controller {
|
|||||||
this.setPubkeyTarget.disabled = true
|
this.setPubkeyTarget.disabled = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Auth based on NIP-42
|
||||||
const signedEvent = await window.nostr.signEvent({
|
const signedEvent = await window.nostr.signEvent({
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
kind: 1,
|
kind: 22242,
|
||||||
tags: [],
|
tags: [
|
||||||
content: `Connect my public key to ${this.userAddressValue} (confirmation ${this.sharedSecretValue})`
|
["site", this.siteValue],
|
||||||
|
["challenge", this.sharedSecretValue]
|
||||||
|
],
|
||||||
|
content: ""
|
||||||
})
|
})
|
||||||
|
|
||||||
const res = await fetch("/settings/set_nostr_pubkey", {
|
const res = await fetch("/settings/set_nostr_pubkey", {
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
module NostrManager
|
|
||||||
class ValidateId < NostrManagerService
|
|
||||||
def initialize(event:)
|
|
||||||
@event = Nostr::Event.new(**event)
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
@event.id == Digest::SHA256.hexdigest(JSON.generate(@event.serialize))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
17
app/services/nostr_manager/verify_auth.rb
Normal file
17
app/services/nostr_manager/verify_auth.rb
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module NostrManager
|
||||||
|
class VerifyAuth < NostrManagerService
|
||||||
|
def initialize(event:, challenge:)
|
||||||
|
@event = event
|
||||||
|
@challenge_expected = challenge
|
||||||
|
@site_expected = Setting.accounts_domain
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
site_given = @event.tags.find{|t| t[0] == "site"}[1]
|
||||||
|
challenge_given = @event.tags.find{|t| t[0] == "challenge"}[1]
|
||||||
|
|
||||||
|
site_given == @site_expected &&
|
||||||
|
challenge_given == @challenge_expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,17 +0,0 @@
|
|||||||
module NostrManager
|
|
||||||
class VerifySignature < NostrManagerService
|
|
||||||
def initialize(event:)
|
|
||||||
@event = Nostr::Event.new(**event)
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
Schnorr.check_sig!(
|
|
||||||
[@event.id].pack('H*'),
|
|
||||||
[@event.pubkey].pack('H*'),
|
|
||||||
[@event.sig].pack('H*')
|
|
||||||
)
|
|
||||||
rescue Schnorr::InvalidSignatureError
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -3,6 +3,7 @@
|
|||||||
<h4 class="mb-0">Public Key</h4>
|
<h4 class="mb-0">Public Key</h4>
|
||||||
<div data-controller="settings--nostr-pubkey"
|
<div data-controller="settings--nostr-pubkey"
|
||||||
data-settings--nostr-pubkey-user-address-value="<%= current_user.address %>"
|
data-settings--nostr-pubkey-user-address-value="<%= current_user.address %>"
|
||||||
|
data-settings--nostr-pubkey-site-value="<%= Setting.accounts_domain %>"
|
||||||
data-settings--nostr-pubkey-shared-secret-value="<%= session[:shared_secret] %>"
|
data-settings--nostr-pubkey-shared-secret-value="<%= session[:shared_secret] %>"
|
||||||
data-settings--nostr-pubkey-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
|
data-settings--nostr-pubkey-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ RSpec.describe "Settings", type: :request do
|
|||||||
|
|
||||||
describe "POST /settings/set_nostr_pubkey" do
|
describe "POST /settings/set_nostr_pubkey" do
|
||||||
before do
|
before do
|
||||||
session_stub = { shared_secret: "rMjWEmvcvtTlQkMd" }
|
session_stub = { shared_secret: "YMeTyOxIEJcfe6vd" }
|
||||||
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
|
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -38,12 +38,13 @@ RSpec.describe "Settings", type: :request do
|
|||||||
|
|
||||||
post set_nostr_pubkey_settings_path, params: {
|
post set_nostr_pubkey_settings_path, params: {
|
||||||
signed_event: {
|
signed_event: {
|
||||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
id: "7cc165c4fe4c9ec3f2b859cb422f01b38beaf6bbd228fea928ea1400ec254a89",
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
||||||
created_at: 1678254161,
|
created_at: 1711963922,
|
||||||
kind: 1,
|
kind: 22242,
|
||||||
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
tags: [["site","accounts.kosmos.org"],["challenge","YMeTyOxIEJcfe6vd"]],
|
||||||
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
|
content: "",
|
||||||
|
sig: "b484a28cd9c92facca0eba80e8ef5303d25ed044c3815e3a068b9887f91d3546ad209a0dd674c59b48cf8057aecd75df5416973d17ed58f68195030af09c28d1"
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
}.to_json, headers: {
|
||||||
"CONTENT_TYPE" => "application/json",
|
"CONTENT_TYPE" => "application/json",
|
||||||
@ -69,12 +70,13 @@ RSpec.describe "Settings", type: :request do
|
|||||||
|
|
||||||
post set_nostr_pubkey_settings_path, params: {
|
post set_nostr_pubkey_settings_path, params: {
|
||||||
signed_event: {
|
signed_event: {
|
||||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
id: "7cc165c4fe4c9ec3f2b859cb422f01b38beaf6bbd228fea928ea1400ec254a89",
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
||||||
created_at: 1678254161,
|
created_at: 1711963922,
|
||||||
kind: 1,
|
kind: 22242,
|
||||||
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
tags: [["site","accounts.kosmos.org"],["challenge","YMeTyOxIEJcfe6vd"]],
|
||||||
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
|
content: "",
|
||||||
|
sig: "b484a28cd9c92facca0eba80e8ef5303d25ed044c3815e3a068b9887f91d3546ad209a0dd674c59b48cf8057aecd75df5416973d17ed58f68195030af09c28d1"
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
}.to_json, headers: {
|
||||||
"CONTENT_TYPE" => "application/json",
|
"CONTENT_TYPE" => "application/json",
|
||||||
@ -91,18 +93,20 @@ RSpec.describe "Settings", type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "With wrong username" do
|
context "With wrong site tag" do
|
||||||
before do
|
before do
|
||||||
|
Setting.accounts_domain = "accounts.wikipedia.org"
|
||||||
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
||||||
|
|
||||||
post set_nostr_pubkey_settings_path, params: {
|
post set_nostr_pubkey_settings_path, params: {
|
||||||
signed_event: {
|
signed_event: {
|
||||||
id: "2e1e20ee762d6a5b5b30835eda9ca03146e4baf82490e53fd75794c08de08ac0",
|
id: "7cc165c4fe4c9ec3f2b859cb422f01b38beaf6bbd228fea928ea1400ec254a89",
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
||||||
created_at: 1678255391,
|
created_at: 1711963922,
|
||||||
kind: 1,
|
kind: 22242,
|
||||||
content: "Connect my public key to admin@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
tags: [["site","accounts.kosmos.org"],["challenge","YMeTyOxIEJcfe6vd"]],
|
||||||
sig: "2ace19c9db892ac6383848721a3e08b13d90d689fdeac60d9633a623d3f08eb7e0d468f1b3e928d1ea979477c2ec46ee6cdb2d053ef2e4ed3c0630a51d249029"
|
content: "",
|
||||||
|
sig: "b484a28cd9c92facca0eba80e8ef5303d25ed044c3815e3a068b9887f91d3546ad209a0dd674c59b48cf8057aecd75df5416973d17ed58f68195030af09c28d1"
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
}.to_json, headers: {
|
||||||
"CONTENT_TYPE" => "application/json",
|
"CONTENT_TYPE" => "application/json",
|
||||||
@ -110,6 +114,10 @@ RSpec.describe "Settings", type: :request do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
Setting.accounts_domain = "accounts.kosmos.org"
|
||||||
|
end
|
||||||
|
|
||||||
it "returns a 422 status" do
|
it "returns a 422 status" do
|
||||||
expect(response).to have_http_status(422)
|
expect(response).to have_http_status(422)
|
||||||
end
|
end
|
||||||
@ -128,12 +136,13 @@ RSpec.describe "Settings", type: :request do
|
|||||||
|
|
||||||
post set_nostr_pubkey_settings_path, params: {
|
post set_nostr_pubkey_settings_path, params: {
|
||||||
signed_event: {
|
signed_event: {
|
||||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
id: "7cc165c4fe4c9ec3f2b859cb422f01b38beaf6bbd228fea928ea1400ec254a89",
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
||||||
created_at: 1678254161,
|
created_at: 1711963922,
|
||||||
kind: 1,
|
kind: 22242,
|
||||||
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
tags: [["site","accounts.kosmos.org"],["challenge","YMeTyOxIEJcfe6vd"]],
|
||||||
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
|
content: "",
|
||||||
|
sig: "b484a28cd9c92facca0eba80e8ef5303d25ed044c3815e3a068b9887f91d3546ad209a0dd674c59b48cf8057aecd75df5416973d17ed58f68195030af09c28d1"
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
}.to_json, headers: {
|
||||||
"CONTENT_TYPE" => "application/json",
|
"CONTENT_TYPE" => "application/json",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user