I have a python script that I run manually to connect to my photo library and download images. This was working for a time, and then it stopped working. Nothing changed, just time. I then let AI try to fix it and the code has changed a lot since then. From what I can tell, this is still sound... but I get the error: googleapiclient.errors.HttpError: <HttpError 403 when requesting https://photoslibrary.googleapis.com/v1/albums?fields=albums%28id%2Ctitle%29&pageSize=10&alt=json returned "Request had insufficient authentication scopes.". Details: "Request had insufficient authentication scopes."> Note: I'm not storing tokens locally, I ripped that out to force me to get a token each time as I troubleshoot this. I just created a new project in GCP, enabled Photos API, set up consent screen, downloaded the credentials.json, and still getting this bloody error. Source code from typing import List import os from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request from google.auth.transport.requests import AuthorizedSession from googleapiclient.discovery import build import requests from models.comic import Comic import logging import json logger = logging.getLogger(__name__) class GooglePhotosIngester: """Handles fetching and processing images from Google Photos""" SCOPES = ['https://www.googleapis.com/auth/photoslibrary.readonly'] def __init__(self): # Get path to credentials directory relative to project root self.root_dir = os.path.dirname(os.path.dirname(__file__)) self.credentials_dir = os.path.join(self.root_dir, 'credentials') self.credentials_path = os.path.join(self.credentials_dir, 'google_photos_credentials.json') self.credentials = None self.service = None # Verify credentials file exists and is readable if not os.path.exists(self.credentials_path): raise FileNotFoundError(f"Credentials file not found at {self.credentials_path}") try: with open(self.credentials_path, 'r') as f: creds_data = json.load(f) logger.info(f"Successfully loaded credentials file. Client ID: {creds_data.get('installed', {}).get('client_id', 'Not found')}") except Exception as e: logger.error(f"Error reading credentials file: {str(e)}") raise def authenticate(self) -> None: """Handles Google Photos authentication with fresh credentials each time""" try: logger.info("Starting authentication flow...") flow = InstalledAppFlow.from_client_secrets_file( self.credentials_path, self.SCOPES ) logger.info("Created InstalledAppFlow with scopes: %s", self.SCOPES) self.credentials = flow.run_local_server( port=0, prompt='consent', include_granted_scopes=False ) logger.info(f"Token: {self.credentials.token}") logger.info(f"Refresh token: {self.credentials.refresh_token}") logger.info(f"Expiry: {self.credentials.expiry}") logger.info(f"Scopes: {self.credentials.scopes}") try: self.credentials.refresh(Request()) except Exception as e: logger.error(f"Refresh failed: {e}") # Dump the user email for clarity authed_session = AuthorizedSession(self.credentials) userinfo = authed_session.get('https://www.googleapis.com/oauth2/v1/userinfo?alt=json') logger.info(f"Authenticated as: {userinfo.json()}") logger.info("Successfully obtained credentials") logger.info("Credential scopes: %s", self.credentials.scopes) logger.info("Credential has refresh token: %s", bool(self.credentials.refresh_token)) # Initialize the Photos Library service with the correct discovery URL logger.info("Building Photos Library service...") self.service = build( 'photoslibrary', 'v1', credentials=self.credentials, discoveryServiceUrl='https://photoslibrary.googleapis.com/$discovery/rest?version=v1' ) logger.info("Successfully built Photos Library service") # Test the connection by listing albums logger.info("Testing connection by listing albums...") albums = self.service.albums().list( fields='albums(id,title)', pageSize=10 ).execute() for album in albums.get('albums', []): logger.info(f"Album title: {album['title']}, ID: {album['id']}") except Exception as e: logger.error(f"Error during authentication: {str(e)}") if hasattr(e, 'resp'): logger.error(f"Response status: {e.resp.status}") logger.error(f"Response content: {e.content}") raise Output logs INFO:ingestion.google_photos:Starting authentication flow... INFO:ingestion.google_photos:Created InstalledAppFlow with scopes: ['https://www.googleapis.com/auth/photoslibrary.readonly'] Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=xxx&redirect_uri=http%3A%2F%2Flocalhost%3A54357%2F&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fphotoslibrary.readonly&state=xxx&prompt=consent&access_type=offline INFO:google_auth_oauthlib.flow:"GET /?state=MtQFaaKY9pwXXGMrvjQO5QLf1qigbV&code=4/0AUJR-x5DwOJVymCBhrL-aAwD-5OrrR1Ju9yhYDfIqyo7TgzxOh4RYnfM0leVMk2GDo5Bhg&scope=https://www.googleapis.com/auth/photoslibrary.readonly HTTP/1.1" 200 65 INFO:ingestion.google_photos:Token: ya29.xxx5g0175 INFO:ingestion.google_photos:Refresh token: 1//0517zxxx8w INFO:ingestion.google_photos:Expiry: 2025-06-14 20:19:21.889031 INFO:ingestion.google_photos:Scopes: ['https://www.googleapis.com/auth/photoslibrary.readonly'] INFO:ingestion.google_photos:Authenticated as: {'error': {'code': 401, 'message': 'Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.', 'status': 'UNAUTHENTICATED'}} INFO:ingestion.google_photos:Successfully obtained credentials INFO:ingestion.google_photos:Credential scopes: ['https://www.googleapis.com/auth/photoslibrary.readonly'] INFO:ingestion.google_photos:Credential has refresh token: True INFO:ingestion.google_photos:Building Photos Library service... INFO:googleapiclient.discovery_cache:file_cache is only supported with oauth2client<4.0.0 INFO:ingestion.google_photos:Successfully built Photos Library service INFO:ingestion.google_photos:Testing connection by listing albums... WARNING:googleapiclient.http:Encountered 403 Forbidden with reason "PERMISSION_DENIED" ERROR:ingestion.google_photos:Error during authentication: <HttpError 403 when requesting https://photoslibrary.googleapis.com/v1/albums?fields=albums%28id%2Ctitle%29&pageSize=10&alt=json returned "Request had insufficient authentication scopes.". Details: "Request had insufficient authentication scopes."> ERROR:ingestion.google_photos:Response status: 403 ERROR:ingestion.google_photos:Response content: b'{\n "error": {\n "code": 403,\n "message": "Request had insufficient authentication scopes.",\n "status": "PERMISSION_DENIED"\n }\n}\n' Traceback (most recent call last): File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main return _run_code(code, main_globals, None, File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code exec(code, run_globals) File "/Users/davidlozzi/.cursor/extensions/ms-python.debugpy-2025.8.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/__main__.py", line 71, in <module> cli.main() File "/Users/davidlozzi/.cursor/extensions/ms-python.debugpy-2025.8.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 501, in main run() File "/Users/davidlozzi/.cursor/extensions/ms-python.debugpy-2025.8.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 384, in run_module run_module_as_main(options.target, alter_argv=True) File "/Users/davidlozzi/.cursor/extensions/ms-python.debugpy-2025.8.0-darwin-arm64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 228, in _run_module_as_main return _run_code(code, main_globals, None, "__main__", mod_spec) File "/Users/davidlozzi/.cursor/extensions/ms-python.debugpy-2025.8.0-darwin-arm64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 118, in _run_code exec(code, run_globals) File "/Users/davidlozzi/git/sw-panels-data/ingestion/pipeline.py", line 387, in <module> main() File "/Users/davidlozzi/git/sw-panels-data/ingestion/pipeline.py", line 384, in main pipeline.ingest_album(album_id, full_ingestion=args.full, incomplete_only=args.incomplete) File "/Users/davidlozzi/git/sw-panels-data/ingestion/pipeline.py", line 308, in ingest_album all_comics = self.photos_ingester.fetch_album_images(album_id) File "/Users/davidlozzi/git/sw-panels-data/ingestion/google_photos.py", line 113, in fetch_album_images self.authenticate() File "/Users/davidlozzi/git/sw-panels-data/ingestion/google_photos.py", line 93, in authenticate albums = self.service.albums().list( File "/Users/davidlozzi/git/sw-panels-data/.venv/lib/python3.9/site-packages/googleapiclient/_helpers.py", line 130, in positional_wrapper return wrapped(*args, **kwargs) File "/Users/davidlozzi/git/sw-panels-data/.venv/lib/python3.9/site-packages/googleapiclient/http.py", line 938, in execute raise HttpError(resp, content, uri=self.uri) googleapiclient.errors.HttpError: <HttpError 403 when requesting https://photoslibrary.googleapis.com/v1/albums?fields=albums%28id%2Ctitle%29&pageSize=10&alt=json returned "Request had insufficient authentication scopes.". Details: "Request had insufficient authentication scopes."> Continue reading...