diff --git a/app/credentials.py b/app/credentials.py index e0cc966..78a3a60 100644 --- a/app/credentials.py +++ b/app/credentials.py @@ -14,7 +14,11 @@ service account); they differ only in the OAuth *scope* and the header prefix: * Google Ads -> ``X-GAds-*`` (scope adwords) + developer token Each provides ``*-Access-Token`` (ready Bearer token, takes precedence) and -``*-Credentials`` (base64 service-account JSON; the proxy mints a token). +``*-Credentials`` (base64 service-account JSON; the proxy mints a token). As an +alternative, the ready access token may also be supplied via the standard +``Authorization: Bearer `` header (the service-specific ``*-Access-Token`` +wins if both are present); this keeps the original header path intact while +offering the conventional bearer-token path. Sklik uses ``X-Sklik-Token`` (the proxy calls client.loginByToken). """ @@ -47,6 +51,29 @@ class GoogleAdsCredentials: login_customer_id: str | None +def _bearer_from_authorization(authorization: str | None) -> str | None: + """Extract the token from a standard ``Authorization: Bearer `` header. + + Only the ``Bearer`` scheme is accepted; any other scheme (e.g. ``Basic``) is + ignored so the caller falls through to the other credential sources and gets + a clear "no credentials" error rather than a token that cannot work. + """ + if not authorization: + return None + parts = authorization.strip().split(None, 1) + if len(parts) == 2 and parts[0].lower() == "bearer": + return parts[1].strip() or None + return None + + +def _authorization_description(token_header: str) -> str: + return ( + "Standard OAuth2 bearer token, sent as 'Authorization: Bearer '. " + f"Alternative to {token_header} (which wins if both are present) and to " + "the service-account credentials header. Must carry the service's scope." + ) + + def _build_google_credentials( access_token: str | None, raw_credentials: str | None, @@ -55,9 +82,17 @@ def _build_google_credentials( *, token_header: str, creds_header: str, + authorization: str | None = None, ) -> GoogleCredentials: - """Parse a Google access token / base64 service-account JSON from headers.""" + """Parse a Google access token / base64 service-account JSON from headers. + + The ready access token may arrive either in the service-specific + ``*-Access-Token`` header (takes precedence, kept for backward + compatibility) or in a standard ``Authorization: Bearer `` header. + """ token = (access_token or "").strip() or None + if token is None: + token = _bearer_from_authorization(authorization) service_account_info: dict | None = None raw = (raw_credentials or "").strip() @@ -81,7 +116,8 @@ def _build_google_credentials( if not token and service_account_info is None: raise MissingCredentialsError( - f"Provide either {token_header} or {creds_header}." + f"Provide either {token_header}, an 'Authorization: Bearer ' " + f"header, or {creds_header}." ) return GoogleCredentials( @@ -98,7 +134,12 @@ def get_ga_credentials( default=None, alias="X-GA-Access-Token", description="Ready OAuth2 access token used directly as a Bearer token. " - "Takes precedence over X-GA-Credentials.", + "Takes precedence over Authorization and X-GA-Credentials.", + ), + authorization: str | None = Header( + default=None, + alias="Authorization", + description=_authorization_description("X-GA-Access-Token"), ), x_ga_credentials: str | None = Header( default=None, @@ -121,6 +162,7 @@ def get_ga_credentials( config.GA_SCOPE, token_header="X-GA-Access-Token", creds_header="X-GA-Credentials", + authorization=authorization, ) @@ -130,7 +172,12 @@ def get_gsc_credentials( default=None, alias="X-GSC-Access-Token", description="Ready OAuth2 access token used directly as a Bearer token. " - "Takes precedence over X-GSC-Credentials.", + "Takes precedence over Authorization and X-GSC-Credentials.", + ), + authorization: str | None = Header( + default=None, + alias="Authorization", + description=_authorization_description("X-GSC-Access-Token"), ), x_gsc_credentials: str | None = Header( default=None, @@ -152,6 +199,7 @@ def get_gsc_credentials( config.GSC_SCOPE, token_header="X-GSC-Access-Token", creds_header="X-GSC-Credentials", + authorization=authorization, ) @@ -167,7 +215,12 @@ def get_google_ads_credentials( default=None, alias="X-GAds-Access-Token", description="Ready OAuth2 access token used directly as a Bearer token. " - "Takes precedence over X-GAds-Credentials.", + "Takes precedence over Authorization and X-GAds-Credentials.", + ), + authorization: str | None = Header( + default=None, + alias="Authorization", + description=_authorization_description("X-GAds-Access-Token"), ), x_gads_credentials: str | None = Header( default=None, @@ -199,6 +252,7 @@ def get_google_ads_credentials( config.GOOGLE_ADS_SCOPE, token_header="X-GAds-Access-Token", creds_header="X-GAds-Credentials", + authorization=authorization, ) login_customer_id = (x_gads_login_customer_id or "").strip().replace("-", "") or None return GoogleAdsCredentials( diff --git a/app/main.py b/app/main.py index 05981c5..5190649 100644 --- a/app/main.py +++ b/app/main.py @@ -45,6 +45,12 @@ jen prefixem hlavičky a oprávněním (scope). **Jeden service account lze pou pro všechny tři** (stačí mu udělit přístup v dané službě a povolit příslušné API). U Google Ads navíc vždy potřebujete *developer token*. +> 💡 Hotový OAuth2 *access token* lze u všech tří Google služeb poslat dvěma +> způsoby: buď v původní hlavičce `X-…-Access-Token`, **nebo** ve standardní +> hlavičce `Authorization: Bearer `. Obě cesty jsou rovnocenné; pokud +> pošlete obě, vyhrává `X-…-Access-Token`. Pořadí priority je +> `X-…-Access-Token` → `Authorization: Bearer` → `X-…-Credentials`. + --- ## Kde vzít přihlašovací údaje @@ -65,9 +71,10 @@ API). U Google Ads navíc vždy potřebujete *developer token*. Service account (jeho `client_email` z JSON) pak musíte **přidat jako uživatele v dané službě** – viz níže. Místo service accountu lze vždy poslat i hotový -OAuth2 *access token* v `*-Access-Token` (např. z +OAuth2 *access token* (např. z [OAuth Playground](https://developers.google.com/oauthplayground/) se správným -scope); platí ~1 hodinu. +scope); platí ~1 hodinu. Token pošlete buď v `*-Access-Token`, nebo ve +standardní hlavičce `Authorization: Bearer `. ### 🔹 Google Analytics 4 (`X-GA-*`) diff --git a/documentation/google-ads.md b/documentation/google-ads.md index b67a2e5..af1b859 100644 --- a/documentation/google-ads.md +++ b/documentation/google-ads.md @@ -15,6 +15,7 @@ Google Ads needs more than a Bearer token: | --- | --- | --- | | `X-GAds-Developer-Token` | **yes** | Developer token from a Google Ads manager account → `developer-token`. | | `X-GAds-Access-Token` | one of these | Ready OAuth2 access token (Bearer). | +| `Authorization` | one of these | Standard `Authorization: Bearer ` header — equivalent alternative to `X-GAds-Access-Token` (the X- header wins if both are sent). The developer token is still required separately. | | `X-GAds-Credentials` | one of these | Base64 service-account JSON (scope `adwords`); needs domain-wide delegation. | | `X-GAds-Login-Customer-Id` | no | Manager (MCC) id → `login-customer-id`. Digits only. | | `X-GAds-Quota-Project` | no | GCP project id → `x-goog-user-project`. | diff --git a/documentation/google-analytics.md b/documentation/google-analytics.md index 6f1cf40..ef7fad5 100644 --- a/documentation/google-analytics.md +++ b/documentation/google-analytics.md @@ -10,11 +10,15 @@ Token has precedence over the service account: | Header | Meaning | | --- | --- | | `X-GA-Access-Token` | Ready OAuth2 access token, used directly as `Authorization: Bearer`. | +| `Authorization` | Standard `Authorization: Bearer ` header — equivalent alternative to `X-GA-Access-Token`. | | `X-GA-Credentials` | **Base64** of a Google service-account JSON key. The proxy mints a short-lived token (scope `https://www.googleapis.com/auth/analytics.readonly`) via `google-auth` and caches it in memory until ~60 s before expiry. | | `X-GA-Quota-Project` | Optional GCP project id → upstream `x-goog-user-project`. | -At least one of `X-GA-Access-Token` / `X-GA-Credentials` is required (otherwise -`401 missing_credentials`). +A ready access token can be supplied either in `X-GA-Access-Token` or in the +standard `Authorization: Bearer ` header. At least one credential source +(token header, `Authorization: Bearer`, or `X-GA-Credentials`) is required, +otherwise `401 missing_credentials`. Priority when several are present: +`X-GA-Access-Token` → `Authorization: Bearer` → `X-GA-Credentials`. The service account (or token) must have access to the GA4 property — add its `client_email` as a viewer in GA Admin → Property Access Management. diff --git a/documentation/google-search-console.md b/documentation/google-search-console.md index fa62536..b5aa369 100644 --- a/documentation/google-search-console.md +++ b/documentation/google-search-console.md @@ -11,6 +11,7 @@ Same Google OAuth model as Analytics, token wins over service account: | Header | Meaning | | --- | --- | | `X-GSC-Access-Token` | Ready OAuth2 access token (Bearer). | +| `Authorization` | Standard `Authorization: Bearer ` header — equivalent alternative to `X-GSC-Access-Token` (the X- header wins if both are sent). | | `X-GSC-Credentials` | Base64 service-account JSON; token minted with scope `https://www.googleapis.com/auth/webmasters.readonly`. | | `X-GSC-Quota-Project` | Optional GCP project id → `x-goog-user-project`. |