Fix npub encoding and validation errors

- Fixed frontend to properly encode npub using nip19.npubEncode()
- Enhanced backend to handle both npub and hex pubkey formats
- Resolved 'Data must be at least 6 characters long' validation error
- Added proper error handling for pubkey format validation
- Should eliminate retry loop issues during authentication
This commit is contained in:
Your Name
2025-08-18 12:32:20 -04:00
parent d0845a8323
commit 0d0a08ad49
2 changed files with 207 additions and 179 deletions

View File

@@ -3,204 +3,219 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nostr Login - OIDC Bridge</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 500px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.logo {
text-align: center;
margin-bottom: 30px;
color: #333;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
input, textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
textarea {
height: 100px;
resize: vertical;
font-family: monospace;
}
.challenge {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #007bff;
margin-bottom: 20px;
word-break: break-all;
font-family: monospace;
font-size: 12px;
}
.button {
background-color: #007bff;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
width: 100%;
margin-bottom: 10px;
}
.button:hover {
background-color: #0056b3;
}
.button.secondary {
background-color: #6c757d;
}
.button.secondary:hover {
background-color: #545b62;
}
.error {
color: #dc3545;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
}
.help-text {
font-size: 12px;
color: #6c757d;
margin-top: 5px;
}
.or-divider {
text-align: center;
margin: 20px 0;
position: relative;
}
.or-divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #ddd;
}
.or-divider span {
background: white;
padding: 0 15px;
color: #6c757d;
}
</style>
<title>Nostr Login - OIDC Bridge v2.1</title>
</head>
<body>
<div class="container">
<div class="logo">
<h2>🔑 Sign in with Nostr</h2>
<p>Authenticate using your Nostr identity</p>
</div>
<div id="login-container">
<!-- Hidden data for backend processing -->
<div id="challenge" style="display: none;"><%= challenge %></div>
<div id="session-id" style="display: none;"><%= uid %></div>
<div id="return-to" style="display: none;"><%= returnTo %></div>
<!-- Error display if needed -->
<% if (typeof error !== 'undefined') { %>
<div class="error">
<strong>Error:</strong> <%= error %>
<div id="error-message" style="color: #dc3545; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 8px; margin: 20px; text-align: center;">
<strong>Authentication Error:</strong> <%= error %>
</div>
<% } %>
<div class="challenge">
<strong>Challenge to sign:</strong><br>
<%= challenge %>
</div>
<!-- Browser Extension Login -->
<button class="button" onclick="signWithExtension()">
🌐 Sign with Browser Extension
</button>
<div class="or-divider">
<span>or</span>
<!-- Version header -->
<div style="text-align: center; margin: 10px 0; padding: 8px; background: #e3f2fd; border: 1px solid #2196f3; border-radius: 4px; font-weight: bold; color: #1976d2;">
🚀 Nostr OIDC Bridge v2.1 - Form Auth + event_json fix
</div>
<!-- Manual Login Form -->
<form method="POST" action="<%= returnTo %>">
<input type="hidden" name="uid" value="<%= uid %>">
<div class="form-group">
<label for="npub">Your Nostr Public Key (npub):</label>
<input type="text" id="npub" name="npub" placeholder="npub1..." required>
<div class="help-text">Your public Nostr identity key</div>
</div>
<div class="form-group">
<label for="event_json">Signed Event (JSON):</label>
<textarea id="event_json" name="event_json" placeholder='{"kind":1,"pubkey":"...","created_at":...,"tags":[],"content":"challenge","sig":"..."}'></textarea>
<div class="help-text">Create and sign a kind 1 event with the challenge as content</div>
</div>
<button type="submit" class="button">
✍️ Verify Signature & Login
</button>
</form>
<div class="help-text" style="margin-top: 30px; text-align: center;">
<strong>Instructions:</strong><br>
1. Use a browser extension (recommended) or<br>
2. Create a Nostr event with the challenge above as content<br>
3. Sign it with your private key and paste the JSON
<!-- Target div for nostr-login widget -->
<div id="nostr-login"></div>
<!-- Version info -->
<div style="position: fixed; bottom: 10px; right: 10px; font-size: 14px; color: #333; background: rgba(255,255,255,0.95); padding: 6px 12px; border-radius: 6px; border: 1px solid #ddd; font-weight: bold;">
v2.1 - Form Auth + event_json fix
</div>
</div>
<!-- Load your hosted nostr-login with full permissions -->
<script src='https://laantungir.net/nostr-login/unpkg.js'></script>
<!-- Import nostr-tools for proper npub encoding -->
<script type="module">
import { nip19 } from 'https://esm.sh/nostr-tools@2.16.2';
window.nip19 = nip19;
</script>
<!-- Integration bridge to handle authentication -->
<script>
async function signWithExtension() {
if (!window.nostr) {
alert('No Nostr extension detected. Please install a Nostr browser extension like nos2x or Alby.');
return;
let isProcessing = false;
// Initialize nostr-login widget
window.addEventListener('DOMContentLoaded', () => {
console.log('Initializing nostr-login widget...');
// Create nostr-login options
const nostrLoginOptions = {
bunkers: 'all',
methods: 'all',
noBanner: true,
startScreen: 'welcome-login',
theme: 'default'
};
console.log('nostr-login options', nostrLoginOptions);
// Initialize the widget
if (window.NostrLogin) {
window.NostrLogin.init(nostrLoginOptions);
} else {
console.error('NostrLogin not found on window object');
// Fallback: try to initialize after a delay
setTimeout(() => {
if (window.NostrLogin) {
console.log('Retrying NostrLogin initialization...');
window.NostrLogin.init(nostrLoginOptions);
}
}, 1000);
}
});
// Listen for nostr-login authentication events
document.addEventListener('nlAuth', async (e) => {
console.log('nostr-login auth event:', e.detail);
if ((e.detail.type === 'login' || e.detail.type === 'signup') && !isProcessing) {
isProcessing = true;
await handleNostrLogin();
}
});
async function handleNostrLogin() {
try {
const challenge = '<%= challenge %>';
const event = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: challenge
};
const signedEvent = await window.nostr.signEvent(event);
console.log('Handling Nostr login...');
// Get the challenge and session info
const challenge = document.getElementById('challenge').textContent.trim();
const sessionId = document.getElementById('session-id').textContent.trim();
const returnTo = document.getElementById('return-to').textContent.trim();
console.log('Challenge:', challenge);
console.log('Session ID:', sessionId);
if (!window.nostr) {
throw new Error('window.nostr not available after login');
}
// Get the user's public key
const pubkey = await window.nostr.getPublicKey();
const npub = window.NostrTools ? window.NostrTools.nip19.npubEncode(pubkey) : `npub${pubkey}`;
// Fill the form and submit
document.getElementById('npub').value = npub;
document.getElementById('event_json').value = JSON.stringify(signedEvent);
document.querySelector('form').submit();
console.log('Got pubkey:', pubkey);
// Properly encode pubkey to npub format using nip19
let npub;
try {
if (window.nip19) {
npub = window.nip19.npubEncode(pubkey);
console.log('Encoded npub:', npub);
} else {
console.warn('nip19 not available, using raw pubkey');
npub = pubkey; // Fallback to raw hex pubkey
}
} catch (error) {
console.error('Error encoding npub:', error);
npub = pubkey; // Fallback to raw hex pubkey
}
// Create and sign the challenge event
const eventTemplate = {
kind: 1,
content: challenge,
created_at: Math.floor(Date.now() / 1000),
tags: []
};
console.log('Signing event template:', eventTemplate);
const signedEvent = await window.nostr.signEvent(eventTemplate);
console.log('Signed event:', signedEvent);
// Verify the signature locally before sending
if (signedEvent.pubkey !== pubkey) {
throw new Error('Signed event pubkey mismatch');
}
if (signedEvent.content !== challenge) {
throw new Error('Signed event content mismatch');
}
// Submit to backend
await submitAuthToBackend(sessionId, pubkey, signedEvent);
} catch (error) {
alert('Error signing with extension: ' + error.message);
console.error('Extension signing error:', error);
console.error('Authentication error:', error);
isProcessing = false;
// Show user-friendly error
showError('Authentication failed: ' + error.message);
}
}
// Load NostrTools if available for npub encoding
if (typeof window !== 'undefined' && !window.NostrTools) {
const script = document.createElement('script');
script.src = 'https://unpkg.com/nostr-tools/lib/nostr.bundle.js';
document.head.appendChild(script);
async function submitAuthToBackend(sessionId, pubkey, signedEvent) {
try {
console.log('Submitting auth to backend...');
// Create a form and submit it traditionally to allow proper redirects
// This avoids CORS issues when redirecting to external domains like Gitea
const form = document.createElement('form');
form.method = 'POST';
form.action = `/complete-auth/${sessionId}`;
// Add form fields
const npubField = document.createElement('input');
npubField.type = 'hidden';
npubField.name = 'npub';
npubField.value = npub; // Use the properly encoded npub
form.appendChild(npubField);
const eventField = document.createElement('input');
eventField.type = 'hidden';
eventField.name = 'event_json';
eventField.value = JSON.stringify(signedEvent);
form.appendChild(eventField);
// Add form to document and submit
document.body.appendChild(form);
console.log('Submitting form for authentication...');
form.submit();
} catch (error) {
console.error('Backend submission error:', error);
throw error;
}
}
function showError(message) {
// Create or update error display
let errorDiv = document.getElementById('error-message');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.id = 'error-message';
errorDiv.style.cssText = 'color: #dc3545; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 8px; margin: 20px; text-align: center;';
document.getElementById('login-container').insertBefore(errorDiv, document.getElementById('login-container').firstChild);
}
errorDiv.innerHTML = `<strong>Authentication Error:</strong> ${message}`;
// Reset processing flag after showing error
setTimeout(() => {
isProcessing = false;
}, 2000);
}
// Handle any initialization errors
window.addEventListener('error', (e) => {
console.error('Global error:', e.error);
if (e.error && e.error.message.includes('nostr')) {
showError('Nostr login initialization failed. Please refresh the page.');
}
});
console.log('Nostr OIDC Bridge login page initialized');
</script>
</body>
</html>