From 64a5fcd9f1b8743cd3060cfef32a6325d20d02a9 Mon Sep 17 00:00:00 2001 From: HolgerHatGarKeineNode <123783602+HolgerHatGarKeineNode@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:51:01 +0200 Subject: [PATCH] Make the mobile login page Lightning-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Nostr login is now driven entirely by the app (it launches the NIP-55 signer via an ACTION_VIEW intent and posts the signed event to /auth/mobile/signed), so the portal page no longer needs window.nostr or an Amber button — it only renders the Lightning QR. The path-based signer callback and token exchange endpoints remain server-side. --- .../livewire/auth/mobile-login.blade.php | 127 ++++-------------- tests/Feature/Auth/MobileAuthTest.php | 21 --- 2 files changed, 24 insertions(+), 124 deletions(-) diff --git a/resources/views/livewire/auth/mobile-login.blade.php b/resources/views/livewire/auth/mobile-login.blade.php index 803f0b0..83d002b 100644 --- a/resources/views/livewire/auth/mobile-login.blade.php +++ b/resources/views/livewire/auth/mobile-login.blade.php @@ -1,19 +1,22 @@ whenEmpty(fn () => str(MobileAuthController::DEFAULT_DEVICE_NAME)) ->value(); - // The completion/confirm controller reads the flow state from the - // session — the wallet callback arrives outside this session, so it - // can't carry the redirect target itself. + // The completion controller reads the flow state from the session — + // the wallet callback arrives outside this session, so it can't + // carry the redirect target itself. session([ 'mobile_auth' => [ 'redirect_uri' => $this->redirectUri, @@ -64,15 +64,13 @@ class extends Component { return; } - $this->issueNostrChallenge(); $this->initChallenge(); } /** * Generate a fresh k1 challenge for the LNURL flow. The Lightning * wallet signs it via LNURL-auth; the resulting LoginKey row is picked - * up by checkAuth() below. The Nostr flow reuses the same row keyed by - * the same k1 (see loginListener). + * up by checkAuth() below. */ protected function initChallenge(): void { @@ -98,65 +96,6 @@ class extends Component { ->generate($this->lnurl)); } - /** - * Session-bound challenge for the window.nostr (NIP-07/NIP-46) login — - * identical mechanism to the regular login component. - */ - protected function issueNostrChallenge(): string - { - $challenge = bin2hex(random_bytes(32)); - $this->nostrChallenge = $challenge; - Session::put('nostr_login_challenge', $challenge); - Session::put('nostr_login_challenge_expires_at', now()->addSeconds(NostrLogin::CHALLENGE_TTL_SECONDS)->timestamp); - - return $challenge; - } - - public function requestNostrChallenge(): string - { - return $this->issueNostrChallenge(); - } - - /** - * window.nostr signed the kind-22242 challenge (via extension or a - * NIP-46 connection to e.g. Amber). Verify it, store the LoginKey and - * hand the navigation to the completion route, which issues the token - * and redirects into the app via the verified App Link. - */ - #[On('nostrLoggedIn')] - public function loginListener($signedEvent = null): void - { - $npub = $this->verifyNostrLoginEvent($signedEvent); - - $user = NostrLogin::findOrCreateUser($npub); - - FetchNostrProfileJob::dispatch($user); - - LoginKey::query()->updateOrCreate( - ['k1' => $this->k1], - ['user_id' => $user->id], - ); - - $this->redirect(route('auth.mobile.complete', ['k1' => $this->k1])); - } - - protected function verifyNostrLoginEvent(mixed $signedEvent): string - { - $expectedChallenge = Session::get('nostr_login_challenge'); - $expiresAt = (int) Session::get('nostr_login_challenge_expires_at', 0); - - if (! is_string($expectedChallenge) || $expectedChallenge === '' || $expiresAt < now()->timestamp) { - Session::forget(['nostr_login_challenge', 'nostr_login_challenge_expires_at']); - throw ValidationException::withMessages(['email' => __('auth.failed')]); - } - - $npub = NostrLogin::verifyEvent($signedEvent, $expectedChallenge); - - Session::forget(['nostr_login_challenge', 'nostr_login_challenge_expires_at']); - - return $npub; - } - public function checkAuth(): void { $loginKey = LoginKey::query() @@ -168,9 +107,9 @@ class extends Component { return; } - // Same handoff pattern as the Lightning login: navigate via the - // client instead of redirecting from inside wire:poll, so a stray - // poll tick can't race the completion request. + // Same handoff pattern as the desktop Lightning login: navigate via + // the client instead of redirecting from inside wire:poll, so a + // stray poll tick can't race the completion request. $this->dispatch( 'mobile-login-ready', url: route('auth.mobile.complete', ['k1' => $this->k1]), @@ -179,7 +118,6 @@ class extends Component { public function resetAuth(): void { - $this->issueNostrChallenge(); $this->initChallenge(); } @@ -198,9 +136,8 @@ class extends Component { ?>
+ x-data="{ loginInProgress: false }" + @mobile-login-ready.window="loginInProgress = true; window.location.href = $event.detail.url">
@@ -230,27 +167,11 @@ class extends Component {
@else - {{ __('Anmelden für die App') }} + {{ __('Login mit Lightning ⚡') }} - {{-- Nostr via window.nostr: extension or NIP-46 remote signer - (Amber via bunker/nostrconnect — the window.nostr.js - widget handles pairing and persists the connection). --}} - - {{ __('Log in mit Nostr') }} - - - - -
- {{ __('Login with lightning ⚡') }} -
+ + {{ __('Scanne den Code mit deiner Lightning-Wallet oder öffne sie direkt.') }} + {{-- Poll for the LoginKey row written by the LNURL callback. - Paused while a login round-trip or navigation is in flight. --}} -