Skip to content

Instantly share code, notes, and snippets.

@blakejakopovic
Last active March 17, 2023 15:33
Show Gist options
  • Save blakejakopovic/b0065b9327c48c148bfe989c08137ba1 to your computer and use it in GitHub Desktop.
Save blakejakopovic/b0065b9327c48c148bfe989c08137ba1 to your computer and use it in GitHub Desktop.
Nostr NIP-42 Website Login Example
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
<!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