Authentication
Authentication Guide
Section titled “Authentication Guide”Recommended Setup
Section titled “Recommended Setup”For most users, the one-command setup is the easiest way to get started:
# Claude Code users (authentication + MCP + commands + skills + credential guard)mureo setup claude-code
# Cursor users (authentication + MCP only)mureo setup cursor
# OpenAI Codex CLI users (MCP + credential guard + workflow skills + shared skills)mureo setup codex
# Gemini CLI users (extension manifest + MCP)mureo setup gemini
# CLI-only users (authentication only, terminal prompts)mureo auth setup
# Browser configuration UI — localhost, no terminal input needed# (supersedes the removed `mureo auth setup --web`)mureo configure--skip-auth and non-interactive invocation
Section titled “--skip-auth and non-interactive invocation”Every mureo setup … subcommand accepts --skip-auth, which installs the MCP config, credential guard, and (where supported) command/skill files without running OAuth. Useful for the double-click installer flow where authentication is handled later via /onboard in Claude Code, $onboard in Codex, or mureo auth setup in a real terminal.
When mureo setup … is invoked from an AI agent’s subprocess (Claude Code’s Bash tool, Codex, etc.) that has no controlling TTY, --skip-auth is implied automatically so the command cannot hang on a typer.confirm prompt. A banner in stdout tells the operator to finish authentication in Terminal.app afterwards.
Each subcommand also exposes explicit --google-ads/--no-google-ads and --meta-ads/--no-meta-ads flags, so you can specify exactly which platforms to configure without any prompt. Passing them alongside --skip-auth (or under a non-TTY) emits a warning and is ignored.
mureo configure — browser configuration UI
Section titled “mureo configure — browser configuration UI”
mureo auth setup --webwas removed; its browser credential flow is now part of the unifiedmureo configureUI.
Prefer mureo configure when you were pointed to mureo by an AI agent that cannot safely receive terminal input, or you simply want a GUI. It starts a short-lived HTTP server on a random localhost port, opens your browser at it, and — beyond Google Ads / Meta Ads / GA4 credential entry via HTML forms and standard OAuth redirects (every field deep-linked to the right console), plus an Amazon Ads card in the dashboard’s Plugin credentials section whose Authorize with Amazon flow obtains the LwA tokens (Amazon’s consent has no loopback callback, so it is a guided paste-code flow: mureo opens the consent page and you paste the redirected address back; see amazon-ads.md) — also lets you pick the Claude host, run basic setup (MCP server + credential-guard hook + skills), add the official MCP providers, switch each platform between mureo-native and the official MCP, and scaffold Demo/BYOD. Flags: --no-browser, --timeout-seconds N (idle shutdown, default 600). The same security hardening (CSRF rotation, OAuth state re-validation, DNS-rebinding guard, localhost-pinned redirect verification, generic error surface, POST size cap, CSP) applies — see SECURITY.md.
The auth step reuses what is already on disk: with a saved Google refresh token (Google Ads and Search Console share one) the Google authorization step is skipped and the UI says so, offering a Re-authorize Google button for when you do want to mint a new token.
When something fails in the UI, the on-screen message is deliberately generic (an error surface must not echo token material). The cause is in the configure log — ~/.mureo/logs/configure.log, printed on startup and written on every platform — including the ones that are otherwise invisible: a Meta token refresh that failed or could not be persisted, an account listing that was rejected, a credentials.json that would not parse. Raise the detail with MUREO_LOG_LEVEL=DEBUG mureo configure. No log line at any level carries a token, secret or credential value. See cli.md — Configure log.
How Credentials Work
Section titled “How Credentials Work”mureo loads credentials from ~/.mureo/credentials.json, falling back to environment variables if the file is missing or incomplete.
credentials.json Format
Section titled “credentials.json Format”Create ~/.mureo/credentials.json with the following structure:
{ "google_ads": { "client_id": "YOUR_OAUTH_CLIENT_ID", "client_secret": "YOUR_OAUTH_CLIENT_SECRET", "refresh_token": "YOUR_REFRESH_TOKEN", "login_customer_id": "1234567890", "developer_token": "OPTIONAL_LEGACY_TOKEN" }, "meta_ads": { "access_token": "YOUR_ACCESS_TOKEN", "app_id": "YOUR_APP_ID", "app_secret": "YOUR_APP_SECRET" }}developer_token is optional (legacy): Google stopped issuing developer tokens on 2026-09-09; sent if present, ignored by the API.
You can include only the platforms you use. For example, if you only use Google Ads, the meta_ads section can be omitted.
Google Ads Fields
Section titled “Google Ads Fields”| Field | Required | Description |
|---|---|---|
developer_token | No | Legacy. Google stopped issuing developer tokens on 2026-09-09; sent if present, ignored by the API. |
client_id | Yes | OAuth 2.0 client ID |
client_secret | Yes | OAuth 2.0 client secret |
refresh_token | Yes | OAuth 2.0 refresh token |
login_customer_id | No | Manager account ID (MCC). If omitted, the target customer_id is used as fallback. |
Meta Ads Fields
Section titled “Meta Ads Fields”| Field | Required | Description |
|---|---|---|
access_token | Yes | Meta Graph API access token (User or System User token) |
app_id | No | Meta App ID |
app_secret | No | Meta App Secret |
Environment Variable Fallback
Section titled “Environment Variable Fallback”If ~/.mureo/credentials.json is missing or lacks the required fields, mureo falls back to environment variables.
Google Ads
Section titled “Google Ads”| Variable | Required | Description |
|---|---|---|
GOOGLE_ADS_DEVELOPER_TOKEN | No | Legacy. Google stopped issuing developer tokens on 2026-09-09; sent if present, ignored by the API. |
GOOGLE_ADS_CLIENT_ID | Yes | OAuth 2.0 client ID |
GOOGLE_ADS_CLIENT_SECRET | Yes | OAuth 2.0 client secret |
GOOGLE_ADS_REFRESH_TOKEN | Yes | OAuth 2.0 refresh token |
GOOGLE_ADS_LOGIN_CUSTOMER_ID | No | Manager account (MCC) customer ID |
Meta Ads
Section titled “Meta Ads”| Variable | Required | Description |
|---|---|---|
META_ADS_ACCESS_TOKEN | Yes | Graph API access token |
META_ADS_APP_ID | No | Meta App ID |
META_ADS_APP_SECRET | No | Meta App Secret |
Amazon Ads
Section titled “Amazon Ads”| Variable | Required | Description |
|---|---|---|
AMAZON_ADS_CLIENT_ID | Yes | Login with Amazon (LwA) application client ID |
AMAZON_ADS_REFRESH_TOKEN | Conditional | LwA refresh token — with the client secret, mureo mints and refreshes access tokens for you |
AMAZON_ADS_CLIENT_SECRET | Conditional | LwA application client secret |
AMAZON_ADS_ACCESS_TOKEN | Conditional | LwA access token (expires in ~60 min) |
AMAZON_ADS_REGION | No | na / eu / fe (default na) |
AMAZON_ADS_ACCOUNT_MODE | No | dynamic / fixed (default dynamic) |
AMAZON_ADS_PROFILE_ID | No | Fixed account mode only |
AMAZON_ADS_ACCOUNT_ID | No | Fixed account mode only |
AMAZON_ADS_MANAGER_ACCOUNT_ID | No | Fixed account mode only |
“Conditional” means the client ID plus either AMAZON_ADS_ACCESS_TOKEN or both of AMAZON_ADS_REFRESH_TOKEN and AMAZON_ADS_CLIENT_SECRET. See amazon-ads.md.
Resolution order: credentials.json takes priority. Environment variables are only checked if the corresponding section in credentials.json is missing or incomplete.
Obtaining Google Ads Credentials
Section titled “Obtaining Google Ads Credentials”1. Google Ads API access (Google Cloud Console)
Section titled “1. Google Ads API access (Google Cloud Console)”- Go to the Google Cloud Console and create a new project (or select an existing one).
- Enable the Google Ads API under APIs & Services > Library.
- On the Google Ads API Overview page, sign up for API access. Enabling the API grants Test access (test accounts only) right away; Basic access (production accounts) needs brand verification and an application on the same page, which Google may approve automatically. No Google Ads manager account is required.
- The access level belongs to this project — every OAuth client you create in it inherits the project’s access level.
2. OAuth 2.0 Client ID and Secret
Section titled “2. OAuth 2.0 Client ID and Secret”- In the same project, navigate to APIs & Services > Credentials.
- Click Create Credentials > OAuth client ID.
- Select Desktop app as the application type.
- Copy the Client ID and Client Secret.
3. Refresh Token
Section titled “3. Refresh Token”Use the google-auth-oauthlib library to obtain a refresh token:
from google_auth_oauthlib.flow import InstalledAppFlow
flow = InstalledAppFlow.from_client_config( { "installed": { "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", } }, scopes=["https://www.googleapis.com/auth/adwords"],)flow.run_local_server(port=8080)print("Refresh token:", flow.credentials.refresh_token)Alternatively, use the Google OAuth Playground with the https://www.googleapis.com/auth/adwords scope.
Scope matters. The refresh token must carry the Google Ads scope
https://www.googleapis.com/auth/adwords. Reusing a refresh token minted for a different scope makes Google Ads API calls fail at runtime withACCESS_TOKEN_SCOPE_INSUFFICIENT.mureo configure/mureo auth setuprequest this scope (plus Search Console) automatically — prefer them over hand-minted tokens. Official reference: Google Ads API — OAuth 2.0 scopes.
Existing users (migrating from a developer token)
Section titled “Existing users (migrating from a developer token)”Google migrated the access level of every Cloud project that made API calls with an approved developer token in the 90 days before 2026-09-09. To confirm your setup:
- Open the project’s Google Ads API > Overview page and check the access level shown there.
- Confirm the developers who work on the integration hold owner or editor IAM roles on the project.
- Leave the old
developer_tokenin credentials.json or delete it — mureo no longer needs it.
Obtaining Meta Ads Credentials
Section titled “Obtaining Meta Ads Credentials”Permissions (OAuth scopes)
Section titled “Permissions (OAuth scopes)”mureo configure / mureo auth setup request the following scopes automatically during sign-in — you do not list them by hand. The full set is the source of truth in mureo/auth_setup.py (_META_OAUTH_SCOPES):
| Scope | Enables |
|---|---|
ads_management | Create / edit campaigns, ad sets, ads, budgets, bids |
ads_read | Read ad data and insights |
business_management | Resolve ad accounts reached through a Business Portfolio (a permission warning may appear during sign-in — it is required and safe to accept) |
pages_show_list | List the Facebook Pages you can link |
pages_manage_ads | Manage ads tied to a Page |
pages_read_engagement | Read a Page’s posts and photos (list posts for the Boost Post flow, meta_ads_page_posts_list; list existing Page photos to pick an Instant Form cover from, meta_ads_pages_photos_list) |
leads_retrieval | Retrieve leads from Lead Ads / Instant Forms |
Notes:
public_profileis granted by default on every Facebook Login and does not need to be requested explicitly, so it is not in the list above.- “Page Public Metadata Access” / “Page Public Content Access” are not required — mureo only ever operates on Pages you administer (it resolves a Page Access Token via
/me/accountsand Business-owned Pages), never arbitrary public Pages. pages_manage_postswas requested through v0.14.0 and is not requested any more. It existed to upload a Page photo for an Instant Form cover; the cover is now picked from photos the Page already has (meta_ads_pages_photos_list), which reads withpages_read_engagement+pages_show_list. A token issued earlier still carries the granted permission — nothing in mureo calls it, and dropping it needs no action from you.- After upgrading mureo to a version that adds new scopes, re-run
mureo auth setup(or re-authenticate inmureo configure) so the token is re-issued with the new permissions — an existing token does not gain scopes retroactively. Removing a scope needs no re-auth.
Access Token
Section titled “Access Token”Option A: Graph API Explorer (for testing)
- Go to Meta Graph API Explorer.
- Select your app.
- Click Generate Access Token. For read/write ads,
ads_management+ads_readis the minimum; add thepages_*/leads_retrievalscopes from the table above to exercise Page, Lead Ads, and Instant Form features. - The resulting token is short-lived (1-2 hours).
Option B: Long-Lived Token (for production)
- Obtain a short-lived user token via the Graph API Explorer.
- Exchange it for a long-lived token (60 days):
curl -X POST "https://graph.facebook.com/v26.0/oauth/access_token" \ -d "grant_type=fb_exchange_token" \ -d "client_id=YOUR_APP_ID" \ -d "client_secret=YOUR_APP_SECRET" \ -d "fb_exchange_token=SHORT_LIVED_TOKEN"Use POST, not GET. The Graph
/oauth/access_tokenendpoint accepts these parameters via the request body, keepingclient_secretand the token out of the URL (and out of any request/proxy logs). mureo’s own token exchange posts them as a form body for the same reason.
Option C: System User Token (recommended for automation — and required for Live apps)
A Business Manager system-user token is the most robust Meta credential, and for many operators it is the only one that works end to end:
- Live-mode apps cannot complete OAuth from the localhost configure UI.
Facebook rejects the
http://localhostredirect on its own consent page, so the failure happens before mureo’s callback is ever reached — the browser login simply dead-ends. A system-user token needs no browser redirect. - Dev-mode apps cannot create ad creatives. Uploading the image asset may succeed, but publishing a new creative to Meta requires a Live app — a development-mode app is blocked with error subcode 1885183. A system-user token minted on the Live app clears this.
Generate one (4 steps):
- Business settings → System users → create a system user with the Admin role.
- Assign the ad account (Manage ads) and the Page (Manage content) to that system user.
- Generate a token for your Live app — pick the 60-day expiry if you
want mureo to track and renew it — and select the scopes
ads_management,ads_read,business_management,pages_manage_ads,pages_read_engagement(add the remainingpages_*/leads_retrievalscopes from the table above for Page / Lead Ads / Instant Form features). - Copy the generated token.
Entering the token in the configure UI. In the Meta Ads authentication step
of mureo configure, open “Paste a system-user token” (next to Login with
Facebook), paste the token, click Validate token (this reports the granted
vs missing scopes and lists the ad accounts the token can reach), pick the ad
account, then Save.
Most of these tokens expire. Business settings offers a 60-day expiry
for a new system-user token; a token issued without one does not expire at
all. mureo asks Meta’s Graph debug_token endpoint which of the two it has
and records the answer beside the token — a date as token_expires_at, or
Meta’s “never” verdict as token_never_expires. With a date, the status card
counts the days down and warns you once fewer than 14 remain. With
“never”, it says the token does not expire and leaves it alone: mureo never
exchanges a permanent token, because that would trade it for a 60-day one.
The App ID and App Secret are what make that inspection possible. Meta
only describes a token to the app that issued it: debug_token is a GET
authenticated with an app access token (app_id|app_secret), and a
system-user token cannot inspect itself. So the card’s optional App ID and
App Secret do two jobs:
- read the expiry — without the pair mureo skips the inspection entirely and the card says the expiry is untracked, naming the two fields to fill in. That is not an error, and it never blocks the save: the token is stored and works either way. If Meta refuses the inspection (usually because the pair belongs to a different app than the one that issued the token), the card says that instead;
- renew a 60-day token — a week before it expires, using Meta’s
documented exchange for expiring system-user tokens
(
grant_type=fb_exchange_tokenwithset_token_expires_in_60_days; see Install Apps, Generate, Refresh, and Revoke Tokens). A token issued without an expiry is never renewed, whatever is stored.
Leave the pair blank and nothing breaks — mureo simply cannot show or track the date. Values already stored are kept when you re-paste a token without re-entering them, and they are what the inspection uses on that later paste.
Prefer that card. Saving META_ADS_ACCESS_TOKEN through the Setup tab’s
mureo Credentials (advanced) form also works — a hand-entered token is
stored as entered and stays off the auto-refresh clock (that write clears
token_obtained_at, token_expires_at, token_type and
token_never_expires, because they all describe the token being replaced and
this form makes no Graph call to learn new ones) —
but it writes one field, so it neither validates the token, nor reads its
expiry, nor lets you pick an ad account.
App ID and App Secret
Section titled “App ID and App Secret”- Go to Meta for Developers.
- Navigate to your app > Settings > Basic.
- Copy the App ID and App Secret.
These are optional for basic use, but required for reading a pasted token’s expiry and required for automatic token refresh (see below). Meta only describes a token to the app that issued it, so the pair has to belong to that app.
Meta Ads Token Auto-Refresh
Section titled “Meta Ads Token Auto-Refresh”mureo can automatically refresh Long-Lived Tokens before they expire, so you never have to manually exchange tokens again.
How It Works
Section titled “How It Works”- When
mureo auth setupsaves a Meta Ads token, it records atoken_obtained_atISO 8601 timestamp incredentials.json. The configure UI’s paste card additionally records what Meta’sdebug_tokenreported for that token —token_expires_atwhen it has an expiry,token_never_expires: truewhen it does not. That inspection needs the app ID and secret of the issuing app; without them nothing is recorded and the expiry stays untracked. - Each time Meta Ads credentials are loaded, mureo checks whether the token is due.
- If
token_never_expiresis set, the token is never exchanged — whatever else is stored. Meta reported it as permanent, and swapping it for a 60-day token would be a downgrade. - If
token_expires_atis known, the token is due 7 days before that date. This is the only correct clock for a pasted system-user token: it may have been minted at any point in its 60-day life, so its age says little about how long it has left. - If it is not known, mureo falls back to the token’s age: 53+ days old, a 7-day margin before the ~60-day life of a long-lived user token that mureo minted itself.
- Either way mureo exchanges it for a fresh token via the Meta Graph API.
set_token_expires_in_60_days=trueis added only whentoken_typesaysSYSTEM_USER— Meta documents that parameter for refreshing a system-user token and not for the long-lived user token exchange, and the token’s own kind is the only thing that distinguishes the two. The new token, timestamp and expiry are written back tocredentials.jsonatomically. - This runs on every path that opens a Meta client: the MCP tools and, since #726, the analytics adapters behind
mureo_analytics_runand the report skills.
Requirements
Section titled “Requirements”| Field | Required | Why |
|---|---|---|
app_id | Yes | Needed to inspect the token (debug_token) and for the token exchange API call |
app_secret | Yes | Same — the two form the app access token both calls authenticate with |
token_obtained_at | Auto | Written by mureo auth setup and the paste card; can be added manually (ISO 8601 format) |
token_expires_at | Auto | Written by the paste card from Meta’s debug_token, and by each refresh. When present it replaces the 53-day age rule with “7 days before this date” |
token_type | Auto | Graph’s debug_token verdict, written by the paste card only. SYSTEM_USER selects the system-user refresh; absent or anything else uses the documented user-token exchange |
token_never_expires | Auto | Graph’s other debug_token verdict (expires_at: 0), written by the paste card only. When true, mureo never exchanges the token and never warns about an expiry |
If app_id or app_secret are missing, auto-refresh is silently skipped and the existing token is used as-is.
token_never_expires must be a JSON boolean — mureo logs and ignores any other value, including the string "false", and treats the token as one whose expiry it never established.
If your credentials live outside mureo’s own file — a host that builds MetaAdsCredentials itself through a mureo.runtime_context_factory plugin — that store has to carry token_never_expires through for the “never renewed” guarantee to apply. Without it the field defaults to False, and a permanent token stored alongside an app ID and secret can still be exchanged for a 60-day one. That is the pre-#740 behaviour, never worse than it, but it is not the fix either.
credentials.json with auto-refresh fields
Section titled “credentials.json with auto-refresh fields”{ "meta_ads": { "access_token": "YOUR_ACCESS_TOKEN", "app_id": "YOUR_APP_ID", "app_secret": "YOUR_APP_SECRET", "token_obtained_at": "2025-12-01T00:00:00Z", "token_expires_at": "2026-01-30T00:00:00Z", "token_type": "SYSTEM_USER" }}When the refresh does not happen
Section titled “When the refresh does not happen”Auto-refresh runs on credential load, so a mureo that has not been used for two months, or an exchange Meta keeps rejecting, leaves an aging token on disk. The dashboard’s mureo integrations list watches for exactly that: past 53 days the Meta row shows how old the token is and why that matters, next to a Re-authenticate button that opens the system-user token card in place — no need to re-run the setup wizard.
That age warning only fires for a token mureo could have exchanged: without
app_id / app_secret there is no exchange to have missed, and a token saved
without a token_obtained_at stamp has an age mureo cannot know.
The expiry warning is separate and carries no such condition. Whenever
token_expires_at is on disk, the Meta row shows the days remaining and warns
below 14 — precisely because the install that cannot auto-extend is the one
that most needs the notice. /daily-check reports the same countdown as a
Watch finding, and an already-expired token as Action needed, so it
reaches you in the morning report rather than in a failed run.
Safety Features
Section titled “Safety Features”- Concurrent protection — an
asyncio.Lockprevents multiple simultaneous refresh attempts. - Atomic file write — credentials are written to a temp file first, then renamed, to prevent corruption.
- 0600 permissions — the credentials file is restricted to the owner only.
- Graceful fallback — if the refresh fails for any reason (network error, expired app secret, etc.), mureo continues with the existing token and logs a warning. No tool calls are blocked.
Interactive Setup Wizard
Section titled “Interactive Setup Wizard”mureo auth setup (also called as part of mureo setup claude-code) walks you through authentication interactively:
- Google Ads OAuth — Enter Client ID/Secret (developer token optional), open browser for OAuth, select account.
- Meta Ads OAuth — Enter App ID/Secret, open browser for OAuth, obtain Long-Lived Token, select account.
- MCP configuration — Choose global (
~/.claude/settings.json) or project-level (.mcp.json).
Project-Level MCP Configuration (.mcp.json)
Section titled “Project-Level MCP Configuration (.mcp.json)”If you choose project-level placement, mureo auth setup creates a .mcp.json file in your project root:
{ "mcpServers": { "mureo": { "command": "python", "args": ["-m", "mureo.mcp"] } }}AI agents that support .mcp.json (e.g., Claude Code) will automatically discover and connect to the mureo MCP server when working in that project directory.
Verifying Credentials
Section titled “Verifying Credentials”Use the mureo auth commands to verify your setup:
# Show authentication status for all platformsmureo auth status
# Check Google Ads credentials (shows masked values)mureo auth check-google
# Check Meta Ads credentials (shows masked values)mureo auth check-metaExample output for mureo auth status:
=== Authentication Status ===
Google Ads: AuthenticatedMeta Ads: AuthenticatedExample output for mureo auth check-google:
{ "developer_token": "***************abcd", "client_id": "123456789.apps.googleusercontent.com", "client_secret": "***************wxyz", "refresh_token": "***************efgh", "login_customer_id": "1234567890"}developer_token is null unless a legacy token is stored; a stored one is shown masked, as above.
Secrets are masked, showing only the last 4 characters. This lets you verify the right credentials are loaded without exposing them.