Last active
March 17, 2023 15:33
-
-
Save blakejakopovic/b0065b9327c48c148bfe989c08137ba1 to your computer and use it in GitHub Desktop.
Nostr NIP-42 Website Login Example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'sinatra' | |
require 'json' | |
# class App < Sinatra::Application | |
configure do | |
enable :sessions | |
end | |
get '/' do | |
pubkey = session[:pubkey] | |
puts session.inspect | |
erb :login, :locals => {:pubkey => pubkey, :session_id => session[:session_id]} | |
end | |
post "/login" do | |
body = JSON.parse(request.body.read) | |
event = body["AUTH"] | |
session[:pubkey] = event["pubkey"] | |
# validate signature of event | |
# validate event created_at | |
# validate event origin tag | |
# validate event auth_challenge tag with session | |
content_type :json | |
response = { | |
status: "success" | |
} | |
JSON.generate(response) | |
end | |
post "/logout" do | |
session.delete(:pubkey) | |
session.delete(:session_id) | |
content_type :json | |
response = { | |
status: "success" | |
} | |
JSON.generate(response) | |
end | |
# end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Login with Nostr!</title> | |
</head> | |
<body> | |
<% if pubkey.nil? %> | |
<button id="loginButton" onclick=login()>Login with Nostr!</button> | |
<p>session_id: <%= session_id %></p> | |
<% else %> | |
<button id="logoutButton" onclick=logout()>Logout with Nostr!</button> | |
<p>pubkey: <%= pubkey %></p> | |
<p>session_id: <%= session_id %></p> | |
<% end %> | |
<script> | |
// Server creates a session id which is used for the auth_challenge | |
let auth_challenge = "<%= session_id %>"; | |
function nostrExtensionLoaded() { | |
if (!window.nostr) { | |
return false; | |
} | |
return true; | |
} | |
function sha256Hex(string) { | |
const utf8 = new TextEncoder().encode(string); | |
return crypto.subtle.digest('SHA-256', utf8).then((hashBuffer) => { | |
const hashArray = Array.from(new Uint8Array(hashBuffer)); | |
const hashHex = hashArray | |
.map((bytes) => bytes.toString(16).padStart(2, '0')) | |
.join(''); | |
return hashHex; | |
}); | |
} | |
async function generateNostrEventId(msg) { | |
const digest = [ | |
0, | |
msg.pubkey, | |
msg.created_at, | |
msg.kind, | |
msg.tags, | |
msg.content, | |
]; | |
const digest_str = JSON.stringify(digest); | |
const hash = await sha256Hex(digest_str); | |
return hash; | |
} | |
async function signNostrAuthEvent(auth_challenge) { | |
try { | |
if (!nostrExtensionLoaded()) { | |
throw "Nostr extension not loaded or available" | |
} | |
let msg = { | |
kind: 22243, // NIP-42++ | |
content: "", | |
tags: [ | |
["origin", "https://localhost:8000"], | |
["challenge", auth_challenge] | |
], | |
}; | |
// set msg fields | |
msg.created_at = Math.floor((new Date()).getTime() / 1000); | |
msg.pubkey = await window.nostr.getPublicKey(); | |
// Generate event id | |
msg.id = await generateNostrEventId(msg); | |
// Sign event | |
signed_msg = await window.nostr.signEvent(msg); | |
} catch (e) { | |
console.log("Failed to sign message with browser extension", e); | |
return false; | |
} | |
return signed_msg; | |
} | |
async function login() { | |
var auth_event = await signNostrAuthEvent(auth_challenge); | |
var xhr = new XMLHttpRequest(); | |
xhr.open('POST', 'login'); | |
xhr.setRequestHeader('Content-Type', 'application/json'); | |
xhr.onload = function() { | |
if (xhr.status === 200) { | |
var response = JSON.parse(xhr.responseText); | |
console.log(response); | |
} | |
window.location.reload(); | |
}; | |
xhr.send(JSON.stringify({"AUTH": auth_event})); | |
} | |
function logout() { | |
var xhr = new XMLHttpRequest(); | |
xhr.open('POST', 'logout'); | |
xhr.setRequestHeader('Content-Type', 'application/json'); | |
xhr.onload = function() { | |
if (xhr.status === 200) { | |
var response = JSON.parse(xhr.responseText); | |
console.log(response); | |
window.location.reload(); | |
} | |
}; | |
xhr.send(JSON.stringify({})); | |
} | |
function checkNostrExtension() { | |
var retryCount = 0; // initialize the retry count to 0 | |
function check() { | |
// Check if window.nostr has loaded | |
if (window.nostr) { | |
nostr_enabled = true; | |
console.log("Nostr Extension loaded!"); | |
return; | |
} | |
// If the window.nostr hasn't loaded yet and we haven't exceeded the retry limit, try again in 250ms | |
if (retryCount < 60) { | |
retryCount++; | |
setTimeout(check, 250); | |
} | |
else { | |
// If we've exceeded the retry limit, log an error message | |
console.log('Nostr Extension not loaded after 15 seconds'); | |
} | |
} | |
// Call the check function to start the retries | |
check(); | |
} | |
// Call the function on page load | |
window.addEventListener('load', checkNostrExtension); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment