# Cloud destination OAuth — troubleshooting

> **Fixed in 2.3.3 — "The link you followed has expired" on connect.** This was a
> plugin bug, not a console/broker problem: `connect_url()` built the OAuth *return*
> URL with `wp_nonce_url()`, which HTML-escapes `&` to `&amp;`. When the broker
> reflected that URL back, WordPress saw the nonce parameter as `amp;_wpnonce`
> instead of `_wpnonce`, the nonce check failed, and WP showed "The link you
> followed has expired." The return URL is now built with raw ampersands
> (`add_query_arg` + `wp_create_nonce`) so the nonce survives the round-trip. If you
> still see errors after 2.3.3, the console items below are the remaining causes.


Two field reports:

1. **Google Drive → "Access blocked"** when clicking **Connect**.
2. **OneDrive → "This link has expired"** when clicking **Connect**.

Both surface at the provider's consent screen, *before* any token reaches the
site. The AI-SiteArk plugin and the shared OAuth broker
(`connect.wp.aiappstore.in`, source in `ai-mailbridge-broker/`) are wired
correctly — the broker requests the right scopes with
`access_type=offline&prompt=consent` (Google) and `offline_access` on a `common`
tenant (Microsoft). So these are **OAuth app / console configuration** issues on
the Google Cloud and Azure app registrations, not code bugs.

The plugin change that shipped alongside this doc makes the failure **legible**:
the broker relays the provider's own error (e.g. `redirect_uri_mismatch`,
`access_denied`, an `AADSTS…` code) back to Settings → Destinations, where it is
now shown verbatim ("Provider said: …") instead of a generic "please try again".
Read that message first — it names the exact cause below.

---

## The storage OAuth flow (how a connect actually happens)

```
Plugin  ──/calendar/authorize?service=drive|onedrive&site=…&return=…──▶  Broker
Broker  ──redirect to Google/Microsoft consent (scope, redirect_uri, state)──▶ Provider
Provider ──/calendar/callback?code=…&state=…──▶ Broker   (exchanges code→tokens)
Broker  ──return?grant=…──▶  Plugin   (handle_cloud_callback → /calendar/claim)
```

Key facts that pin the two failures:

- The broker's **redirect URI is `https://connect.wp.aiappstore.in/calendar/callback`**
  (`aimb_cal_redirect()`), *not* the mail `/callback`. It must be registered on
  **both** the Google and Azure apps.
- The broker `cal_state_` record has a **15-minute TTL**; the `grant` is
  **single-use, 5-minute** (`ai-mailbridge-broker/index.php`).
- Storage scopes: Google `drive.file openid email profile`; Microsoft
  `offline_access Files.ReadWrite openid email profile`.

---

## 1. Google Drive — "Access blocked"

Google shows "Access blocked" for one of three concrete reasons. The relayed
error text disambiguates:

| Provider error | Root cause | Fix (Google Cloud Console → the AI-Inquiro OAuth client) |
|---|---|---|
| `redirect_uri_mismatch` / "request is invalid" | `/calendar/callback` not on the app | **APIs & Services → Credentials → OAuth client → Authorized redirect URIs** → add `https://connect.wp.aiappstore.in/calendar/callback` → Save (allow a few minutes to propagate). |
| "hasn't completed the Google verification process" / `access_denied` | App is in **Testing** publishing status and the connecting Google account isn't a listed test user | **OAuth consent screen** → either add the account under **Test users**, or **Publish app** to Production. `drive.file` is a *non-sensitive* scope, so production does **not** require Google's security review. |
| "Drive API has not been used…/ is disabled" | Drive API not enabled on the project | **APIs & Services → Library → Google Drive API → Enable.** |

Fastest path: enable the Drive API, add the redirect URI, set the consent screen
to **In production**. Calendar sync (AI-Inquiro) keeps working — same client, its
own redirect URI stays registered.

## 2. OneDrive — "This link has expired"

Microsoft renders "link has expired" (often behind `AADSTS900561` /
`AADSTS9002313` / `AADSTS700016`) when the authorize request it received was
malformed or came from an app it can't match. On a *fresh* click that is almost
always one of:

| Symptom / relayed error | Root cause | Fix (Azure Portal → App registrations → the AI-Inquiro app) |
|---|---|---|
| "link has expired" / `AADSTS9002313` (invalid request) | `/calendar/callback` reply URL not registered | **Authentication → Web → Redirect URIs** → add `https://connect.wp.aiappstore.in/calendar/callback` → Save. |
| `AADSTS50020` / "account from an external identity provider" / can't sign in with a personal Microsoft account | App **Supported account types** excludes consumers, but the user connects a personal OneDrive | Set the registration to **Accounts in any org directory *and personal Microsoft accounts*** (the `common` audience the broker already targets). |
| `AADSTS7000215` / `invalid_client` | Client secret expired | **Certificates & secrets** → new client secret → update `AIMB_MS_CLIENT_SECRET` in the broker config → redeploy broker. |
| Genuinely came back after >15 min idle on the consent page | Broker `cal_state_` TTL lapsed | Just click **Connect** again — this is expected, not a bug. |

Reconnecting once the console is fixed re-mints a refresh token; nothing needs to
be cleared on the site.

---

## Runtime (already-connected) token failures

If a destination was connected but later a scheduled upload fails, the adapter
now reports **"…authorization has expired or was revoked. Please reconnect it in
Settings → Destinations."** (instead of a misleading "not connected"). Causes:
the user revoked access in their Google/Microsoft account, the refresh token
aged out (Google test-mode refresh tokens expire after 7 days — another reason to
**publish** the Google app), or the client secret rotated. Fix: **Disconnect**
then **Connect** again on the Destinations tab.

## Broker health check

```bash
curl -s https://connect.wp.aiappstore.in/health
# → {"ok":true,"service":"ai-mailbridge-broker","version":"1.2.0","supported":["microsoft","google","dropbox"]}
```

If that fails, the broker itself is down — fix that before chasing console config.
