author: Blink status: draft extends: LUD-06, LUD-16 requires: LUD-01, LUD-04, LUD-06, LUD-16
This LUD extends the Lightning Address (LUD-16) and payRequest (LUD-06) protocols to enable providers to require sender authentication and verification before processing payments. It uses LUD-04 key derivation (via LUD-05 or LUD-13) with a payment-specific signature scheme, combined with a web-based registration flow, while maintaining backwards compatibility with non-supporting wallets.
Providers operating under different compliance frameworks have varying requirements:
- Sender verification
- Transaction purpose declaration
- Source of funds attestation
- Recipient validation
Currently, there's no standardized way for a Lightning Address provider to:
- Signal that authentication is required
- Guide wallets through provider-specific registration flows
- Authorize invoice generation after verification
- Handle multiple senders to the same destination
This LUD solves these problems by introducing an authorization layer that wallets can implement once and use across any compliant provider.
LUD-18 defines payerData.auth which allows a service to request a linkingKey signature as part of the payer identity committed to an invoice. While both use LUD-04 key derivation, their purposes differ:
- LUD-18
payerData.auth: Identifies the payer within a single payment. Thek1is signed directly (per LUD-04) and the result is committed to the invoicedescriptionHash. It is a per-invoice identity attestation. - LUD-22: Authorizes a sender before invoice generation. The signature binds to the payment parameters (amount, recipient, timestamp) and is used to gate access based on the sender's authorization status. It is a per-provider authorization layer.
These two mechanisms are complementary and MAY coexist in the same flow. A provider MAY use LUD-22 to verify sender authorization and simultaneously use LUD-18 payerData.auth to commit payer identity to the invoice. They operate at different stages: LUD-22 controls whether an invoice is generated at all, while LUD-18 enriches the invoice metadata.
Wallet Provider
| |
| 1. GET /.well-known/lnurlp/<identifier> |
|------------------------------------------>|
| |
| 2. Response with auth requirements |
|<------------------------------------------|
| (authRequired, authMethods, |
| registrationUrl, k1) |
| |
| 3. Check stored auth for this domain |
| [If none, open registrationUrl, |
| user completes registration] |
| |
| 4. GET /callback?amount=... |
| &auth=<sig>&key=<linkingKey> |
| ×tamp=<unix> |
|------------------------------------------>|
| |
| 5. Validate auth, generate invoice |
|<------------------------------------------|
| {pr: <bolt11>, routes: []} |
When authentication is required, the following OPTIONAL fields are added to the LUD-06 response:
{
"callback": "https://provider.com/lnurlp/callback",
"maxSendable": 1000000000,
"minSendable": 1000,
"metadata": "[[\"text/identifier\", \"user@provider.com\"]]",
"tag": "payRequest",
"authRequired": true,
"authMethods": ["lnurl-auth"],
"registrationUrl": "https://provider.com/register?return=wallet://callback",
"k1": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"authExpiresAt": 1704067200
}-
authRequired(boolean, optional): Indicates whether authentication is mandatory. Defaults tofalsefor backwards compatibility. -
authMethods(array of strings, required ifauthRequiredis true): List of supported authentication methods. This LUD defines"lnurl-auth"as the only method. Future LUDs may define additional values (e.g.,"nostr-connect","oauth2"). If a wallet does not support any of the listed methods, it SHOULD fall back to opening theregistrationUrlfor manual completion. -
registrationUrl(string, required ifauthRequiredis true): HTTPS URL where users complete registration. Wallets MUST display the domain of this URL to the user before opening it. Wallets SHOULD warn the user if it differs from the LNURL domain. MUST include a return mechanism to the wallet (deep link, callback URL, or query parameter). -
k1(string, hex-encoded, required ifauthRequiredis true): 32-byte random challenge for the authentication signature. Consistent with LUD-04 naming. Providers MUST generate a cryptographically random, single-usek1for each payRequest response. -
authExpiresAt(number, Unix timestamp, optional): When the current authentication expires. Wallet should re-authenticate after this time.
This LUD defines a single authentication method based on LUD-04 key derivation. Future LUDs may define additional authentication methods (such as Nostr Connect or OAuth 2.0) that extend this specification.
Uses the domain-specific linkingKey from LUD-04 (derived per LUD-05 or LUD-13) as a persistent sender identity. Unlike LUD-04 which signs a raw k1 challenge, this method defines a payment-specific signature scheme that binds the authentication to the payment parameters.
-
Initial Request: Provider returns
k1(random 32 bytes, hex-encoded) in the payRequest response. -
Registration (if needed):
- Wallet opens
registrationUrlin browser/webview - User completes provider's registration requirements
- Provider associates
linkingKeywith the sender's verified identity - Browser redirects back to wallet with success signal
- Wallet opens
-
Payment Authorization:
Wallet constructs the message to sign as a UTF-8 string:
message = "lnurl-pay-auth:" || k1 || ":" || amount || ":" || recipient || ":" || timestampWhere:
k1: hex-encoded 32-byte challenge as received from the provideramount: payment amount in millisatoshis (decimal string, e.g."1000000")recipient: the full Lightning Address being paid (e.g."user@provider.com")timestamp: current Unix timestamp in seconds (decimal string, e.g."1704067200")
Wallet then signs
sha256(utf8ToBytes(message))onsecp256k1usinglinkingPrivKey(derived per LUD-05 or LUD-13) and DER-encodes the signature. -
Callback Request:
<callback><?|&>amount=<milliSatoshi>&auth=<hex(DER-encoded signature)>&key=<hex(linkingKey)>×tamp=<unix_seconds> -
Provider Validation:
- Reconstruct the message using the received
amount,timestamp, the storedk1, and the knownrecipientidentifier - Verify the ECDSA signature against the provided
key(compressed secp256k1 public key) - Check
k1has not been used before (providers MUST store usedk1s) - Check
timestampis within acceptable window (e.g., 10 minutes) - Check
linkingKeyis associated with an authorized sender - Generate invoice with committed metadata
- Reconstruct the message using the received
- No token management required (fresh signature per payment)
- Reuses LUD-04 key derivation -- wallets that already implement LUD-04/05/13 only need to add the new signing scheme
- Provider only learns domain-specific identity (different per provider, per LUD-05)
- The
"lnurl-pay-auth:"prefix acts as a domain separator, preventing cross-protocol signature reuse - Including
amount,recipient, andtimestampin the signed message prevents replay attacks
When a wallet that doesn't implement this LUD makes a request:
- Provider MAY return standard LUD-06 response without auth fields (if auth not strictly required)
- OR Provider returns error with registration URL:
{
"status": "ERROR",
"reason": "Please complete verification at https://provider.com/register to send payments to this address."
}The error message SHOULD include actionable instructions for manual completion.
This LUD introduces a code field in error responses to allow wallets to programmatically handle specific auth states. This extends the standard {"status": "ERROR", "reason": "..."} format from LUD-01. Non-supporting wallets will still see the human-readable reason field.
Returned when auth is mandatory and not provided, either at initial request time or at callback time (e.g., amount threshold exceeded):
{
"status": "ERROR",
"code": "AUTH_REQUIRED",
"reason": "Authentication required before generating invoice",
"registrationUrl": "https://provider.com/register",
"authMethods": ["lnurl-auth"],
"k1": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
}{
"status": "ERROR",
"code": "AUTH_INVALID",
"reason": "Invalid or expired authentication"
}{
"status": "ERROR",
"code": "VERIFICATION_INSUFFICIENT",
"reason": "Additional verification required for this amount",
"registrationUrl": "https://provider.com/verify"
}Returned when a specific payment requires extra information to complete the exchange. This can be triggered by various factors (recipient jurisdiction, regulatory flags, first-time recipient, etc.) not known until the payment attempt:
{
"status": "ERROR",
"code": "INFO_REQUIRED",
"reason": "Additional information required to complete this exchange",
"registrationUrl": "https://provider.com/provide-info?return=wallet://callback",
"requiredFields": ["sourceOfFunds", "purpose", "recipientRelationship"]
}The wallet MUST open the registrationUrl to allow the user to provide the required information. After completion, the provider will update the sender's authorization status and the wallet can retry the payment.
Returned when verification is submitted but under review:
{
"status": "ERROR",
"code": "AUTH_PENDING",
"reason": "Verification submitted and pending review. Estimated completion: 24 hours.",
"checkStatusUrl": "https://provider.com/auth/status?key=<linkingKey>",
"estimatedCompletion": 1704067200
}Wallet should poll checkStatusUrl or wait for user notification.
Returned when user or provider has revoked authorization:
{
"status": "ERROR",
"code": "AUTH_REVOKED",
"reason": "Authorization revoked. Please re-register.",
"registrationUrl": "https://provider.com/re-register"
}Wallet must clear stored auth and require re-registration.
If user opens registrationUrl but doesn't complete registration:
- Provider: Should have session timeout (e.g., 30 minutes)
- Wallet: When payment is attempted again, provider returns
AUTH_REQUIREDagain - No harm: User can restart registration anytime
If browser fails to return to wallet after registration:
- Wallet: Should detect when user returns to app (app lifecycle)
- UX: Show "Check payment status" button to retry
- Provider: Registration still completes, auth stored server-side
- Recovery: Wallet can re-query with auth, provider recognizes completed registration
Wallet linkingKey is derived from wallet seed:
- Same wallet, different device: Same
linkingKeyderived → Same auth recognized - Different wallet: Different
linkingKey→ Provider determines whether registration or login is required - Recovery: No issue, auth tied to key not device
If provider adds new required fields after user is already authorized:
- Return:
INFO_REQUIREDwith new fields - Flow: Same as additional info error
- UX: User provides only new fields, not full re-registration
The auth parameters (auth, key, timestamp) add to the callback URL length. If combined with other LUD parameters (e.g., LUD-12 comment, LUD-18 payerdata), URLs may exceed browser/server limits (~2000 chars):
- Providers SHOULD support POST as an alternative to GET for the callback when auth parameters are present
- Wallet SHOULD keep total URL length under 1500 chars
- If POST is used, auth parameters are sent as a JSON request body with
Content-Type: application/json
Too many failed auth attempts:
{
"status": "ERROR",
"code": "RATE_LIMITED",
"reason": "Too many attempts. Please try again in 1 hour.",
"retryAfter": 3600
}If a wallet does not support any of the methods listed in authMethods (e.g., a future method the wallet hasn't implemented):
- Wallet: SHOULD show the
registrationUrlto the user and explain that manual completion is required - User: Can complete registration manually via browser and may be able to use a different wallet that supports the required method
- Fallback: The provider's
reasonfield in the error response SHOULD include human-readable instructions
Wallets that don't implement this LUD:
- Will see standard error messages
- Can still use the service by manually visiting
registrationUrl - Providers SHOULD detect non-support via absence of auth parameters
Providers implementing this LUD:
- MUST still accept requests without auth if
authRequiredis not set - SHOULD provide clear error messages for manual completion
- MAY offer reduced functionality for non-authenticated users
Auth is tied to the PROVIDER DOMAIN, not individual recipient addresses.
Wallet stores auth state per domain:
interface AuthState {
"provider-a.com": { method: "lnurl-auth", linkingKey: "02abc..." },
"provider-b.com": { method: "lnurl-auth", linkingKey: "03def..." }
}Scenario:
- User sends to
user123@provider-a.com→ Auth required, wallet completes registration with provider-a.com - User sends to
user789@provider-a.com→ Auth already stored ✓ (same domain) - User sends to
user456@provider-b.com→ Auth required, different provider
Multiple wallets can send to the same recipient (e.g., recipient-id@provider.com):
- Sender A (wallet A with linkingKey A) authenticates with provider-a.com → can send to any address on provider-a.com
- Sender B (wallet B with linkingKey B) authenticates with provider-a.com → can send to any address on provider-a.com
- Provider tracks authorized senders per domain:
[linkingKeyA, linkingKeyB] - Each sender's authorization status and limits are independent
Providers MUST enforce both of the following:
-
Single-use
k1: Providers MUST maintain a cache of issuedk1values and reject anyk1that has already been used in a successful authentication. This is consistent with LUD-04's requirement fork1management. -
Payment-bound signatures: The signed message includes the
k1,amount,recipient, andtimestamp, which binds the signature to a specific payment context. Providers MUST reject signatures where thetimestampis outside an acceptable window (RECOMMENDED: 10 minutes).
The signed message format is:
message = "lnurl-pay-auth:" || k1 || ":" || amount || ":" || recipient || ":" || timestamp
The "lnurl-pay-auth:" prefix acts as a domain separator, preventing this signature from being confused with LUD-04 signatures (which sign raw k1 bytes) or any other protocol.
The registrationUrl is returned from an HTTPS endpoint but could be manipulated if DNS or TLS were compromised. To mitigate phishing:
- Wallets MUST display the domain of
registrationUrlto the user before opening it. - Wallets SHOULD warn the user if the
registrationUrldomain differs from the LNURL provider domain. - Wallets SHOULD NOT automatically submit sensitive data to the
registrationUrl-- it is opened in a browser for user-driven interaction only.
- Provider only learns the domain-specific
linkingKey(different per provider, per LUD-05), so providers cannot correlate users across domains. - After registration, the provider may associate the
linkingKeywith additional sender information depending on the provider's requirements. - The
k1challenge is returned on every payRequest GET, before any identity is revealed. Providers SHOULD NOT use unique challenges as a fingerprinting mechanism to correlate unauthenticated requests (e.g., by IP) with later-authenticated payments. - The
registrationUrlMAY contain session or tracking parameters. Wallets SHOULD display the full URL to the user before opening.
-
Storage: Store auth state per provider domain:
interface ProviderAuth { domain: string; method: string; // "lnurl-auth" for this LUD, extensible for future methods linkingKey: string; // hex-encoded compressed secp256k1 public key registeredAt: number; // Unix timestamp of registration completion expiresAt?: number; // Unix timestamp from authExpiresAt, if provided }
-
Auto-Retry: If provider returns
AUTH_INVALID, wallet should clear stored auth for that domain and prompt re-registration. -
Re-fetch
k1: Before each payment, wallet should re-fetch the payRequest endpoint to obtain a freshk1. Do not reusek1values from previous requests. -
UX: Show provider domain name to the user before opening
registrationUrl.
k1Management: Generate cryptographically randomk1values and store them in a cache. Remove usedk1s after successful authentication, consistent with LUD-04.- Registration Flow: Keep registration simple, return to wallet quickly. Providers SHOULD request all required information in a single registration step to minimize round-trips.
- Rate Limiting: Apply per-
linkingKeyrate limits to prevent abuse. - Subdomain Choice: Consistent with LUD-04, providers should carefully choose which subdomain (if any) hosts the LNURL endpoint and stick to it. Changing the domain will result in different
linkingKeys for each user.
Lightning Address: user123@provider.com
Initial Response:
{
"callback": "https://provider.com/lnurlp/callback",
"maxSendable": 500000000000,
"minSendable": 1000000,
"metadata": "[[\"text/identifier\", \"user123@provider.com\"], [\"text/plain\", \"Send to recipient account\"]]",
"tag": "payRequest",
"authRequired": true,
"authMethods": ["lnurl-auth"],
"registrationUrl": "https://provider.com/register?wallet_callback=wallet://lnurl-callback",
"k1": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}After Registration:
- User opens registration URL in browser
- Enters recipient ID and completes provider's registration requirements
- Provider associates wallet's
linkingKeywith verified sender identity
Payment Request:
GET https://provider.com/lnurlp/callback
?amount=100000000
&auth=<hex(DER-encoded signature)>
&key=<hex(linkingKey)>
×tamp=1704067200
Where the signature is over:
"lnurl-pay-auth:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08:100000000:user123@provider.com:1704067200"
Scenario: Provider allows small payments without auth, but requires auth for larger amounts.
Initial Response (no auth required for small amounts):
{
"callback": "https://provider.com/lnurlp/callback",
"maxSendable": 100000000000,
"minSendable": 1000,
"metadata": "[[\"text/identifier\", \"user789@provider.com\"]]",
"tag": "payRequest"
}Small Payment (no auth):
GET https://provider.com/lnurlp/callback?amount=10000
Response: Returns invoice normally
Large Payment (auth suddenly required):
GET https://provider.com/lnurlp/callback?amount=5000000000
Provider Response (auth required for this amount):
{
"status": "ERROR",
"code": "AUTH_REQUIRED",
"reason": "Authentication required for amounts over 1,000,000 millisats",
"registrationUrl": "https://provider.com/register?return=wallet://callback",
"authMethods": ["lnurl-auth"],
"k1": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}Flow:
- Wallet opens
registrationUrlin browser - User completes registration on provider site
- Provider stores wallet's
linkingKey - Browser redirects back to wallet
- Wallet retries with auth → succeeds
- Future payments to provider.com use stored auth
Scenario: User is already authenticated with provider.com, but this specific payment requires additional information (recipient jurisdiction, amount threshold, first-time recipient, regulatory flag, etc.).
Payment Request:
GET https://provider.com/lnurlp/callback
?amount=5000000000
&auth=<hex(DER-encoded signature)>
&key=<hex(linkingKey)>
×tamp=1704067200
Provider Response (additional info required):
{
"status": "ERROR",
"code": "INFO_REQUIRED",
"reason": "Additional verification required for this exchange",
"registrationUrl": "https://provider.com/verify-payment?return=wallet://callback",
"requiredFields": ["sourceOfFunds", "purpose"]
}Flow:
- Wallet opens
registrationUrlin browser - User provides required information on provider site
- Provider stores info and updates authorization for this payment
- Browser redirects back to wallet
- Wallet retries payment → succeeds
Request from Basic Wallet (no LUD-22 support):
GET https://provider.com/.well-known/lnurlp/user
Response:
{
"status": "ERROR",
"reason": "Authentication required. Please visit https://provider.com/register to complete verification, then retry the payment."
}k1: 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
amount: 1000000 (millisatoshis)
recipient: user@provider.com
timestamp: 1704067200
message: "lnurl-pay-auth:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f:1000000:user@provider.com:1704067200"
sha256(utf8ToBytes(message)): <to be computed>
linkingPrivKey: <derived per LUD-05 for domain "provider.com">
signature: <DER-encoded ECDSA signature of the sha256 hash above>
linkingKey: <compressed secp256k1 public key corresponding to linkingPrivKey>
https://provider.com/lnurlp/callback?amount=1000000&auth=<hex(signature)>&key=<hex(linkingKey)>×tamp=1704067200
Note: Complete test vectors with actual cryptographic values will be added once reference implementations are available.
- LUD-01: Base LNURL encoding
- LUD-04: Auth base spec
- LUD-05: BIP32-based seed generation
- LUD-06: payRequest base spec
- LUD-13: signMessage-based seed generation
- LUD-16: Lightning Address
- LUD-18: Payer identity in payRequest
This LUD defines lnurl-auth as the only authentication method. The authMethods field is designed to be extensible. Future LUDs may define additional methods such as:
- Nostr Connect (NIP-46): For Nostr-native providers using remote signer authorization via relays.
- OAuth 2.0 / OIDC: For providers with existing OAuth2 infrastructure and complex verification flows.
When additional methods are defined, they will specify their own signature/token format, callback parameters, and any additional fields in the payRequest response.