Skip to content

Re-open Meta's page selection on every connect with auth_type=rerequest - #314

Merged
paulocastellano merged 8 commits into
trypostit:mainfrom
gynsus:pr/meta-rerequest
Sep 8, 2026
Merged

Re-open Meta's page selection on every connect with auth_type=rerequest#314
paulocastellano merged 8 commits into
trypostit:mainfrom
gynsus:pr/meta-rerequest

Conversation

@gynsus

@gynsus gynsus commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

The trap

Create a new Facebook Page, hit "Connect Facebook" — and TryPost silently connects the old Page again. No page picker, no way to reach the new Page from inside the app.

Meta's OAuth dialog reuses the previous grant, including its page selection: once the app has been authorized for one Page, subsequent logins skip the consent screen entirely and /me/accounts keeps returning only the originally ticked Pages. Disconnecting in TryPost doesn't help, because the grant lives on Meta's side. The only escape is manual surgery in facebook.com → Settings → Business Integrations, which users find only after filing a support ticket (ours did).

The fix

Pass auth_type=rerequest on the OAuth redirect for both Meta flows (Facebook and Instagram-via-Facebook). Meta then re-opens the consent dialog on every connect, with the "Edit settings" page selection available, so adding a newly created Page is just: connect again → tick the Page.

The cost is one extra consent screen for users reconnecting an unchanged setup — which is also the screen that tells them what they previously granted, so it's arguably a feature.

Testing

FacebookControllerTest / InstagramFacebookControllerTest pass, with the connect-redirect mock asserting the new parameter. Verified live: with rerequest in place, a freshly created Page shows up in the page selection dialog on the very next connect.

gynsus and others added 8 commits August 30, 2026 16:15
FacebookProvider already exposes reRequest(), which sets exactly the
auth_type=rerequest field the connect flows need. Calling it directly
replaces with(['auth_type' => 'rerequest']) and drops the comment the
raw parameter needed, since the method name carries the intent.

It also avoids a footgun: AbstractProvider::with() assigns $parameters
rather than merging into them, so a second with() anywhere in the chain
would have silently dropped auth_type.

Cover InstagramFacebookController@connect, which had no test at all, so
both halves of the fix are verified rather than just the Facebook one.
TikTok skips the authorization page whenever the browser holds a valid
session and the app was authorized before, handing back a code for
whoever happens to be logged in. A user who wants to connect a different
account never reaches the account chooser; the only way out is logging
out of TikTok in the browser first. Same class of trap as Meta reusing
its page selection, different mechanism.

Login Kit documents disable_auto_auth for this: 0 skips the page for
valid sessions, 1 always shows it.

redirectToProvider() had no way to pass extra authorize-URL parameters,
so it takes an optional array now. It only calls with() when that array
is non-empty, leaving the Discord, Pinterest and X redirects byte for
byte as they were.
…hod in SocialController. This change cleans up the code by eliminating unnecessary comments that no longer apply to the method's implementation.
with() assigns $parameters rather than merging, and nothing else ever
populates them, so with([]) is a no-op and the guard around it bought
nothing. Dropping it restores the fluent chain the method had before.

The Discord, Pinterest and X connect tests mock the driver whole, so
they need the call stubbed even though the array stays empty.
prompt=consent reopens the consent screen but never the account chooser,
so a browser holding one Google session authorizes that account every
time. channels?mine=true is scoped to whichever identity minted the
token, so a Brand Account channel under a different Google account is
absent from the response rather than merely unpicked, and the callback
connects channels[0] regardless.

Google documents prompt as a space-delimited list, and select_account
and consent are distinct values, so the two compose.

The connect test stubbed with() without constraining its argument, which
left every redirect parameter unasserted; it now pins the whole array.
Instagram reuses the browser session on a repeat connect, so a user
logged in as one account cannot reach another; the callback answers
wrong_account with no way forward. Business Login documents force_reauth
for exactly this, against www.instagram.com/oauth/authorize, which is
the host this provider already targets.

The parameter goes in the provider rather than through with(), because
getAuthUrl() builds the query by hand and never merges $parameters.

The new test drives the real provider through the connect route instead
of mocking Socialite, since the URL is assembled below the controller.
@paulocastellano
paulocastellano merged commit 0a6cd9a into trypostit:main Sep 8, 2026
10 checks passed
paulocastellano added a commit that referenced this pull request Sep 8, 2026
The mocked connect tests prove reRequest() and with() were called, not
that anything lands in the URL Socialite builds. If a Socialite upgrade
changed what reRequest() emits, the mocks would stay green and the bug
those parameters fix would come back unnoticed.

These drive the real provider through the connect route instead, and
assert on the parameter rather than the whole URL, so a version bump in
the vendor's authorize host does not break them.

Verified by removing each parameter in turn and confirming its test
fails: auth_type=rerequest (Facebook, Instagram-Facebook),
disable_auto_auth=1 (TikTok), prompt=select_account consent (YouTube).
Instagram already had one from #314.
paulocastellano added a commit that referenced this pull request Sep 8, 2026
…#345)

* Make the social connect tests assert where the redirect actually goes

Every connect test sent X-Inertia: true and asserted a 409. That status
was never evidence of anything: the request carries no X-Inertia-Version,
so Inertia treats it as a version mismatch and answers 409 with
X-Inertia-Location pointing back at the same URL. Any error inside the
controller produced the same 409, and the assertion passed regardless.

Proof of how empty this was: dropping a throw into redirectToProvider,
right after the session is remembered, left every one of these tests
green.

Without the header, Inertia::location returns a plain redirect, so the
destination is assertable. Socialite-mocked flows use assertRedirect
against the mocked target; Threads and Mastodon build their URL by hand,
so those assert the authorize endpoint they are supposed to reach.

Verified by breaking each connect path in turn and confirming the tests
that cover it fail: redirectToProvider (Discord, Pinterest, TikTok, X),
the seven controllers with their own connect method, and Mastodon's
authorize action, which the connect probe does not reach.

No production code changes.

* Assert the connect parameters reach the authorize URL

The mocked connect tests prove reRequest() and with() were called, not
that anything lands in the URL Socialite builds. If a Socialite upgrade
changed what reRequest() emits, the mocks would stay green and the bug
those parameters fix would come back unnoticed.

These drive the real provider through the connect route instead, and
assert on the parameter rather than the whole URL, so a version bump in
the vendor's authorize host does not break them.

Verified by removing each parameter in turn and confirming its test
fails: auth_type=rerequest (Facebook, Instagram-Facebook),
disable_auto_auth=1 (TikTok), prompt=select_account consent (YouTube).
Instagram already had one from #314.

* Cover the remaining connect flows against the real authorize URL

Discord, Pinterest, X and LinkedIn were still tested only through a
Socialite mock, which compares the fixture against itself and says
nothing about the URL the app builds.

That the fixture drifts is not hypothetical: the X mock returns
https://twitter.com/i/oauth2/authorize while the provider has been
emitting https://x.com/i/oauth2/authorize, and no test noticed.

Each of these pins what the flow depends on: Discord's bot scope and
permission bitfield, plus the absence of disable_guild_select, which
would silence the server picker the same way Meta used to silence the
page picker; X's PKCE challenge and offline.access, without which there
is no refresh token; the Pinterest and LinkedIn scopes that publishing
needs.

Verified by breaking each one and confirming its test fails: the bot
scope in DiscordProvider, offline.access in XController, pins:write in
PinterestController, and the organization scopes in the LinkedIn config.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants