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:
371
views/login.ejs
371
views/login.ejs
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user