commit d0845a8323679b5fec8801f6cdcbb31b880c281c Author: Your Name Date: Sun Aug 17 20:07:36 2025 -0400 first diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f95733b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:22-alpine + +WORKDIR /app + +# Copy package files first for better Docker layer caching +COPY package.json ./ + +# Install dependencies +RUN npm install --production + +# Copy application files +COPY app.js ./ +COPY views/ ./views/ + +# Expose port +EXPOSE 3001 + +# Start the application +CMD ["node", "app.js"] diff --git a/app.js b/app.js new file mode 100644 index 0000000..28a410e --- /dev/null +++ b/app.js @@ -0,0 +1,454 @@ +import express from 'express'; +import crypto from 'crypto'; +import jwt from 'jsonwebtoken'; +import { nip19, getEventHash, verifyEvent, SimplePool } from 'nostr-tools'; + +const app = express(); +const PORT = 3001; + +// In-memory storage for demo purposes +const users = new Map(); +const authCodes = new Map(); +const sessions = new Map(); + +// JWT signing key (in production, use a proper key) +const JWT_SECRET = 'your-super-secret-jwt-signing-key-change-in-production'; +const ISSUER = 'https://auth.laantungir.net'; // Production HTTPS URL +const CLIENT_ID = 'GITEA'; +const CLIENT_SECRET = 'gitea-secret'; + +// Nostr relays for fetching user metadata +const NOSTR_RELAYS = [ + 'wss://relay.damus.io', + 'wss://nos.lol', + 'wss://relay.nostr.band', + 'wss://nostr-pub.wellorder.net' +]; + +// Initialize Nostr pool +const pool = new SimplePool(); + +// NIP-05 related functions +async function fetchUserMetadata(pubkey) { + try { + console.log('🔍 Fetching metadata for pubkey:', pubkey); + + const filter = { + kinds: [0], + authors: [pubkey], + limit: 1 + }; + + const events = await pool.querySync(NOSTR_RELAYS, filter); + + if (events.length > 0) { + const metadata = JSON.parse(events[0].content); + console.log('✅ Found metadata:', metadata); + return metadata; + } + + console.log('❌ No metadata found'); + return null; + } catch (error) { + console.error('❌ Error fetching metadata:', error); + return null; + } +} + +async function validateNip05(nip05, pubkey) { + try { + console.log('🔍 Validating NIP-05:', nip05); + + // Parse the NIP-05 identifier + let localPart, domain; + if (nip05.includes('@')) { + [localPart, domain] = nip05.split('@'); + } else { + // Handle _@domain.com format (root identifier) + localPart = '_'; + domain = nip05; + } + + // Fetch the well-known endpoint + const url = `https://${domain}/.well-known/nostr.json?name=${localPart}`; + console.log('🔍 Fetching well-known:', url); + + const response = await fetch(url, { + timeout: 5000, + headers: { + 'User-Agent': 'Nostr-OIDC-Bridge/1.0' + } + }); + + if (!response.ok) { + console.log('❌ Well-known fetch failed:', response.status); + return false; + } + + const data = await response.json(); + console.log('✅ Well-known response:', data); + + // Check if the pubkey matches + const expectedPubkey = data.names?.[localPart]; + const isValid = expectedPubkey === pubkey; + + console.log(`${isValid ? '✅' : '❌'} NIP-05 validation:`, { + expected: expectedPubkey, + actual: pubkey, + valid: isValid + }); + + return isValid; + } catch (error) { + console.error('❌ Error validating NIP-05:', error); + return false; + } +} + +function generateUsernameAndEmail(nip05, npub, pubkey) { + let username, email; + + if (nip05) { + if (nip05.includes('@')) { + // For user@domain.com format, use local part as username + const [local, domain] = nip05.split('@'); + + // Use local part as username (truncate if needed) + username = local.length <= 40 ? local : local.substring(0, 40); + + // Use full NIP-05 as email + email = nip05; + } else { + // For root identifier (domain.com), use domain as username + username = nip05.length <= 40 ? nip05 : nip05.substring(0, 40); + email = `_@${nip05}`; // Convert to standard email format + } + } else { + // Fallback to shortened npub + username = npub.length <= 40 ? npub.substring(0, 40) : npub.substring(0, 40); + email = `${pubkey.substring(0, 8)}@nostr.local`; + } + + return { username, email }; +} + +// Trust proxy for production deployment behind nginx +app.set('trust proxy', true); + +// Middleware +app.use(express.urlencoded({ extended: true })); +app.use(express.json()); +app.set('view engine', 'ejs'); +app.set('views', './views'); + +// Debug logging middleware +app.use((req, res, next) => { + console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); + if (req.query && Object.keys(req.query).length > 0) { + console.log('Query params:', req.query); + } + next(); +}); + +// OIDC Discovery endpoint - manually controlled for production +app.get('/.well-known/openid-configuration', (req, res) => { + res.json({ + issuer: ISSUER, + authorization_endpoint: `${ISSUER}/auth`, + token_endpoint: `${ISSUER}/token`, + userinfo_endpoint: `${ISSUER}/userinfo`, + jwks_uri: `${ISSUER}/jwks`, + scopes_supported: ['openid', 'profile', 'email'], + response_types_supported: ['code'], + grant_types_supported: ['authorization_code'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['HS256'], + claims_supported: ['sub', 'name', 'preferred_username', 'email'], + }); +}); + +// Authorization endpoint +app.get('/auth', (req, res) => { + const { client_id, redirect_uri, response_type, scope, state } = req.query; + + console.log('🔵 Authorization request:', { client_id, redirect_uri, response_type, scope, state }); + + // Validate client + if (client_id !== CLIENT_ID) { + const errorUrl = `${redirect_uri}?error=invalid_client&state=${state}`; + console.log('❌ Invalid client, redirecting to:', errorUrl); + return res.redirect(errorUrl); + } + + // Generate session + const sessionId = crypto.randomBytes(32).toString('hex'); + const challenge = crypto.randomBytes(32).toString('hex'); + + sessions.set(sessionId, { + client_id, + redirect_uri, + scope, + state, + challenge, + created_at: Date.now() + }); + + console.log('✅ Created session:', sessionId); + + // Render login page + res.render('login', { + challenge, + uid: sessionId, + returnTo: `/complete-auth/${sessionId}` + }); +}); + +// Handle Nostr authentication +app.post('/complete-auth/:sessionId', async (req, res) => { + try { + const { sessionId } = req.params; + const { npub, event_json } = req.body; + + const session = sessions.get(sessionId); + if (!session) { + throw new Error('Session not found or expired'); + } + + // Verify the Nostr event + const event = JSON.parse(event_json); + + if (!verifyEvent(event)) { + throw new Error('Invalid signature'); + } + + if (event.content !== session.challenge) { + throw new Error('Challenge mismatch'); + } + + const { data: pubkey } = nip19.decode(npub); + if (event.pubkey !== pubkey) { + throw new Error('Public key mismatch'); + } + + console.log('✅ Nostr authentication successful for:', pubkey); + + // Create or get user + let user = users.get(pubkey); + if (!user) { + // Try to fetch NIP-05 information + let nip05 = null; + let validatedNip05 = false; + + try { + console.log('🔍 Attempting to fetch NIP-05 for user...'); + const metadata = await fetchUserMetadata(pubkey); + + if (metadata && metadata.nip05) { + console.log('🔍 Found NIP-05 in metadata:', metadata.nip05); + validatedNip05 = await validateNip05(metadata.nip05, pubkey); + + if (validatedNip05) { + nip05 = metadata.nip05; + console.log('✅ NIP-05 validated successfully:', nip05); + } else { + console.log('❌ NIP-05 validation failed'); + } + } else { + console.log('❌ No NIP-05 found in metadata'); + } + } catch (error) { + console.error('❌ Error during NIP-05 lookup:', error); + } + + // Generate the best username and email + const { username, email } = generateUsernameAndEmail(validatedNip05 ? nip05 : null, npub, pubkey); + + user = { + id: pubkey, + name: nip05 && validatedNip05 ? nip05 : npub, + username: username, + email: email, + nip05: validatedNip05 ? nip05 : null, + }; + users.set(pubkey, user); + console.log('✅ Created new user:', user); + } + + // Generate authorization code + const authCode = crypto.randomBytes(32).toString('hex'); + authCodes.set(authCode, { + user_id: pubkey, + client_id: session.client_id, + redirect_uri: session.redirect_uri, + scope: session.scope, + created_at: Date.now(), + expires_at: Date.now() + (10 * 60 * 1000) // 10 minutes + }); + + console.log('✅ Generated auth code:', authCode); + + // Clean up session + sessions.delete(sessionId); + + // Redirect back to Gitea with auth code + const redirectUrl = `${session.redirect_uri}?code=${authCode}&state=${session.state}`; + console.log('✅ Redirecting to Gitea:', redirectUrl); + + res.redirect(redirectUrl); + + } catch (error) { + console.error('❌ Authentication error:', error); + const session = sessions.get(req.params.sessionId); + const challenge = session ? session.challenge : crypto.randomBytes(32).toString('hex'); + + res.render('login', { + error: error.message, + challenge, + uid: req.params.sessionId, + returnTo: `/complete-auth/${req.params.sessionId}` + }); + } +}); + +// Token endpoint +app.post('/token', (req, res) => { + try { + const { grant_type, code, client_id, client_secret, redirect_uri } = req.body; + + console.log('🔵 Token request:', { grant_type, code, client_id, redirect_uri }); + + // Validate grant type + if (grant_type !== 'authorization_code') { + return res.status(400).json({ error: 'unsupported_grant_type' }); + } + + // Validate client + if (client_id !== CLIENT_ID || client_secret !== CLIENT_SECRET) { + return res.status(401).json({ error: 'invalid_client' }); + } + + // Get auth code + const authData = authCodes.get(code); + if (!authData) { + return res.status(400).json({ error: 'invalid_grant' }); + } + + // Check expiration + if (Date.now() > authData.expires_at) { + authCodes.delete(code); + return res.status(400).json({ error: 'invalid_grant' }); + } + + // Validate redirect URI + if (authData.redirect_uri !== redirect_uri) { + return res.status(400).json({ error: 'invalid_grant' }); + } + + const user = users.get(authData.user_id); + if (!user) { + return res.status(400).json({ error: 'invalid_grant' }); + } + + // Generate tokens + const now = Math.floor(Date.now() / 1000); + + const accessToken = jwt.sign({ + sub: user.id, + aud: client_id, + iss: ISSUER, + iat: now, + exp: now + 3600, // 1 hour + scope: authData.scope + }, JWT_SECRET); + + const idToken = jwt.sign({ + sub: user.id, + aud: client_id, + iss: ISSUER, + iat: now, + exp: now + 3600, + name: user.name, + preferred_username: user.username, + email: user.email, + }, JWT_SECRET); + + // Clean up auth code (one-time use) + authCodes.delete(code); + + console.log('✅ Issued tokens for user:', user.id); + + res.json({ + access_token: accessToken, + token_type: 'Bearer', + expires_in: 3600, + id_token: idToken, + scope: authData.scope + }); + + } catch (error) { + console.error('❌ Token error:', error); + res.status(500).json({ error: 'server_error' }); + } +}); + +// Userinfo endpoint function +const handleUserInfo = (req, res) => { + try { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'invalid_token' }); + } + + const token = authHeader.substring(7); + const decoded = jwt.verify(token, JWT_SECRET); + + const user = users.get(decoded.sub); + if (!user) { + return res.status(401).json({ error: 'invalid_token' }); + } + + console.log('✅ UserInfo request for user:', user.id); + + res.json({ + sub: user.id, + name: user.name, + preferred_username: user.username, + email: user.email, + }); + + } catch (error) { + console.error('❌ Userinfo error:', error); + res.status(401).json({ error: 'invalid_token' }); + } +}; + +// Userinfo endpoints (both /userinfo and /me for compatibility) +app.get('/userinfo', handleUserInfo); +app.get('/me', handleUserInfo); + +// JWKS endpoint (simplified) +app.get('/jwks', (req, res) => { + // This is a simplified implementation + // In production, use proper JWK format + res.json({ keys: [] }); +}); + +// Health check +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// Error handler +app.use((err, req, res, next) => { + console.error('❌ Express Error:', err); + if (res.headersSent) { + return next(err); + } + res.status(500).json({ error: 'Internal server error', message: err.message }); +}); + +// Start server +app.listen(PORT, '0.0.0.0', () => { + console.log(`🚀 Simple Nostr OIDC Bridge running on http://0.0.0.0:${PORT}`); + console.log(`🔗 OIDC Discovery: ${ISSUER}/.well-known/openid-configuration`); +}); diff --git a/app.js.old b/app.js.old new file mode 100644 index 0000000..605d4b8 --- /dev/null +++ b/app.js.old @@ -0,0 +1,343 @@ +import express from 'express'; +import { Provider } from 'oidc-provider'; +import { nip19, getEventHash, getSignature, verifyEvent } from 'nostr-tools'; +import crypto from 'crypto'; + +const app = express(); +const PORT = 3001; + +// In-memory storage for demo purposes +const users = new Map(); +const sessions = new Map(); +const authCodes = new Map(); + +// Production OIDC configuration +const configuration = { + clients: [{ + client_id: 'gitea', + client_secret: 'gitea-secret', + redirect_uris: ['https://git.laantungir.net/user/oauth2/nostr/callback'], + response_types: ['code'], + grant_types: ['authorization_code'], + scope: 'openid profile email', + }], + claims: { + openid: ['sub'], + profile: ['name', 'preferred_username'], + email: ['email'], + }, + features: { + devInteractions: { enabled: false }, + introspection: { enabled: true }, + revocation: { enabled: true }, + }, + ttl: { + AccessToken: 1 * 60 * 60, // 1 hour + AuthorizationCode: 10 * 60, // 10 minutes + IdToken: 1 * 60 * 60, // 1 hour + DeviceCode: 10 * 60, // 10 minutes + RefreshToken: 1 * 24 * 60 * 60, // 1 day + }, + issueRefreshToken: async (ctx, token, client) => { + return client.grantTypes.includes('refresh_token') && token.scopes.has('offline_access'); + }, + interactions: { + url: async (ctx, interaction) => { + return `/interaction/${interaction.uid}`; + }, + }, + findAccount: async (ctx, sub, token) => { + const user = users.get(sub); + if (!user) return undefined; + + return { + accountId: sub, + claims: async (use, scope) => { + return { + sub: user.pubkey, + name: user.name || user.pubkey.slice(0, 16), + preferred_username: user.name || user.pubkey.slice(0, 16), + email: user.email || `${user.pubkey.slice(0, 16)}@nostr.local`, + }; + }, + }; + }, +}; + +// Initialize OIDC provider with production issuer +const oidc = new Provider('https://auth.laantungir.net', configuration); + +// Custom interaction handling +oidc.use(async (ctx, next) => { + if (ctx.path.startsWith('/interaction/')) { + const uid = ctx.path.split('/')[2]; + const interaction = await oidc.interactionDetails(ctx.req, ctx.res); + + if (ctx.method === 'GET') { + // Show login form + const challenge = crypto.randomBytes(32).toString('hex'); + sessions.set(uid, { challenge, interaction }); + + // Render login page + ctx.type = 'html'; + ctx.body = await renderLoginPage(challenge, uid, interaction); + return; + } + + if (ctx.method === 'POST') { + // Handle login submission + const body = await parseBody(ctx); + const sessionData = sessions.get(uid); + + if (!sessionData) { + ctx.status = 400; + ctx.body = { error: 'Session expired' }; + return; + } + + try { + let nostrEvent; + + if (body.signature && body.pubkey) { + // Manual signing + nostrEvent = { + kind: 1, + pubkey: body.pubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: `Authenticating to ${sessionData.challenge}`, + id: '', + sig: body.signature + }; + nostrEvent.id = getEventHash(nostrEvent); + } else if (body.event) { + // Extension signing + nostrEvent = JSON.parse(body.event); + } else { + throw new Error('No signature provided'); + } + + // Verify the event + if (!verifyEvent(nostrEvent)) { + throw new Error('Invalid signature'); + } + + // Check if challenge matches + if (!nostrEvent.content.includes(sessionData.challenge)) { + throw new Error('Challenge mismatch'); + } + + // Create or get user + const pubkey = nostrEvent.pubkey; + const npub = nip19.npubEncode(pubkey); + + if (!users.has(pubkey)) { + users.set(pubkey, { + pubkey, + npub, + name: npub.slice(0, 16), + email: `${pubkey.slice(0, 16)}@nostr.local`, + }); + } + + const user = users.get(pubkey); + + // Complete the interaction + const result = { + login: { + account: pubkey, + }, + }; + + await oidc.interactionFinished(ctx.req, ctx.res, result, { + mergeWithLastSubmission: false, + }); + + sessions.delete(uid); + return; + + } catch (error) { + console.error('Authentication error:', error); + ctx.status = 400; + ctx.body = { error: error.message }; + return; + } + } + } + + await next(); +}); + +// Helper function to parse POST body +async function parseBody(ctx) { + return new Promise((resolve, reject) => { + let body = ''; + ctx.req.on('data', chunk => { + body += chunk.toString(); + }); + ctx.req.on('end', () => { + try { + const parsed = new URLSearchParams(body); + const result = {}; + for (const [key, value] of parsed) { + result[key] = value; + } + resolve(result); + } catch (error) { + reject(error); + } + }); + }); +} + +// Helper function to render login page +async function renderLoginPage(challenge, uid, interaction) { + return ` + + + + Nostr OIDC Bridge + + + + + +
+

🔐 Sign in with Nostr

+

To authenticate, please sign the following challenge with your Nostr key:

+ +
+ Challenge:
+ Authenticating to ${challenge} +
+ +
+

🔌 Method 1: Browser Extension (Recommended)

+

If you have a Nostr browser extension (nos2x, Alby, etc.), click below:

+ +
+
+ +
+

🔑 Method 2: Manual Signing

+

If you don't have an extension, you can manually sign the challenge:

+ +
+ + + + + + + +
+
+
+ + + +`; +} + +// Middleware setup +app.use(express.static('public')); +app.use(oidc.callback()); + +app.listen(PORT, '0.0.0.0', () => { + console.log(`🚀 Nostr OIDC Bridge running on http://0.0.0.0:${PORT}`); + console.log(`🔗 Discovery URL: https://auth.laantungir.net/.well-known/openid-configuration`); +}); diff --git a/app.js.old2 b/app.js.old2 new file mode 100644 index 0000000..ff2ec76 --- /dev/null +++ b/app.js.old2 @@ -0,0 +1,356 @@ +import express from 'express'; +import { Provider } from 'oidc-provider'; +import { nip19, getEventHash, verifyEvent, finalizeEvent, getPublicKey } from 'nostr-tools'; +import crypto from 'crypto'; + +const app = express(); +const PORT = 3001; + +// In-memory storage for demo purposes +const users = new Map(); +const sessions = new Map(); +const authCodes = new Map(); + +// Production OIDC configuration +const configuration = { + clients: [{ + client_id: 'GITEA', + client_secret: 'gitea-secret', + redirect_uris: ['https://git.laantungir.net/user/oauth2/NOSTR/callback'], + response_types: ['code'], + grant_types: ['authorization_code'], + scope: 'openid profile email', + }], + claims: { + openid: ['sub'], + profile: ['name', 'preferred_username'], + email: ['email'], + }, + // Add proxy-related settings + cookies: { + secure: true, + sameSite: 'none' + }, + features: { + devInteractions: { enabled: false }, + introspection: { enabled: true }, + revocation: { enabled: true }, + }, + ttl: { + AccessToken: 1 * 60 * 60, // 1 hour + AuthorizationCode: 10 * 60, // 10 minutes + IdToken: 1 * 60 * 60, // 1 hour + DeviceCode: 10 * 60, // 10 minutes + RefreshToken: 1 * 24 * 60 * 60, // 1 day + }, + issueRefreshToken: async (ctx, token, client) => { + return client.grantTypes.includes('refresh_token') && token.scopes.has('offline_access'); + }, + interactions: { + url: async (ctx, interaction) => { + return `/interaction/${interaction.uid}`; + }, + }, + findAccount: async (ctx, sub, token) => { + const user = users.get(sub); + if (!user) return undefined; + + return { + accountId: sub, + claims: async (use, scope) => { + return { + sub: user.pubkey, + name: user.name || user.pubkey.slice(0, 16), + preferred_username: user.name || user.pubkey.slice(0, 16), + email: user.email || `${user.pubkey.slice(0, 16)}@nostr.local`, + }; + }, + }; + }, +}; + +// Initialize OIDC provider with production issuer +const oidc = new Provider('https://auth.laantungir.net', configuration); + +// Add middleware to handle proxy forwarding +oidc.use(async (ctx, next) => { + ctx.secure = true; // Force secure context + ctx.protocol = 'https'; // Force HTTPS protocol + await next(); +}); + +// Custom interaction handling +oidc.use(async (ctx, next) => { + if (ctx.path.startsWith('/interaction/')) { + const uid = ctx.path.split('/')[2]; + const interaction = await oidc.interactionDetails(ctx.req, ctx.res); + + if (ctx.method === 'GET') { + // Show login form + const challenge = crypto.randomBytes(32).toString('hex'); + sessions.set(uid, { challenge, interaction }); + + // Render login page + ctx.type = 'html'; + ctx.body = await renderLoginPage(challenge, uid, interaction); + return; + } + + if (ctx.method === 'POST') { + // Handle login submission + const body = await parseBody(ctx); + const sessionData = sessions.get(uid); + + if (!sessionData) { + ctx.status = 400; + ctx.body = { error: 'Session expired' }; + return; + } + + try { + let nostrEvent; + + if (body.signature && body.pubkey) { + // Manual signing + nostrEvent = { + kind: 1, + pubkey: body.pubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: `Authenticating to ${sessionData.challenge}`, + id: '', + sig: body.signature + }; + nostrEvent.id = getEventHash(nostrEvent); + } else if (body.event) { + // Extension signing + nostrEvent = JSON.parse(body.event); + } else { + throw new Error('No signature provided'); + } + + // Verify the event + if (!verifyEvent(nostrEvent)) { + throw new Error('Invalid signature'); + } + + // Check if challenge matches + if (!nostrEvent.content.includes(sessionData.challenge)) { + throw new Error('Challenge mismatch'); + } + + // Create or get user + const pubkey = nostrEvent.pubkey; + const npub = nip19.npubEncode(pubkey); + + if (!users.has(pubkey)) { + users.set(pubkey, { + pubkey, + npub, + name: npub.slice(0, 16), + email: `${pubkey.slice(0, 16)}@nostr.local`, + }); + } + + const user = users.get(pubkey); + + // Complete the interaction + const result = { + login: { + account: pubkey, + }, + }; + + await oidc.interactionFinished(ctx.req, ctx.res, result, { + mergeWithLastSubmission: false, + }); + + sessions.delete(uid); + return; + + } catch (error) { + console.error('Authentication error:', error); + ctx.status = 400; + ctx.body = { error: error.message }; + return; + } + } + } + + await next(); +}); + +// Helper function to parse POST body +async function parseBody(ctx) { + return new Promise((resolve, reject) => { + let body = ''; + ctx.req.on('data', chunk => { + body += chunk.toString(); + }); + ctx.req.on('end', () => { + try { + const parsed = new URLSearchParams(body); + const result = {}; + for (const [key, value] of parsed) { + result[key] = value; + } + resolve(result); + } catch (error) { + reject(error); + } + }); + }); +} + +// Helper function to render login page +async function renderLoginPage(challenge, uid, interaction) { + return ` + + + + Nostr OIDC Bridge + + + + + +
+

🔐 Sign in with Nostr

+

To authenticate, please sign the following challenge with your Nostr key:

+ +
+ Challenge:
+ Authenticating to ${challenge} +
+ +
+

🔌 Method 1: Browser Extension (Recommended)

+

If you have a Nostr browser extension (nos2x, Alby, etc.), click below:

+ +
+
+ +
+

🔑 Method 2: Manual Signing

+

If you don't have an extension, you can manually sign the challenge:

+ +
+ + + + + + + +
+
+
+ + + +`; +} + +// Middleware setup +app.use(express.static('public')); +app.set('trust proxy', true); // Trust proxy headers +app.use(oidc.callback()); + +app.listen(PORT, '0.0.0.0', () => { + console.log(`🚀 Nostr OIDC Bridge running on http://0.0.0.0:${PORT}`); + console.log(`🔗 Discovery URL: https://auth.laantungir.net/.well-known/openid-configuration`); +}); diff --git a/nostr-oidc-bridge.code-workspace b/nostr-oidc-bridge.code-workspace new file mode 100644 index 0000000..640a9d8 --- /dev/null +++ b/nostr-oidc-bridge.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../nostr-login" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..adc4760 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ +"type": "module", + "name": "nostr-oidc-bridge", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "ejs": "^3.1.10", + "express": "^5.1.0", + "jose": "^6.0.12", + "jsonwebtoken": "^9.0.2", + "nostr-tools": "^2.16.2", + "oidc-provider": "^9.4.0", + "ws": "^8.18.3" + } +} diff --git a/views/login.ejs b/views/login.ejs new file mode 100644 index 0000000..98c5d0e --- /dev/null +++ b/views/login.ejs @@ -0,0 +1,206 @@ + + + + + + Nostr Login - OIDC Bridge + + + +
+ + + <% if (typeof error !== 'undefined') { %> +
+ Error: <%= error %> +
+ <% } %> + +
+ Challenge to sign:
+ <%= challenge %> +
+ + + + +
+ or +
+ + +
+ + +
+ + +
Your public Nostr identity key
+
+ +
+ + +
Create and sign a kind 1 event with the challenge as content
+
+ + +
+ +
+ Instructions:
+ 1. Use a browser extension (recommended) or
+ 2. Create a Nostr event with the challenge above as content
+ 3. Sign it with your private key and paste the JSON +
+
+ + + +