mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-17 04:30:31 +00:00
Render the handoff page directly instead of 302-redirecting to /app/auth
Chrome follows a server 302 internally and never dispatches the /app/auth App Link, so the handoff page stayed in the browser and the token never reached the app. The signed callback (and complete/confirm) now render the handoff page directly with the einundzwanzig:// deep-link button — the signer opens the callback in the browser, the user lands on the handoff page and taps once to return to the app, which stores the token.
This commit is contained in:
@@ -9,8 +9,8 @@ use App\Models\LoginKey;
|
||||
use App\Models\User;
|
||||
use App\Support\NostrLogin;
|
||||
use Dedoc\Scramble\Attributes\ExcludeRouteFromDocs;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -235,16 +235,15 @@ final class MobileAuthController extends Controller
|
||||
'ip' => $request->ip(),
|
||||
]);
|
||||
|
||||
return redirect()->to($this->issueToken($user, $request));
|
||||
return $this->handoffResponse($user, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a mobile login after the wallet/signer callback has stored a
|
||||
* matching LoginKey row. Called as a full-page GET from the mobile login
|
||||
* component once wire:poll detects readiness. Issues the token and
|
||||
* redirects into the app via the deep link.
|
||||
* component once wire:poll detects readiness (Lightning flow).
|
||||
*/
|
||||
public function complete(Request $request, string $k1): RedirectResponse
|
||||
public function complete(Request $request, string $k1): mixed
|
||||
{
|
||||
$loginKey = LoginKey::query()
|
||||
->where('k1', $k1)
|
||||
@@ -261,28 +260,36 @@ final class MobileAuthController extends Controller
|
||||
return redirect()->route('auth.mobile');
|
||||
}
|
||||
|
||||
return $this->issueTokenAndRedirect($user, $request);
|
||||
return $this->handoffResponse($user, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the app for a user who is already authenticated in the
|
||||
* in-app browser session (confirmation button on /auth/mobile).
|
||||
*/
|
||||
public function confirm(Request $request): RedirectResponse
|
||||
public function confirm(Request $request): mixed
|
||||
{
|
||||
return $this->issueTokenAndRedirect($request->user(), $request);
|
||||
return $this->handoffResponse($request->user(), $request);
|
||||
}
|
||||
|
||||
private function issueTokenAndRedirect(User $user, Request $request): RedirectResponse
|
||||
/**
|
||||
* Issue the token and render the handoff page, whose "back to app"
|
||||
* button carries the einundzwanzig:// deep link. The page is rendered
|
||||
* directly (rather than 302-redirecting to the /app/auth App Link)
|
||||
* because Chrome follows a server redirect internally and never
|
||||
* dispatches the App Link intent — the signed callback opens in the
|
||||
* browser, so the user lands here and taps once to return to the app.
|
||||
*/
|
||||
private function handoffResponse(User $user, Request $request): View
|
||||
{
|
||||
return redirect()->away($this->issueToken($user, $request));
|
||||
return view('auth.app-handoff', [
|
||||
'deepLink' => $this->issueToken($user, $request),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a personal access token for the session's device and build the
|
||||
* app handoff URL. The handoff URL is a verified Android App Link, so
|
||||
* navigating to it opens the app directly; its browser fallback page
|
||||
* offers the einundzwanzig:// deep link behind a button.
|
||||
* einundzwanzig:// deep link that hands it to the app.
|
||||
*/
|
||||
private function issueToken(User $user, Request $request): string
|
||||
{
|
||||
@@ -292,7 +299,7 @@ final class MobileAuthController extends Controller
|
||||
|
||||
$token = $this->createDeviceToken($user, $deviceName);
|
||||
|
||||
return route('auth.mobile.handoff').'?token='.urlencode($token->plainTextToken);
|
||||
return self::ALLOWED_REDIRECT_URIS[0].'?token='.urlencode($token->plainTextToken);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -86,7 +86,7 @@ it('rejects a nostr signer callback whose event is bound to a different challeng
|
||||
expect(LoginKey::query()->where('k1', $k1)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('completes the login via the path-based signer callback and redirects to the app handoff', function () {
|
||||
it('completes the login via the path-based signer callback and renders the app handoff', function () {
|
||||
Queue::fake();
|
||||
|
||||
$k1 = bin2hex(random_bytes(32));
|
||||
@@ -98,8 +98,9 @@ it('completes the login via the path-based signer callback and redirects to the
|
||||
->withSession(['mobile_auth' => ['redirect_uri' => 'einundzwanzig://auth', 'device_name' => 'Pixel 10']])
|
||||
->get('/auth/mobile/signed/'.$k1.'/'.rawurlencode(json_encode($signedEvent)));
|
||||
|
||||
$location = $response->headers->get('Location');
|
||||
expect($location)->toStartWith(route('auth.mobile.handoff').'?token=');
|
||||
// The signed callback renders the handoff page directly (no 302) so the
|
||||
// user can tap the einundzwanzig:// deep-link button to return to the app.
|
||||
$response->assertOk()->assertSee('einundzwanzig://auth?token=', false);
|
||||
|
||||
$user = User::query()->where('nostr', $npub)->first();
|
||||
expect($user)->not->toBeNull();
|
||||
@@ -111,10 +112,11 @@ it('completes the login via the path-based signer callback and redirects to the
|
||||
expect(LoginKey::query()->where('k1', $k1)->exists())->toBeTrue();
|
||||
Queue::assertPushed(FetchNostrProfileJob::class);
|
||||
|
||||
// The handoff fallback page offers the deep link behind a button.
|
||||
$this->get($location)
|
||||
// The deep-link token must authenticate API requests.
|
||||
$plainTextToken = urldecode(str($response->getContent())->after('einundzwanzig://auth?token=')->before('"')->value());
|
||||
$this->getJson('/api/user', ['Authorization' => 'Bearer '.$plainTextToken])
|
||||
->assertOk()
|
||||
->assertSee('einundzwanzig://auth?token=', false);
|
||||
->assertJsonPath('id', $user->id);
|
||||
});
|
||||
|
||||
it('exchanges a signed event for a token via the mobile token endpoint', function () {
|
||||
@@ -160,7 +162,7 @@ it('rejects a path-based signer callback with a tampered challenge', function ()
|
||||
expect(LoginKey::query()->where('k1', $k1)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('issues a token and redirects into the app when completing a verified login', function () {
|
||||
it('issues a token and renders the handoff when completing a verified login', function () {
|
||||
$user = User::factory()->create();
|
||||
$k1 = bin2hex(random_bytes(32));
|
||||
LoginKey::query()->create(['k1' => $k1, 'user_id' => $user->id]);
|
||||
@@ -169,15 +171,14 @@ it('issues a token and redirects into the app when completing a verified login',
|
||||
->withSession(['mobile_auth' => ['redirect_uri' => 'einundzwanzig://auth', 'device_name' => 'Pixel 10']])
|
||||
->get('/auth/mobile/complete/'.$k1);
|
||||
|
||||
$location = $response->headers->get('Location');
|
||||
expect($location)->toStartWith(route('auth.mobile.handoff').'?token=');
|
||||
$response->assertOk()->assertSee('einundzwanzig://auth?token=', false);
|
||||
|
||||
$token = $user->tokens()->first();
|
||||
expect($token)->not->toBeNull()
|
||||
->and($token->name)->toBe('Pixel 10');
|
||||
|
||||
// The plain-text token handed to the app must authenticate API requests.
|
||||
$plainTextToken = urldecode(str($location)->after('?token=')->value());
|
||||
$plainTextToken = urldecode(str($response->getContent())->after('einundzwanzig://auth?token=')->before('"')->value());
|
||||
$this->getJson('/api/user', ['Authorization' => 'Bearer '.$plainTextToken])
|
||||
->assertOk()
|
||||
->assertJsonPath('id', $user->id);
|
||||
@@ -197,7 +198,7 @@ it('lets an already authenticated user connect the app and replaces tokens of th
|
||||
->withSession(['mobile_auth' => ['redirect_uri' => 'einundzwanzig://auth', 'device_name' => 'Pixel 10']])
|
||||
->post('/auth/mobile/confirm');
|
||||
|
||||
expect($response->headers->get('Location'))->toStartWith(route('auth.mobile.handoff').'?token=');
|
||||
$response->assertOk()->assertSee('einundzwanzig://auth?token=', false);
|
||||
expect($user->tokens()->where('name', 'Pixel 10')->count())->toBe(1);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user