token variant

This commit is contained in:
JiriUhlir
2026-06-22 05:26:20 +02:00
parent 125b112967
commit 28b29b8331
5 changed files with 77 additions and 10 deletions
+60 -6
View File
@@ -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 * Google Ads -> ``X-GAds-*`` (scope adwords) + developer token
Each provides ``*-Access-Token`` (ready Bearer token, takes precedence) and 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 <token>`` 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). Sklik uses ``X-Sklik-Token`` (the proxy calls client.loginByToken).
""" """
@@ -47,6 +51,29 @@ class GoogleAdsCredentials:
login_customer_id: str | None login_customer_id: str | None
def _bearer_from_authorization(authorization: str | None) -> str | None:
"""Extract the token from a standard ``Authorization: Bearer <token>`` 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 <token>'. "
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( def _build_google_credentials(
access_token: str | None, access_token: str | None,
raw_credentials: str | None, raw_credentials: str | None,
@@ -55,9 +82,17 @@ def _build_google_credentials(
*, *,
token_header: str, token_header: str,
creds_header: str, creds_header: str,
authorization: str | None = None,
) -> GoogleCredentials: ) -> 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 <token>`` header.
"""
token = (access_token or "").strip() or None token = (access_token or "").strip() or None
if token is None:
token = _bearer_from_authorization(authorization)
service_account_info: dict | None = None service_account_info: dict | None = None
raw = (raw_credentials or "").strip() raw = (raw_credentials or "").strip()
@@ -81,7 +116,8 @@ def _build_google_credentials(
if not token and service_account_info is None: if not token and service_account_info is None:
raise MissingCredentialsError( raise MissingCredentialsError(
f"Provide either {token_header} or {creds_header}." f"Provide either {token_header}, an 'Authorization: Bearer <token>' "
f"header, or {creds_header}."
) )
return GoogleCredentials( return GoogleCredentials(
@@ -98,7 +134,12 @@ def get_ga_credentials(
default=None, default=None,
alias="X-GA-Access-Token", alias="X-GA-Access-Token",
description="Ready OAuth2 access token used directly as a Bearer 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( x_ga_credentials: str | None = Header(
default=None, default=None,
@@ -121,6 +162,7 @@ def get_ga_credentials(
config.GA_SCOPE, config.GA_SCOPE,
token_header="X-GA-Access-Token", token_header="X-GA-Access-Token",
creds_header="X-GA-Credentials", creds_header="X-GA-Credentials",
authorization=authorization,
) )
@@ -130,7 +172,12 @@ def get_gsc_credentials(
default=None, default=None,
alias="X-GSC-Access-Token", alias="X-GSC-Access-Token",
description="Ready OAuth2 access token used directly as a Bearer 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( x_gsc_credentials: str | None = Header(
default=None, default=None,
@@ -152,6 +199,7 @@ def get_gsc_credentials(
config.GSC_SCOPE, config.GSC_SCOPE,
token_header="X-GSC-Access-Token", token_header="X-GSC-Access-Token",
creds_header="X-GSC-Credentials", creds_header="X-GSC-Credentials",
authorization=authorization,
) )
@@ -167,7 +215,12 @@ def get_google_ads_credentials(
default=None, default=None,
alias="X-GAds-Access-Token", alias="X-GAds-Access-Token",
description="Ready OAuth2 access token used directly as a Bearer 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( x_gads_credentials: str | None = Header(
default=None, default=None,
@@ -199,6 +252,7 @@ def get_google_ads_credentials(
config.GOOGLE_ADS_SCOPE, config.GOOGLE_ADS_SCOPE,
token_header="X-GAds-Access-Token", token_header="X-GAds-Access-Token",
creds_header="X-GAds-Credentials", creds_header="X-GAds-Credentials",
authorization=authorization,
) )
login_customer_id = (x_gads_login_customer_id or "").strip().replace("-", "") or None login_customer_id = (x_gads_login_customer_id or "").strip().replace("-", "") or None
return GoogleAdsCredentials( return GoogleAdsCredentials(
+9 -2
View File
@@ -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é 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*. 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 <token>`. 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 ## 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 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ý 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 [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 <token>`.
### 🔹 Google Analytics 4 (`X-GA-*`) ### 🔹 Google Analytics 4 (`X-GA-*`)
+1
View File
@@ -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-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). | | `X-GAds-Access-Token` | one of these | Ready OAuth2 access token (Bearer). |
| `Authorization` | one of these | Standard `Authorization: Bearer <token>` 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-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-Login-Customer-Id` | no | Manager (MCC) id → `login-customer-id`. Digits only. |
| `X-GAds-Quota-Project` | no | GCP project id → `x-goog-user-project`. | | `X-GAds-Quota-Project` | no | GCP project id → `x-goog-user-project`. |
+6 -2
View File
@@ -10,11 +10,15 @@ Token has precedence over the service account:
| Header | Meaning | | Header | Meaning |
| --- | --- | | --- | --- |
| `X-GA-Access-Token` | Ready OAuth2 access token, used directly as `Authorization: Bearer`. | | `X-GA-Access-Token` | Ready OAuth2 access token, used directly as `Authorization: Bearer`. |
| `Authorization` | Standard `Authorization: Bearer <token>` 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-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`. | | `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 A ready access token can be supplied either in `X-GA-Access-Token` or in the
`401 missing_credentials`). standard `Authorization: Bearer <token>` 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 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. `client_email` as a viewer in GA Admin → Property Access Management.
+1
View File
@@ -11,6 +11,7 @@ Same Google OAuth model as Analytics, token wins over service account:
| Header | Meaning | | Header | Meaning |
| --- | --- | | --- | --- |
| `X-GSC-Access-Token` | Ready OAuth2 access token (Bearer). | | `X-GSC-Access-Token` | Ready OAuth2 access token (Bearer). |
| `Authorization` | Standard `Authorization: Bearer <token>` 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-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`. | | `X-GSC-Quota-Project` | Optional GCP project id → `x-goog-user-project`. |