-
Notifications
You must be signed in to change notification settings - Fork 25
LNURL authentication #231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
LNURL authentication #231
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,7 +34,7 @@ import { ConfigContext } from '../../../providers/config' | |
| import { FiatContext } from '../../../providers/fiat' | ||
| import { ArkNote } from '@arkade-os/sdk' | ||
| import { LimitsContext } from '../../../providers/limits' | ||
| import { checkLnUrlConditions, fetchInvoice, fetchArkAddress, isValidLnUrl } from '../../../lib/lnurl' | ||
| import { checkLnUrlConditions, fetchInvoice, fetchArkAddress, isValidLnUrl, parseLoginLnUrl } from '../../../lib/lnurl' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LNURL login detection will currently crash for lightning addresses
I’d handle this at the parser level (see Also applies to: 158-167 🤖 Prompt for AI Agents |
||
| import { extractError } from '../../../lib/error' | ||
| import { getInvoiceSatoshis } from '@arkade-os/boltz-swap' | ||
| import { LightningContext } from '../../../providers/lightning' | ||
|
|
@@ -159,6 +159,13 @@ export default function SendForm() { | |
| useEffect(() => { | ||
| if (!sendInfo.lnUrl) return | ||
| if (sendInfo.lnUrl && sendInfo.invoice) return | ||
| const { k1 } = parseLoginLnUrl(sendInfo.lnUrl) | ||
| if (k1) { | ||
| // Navigate to login confirmation screen | ||
| navigate(Pages.LnUrlLogin) | ||
| return | ||
| } | ||
|
|
||
| checkLnUrlConditions(sendInfo.lnUrl) | ||
| .then((conditions) => { | ||
| if (!conditions) return setError('Unable to fetch LNURL conditions') | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| import { useContext, useEffect, useState } from 'react' | ||
| import Button from '../../../components/Button' | ||
| import ErrorMessage from '../../../components/Error' | ||
| import ButtonsOnBottom from '../../../components/ButtonsOnBottom' | ||
| import { NavigationContext, Pages } from '../../../providers/navigation' | ||
| import { FlowContext } from '../../../providers/flow' | ||
| import Padded from '../../../components/Padded' | ||
| import Header from '../../../components/Header' | ||
| import Content from '../../../components/Content' | ||
| import FlexCol from '../../../components/FlexCol' | ||
| import Loading from '../../../components/Loading' | ||
| import { WalletContext } from '../../../providers/wallet' | ||
| import { loginLnUrl, parseLoginLnUrl } from '../../../lib/lnurl' | ||
| import { extractError } from '../../../lib/error' | ||
| import { consoleError } from '../../../lib/logs' | ||
| import WarningBox from '../../../components/Warning' | ||
| import Table from '../../../components/Table' | ||
| import TypeIcon from '../../../icons/Type' | ||
| import InfoIcon from '../../../icons/Info' | ||
| import Success from '../../../components/Success' | ||
|
|
||
| export default function LnUrlLogin() { | ||
| const { sendInfo, setSendInfo } = useContext(FlowContext) | ||
| const { navigate } = useContext(NavigationContext) | ||
| const { svcWallet } = useContext(WalletContext) | ||
|
|
||
| const [error, setError] = useState('') | ||
| const [loading, setLoading] = useState(false) | ||
| const [success, setSuccess] = useState(false) | ||
| const [domain, setDomain] = useState('') | ||
| const [k1, setK1] = useState('') | ||
|
|
||
| useEffect(() => { | ||
| if (!sendInfo.lnUrl) { | ||
| navigate(Pages.SendForm) | ||
| return | ||
| } | ||
|
|
||
| const { k1: parsedK1, url } = parseLoginLnUrl(sendInfo.lnUrl) | ||
| if (!parsedK1) { | ||
| navigate(Pages.SendForm) | ||
| return | ||
| } | ||
|
|
||
| setK1(parsedK1) | ||
|
|
||
| // Extract domain from URL | ||
| try { | ||
| const urlObj = new URL(url) | ||
| setDomain(urlObj.hostname) | ||
| } catch { | ||
| setDomain('Unknown service') | ||
| } | ||
| }, [sendInfo.lnUrl, navigate]) | ||
|
|
||
| const handleConfirm = async () => { | ||
| if (!sendInfo.lnUrl || !svcWallet || !k1) return | ||
|
|
||
| setLoading(true) | ||
| setError('') | ||
|
|
||
| try { | ||
| const { url } = parseLoginLnUrl(sendInfo.lnUrl) | ||
| const response = await loginLnUrl(url, k1, svcWallet.identity) | ||
| const data = await response.json() | ||
|
|
||
| if (data.status === 'OK') { | ||
| setSuccess(true) | ||
| setLoading(false) | ||
| } else { | ||
| setError(data.reason || 'Login failed') | ||
| setLoading(false) | ||
| } | ||
| } catch (err) { | ||
| consoleError(err, 'error logging in with LNURL') | ||
| setError(extractError(err) || 'Failed to login') | ||
| setLoading(false) | ||
| } | ||
| } | ||
|
|
||
| const handleCancel = () => { | ||
| setSendInfo({ ...sendInfo, lnUrl: undefined }) | ||
| navigate(Pages.SendForm) | ||
| } | ||
|
|
||
| const handleBack = () => { | ||
| setSendInfo({ ...sendInfo, lnUrl: undefined }) | ||
| navigate(Pages.Wallet) | ||
| } | ||
|
|
||
| if (!sendInfo.lnUrl || !k1) { | ||
| return <Loading text='Loading...' /> | ||
| } | ||
|
|
||
| const tableData = [ | ||
| ['Service', domain, <TypeIcon key='service-icon' />], | ||
| ['Challenge', k1, <InfoIcon key='challenge-icon' />], | ||
| ] | ||
|
|
||
| return ( | ||
| <> | ||
| <Header text='LNURL Login' back={success ? handleBack : handleCancel} /> | ||
| <Content> | ||
| {loading ? ( | ||
| <Loading text='Signing...' /> | ||
| ) : success ? ( | ||
| <Success headline='Login successful!' text={`You have successfully signed in to ${domain}`} /> | ||
| ) : ( | ||
| <Padded> | ||
| <FlexCol> | ||
| <ErrorMessage error={Boolean(error)} text={error} /> | ||
| <WarningBox text='By signing this message, you are authenticating yourself to this service. Make sure you trust this website before proceeding.' /> | ||
| <Table data={tableData} /> | ||
| </FlexCol> | ||
| </Padded> | ||
| )} | ||
| </Content> | ||
| <ButtonsOnBottom> | ||
| {loading ? null : success ? ( | ||
| <Button onClick={handleBack} label='Back to Wallet' /> | ||
| ) : ( | ||
| <> | ||
| <Button onClick={handleCancel} label='Cancel' secondary /> | ||
| <Button onClick={handleConfirm} label='Sign' disabled={Boolean(error)} /> | ||
| </> | ||
| )} | ||
| </ButtonsOnBottom> | ||
| </> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
parseLoginLnUrl should tolerate non-bech32 lnurls to avoid crashes; loginLnUrl shape looks OK
parseLoginLnUrlcurrently assumes that everylnurlargument is a bech32-encoded LNURL and immediately doesbech32.decodeToBytes(lnurl). Callers likeSendFormcan pass lightning addresses (e.g.user@domain.com) or, in the future, raw URLs (LUD-17 style). In those casesbech32.decodeToByteswill throw, and because callers don’t wrapparseLoginLnUrlin a try/catch, the whole React effect will crash.You can fix this centrally (and make the caller code simpler) by:
lnurlstring if decoding fails.?when buildingURLSearchParams.k1whentag=loginandk1is present.For example:
With this change:
SendFormandLnUrlLogincan safely callparseLoginLnUrlfor anylnurl(bech32, lightning address, or raw URL) without worrying about exceptions.loginLNURLs with ak1will trigger the auth flow; lightning addresses and pay/withdraw LNURLs will fall through to the existing payment logic.The
loginLnUrlhelper that signsk1, injectssigandkeyinto the query string, and uses a CORS proxy onlocalhostlooks structurally fine; once the above parser change is in place, I’d just recommend exercising it against a couple of real LNURL-auth services to confirm the signature encoding and query format match what they expect.Also applies to: 80-122
🏁 Script executed:
Length of output: 3215
parseLoginLnUrl must tolerate non-bech32 lnurls to prevent crashes; fix confirmed needed at Form.tsx:162 and LnUrlLogin.tsx:39
Verification confirms the critical issue:
parseLoginLnUrlassumes bech32 encoding and callsbech32.decodeToBytes(lnurl)without fallback. Call sites atsrc/screens/Wallet/Send/Form.tsx:162andsrc/screens/Wallet/Send/LnUrlLogin.tsx:39are unprotected by try/catch. When callers (SendForm, LnUrlLogin effects) pass lightning addresses or raw URLs,decodeToBytesthrows and crashes the React effect.Implement the proposed fix to make
parseLoginLnUrltolerant:This eliminates the crash risk and allows
Form.tsx:162andLnUrlLogin.tsx:39to safely handle any LNURL format. Only trueloginLNURLs withk1will trigger auth; others fall through to existing payment logic.