From 736ebfed806b1429514f8dc7c0be067ad56e3ab8 Mon Sep 17 00:00:00 2001 From: Chris Messina Date: Thu, 22 Oct 2026 14:43:19 -0700 Subject: [PATCH] feat: allow Raycast's OAuth redirect for device sign-in Add raycast://oauth?package_name=Extension, the redirect Raycast's OAuth.PKCEClient uses (RedirectMethod.App), to the authorize page's allowed redirect prefixes, so the /dev/null Raycast extension can approve its SSH key as a device the way the app does. Co-Authored-By: Claude Opus 5.5 --- config/mobile.php | 5 +++-- .../MobileAuthorizationControllerTest.php | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/config/mobile.php b/config/mobile.php index c0a953c58..59b5579a0 100644 --- a/config/mobile.php +++ b/config/mobile.php @@ -6,8 +6,9 @@ /* * Where the authorize page may send the devnull app back to with its code. A redirect_uri has to start with one of - * these, so a code never leaves for anywhere but the app's own URL scheme. + * these, so a code never leaves for anywhere but the app's own URL scheme, or Raycast's, for the /dev/null Raycast + * extension (OAuth.RedirectMethod.App). A code is only good with the key and verifier that asked for it. */ - 'redirect_uri_prefixes' => ['devnull://'], + 'redirect_uri_prefixes' => ['devnull://', 'raycast://oauth?package_name=Extension'], ]; diff --git a/tests/Feature/Controllers/MobileAuthorizationControllerTest.php b/tests/Feature/Controllers/MobileAuthorizationControllerTest.php index 2fc04f408..348db563e 100644 --- a/tests/Feature/Controllers/MobileAuthorizationControllerTest.php +++ b/tests/Feature/Controllers/MobileAuthorizationControllerTest.php @@ -89,8 +89,8 @@ 'a plain challenge' => [['code_challenge_method' => 'plain'], 'The selected code challenge method is invalid.'], 'no state' => [['state' => null], 'The state field is required.'], 'no redirect' => [['redirect_uri' => null], 'The redirect uri field is required.'], - 'a redirect to a website' => [['redirect_uri' => 'https://evil.example/callback'], 'The redirect uri field must start with one of the following: devnull://.'], - 'a redirect to another app' => [['redirect_uri' => 'otherapp://auth/callback'], 'The redirect uri field must start with one of the following: devnull://.'], + 'a redirect to a website' => [['redirect_uri' => 'https://evil.example/callback'], 'The redirect uri field must start with one of the following: devnull://, raycast://oauth?package_name=Extension.'], + 'a redirect to another app' => [['redirect_uri' => 'otherapp://auth/callback'], 'The redirect uri field must start with one of the following: devnull://, raycast://oauth?package_name=Extension.'], 'a redirect with a fragment' => [['redirect_uri' => 'devnull://auth/callback#x'], 'The redirect uri field format is invalid.'], ]); @@ -124,12 +124,24 @@ ->toEndWith('&state=af0ifjsldkj'); }); +it('sends the code back to the Raycast extension that asked for it', function (): void { + $response = $this->actingAs(User::factory()->create()) + ->post(route('mobile-authorization.store'), [ + ...$this->parameters, + 'device_name' => 'Raycast on MacBook', + 'redirect_uri' => 'raycast://oauth?package_name=Extension', + ]); + + expect((string) $response->headers->get('Location'))->toStartWith('raycast://oauth?package_name=Extension&code=') + ->toEndWith('&state=af0ifjsldkj'); +}); + it('approves nothing the app did not ask for right', function (): void { $response = $this->actingAs(User::factory()->create()) ->post(route('mobile-authorization.store'), [...$this->parameters, 'redirect_uri' => 'https://evil.example/callback']); $response->assertBadRequest() - ->assertInertia(fn (Assert $page): Assert => $page->where('error', 'The redirect uri field must start with one of the following: devnull://.')); + ->assertInertia(fn (Assert $page): Assert => $page->where('error', 'The redirect uri field must start with one of the following: devnull://, raycast://oauth?package_name=Extension.')); expect($this->codesHandedOut)->toBe(0); });