login-with-lightning
v1.0.1
Published
Drop-in Lightning Login widget — add passwordless LNURL-auth to any website in minutes.
Maintainers
Readme
Login with Lightning ⚡
Prove you control this key.
You can't access an account without authentication. login-with-lightning is the gate — drop-in LNURL-auth for any website. Users scan a QR code with their Lightning wallet and they're in. No emails, no passwords, no third-party OAuth. Just cryptographic key pairs and the open Lightning protocol.
Part of the constraint chain: identity (login-with-lightning) enables trust (ai-wot) enables payment (lightning-agent).
Installation
npm install login-with-lightningPeer dependencies: express (v4+)
Quick Start
Server (Express)
const express = require('express');
const { lightningAuth } = require('login-with-lightning/server');
const app = express();
const auth = lightningAuth({
callbackUrl: 'https://yoursite.com/auth/lightning/verify',
jwtSecret: 'your-secret-key-change-this'
});
app.use('/auth', auth);
// Protected route
app.get('/api/me', auth.requireAuth, (req, res) => {
res.json({ pubkey: req.user.pubkey });
});
app.listen(3000);Client (Script Tag)
<script src="/path/to/widget.js"></script>
<div id="login"></div>
<script>
LightningLoginWidget.create('#login', {
endpoint: '/auth/lightning',
onSuccess: (token, pubkey) => {
console.log('Authenticated!', pubkey);
}
});
</script>Client (ES Module / Bundler)
const { LightningLoginWidget } = require('login-with-lightning/client');
const widget = new LightningLoginWidget({
endpoint: '/auth/lightning',
onSuccess: (token, pubkey) => {
console.log('Authenticated!', pubkey);
}
});
widget.mount('#login');React
const { LightningLogin, useLightningAuth } = require('login-with-lightning/client/react');
function App() {
const { token, pubkey, logout } = useLightningAuth();
return (
<div>
<LightningLogin
endpoint="/auth/lightning"
theme="dark"
onSuccess={(token, pubkey) => console.log('Logged in!', pubkey)}
/>
{pubkey && <p>Logged in as {pubkey}</p>}
{token && <button onClick={logout}>Logout</button>}
</div>
);
}Demo
Run the included demo to see it in action:
cd demo
npm install
node server.js
# Open http://localhost:3000API Reference
Server
lightningAuth(options)
Creates Express router middleware with LNURL-auth routes.
Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| callbackUrl | string | required | Public URL wallets will call back to (must be /auth/lightning/verify) |
| jwtSecret | string | required | Secret for signing JWT tokens |
| jwtExpiresIn | string | '24h' | JWT token lifetime (e.g. '1h', '7d') |
| challengeTtlMs | number | 300000 | Challenge validity in milliseconds (default 5 min) |
| onAuth | function | null | Callback (pubkey, token) fired on successful authentication |
Routes created:
| Route | Description |
|-------|-------------|
| GET /lightning | Generate challenge — returns { k1, lnurl, expiresAt, qr } |
| GET /lightning/verify | Wallet callback — verifies signature, returns { status: "OK" } |
| GET /lightning/status/:k1 | Frontend polling — returns { status, token?, pubkey? } |
Utilities on the router:
router.requireAuth— Express middleware that checksAuthorization: Bearer <token>header. Setsreq.userwith{ pubkey, iat, exp }.router.verifyToken(token)— Manually verify a JWT. Returns decoded payload ornull.
Client
new LightningLoginWidget(options)
Creates a login widget instance.
Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| endpoint | string | '/auth/lightning' | Server auth endpoint path |
| theme | string | 'dark' | 'dark' or 'light' |
| buttonText | string | 'Login with Lightning ⚡' | Button label |
| title | string | 'Login with Lightning ⚡' | Modal title |
| subtitle | string | 'Scan with your Lightning wallet' | Modal subtitle |
| pollInterval | number | 2000 | Status polling interval in ms |
| storageKey | string | 'lwl_token' | localStorage key for persisting JWT |
| storeToken | boolean | true | Whether to store the JWT in localStorage |
| onSuccess | function | null | Callback (token, pubkey) on successful auth |
| onError | function | null | Callback (error) on failure |
| onCancel | function | null | Callback when user closes modal |
| accentColor | string | null | Custom accent color (CSS color value) |
| css | string | null | Custom CSS to inject instead of defaults |
Methods:
| Method | Description |
|--------|-------------|
| mount(target) | Mount the button into a DOM element (selector string or element) |
| unmount() | Remove the widget and clean up |
| open() | Programmatically open the login modal |
| closeModal() | Close the modal |
| getToken() | Get the stored JWT token |
| logout() | Remove stored token and re-render button |
Static:
LightningLoginWidget.create(selector, options)— Create and mount in one call.
React Component
<LightningLogin />
React component wrapping the vanilla widget. Accepts all widget options as props.
useLightningAuth(storageKey?)
React hook that returns { token, pubkey, logout }.
How LNURL-auth Works
LNURL-auth (LUD-04) is a passwordless authentication protocol:
- Server generates a challenge — a random 32-byte
k1value, encoded as an LNURL (bech32-encoded URL) - User scans QR code — their Lightning wallet reads the LNURL and extracts the challenge
- Wallet signs the challenge — using secp256k1 (the same cryptography as Bitcoin), the wallet signs
k1with the user's private key - Wallet sends signature back —
GET callback?k1=...&sig=...&key=... - Server verifies — checks the signature against the public key. If valid, the user is authenticated
- Session established — server issues a JWT containing the user's public key
The user's identity is their public key. No personal information is exchanged. Different services see different derived keys (per the LNURL-auth spec), preserving privacy.
Security Considerations
- JWT Secret: Use a strong, random secret in production. Never use the demo default.
- HTTPS Required: The
callbackUrlmust be HTTPS in production. Lightning wallets won't call back to HTTP URLs (except localhost for development). - Challenge Expiry: Challenges expire after 5 minutes by default. Adjust
challengeTtlMsas needed. - One-time Use: Each challenge can only be used once. Replaying a signature will fail.
- Token Storage: JWTs are stored in
localStorageby default. For higher security, setstoreToken: falseand handle storage yourself (e.g., httpOnly cookies via your server). - No Password Equivalent: Unlike passwords, LNURL-auth keys can't be phished — each domain gets a unique derived key, and the signing happens entirely in the user's wallet.
Compatible Wallets
Any wallet supporting LNURL-auth (LUD-04), including:
- Phoenix
- Zeus
- Breez
- BlueWallet
- Alby (browser extension)
- Blixt
- And many more
Package Structure
login-with-lightning/
src/
server/
middleware.js — Express middleware
jwt.js — JWT helpers
index.js — Server exports
client/
widget.js — Vanilla JS widget (includes built-in QR generator)
widget.css — Standalone CSS file
react.jsx — React wrapper component
index.js — Client exports
index.js — Package entry point
demo/
server.js — Demo Express server
public/
index.html — Demo page
LICENSE — MIT
README.mdLicense
MIT
