whisper-money/tests/Feature
Víctor Falcón 2041181dc2
fix(mcp): serve OAuth authorize on a dedicated host so the PWA can't capture it (#709)
## Problem

Connecting the MCP connector from **ChatGPT on Android** fails. The
installed Whisper Money PWA is a Chrome **WebAPK** that auto-verifies as
an Android **App Link handler** for the whole app origin (manifest
`scope: "/"`), so `https://whisper.money/oauth/authorize` gets routed
**into the app**. Once inside the standalone app, the redirect back to
the OAuth client can't complete → the connection fails. (Claude works
because it opens OAuth in a Custom Tab.)

Confirmed on an Android emulator: the WebAPK shows `AutoVerify=true`,
`whisper.money: verified`. DB evidence: ChatGPT registers + reaches
consent (auth codes issued) but never exchanges a token.

## Why not `handle_links`

`handle_links: "not-preferred"` (tried in #707, reverted in #708) is
**origin-wide** — it would push *every* `whisper.money` link (bank-auth
callback, email verification, shared deep links) to the browser, not
just `/oauth`. We want links to keep opening the installed app.

## Fix (surgical)

Move the OAuth **authorization server** to a dedicated host outside the
PWA scope. `config('mcp.authorization_server')` becomes env-driven
(`MCP_AUTHORIZATION_SERVER`); in prod → `https://oauth.whisper.money`
(DNS already points at the same app).

Every endpoint derives from the request host (no forced root URL), so
pointing the auth server at the subdomain makes `issuer` +
`authorize`/`token`/`register` all resolve to `oauth.whisper.money` —
**same origin as each other**, no cross-origin metadata mismatch. The
protected resource (`/mcp/oauth`) and **all other app links stay on
`whisper.money`**, so deep-linking into the app is fully preserved. Only
the OAuth flow leaves the app — into the browser, where the round-trip
completes.

## Activation (after merge + deploy)

1. Set `MCP_AUTHORIZATION_SERVER=https://oauth.whisper.money` in prod
env, redeploy.
2. I'll curl the discovery chain to confirm it resolves to the
subdomain.
3. Test the ChatGPT connect on a real phone.

Safe until step 1: env unset → `authorization_server` stays `null` →
current behavior. No effect on local/dev.

## Tests

Added a Pest test: with `mcp.authorization_server` configured,
protected-resource metadata advertises the dedicated host and
auth-server metadata (fetched from that host) keeps `issuer` + all
endpoints on it. App has no `TrustHosts` restriction (already serves the
subdomain) and `SESSION_DOMAIN=null` (host-only cookies — subdomain gets
its own session, no security downgrade).
2026-07-21 13:51:03 +02:00
..
Ai
Api
Auth
Commands
Console
Events
Jobs
Listeners
Mail
Mcp
Onboarding
OpenBanking
Services
Settings
Spaces
Sync
AccountBalanceControllerTest.php
AccountControllerTest.php
AccountImportConfigTest.php
AccountUserCurrencyServiceTest.php
AiConsentSettingsTest.php
AiConsentTest.php
AlignAccountsEncryptedFlagMigrationTest.php
ApplyRealEstateRevaluationTest.php
AuthenticatedLayoutSafeAreaTest.php
AutomationRuleApplicationTest.php
AutomationRuleEvaluationTest.php
AutomationRuleTest.php
BackfillAccountIbansCommandTest.php
BackfillXxxAccountCurrenciesTest.php
BalanceLookupTest.php
BudgetHistoricalAssignmentTest.php
BudgetPeriodDateTest.php
BudgetPeriodServiceTest.php
BudgetTest.php
BudgetTransactionServiceTest.php
BulkUpdateTransactionsTest.php
CancelFreeEnableBankingConnectionsCommandTest.php
CashflowAnalyticsTest.php
CashflowPageTest.php
CatchAllBudgetTest.php
CategoryMonthlyBreakdownTest.php
CurrencyConversionServiceTest.php
DashboardAnalyticsTest.php
DashboardTest.php
DecryptTransactionsTest.php
DeleteUserCommandTest.php
DemoAccountRestrictionsTest.php
DisconnectBankingConnectionsCommandTest.php
DiscordWebhookTest.php
EncryptionTest.php
ExampleTest.php
ExchangeRateServiceTest.php
GenerateStripePromotionCodesCommandTest.php
IdorVulnerabilityTest.php
ImportDataTest.php
InertiaSharedDataTest.php
IntegrationRequestTest.php
LabelTest.php
LandingAuthOverrideTest.php
LoanTest.php
LocalizationTest.php
LoggingConfigTest.php
MailSenderTest.php
NewTransactionsMarkerTest.php
PersistUpsellSourceFromStripeTest.php
PlaintextTransactionsTest.php
PlanFeatureTest.php
PopoverSafeAreaTest.php
PostStripeEventToDiscordTest.php
PurgeResidualEncryptionArtifactsJobTest.php
PwaTest.php
QueueConfigTest.php
ReEvaluateTransactionRulesTest.php
RealEstateAvailabilityTest.php
RealEstateTest.php
ResendLeadVerificationEmailsCommandTest.php
ResendSyncCommandTest.php
ResendSyncLeadsCommandTest.php
RouteNotificationForMailTest.php
RuleEngineParityTest.php
SavedFilterTest.php
SelfServeRefundTest.php
SendAiCohortReportCommandTest.php
SendAiConsentFollowUpEmailsCommandTest.php
SendDailyStatsReportCommandTest.php
SendExperimentFunnelReportCommandTest.php
SendStuckCohortReportCommandTest.php
SendSubscriptionFunnelReportCommandTest.php
SentryConfigTest.php
SentryUserMiddlewareTest.php
SetLocaleTest.php
SitemapTest.php
StrayHttpRequestGuardTest.php
StripeSubscriptionStatsCommandTest.php
SubscriptionExperimentTest.php
SubscriptionTest.php
SuggestionPersistenceTest.php
SyncBankingConnectionsCommandTest.php
SyncStripePricesCommandTest.php
TrackLastActiveAtTest.php
TransactionAnalysisTest.php
TransactionFilterTest.php
TransactionSideClassificationTest.php
TransactionTest.php
UserLeadTest.php
WelcomeBanksOrderingTest.php