403 „insufficient authentication scopes“ bei Google Photos API trotz korrektem OAuth-Setup

ich versuche, mit Python und der Google Photos Library API auf meine Google Fotos-Favoriten zuzugreifen.
Trotz korrektem OAuth-Flow und Berechtigung erhalte ich bei jedem API-Request folgendes Fehler-Response:

JSON

{
  "error": {
    "code": 403,
    "message": "Request had insufficient authentication scopes.",
    "status": "PERMISSION_DENIED"
  }
}

Was ich bereits geprüft habe:

  • Die Google Photos Library API ist im richtigen Cloud-Projekt aktiviert.

  • Ich nutze die client_secret.json aus diesem Projekt.

  • Meine E-Mail ist als Testnutzer im Consent-Screen eingetragen.

  • Der OAuth-Flow läuft sauber durch, im Browser wird die Berechtigung „Google Fotos-Galerie ansehen“ angezeigt.

  • Der generierte Token enthält den Scope https://www.googleapis.com/auth/photoslibrary.readonly.

  • Mein Google-Konto ist ein reguläres Gmail-Konto und ich habe Fotos in den Favoriten.

  • Ich habe es auch mit einem neuen Gmail-Konto und neuem Projekt/OAuth-Client getestet – der Fehler bleibt.

  • Ich habe verschiedene Ports, Fallback-Flows (run_console) und einen neuen Token probiert.

Beispiel für meine API-Requests:

Python

headers = {
  "Authorization": "Bearer <mein_token>",
  "Content-Type": "application/json"
}
body = {
  "filters": {
    "featureFilter": {"includedFeatures": ["FAVORITES"]}
  }
}
response = requests.post("https://photoslibrary.googleapis.com/v1/mediaItems:search", headers=headers, json=body)

Auch ein einfacher GET auf https://photoslibrary.googleapis.com/v1/mediaItems bringt den gleichen Fehler.

Was mir auffällt:

  • Die App wird im Google-Konto als berechtigt angezeigt.

  • Der Token hat den richtigen Scope (geprüft mit creds.scopes).

  • Die Fotos-Favoriten sind vorhanden.


Frage:
Was muss ich in meiner Google Cloud Console, im OAuth-Flow, in der API-Konfiguration oder sonst wo ändern, damit ich mit dem Scope photoslibrary.readonly auf meine Google Fotos zugreifen kann?
Gibt es neue Einschränkungen für Testnutzer, neue Projekte oder bestimmte Regionen/Konten?

Vielen Dank für Ihre Unterstützung!


Screenshots und Code kann ich auf Wunsch nachreichen.

1 Like

Der Fehler 403 insufficient authentication scopes taucht bei der Google Photos API meistens dann auf, wenn zwar ein Token vorhanden ist, dieser aber nicht mit exakt den benötigten Scopes erstellt wurde oder die App noch im Testmodus steckt. Ein paar Punkte, die du noch prüfen solltest:

  1. Scopes beim ersten OAuth-Lauf prüfen

    • Auch wenn photoslibrary.readonly angezeigt wird, musst du sicherstellen, dass dein Token wirklich mit diesem Scope generiert wurde. Alte Tokens werden oft weiterverwendet.
      → Lösche den gespeicherten Token (token.json o.ä.) und starte den OAuth-Flow komplett neu.
  2. Consent Screen – Status

    • Wenn deine App im Testmodus ist, können nur die dort eingetragenen Testnutzer Zugriff bekommen.

    • Stelle sicher, dass du dich mit exakt der Mailadresse anmeldest, die auch im Consent-Screen hinterlegt ist.

  3. Zusätzliche Scopes testen

    • Manche Endpunkte der Photos API reagieren sensibel, wenn nur ein Minimal-Scope vorhanden ist.

    • Probier z. B. https://www.googleapis.com/auth/photoslibrary (voller Zugriff) oder photoslibrary.readonly.appcreateddata, um zu sehen, ob das Verhalten gleich bleibt.

  4. Google Cloud Console – API aktivieren

    • Prüfe noch einmal, dass die Google Photos Library API in genau dem Projekt aktiviert ist, aus dem du deine client_secret.json verwendest.
  5. Bekannte Einschränkungen

    • Neue Projekte/Clients im Testmodus haben manchmal Limitierungen. Falls du längerfristig nutzen willst, musst du den Consent-Screen veröffentlichen („In Produktion setzen“).

Empfohlene Schritte:

  • Token-Datei löschen → OAuth neu durchlaufen → prüfen, ob der Scope korrekt gesetzt wird.

  • Testweise photoslibrary (voller Zugriff) hinzufügen, um Scope-Probleme auszuschließen.

  • Falls es dann funktioniert → wieder auf readonly einschränken.

  • Wenn es im Testmodus bleibt: dich selbst als Testnutzer eintragen oder den Consent-Screen veröffentlichen.

:backhand_index_pointing_right: In den meisten Fällen verschwindet der Fehler, sobald ein frischer Token mit exakt passendem Scope erstellt wird.

Hi,

ich habe heute noch eine neue Anwendung erstellt (Desktop) und somit eine neue secret.json bekommen, davor hab ich aber auch
https://www.googleapis.com/auth/photoslibrary zugelassen.

doch bekomme immer noch:

H:\>py notionFavorites.py
Token scopes: ['https://www.googleapis.com/auth/photoslibrary']
Status: 403
Antwort: {
  "error": {
    "code": 403,
    "message": "Request had insufficient authentication scopes.",
    "status": "PERMISSION_DENIED"
  }
}

❌ Fehler: {
  "error": {
    "code": 403,
    "message": "Request had insufficient authentication scopes.",
    "status": "PERMISSION_DENIED"
  }
}

Gefundene Favoriten: 0

Meine createToken Datei sieht so aus und funktioniert wunderbar:

import pickle
from google_auth_oauthlib.flow import InstalledAppFlow

# --- KONFIG ---
SCOPES = ['https://www.googleapis.com/auth/photoslibrary']
CREDENTIALS_FILE = r"H:\client_secret1.json"   # Pfad zu deiner client_secret.json
TOKEN_FILE = r"H:\token1.pickle"              # Hier wird das Token gespeichert


def main():
    flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES)

    try:
        # Erst versuchen: Lokaler Webserver (Browser öffnet sich automatisch)
        creds = flow.run_local_server(port=8080)
        print("✅ Token über run_local_server erstellt.")
    except Exception as e:
        print("⚠️ run_local_server fehlgeschlagen:", e)
        print("👉 Fallback: run_console (manueller Code-Eingabe)")
        creds = flow.run_console()

    # Token speichern
    with open(TOKEN_FILE, "wb") as token:
        pickle.dump(creds, token)

    print(f"✅ Token gespeichert unter: {TOKEN_FILE}")

if __name__ == "__main__":
    main()

Meine andere Datei liefert aber den obigen Fehler:

from __future__ import print_function
import os
import pickle
import requests
from google.auth.transport.requests import Request

# ------------------ KONFIGURATION ------------------
NOTION_TOKEN = "xxx"  # Dein Notion-API-Token
DATABASE_ID = "xxx"  # Deine Notion Datenbank-ID
CREDENTIALS_FILE = r"H:\client_secret1.json"  # Von Google Cloud heruntergeladen
TOKEN_FILE = r"H:\token1.pickle"
SCOPES = ['https://www.googleapis.com/auth/photoslibrary']

# ------------------ GOOGLE PHOTOS ------------------
def get_google_creds():
    creds = None
    if os.path.exists(TOKEN_FILE):
        with open(TOKEN_FILE, 'rb') as token:
            creds = pickle.load(token)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            raise RuntimeError("❌ Kein gültiger Token. Bitte token.pickle erzeugen!")
    return creds

def get_favorites(creds):
    url = "https://photoslibrary.googleapis.com/v1/mediaItems:search"
    headers = {
        "Authorization": "Bearer " + creds.token,
        "Content-Type": "application/json"
    }
    body = {
        "filters": {
            "featureFilter": {
                "includedFeatures": ["FAVORITES"]
            }
        }
    }
    response = requests.post(url, headers=headers, json=body)
    if response.status_code == 200:
        results = response.json()
        return results.get('mediaItems', [])
    else:
        print(f"❌ Fehler: {response.text}")
        return [] 
        
def test_photos_api(creds):
    import requests
    url = "https://photoslibrary.googleapis.com/v1/mediaItems"
    headers = {
        "Authorization": "Bearer " + creds.token,
        "Content-Type": "application/json"
    }
    r = requests.get(url, headers=headers)
    print("Status:", r.status_code)
    print("Antwort:", r.text)        

# ------------------ NOTION ------------------
def upload_to_notion(photo):
    url = "https://api.notion.com/v1/pages"
    headers = {
        "Authorization": f"Bearer {NOTION_TOKEN}",
        "Content-Type": "application/json",
        "Notion-Version": "2022-06-28"
    }
    data = {
        "parent": {"database_id": DATABASE_ID},
        "properties": {
            "Bildname": {"title": [{"text": {"content": photo['filename']}}]},
            "Id": {"rich_text": [{"text": {"content": photo['id']}}]},
            "Datum": {"date": {"start": photo.get('mediaMetadata', {}).get('creationTime', None)}},
            "Fotos": {
                "files": [
                    {
                        "name": photo['filename'],
                        "external": {"url": photo['baseUrl'] + "=w2048-h1024"}
                    }
                ]
            }
        }
    }
    r = requests.post(url, headers=headers, json=data)
    if r.status_code == 201:
        print(f"✅ {photo['filename']} in Notion eingetragen")
    else:
        print(f"❌ Fehler bei {photo['filename']}: {r.text}")

# ------------------ MAIN ------------------
def main():
    
    creds = get_google_creds()
    print("Token scopes:", creds.scopes)
    test_photos_api(creds)
    favorites = get_favorites(creds)
    print(f"Gefundene Favoriten: {len(favorites)}")
    for photo in favorites:
        upload_to_notion(photo)

if __name__ == '__main__':
    main()

Hab ich da vielleicht eine fehler drin?

Danke für die Hilfe
Gruß Rose

Was this ever resolved? I have the exact same issue and cannot seem to get around ““Request had insufficient authentication scopes.”.

ich habe bis jetzt keine Lösung gefunden