Skip to content

google_auth

google_auth

Shared Google OAuth helpers: access token read + one-shot 401 refresh.

All Google connectors (Gmail, Calendar, Contacts, Drive, Tasks) authenticate with the same OAuth flow and store identical token payloads at ~/.openjarvis/connectors/*.json — typically a shared google.json file plus per-product copies. They all need the same refresh-on-401 behavior, so the wrapper lives here instead of being duplicated per connector.

Use call_with_refresh(api_fn, credentials_path, *args, **kwargs) around any token-taking API helper. On a 401 the wrapper exchanges the stored refresh_token for a new access_token, updates the credentials file, and retries the call once. All other status codes propagate.

Classes

GoogleAuthError

Bases: RuntimeError

Raised when Google credentials are missing or refresh-token grant fails.

Functions

current_access_token

current_access_token(credentials_path: str) -> str

Return the current access token from the credentials file (empty if absent).

Source code in src/openjarvis/connectors/google_auth.py
def current_access_token(credentials_path: str) -> str:
    """Return the current access token from the credentials file (empty if absent)."""
    tokens = load_tokens(credentials_path) or {}
    return tokens.get("access_token", tokens.get("token", ""))

refresh_access_token

refresh_access_token(credentials_path: str) -> str

Exchange the stored refresh_token for a fresh access_token and persist it.

Returns the new access_token. Raises :class:GoogleAuthError when the credentials file is missing, lacks a refresh_token / client credentials, or when Google rejects the refresh grant (e.g. the refresh_token has been revoked and the user needs to re-authenticate).

Source code in src/openjarvis/connectors/google_auth.py
def refresh_access_token(credentials_path: str) -> str:
    """Exchange the stored refresh_token for a fresh access_token and persist it.

    Returns the new access_token. Raises :class:`GoogleAuthError` when the
    credentials file is missing, lacks a refresh_token / client credentials,
    or when Google rejects the refresh grant (e.g. the refresh_token has been
    revoked and the user needs to re-authenticate).
    """
    tokens = load_tokens(credentials_path)
    if not tokens:
        raise GoogleAuthError(
            f"No credentials at {credentials_path}; re-run the connector OAuth flow."
        )
    refresh_token = tokens.get("refresh_token", "")
    client_id = tokens.get("client_id", "")
    client_secret = tokens.get("client_secret", "")
    if not (refresh_token and client_id and client_secret):
        raise GoogleAuthError(
            "Stored Google credentials are missing refresh_token / client_id / "
            "client_secret; re-run the connector OAuth flow to mint a full token."
        )

    resp = httpx.post(
        _GOOGLE_TOKEN_ENDPOINT,
        data={
            "client_id": client_id,
            "client_secret": client_secret,
            "refresh_token": refresh_token,
            "grant_type": "refresh_token",
        },
        timeout=30.0,
    )
    if resp.status_code != 200:
        raise GoogleAuthError(
            f"Google token refresh failed ({resp.status_code}): {resp.text[:200]}"
        )
    payload = resp.json()
    new_token = payload.get("access_token", "")
    if not new_token:
        raise GoogleAuthError(
            "Google token refresh returned 200 but no access_token in payload."
        )

    tokens["access_token"] = new_token
    # Keep the legacy "token" key in sync for older code paths that read it.
    tokens["token"] = new_token
    if "expires_in" in payload:
        tokens["expires_in"] = payload["expires_in"]
    save_tokens(credentials_path, tokens)
    logger.info(
        "Refreshed Google access token (expires_in=%s)", payload.get("expires_in")
    )
    return new_token

call_with_refresh

call_with_refresh(api_fn: Callable[..., Dict[str, Any]], credentials_path: str, *args: Any, **kwargs: Any) -> Dict[str, Any]

Invoke api_fn(token, *args, **kwargs) with one-shot 401 auto-refresh.

Loads the current access token from disk, calls the helper, and if Google returns 401 (the access token has expired or been revoked) uses the stored refresh_token to mint a new access_token, updates the credentials file, and retries the call exactly once.

Any other HTTPStatusError is re-raised unchanged — auth-related retries end here; transient 5xx / timeout retries belong further up the stack.

Source code in src/openjarvis/connectors/google_auth.py
def call_with_refresh(
    api_fn: Callable[..., Dict[str, Any]],
    credentials_path: str,
    *args: Any,
    **kwargs: Any,
) -> Dict[str, Any]:
    """Invoke ``api_fn(token, *args, **kwargs)`` with one-shot 401 auto-refresh.

    Loads the current access token from disk, calls the helper, and if Google
    returns 401 (the access token has expired or been revoked) uses the stored
    refresh_token to mint a new access_token, updates the credentials file,
    and retries the call exactly once.

    Any other ``HTTPStatusError`` is re-raised unchanged — auth-related retries
    end here; transient 5xx / timeout retries belong further up the stack.
    """
    token = current_access_token(credentials_path)
    try:
        return api_fn(token, *args, **kwargs)
    except httpx.HTTPStatusError as exc:
        if exc.response is None or exc.response.status_code != 401:
            raise
        logger.info(
            "Google returned 401 on %s — refreshing access token and retrying.",
            getattr(api_fn, "__name__", "<api_fn>"),
        )
        new_token = refresh_access_token(credentials_path)
        return api_fn(new_token, *args, **kwargs)