Refactor Nostr auth, add login via Nostr (web extension) #188
@ -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'
|
||||||
|
|
||||||
|
2
Gemfile
2
Gemfile
@ -62,7 +62,7 @@ gem "sentry-rails"
|
|||||||
gem 'discourse_api'
|
gem 'discourse_api'
|
||||||
gem "lnurl"
|
gem "lnurl"
|
||||||
gem 'manifique'
|
gem 'manifique'
|
||||||
gem 'nostr'
|
gem 'nostr', '~> 0.6.0'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# Use sqlite3 as the database for Active Record
|
# Use sqlite3 as the database for Active Record
|
||||||
|
@ -155,7 +155,7 @@ GEM
|
|||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
e2mmap (0.1.0)
|
e2mmap (0.1.0)
|
||||||
ecdsa (1.2.0)
|
ecdsa (1.2.0)
|
||||||
ecdsa_ext (0.5.0)
|
ecdsa_ext (0.5.1)
|
||||||
ecdsa (~> 1.2.0)
|
ecdsa (~> 1.2.0)
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
et-orbi (1.2.7)
|
et-orbi (1.2.7)
|
||||||
@ -278,9 +278,9 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.0-x86_64-linux)
|
nokogiri (1.16.0-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nostr (0.5.0)
|
nostr (0.6.0)
|
||||||
bech32 (~> 1.4)
|
bech32 (~> 1.4)
|
||||||
bip-schnorr (~> 0.6)
|
bip-schnorr (~> 0.7)
|
||||||
ecdsa (~> 1.2)
|
ecdsa (~> 1.2)
|
||||||
event_emitter (~> 0.2)
|
event_emitter (~> 0.2)
|
||||||
faye-websocket (~> 0.11)
|
faye-websocket (~> 0.11)
|
||||||
@ -517,7 +517,7 @@ DEPENDENCIES
|
|||||||
lockbox
|
lockbox
|
||||||
manifique
|
manifique
|
||||||
net-ldap
|
net-ldap
|
||||||
nostr
|
nostr (~> 0.6.0)
|
||||||
pagy (~> 6.0, >= 6.0.2)
|
pagy (~> 6.0, >= 6.0.2)
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
puma (~> 4.1)
|
puma (~> 4.1)
|
||||||
|
@ -42,6 +42,11 @@
|
|||||||
focus:ring-red-500 focus:ring-opacity-75;
|
focus:ring-red-500 focus:ring-opacity-75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-outline-purple {
|
||||||
|
@apply border-2 border-purple-500 hover:bg-purple-100
|
||||||
|
focus:ring-purple-400 focus:ring-opacity-75;
|
||||||
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
@apply bg-gray-100 hover:bg-gray-200 text-gray-400
|
@apply bg-gray-100 hover:bg-gray-200 text-gray-400
|
||||||
focus:ring-gray-300 focus:ring-opacity-75;
|
focus:ring-gray-300 focus:ring-opacity-75;
|
||||||
|
@ -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
|
||||||
|
62
app/controllers/users/sessions_controller.rb
Normal file
62
app/controllers/users/sessions_controller.rb
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Users::SessionsController < Devise::SessionsController
|
||||||
|
# before_action :configure_sign_in_params, only: [:create]
|
||||||
|
|
||||||
|
# GET /resource/sign_in
|
||||||
|
def new
|
||||||
|
session[:shared_secret] = SecureRandom.base64(12)
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /resource/sign_in
|
||||||
|
# def create
|
||||||
|
# super
|
||||||
|
# end
|
||||||
|
|
||||||
|
# DELETE /resource/sign_out
|
||||||
|
# def destroy
|
||||||
|
# super
|
||||||
|
# end
|
||||||
|
|
||||||
|
# POST /users/nostr_login
|
||||||
|
def nostr_login
|
||||||
|
signed_event = Nostr::Event.new(**nostr_event_from_params)
|
||||||
|
|
||||||
|
is_valid_sig = signed_event.verify_signature
|
||||||
|
|||||||
|
is_valid_auth = NostrManager::VerifyAuth.call(
|
||||||
|
event: signed_event,
|
||||||
|
challenge: session[:shared_secret]
|
||||||
|
)
|
||||||
|
|
||||||
|
session[:shared_secret] = nil
|
||||||
|
|
||||||
|
unless is_valid_sig && is_valid_auth
|
||||||
|
flash[:alert] = "Login verification failed"
|
||||||
|
http_status :unauthorized and return
|
||||||
|
end
|
||||||
|
|
||||||
|
user = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
|
||||||
|
|
||||||
|
if user.present?
|
||||||
|
set_flash_message!(:notice, :signed_in)
|
||||||
|
sign_in("user", user)
|
||||||
|
render json: { redirect_url: after_sign_in_path_for(user) }, status: :ok
|
||||||
|
else
|
||||||
|
flash[:alert] = "Failed to find your account. Nostr login may be disabled."
|
||||||
|
http_status :unauthorized
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def set_flash_message(key, kind, options = {})
|
||||||
|
# Hide flash message after redirecting from a signin route while logged in
|
||||||
|
super unless key == :alert && kind == "already_authenticated"
|
||||||
|
end
|
||||||
|
|
||||||
|
# If you have extra params to permit, append them to the sanitizer.
|
||||||
|
# def configure_sign_in_params
|
||||||
|
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
|
||||||
|
# end
|
||||||
|
end
|
53
app/javascript/controllers/nostr_login_controller.js
Normal file
53
app/javascript/controllers/nostr_login_controller.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Connects to data-controller="nostr-login"
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ "loginForm", "loginButton" ]
|
||||||
|
static values = { site: String, sharedSecret: String }
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (window.nostr) {
|
||||||
|
this.loginButtonTarget.disabled = false
|
||||||
|
this.loginFormTarget.classList.remove("hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async login () {
|
||||||
|
this.loginButtonTarget.disabled = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Auth based on NIP-42
|
||||||
|
const signedEvent = await window.nostr.signEvent({
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: 22242,
|
||||||
|
tags: [
|
||||||
|
["site", this.siteValue],
|
||||||
|
["challenge", this.sharedSecretValue]
|
||||||
|
],
|
||||||
|
content: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await fetch("/users/nostr_login", {
|
||||||
|
method: "POST", credentials: "include", headers: {
|
||||||
|
"Accept": "application/json", 'Content-Type': 'application/json',
|
||||||
|
"X-CSRF-Token": this.csrfToken
|
||||||
|
}, body: JSON.stringify({ signed_event: signedEvent })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
res.json().then(r => { window.location.href = r.redirect_url })
|
||||||
|
} else {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Unable to authenticate:', error.message)
|
||||||
|
} finally {
|
||||||
|
this.loginButtonTarget.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get csrfToken () {
|
||||||
|
const element = document.head.querySelector('meta[name="csrf-token"]')
|
||||||
|
return element.getAttribute("content")
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
@ -55,4 +55,27 @@
|
|||||||
<%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %>
|
<%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<div data-controller="nostr-login"
|
||||||
|
data-nostr-login-target="loginForm"
|
||||||
|
data-nostr-login-site-value="<%= Setting.accounts_domain %>"
|
||||||
|
data-nostr-login-shared-secret-value="<%= session[:shared_secret] %>"
|
||||||
|
class="hidden">
|
||||||
|
<div class="relative my-6">
|
||||||
|
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
|
<div class="w-full border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center">
|
||||||
|
<span class="bg-white px-2 text-sm text-gray-500 italic">or</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<button disabled tabindex="5"
|
||||||
|
class="w-full btn-md btn-gray text-purple-600"
|
||||||
|
data-nostr-login-target="loginButton"
|
||||||
|
data-action="nostr-login#login">
|
||||||
|
Log in with Nostr
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<% 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 %>">
|
||||||
|
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
require 'sidekiq/web'
|
require 'sidekiq/web'
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
devise_for :users, controllers: { confirmations: 'users/confirmations' }
|
devise_for :users, controllers: {
|
||||||
|
confirmations: 'users/confirmations',
|
||||||
|
sessions: 'users/sessions'
|
||||||
|
}
|
||||||
|
|
||||||
|
devise_scope :user do
|
||||||
|
post 'users/nostr_login', to: 'users/sessions#nostr_login'
|
||||||
|
end
|
||||||
|
|
||||||
get 'welcome', to: 'welcome#index'
|
get 'welcome', to: 'welcome#index'
|
||||||
get 'check_your_email', to: 'welcome#check_your_email'
|
get 'check_your_email', to: 'welcome#check_your_email'
|
||||||
|
9
spec/fixtures/nostr/valid_auth_event.json
vendored
Normal file
9
spec/fixtures/nostr/valid_auth_event.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "7cc165c4fe4c9ec3f2b859cb422f01b38beaf6bbd228fea928ea1400ec254a89",
|
||||||
|
"pubkey": "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
||||||
|
"created_at": 1711963922,
|
||||||
|
"kind": 22242,
|
||||||
|
"tags": [["site","accounts.kosmos.org"],["challenge","YMeTyOxIEJcfe6vd"]],
|
||||||
|
"content": "",
|
||||||
|
"sig": "b484a28cd9c92facca0eba80e8ef5303d25ed044c3815e3a068b9887f91d3546ad209a0dd674c59b48cf8057aecd75df5416973d17ed58f68195030af09c28d1"
|
||||||
|
}
|
@ -3,6 +3,7 @@ require 'rails_helper'
|
|||||||
RSpec.describe "Settings", type: :request do
|
RSpec.describe "Settings", type: :request do
|
||||||
let(:user) { create :user, cn: 'mark', ou: 'kosmos.org' }
|
let(:user) { create :user, cn: 'mark', ou: 'kosmos.org' }
|
||||||
let(:other_user) { create :user, id: 2, cn: 'markymark', ou: 'kosmos.org', email: 'markymark@interscope.com' }
|
let(:other_user) { create :user, id: 2, cn: 'markymark', ou: 'kosmos.org', email: 'markymark@interscope.com' }
|
||||||
|
let(:auth_event) { JSON.parse(File.read("#{Rails.root}/spec/fixtures/nostr/valid_auth_event.json")) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
login_as user, :scope => :user
|
login_as user, :scope => :user
|
||||||
@ -25,7 +26,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
|
||||||
|
|
||||||
@ -36,19 +37,12 @@ RSpec.describe "Settings", type: :request do
|
|||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||||
).and_return(0)
|
).and_return(0)
|
||||||
|
|
||||||
post set_nostr_pubkey_settings_path, params: {
|
post set_nostr_pubkey_settings_path,
|
||||||
signed_event: {
|
params: { signed_event: auth_event }.to_json,
|
||||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
headers: {
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
"CONTENT_TYPE" => "application/json",
|
||||||
created_at: 1678254161,
|
"HTTP_ACCEPT" => "application/json"
|
||||||
kind: 1,
|
|
||||||
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
|
||||||
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
|
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
|
||||||
"CONTENT_TYPE" => "application/json",
|
|
||||||
"HTTP_ACCEPT" => "application/json"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a success status" do
|
it "returns a success status" do
|
||||||
@ -67,19 +61,12 @@ RSpec.describe "Settings", type: :request do
|
|||||||
).and_return(other_user)
|
).and_return(other_user)
|
||||||
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,
|
||||||
signed_event: {
|
params: { signed_event: auth_event }.to_json,
|
||||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
headers: {
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
"CONTENT_TYPE" => "application/json",
|
||||||
created_at: 1678254161,
|
"HTTP_ACCEPT" => "application/json"
|
||||||
kind: 1,
|
|
||||||
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
|
||||||
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
|
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
|
||||||
"CONTENT_TYPE" => "application/json",
|
|
||||||
"HTTP_ACCEPT" => "application/json"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a 422 status" do
|
it "returns a 422 status" do
|
||||||
@ -91,23 +78,21 @@ 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,
|
||||||
signed_event: {
|
params: { signed_event: auth_event }.to_json,
|
||||||
id: "2e1e20ee762d6a5b5b30835eda9ca03146e4baf82490e53fd75794c08de08ac0",
|
headers: {
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
"CONTENT_TYPE" => "application/json",
|
||||||
created_at: 1678255391,
|
"HTTP_ACCEPT" => "application/json"
|
||||||
kind: 1,
|
|
||||||
content: "Connect my public key to admin@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
|
||||||
sig: "2ace19c9db892ac6383848721a3e08b13d90d689fdeac60d9633a623d3f08eb7e0d468f1b3e928d1ea979477c2ec46ee6cdb2d053ef2e4ed3c0630a51d249029"
|
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
end
|
||||||
"CONTENT_TYPE" => "application/json",
|
|
||||||
"HTTP_ACCEPT" => "application/json"
|
after do
|
||||||
}
|
Setting.accounts_domain = "accounts.kosmos.org"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a 422 status" do
|
it "returns a 422 status" do
|
||||||
@ -126,19 +111,12 @@ RSpec.describe "Settings", type: :request do
|
|||||||
|
|
||||||
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,
|
||||||
signed_event: {
|
params: { signed_event: auth_event }.to_json,
|
||||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
headers: {
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
"CONTENT_TYPE" => "application/json",
|
||||||
created_at: 1678254161,
|
"HTTP_ACCEPT" => "application/json"
|
||||||
kind: 1,
|
|
||||||
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
|
||||||
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
|
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
|
||||||
"CONTENT_TYPE" => "application/json",
|
|
||||||
"HTTP_ACCEPT" => "application/json"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a 422 status" do
|
it "returns a 422 status" do
|
||||||
|
94
spec/requests/users/sessions_spec.rb
Normal file
94
spec/requests/users/sessions_spec.rb
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe "Devise login sessions", type: :request do
|
||||||
|
let(:user) { create :user, cn: 'fiatjaf', ou: 'kosmos.org' }
|
||||||
|
let(:auth_event) { JSON.parse(File.read("#{Rails.root}/spec/fixtures/nostr/valid_auth_event.json")) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
login_as user, :scope => :user
|
||||||
|
|
||||||
|
allow_any_instance_of(User).to receive(:dn)
|
||||||
|
.and_return("cn=#{user.cn},ou=kosmos.org,cn=users,dc=kosmos,dc=org")
|
||||||
|
allow_any_instance_of(User).to receive(:nostr_pubkey).and_return(nil)
|
||||||
|
|
||||||
|
allow(LdapManager::FetchUserByNostrKey).to receive(:call).with(
|
||||||
|
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||||
|
).and_return(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /users/nostr_login" do
|
||||||
|
before do
|
||||||
|
session_stub = { shared_secret: "YMeTyOxIEJcfe6vd" }
|
||||||
|
allow_any_instance_of(Users::SessionsController).to receive(:session).and_return(session_stub)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "With key configured for an account" do
|
||||||
|
before do
|
||||||
|
expect(LdapManager::FetchUserByNostrKey).to receive(:call).with(
|
||||||
|
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||||
|
).and_return(user)
|
||||||
|
|
||||||
|
post users_nostr_login_path,
|
||||||
|
params: { signed_event: auth_event }.to_json,
|
||||||
|
headers: {
|
||||||
|
"CONTENT_TYPE" => "application/json",
|
||||||
|
"HTTP_ACCEPT" => "application/json"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a success status" do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "With wrong site tag" do
|
||||||
|
before do
|
||||||
|
Setting.accounts_domain = "accounts.wikipedia.org"
|
||||||
|
expect(LdapManager::FetchUserByNostrKey).not_to receive(:call)
|
||||||
|
|
||||||
|
post users_nostr_login_path,
|
||||||
|
params: { signed_event: auth_event }.to_json,
|
||||||
|
headers: {
|
||||||
|
"CONTENT_TYPE" => "application/json",
|
||||||
|
"HTTP_ACCEPT" => "application/json"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
Setting.accounts_domain = "accounts.kosmos.org"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 422 status" do
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "informs the user about the failure" do
|
||||||
|
expect(flash[:alert]).to eq("Login verification failed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "With wrong shared secret" do
|
||||||
|
before do
|
||||||
|
session_stub = { shared_secret: "ho-chi-minh" }
|
||||||
|
allow_any_instance_of(Users::SessionsController).to receive(:session).and_return(session_stub)
|
||||||
|
|
||||||
|
expect(LdapManager::FetchUserByNostrKey).not_to receive(:call)
|
||||||
|
|
||||||
|
post users_nostr_login_path,
|
||||||
|
params: { signed_event: auth_event }.to_json,
|
||||||
|
headers: {
|
||||||
|
"CONTENT_TYPE" => "application/json",
|
||||||
|
"HTTP_ACCEPT" => "application/json"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 422 status" do
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "informs the user about the failure" do
|
||||||
|
expect(flash[:alert]).to eq("Login verification failed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user
so because the event signature is correct and because that event contains the session shared_secret I can trust that it's the user with the pubkey to login. 🤔
Yes, when the signature is correct, we know that the owner of the privkey signed it (unless someone stole their key), so we can look up the user via the pubkey from that event/note.
The shared secret is to ensure that no other, evil site tried to obtain an auth event from the user, because the other site cannot know our shared secret.