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).
npm install login-with-lightningPeer dependencies: express (v4+)
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);<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>const { LightningLoginWidget } = require('login-with-lightning/client');
const widget = new LightningLoginWidget({
endpoint: '/auth/lightning',
onSuccess: (token, pubkey) => {
console.log('Authenticated!', pubkey);
}
});
widget.mount('#login');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>
);
}Run the included demo to see it in action:
cd demo
npm install
node server.js
# Open http://localhost:3000Creates 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.
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 wrapping the vanilla widget. Accepts all widget options as props.
React hook that returns { token, pubkey, logout }.
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.
- 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.
Any wallet supporting LNURL-auth (LUD-04), including:
- Phoenix
- Zeus
- Breez
- BlueWallet
- Alby (browser extension)
- Blixt
- And many more
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.md
MIT