@@ -202,7 +202,7 @@ func TestOAuthCallbackAutoSelectsSingleInstallTargetAndUpsertsRegistry(t *testin
202202 }
203203}
204204
205- func TestOAuthCallbackRendersInstallTargetPickerWhenMultipleTargetsAvailable (t * testing.T ) {
205+ func TestOAuthCallbackRedirectsToReactInstallTargetPickerWhenSameOrigin (t * testing.T ) {
206206 backend := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
207207 switch r .URL .Path {
208208 case "/internal/v2/spritz/channel-install-targets/list" :
@@ -262,7 +262,7 @@ func TestOAuthCallbackRendersInstallTargetPickerWhenMultipleTargetsAvailable(t *
262262 SlackBotScopes : []string {"chat:write" },
263263 BackendBaseURL : backend .URL ,
264264 BackendInternalToken : "backend-internal-token" ,
265- SpritzBaseURL : "https://spritz .example.test" ,
265+ SpritzBaseURL : "https://gateway .example.test" ,
266266 SpritzServiceToken : "spritz-service-token" ,
267267 PrincipalID : "shared-slack-gateway" ,
268268 HTTPTimeout : 5 * time .Second ,
@@ -284,7 +284,7 @@ func TestOAuthCallbackRendersInstallTargetPickerWhenMultipleTargetsAvailable(t *
284284 if err != nil {
285285 t .Fatalf ("parse picker redirect: %v" , err )
286286 }
287- if redirectURL .Scheme != "https" || redirectURL .Host != "spritz .example.test" {
287+ if redirectURL .Scheme != "https" || redirectURL .Host != "gateway .example.test" {
288288 t .Fatalf ("expected picker redirect to use Spritz host, got %s" , redirectURL .String ())
289289 }
290290 if redirectURL .Path != "/settings/slack/install/select" {
@@ -341,6 +341,104 @@ func TestOAuthCallbackRendersInstallTargetPickerWhenMultipleTargetsAvailable(t *
341341 }
342342}
343343
344+ func TestOAuthCallbackRendersGatewayInstallTargetPickerWhenCrossOrigin (t * testing.T ) {
345+ backend := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
346+ switch r .URL .Path {
347+ case "/internal/v2/spritz/channel-install-targets/list" :
348+ writeJSON (w , http .StatusOK , map [string ]any {
349+ "status" : "resolved" ,
350+ "targets" : []map [string ]any {
351+ {
352+ "id" : "ag_123" ,
353+ "profile" : map [string ]any {
354+ "name" : "Personal Helper" ,
355+ },
356+ "ownerLabel" : "Personal" ,
357+ "presetInputs" : map [string ]any {
358+ "agentId" : "ag_123" ,
359+ },
360+ },
361+ {
362+ "id" : "ag_456" ,
363+ "profile" : map [string ]any {
364+ "name" : "Workspace Helper" ,
365+ },
366+ "ownerLabel" : "Workspace" ,
367+ "presetInputs" : map [string ]any {
368+ "agentId" : "ag_456" ,
369+ },
370+ },
371+ },
372+ })
373+ case "/internal/v1/spritz/channel-installations/upsert" :
374+ t .Fatal ("upsert should not happen before the picker selection" )
375+ default :
376+ t .Fatalf ("unexpected backend path %s" , r .URL .Path )
377+ }
378+ }))
379+ defer backend .Close ()
380+
381+ slackAPI := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
382+ writeJSON (w , http .StatusOK , map [string ]any {
383+ "ok" : true ,
384+ "app_id" : "A_app_1" ,
385+ "scope" : "chat:write" ,
386+ "access_token" : "xoxb-installed" ,
387+ "bot_user_id" : "U_bot" ,
388+ "team" : map [string ]any {"id" : "T_workspace_1" },
389+ "authed_user" : map [string ]any {"id" : "U_installer" },
390+ })
391+ }))
392+ defer slackAPI .Close ()
393+
394+ gateway := newSlackGateway (config {
395+ PublicURL : "https://gateway.example.test" ,
396+ SlackClientID : "client-id" ,
397+ SlackClientSecret : "client-secret" ,
398+ SlackSigningSecret : "signing-secret" ,
399+ OAuthStateSecret : "oauth-state-secret" ,
400+ SlackAPIBaseURL : slackAPI .URL ,
401+ SlackBotScopes : []string {"chat:write" },
402+ BackendBaseURL : backend .URL ,
403+ BackendInternalToken : "backend-internal-token" ,
404+ SpritzBaseURL : "https://spritz.example.test" ,
405+ SpritzServiceToken : "spritz-service-token" ,
406+ PrincipalID : "shared-slack-gateway" ,
407+ HTTPTimeout : 5 * time .Second ,
408+ DedupeTTL : time .Minute ,
409+ }, slog .New (slog .NewTextHandler (io .Discard , nil )))
410+ state , err := gateway .state .generate ()
411+ if err != nil {
412+ t .Fatalf ("state generate failed: %v" , err )
413+ }
414+
415+ req := httptest .NewRequest (http .MethodGet , "/slack/oauth/callback?code=test-code&state=" + url .QueryEscape (state ), nil )
416+ rec := httptest .NewRecorder ()
417+ gateway .handleOAuthCallback (rec , req )
418+
419+ if rec .Code != http .StatusOK {
420+ t .Fatalf ("expected gateway picker page, got %d: %s" , rec .Code , rec .Body .String ())
421+ }
422+ body := rec .Body .String ()
423+ if ! strings .Contains (body , "Choose an install target" ) {
424+ t .Fatalf ("expected picker title, got %q" , body )
425+ }
426+ if ! strings .Contains (body , "Personal Helper" ) || ! strings .Contains (body , "Workspace Helper" ) {
427+ t .Fatalf ("expected picker targets, got %q" , body )
428+ }
429+ if ! strings .Contains (body , `/slack/install/select` ) {
430+ t .Fatalf ("expected picker form action, got %q" , body )
431+ }
432+ if strings .Contains (body , "xoxb-installed" ) {
433+ t .Fatalf ("expected picker state to keep bot token encrypted, got %q" , body )
434+ }
435+ for _ , cookie := range rec .Result ().Cookies () {
436+ if strings .HasPrefix (cookie .Name , pendingInstallCookieName ) {
437+ t .Fatalf ("expected cross-origin picker to avoid pending install cookie, got %#v" , cookie )
438+ }
439+ }
440+ }
441+
344442func TestInstallTargetSelectionAPIUsesRequestScopedPendingCookie (t * testing.T ) {
345443 backend := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
346444 if r .URL .Path != "/internal/v2/spritz/channel-install-targets/list" {
@@ -7628,6 +7726,30 @@ func TestReactRouteURLUsesSpritzBaseURL(t *testing.T) {
76287726 }
76297727}
76307728
7729+ func TestReactRoutesShareGatewayOrigin (t * testing.T ) {
7730+ sameOrigin := newSlackGateway (
7731+ config {
7732+ PublicURL : "https://spritz.example.test/slack-gateway" ,
7733+ SpritzBaseURL : "https://spritz.example.test" ,
7734+ },
7735+ slog .New (slog .NewTextHandler (io .Discard , nil )),
7736+ )
7737+ if ! sameOrigin .reactRoutesShareGatewayOrigin () {
7738+ t .Fatal ("expected same host and scheme to share gateway origin" )
7739+ }
7740+
7741+ crossOrigin := newSlackGateway (
7742+ config {
7743+ PublicURL : "https://gateway.example.test" ,
7744+ SpritzBaseURL : "https://spritz.example.test" ,
7745+ },
7746+ slog .New (slog .NewTextHandler (io .Discard , nil )),
7747+ )
7748+ if crossOrigin .reactRoutesShareGatewayOrigin () {
7749+ t .Fatal ("expected different hosts not to share gateway origin" )
7750+ }
7751+ }
7752+
76317753func signSlackRequest (header http.Header , signingSecret string , body []byte , now time.Time ) {
76327754 timestamp := fmt .Sprintf ("%d" , now .Unix ())
76337755 base := "v0:" + timestamp + ":" + string (body )
0 commit comments