Skip to content

Latest commit

 

History

History
576 lines (442 loc) · 24.1 KB

File metadata and controls

576 lines (442 loc) · 24.1 KB

LUD-22: Payment Authorization for Lightning Addresses

author: Blink status: draft extends: LUD-06, LUD-16 requires: LUD-01, LUD-04, LUD-06, LUD-16


Abstract

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.

Motivation

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:

  1. Signal that authentication is required
  2. Guide wallets through provider-specific registration flows
  3. Authorize invoice generation after verification
  4. 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.

Relationship with LUD-18

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. The k1 is signed directly (per LUD-04) and the result is committed to the invoice descriptionHash. 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.

Protocol Flow Overview

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>      |
  |         &timestamp=<unix>                 |
  |------------------------------------------>|
  |                                           |
  | 5. Validate auth, generate invoice        |
  |<------------------------------------------|
  |     {pr: <bolt11>, routes: []}            |

New Fields in payRequest Response

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
}

Field Definitions

  • authRequired (boolean, optional): Indicates whether authentication is mandatory. Defaults to false for backwards compatibility.

  • authMethods (array of strings, required if authRequired is 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 the registrationUrl for manual completion.

  • registrationUrl (string, required if authRequired is 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 if authRequired is true): 32-byte random challenge for the authentication signature. Consistent with LUD-04 naming. Providers MUST generate a cryptographically random, single-use k1 for each payRequest response.

  • authExpiresAt (number, Unix timestamp, optional): When the current authentication expires. Wallet should re-authenticate after this time.

Authentication Method

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.

LNURL-Auth Payment Authorization

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.

Flow

  1. Initial Request: Provider returns k1 (random 32 bytes, hex-encoded) in the payRequest response.

  2. Registration (if needed):

    • Wallet opens registrationUrl in browser/webview
    • User completes provider's registration requirements
    • Provider associates linkingKey with the sender's verified identity
    • Browser redirects back to wallet with success signal
  3. Payment Authorization:

    Wallet constructs the message to sign as a UTF-8 string:

    message = "lnurl-pay-auth:" || k1 || ":" || amount || ":" || recipient || ":" || timestamp
    

    Where:

    • k1: hex-encoded 32-byte challenge as received from the provider
    • amount: 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)) on secp256k1 using linkingPrivKey (derived per LUD-05 or LUD-13) and DER-encodes the signature.

  4. Callback Request:

    <callback><?|&>amount=<milliSatoshi>&auth=<hex(DER-encoded signature)>&key=<hex(linkingKey)>&timestamp=<unix_seconds>
    
  5. Provider Validation:

    • Reconstruct the message using the received amount, timestamp, the stored k1, and the known recipient identifier
    • Verify the ECDSA signature against the provided key (compressed secp256k1 public key)
    • Check k1 has not been used before (providers MUST store used k1s)
    • Check timestamp is within acceptable window (e.g., 10 minutes)
    • Check linkingKey is associated with an authorized sender
    • Generate invoice with committed metadata

Key Properties

  • 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, and timestamp in the signed message prevents replay attacks

Error Handling

Graceful Degradation (Non-Supporting Wallets)

When a wallet that doesn't implement this LUD makes a request:

  1. Provider MAY return standard LUD-06 response without auth fields (if auth not strictly required)
  2. 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.

Supporting Wallet Errors

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.

Authentication Required

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"
}

Invalid Authentication

{
  "status": "ERROR",
  "code": "AUTH_INVALID",
  "reason": "Invalid or expired authentication"
}

Insufficient Verification Level

{
  "status": "ERROR",
  "code": "VERIFICATION_INSUFFICIENT",
  "reason": "Additional verification required for this amount",
  "registrationUrl": "https://provider.com/verify"
}

Additional Information Required

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.

Pending Authorization

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.

Authorization Revoked

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.

Edge Cases and Handling

User Abandons 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_REQUIRED again
  • No harm: User can restart registration anytime

Deep Link Failure

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

Multiple Devices

Wallet linkingKey is derived from wallet seed:

  • Same wallet, different device: Same linkingKey derived → Same auth recognized
  • Different wallet: Different linkingKey → Provider determines whether registration or login is required
  • Recovery: No issue, auth tied to key not device

Provider Changes Requirements

If provider adds new required fields after user is already authorized:

  • Return: INFO_REQUIRED with new fields
  • Flow: Same as additional info error
  • UX: User provides only new fields, not full re-registration

Callback URL Too Long

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

Rate Limiting

Too many failed auth attempts:

{
  "status": "ERROR",
  "code": "RATE_LIMITED",
  "reason": "Too many attempts. Please try again in 1 hour.",
  "retryAfter": 3600
}

Unsupported Auth Methods

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 registrationUrl to 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 reason field in the error response SHOULD include human-readable instructions

Backwards Compatibility

For Wallets

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

For Providers

Providers implementing this LUD:

  • MUST still accept requests without auth if authRequired is not set
  • SHOULD provide clear error messages for manual completion
  • MAY offer reduced functionality for non-authenticated users

Auth Scope: Per-Provider Domain

Auth is tied to the PROVIDER DOMAIN, not individual recipient addresses.

How It Works

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:

  1. User sends to user123@provider-a.com → Auth required, wallet completes registration with provider-a.com
  2. User sends to user789@provider-a.com → Auth already stored ✓ (same domain)
  3. User sends to user456@provider-b.com → Auth required, different provider

Multi-Sender to Same Recipient

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

Security Considerations

Replay Attack Prevention

Providers MUST enforce both of the following:

  1. Single-use k1: Providers MUST maintain a cache of issued k1 values and reject any k1 that has already been used in a successful authentication. This is consistent with LUD-04's requirement for k1 management.

  2. Payment-bound signatures: The signed message includes the k1, amount, recipient, and timestamp, which binds the signature to a specific payment context. Providers MUST reject signatures where the timestamp is 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.

registrationUrl Security

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 registrationUrl to the user before opening it.
  • Wallets SHOULD warn the user if the registrationUrl domain 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.

Privacy

  • 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 linkingKey with additional sender information depending on the provider's requirements.
  • The k1 challenge 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 registrationUrl MAY contain session or tracking parameters. Wallets SHOULD display the full URL to the user before opening.

Implementation Guidelines

For Wallet Developers (Custodial)

  1. 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
    }
  2. Auto-Retry: If provider returns AUTH_INVALID, wallet should clear stored auth for that domain and prompt re-registration.

  3. Re-fetch k1: Before each payment, wallet should re-fetch the payRequest endpoint to obtain a fresh k1. Do not reuse k1 values from previous requests.

  4. UX: Show provider domain name to the user before opening registrationUrl.

For Providers

  1. k1 Management: Generate cryptographically random k1 values and store them in a cache. Remove used k1s after successful authentication, consistent with LUD-04.
  2. Registration Flow: Keep registration simple, return to wallet quickly. Providers SHOULD request all required information in a single registration step to minimize round-trips.
  3. Rate Limiting: Apply per-linkingKey rate limits to prevent abuse.
  4. 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.

Examples

Example 1: Payment with LNURL-Auth

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 linkingKey with verified sender identity

Payment Request:

GET https://provider.com/lnurlp/callback
  ?amount=100000000
  &auth=<hex(DER-encoded signature)>
  &key=<hex(linkingKey)>
  &timestamp=1704067200

Where the signature is over:

"lnurl-pay-auth:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08:100000000:user123@provider.com:1704067200"

Example 2: Conditional Auth Based on Amount

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:

  1. Wallet opens registrationUrl in browser
  2. User completes registration on provider site
  3. Provider stores wallet's linkingKey
  4. Browser redirects back to wallet
  5. Wallet retries with auth → succeeds
  6. Future payments to provider.com use stored auth

Example 3: Additional Requirements for Authenticated User (Error-Driven)

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)>
  &timestamp=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:

  1. Wallet opens registrationUrl in browser
  2. User provides required information on provider site
  3. Provider stores info and updates authorization for this payment
  4. Browser redirects back to wallet
  5. Wallet retries payment → succeeds

Example 4: Error for Non-Supporting Wallet

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."
}

Test Vectors

Signed Message Construction

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>

Callback URL

https://provider.com/lnurlp/callback?amount=1000000&auth=<hex(signature)>&key=<hex(linkingKey)>&timestamp=1704067200

Note: Complete test vectors with actual cryptographic values will be added once reference implementations are available.

References

  • 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

Future Extensions

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.