2025, Sep 24 05:00
Resolve Google Calendar API 403 in gcloud ADC: use the correct OAuth scope for events.list
Getting 403 on Google Calendar API events.list with gcloud Application Default Credentials? Fix the OAuth scope mismatch by granting calendar.events.readonly.
When you authenticate with gcloud for Application Default Credentials and try to call Google Calendar API’s events.list, it’s easy to hit a 403 even if the login succeeds and the credentials file is present. The reason isn’t the token file or the calendarId, but a mismatch between the API method and the OAuth scope you granted at login.
Problem in context
You authenticate like this, passing cloud-platform and a Calendar scope:
gcloud auth application-default login --client-id-file google_oauth_client_id.json --scopes="https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/calendar.calendars.readonly"Then you build a Calendar client and call events.list, but receive a 403. Inspecting the credentials object shows no scopes populated, which makes the error look like an issue with how the token is loaded. In fact, the underlying problem is that the granted scopes do not cover the specific method you’re calling.
Repro code that fails
The following Python snippet demonstrates the flow that triggers the 403 when only calendar.calendars.readonly is granted:
from google.auth import default as adc_default
from google.auth.transport.requests import Request as HttpRequest
from googleapiclient.discovery import build as api_build
PERMS = [
    "https://www.googleapis.com/auth/calendar.calendars.readonly"
]
creds_obj, proj_id = adc_default(scopes=PERMS, quota_project_id="my-project-id")
creds_obj.refresh(HttpRequest())
user_token = creds_obj.token
cal_api = api_build("calendar", "v3", credentials=creds_obj)
items = cal_api.events().list(
    calendarId="My Calendar Id",
    maxResults=10,
    singleEvents=True,
    orderBy="startTime"
).execute()
print((creds_obj.scopes, creds_obj.default_scopes, creds_obj.granted_scopes))What’s actually going on
The Calendar API method you’re calling defines which scopes are accepted. For events.list, the required scope includes calendar.events.readonly. If you authenticated with calendar.calendars.readonly instead, the access token won’t have permission to list events and the server returns 403. This has nothing to do with whether the token file mentions scopes; the key point is whether your token was minted with a scope that matches the method. You can confirm what your token carries by checking it against the tokeninfo endpoint:
https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={ACCESS_TOKEN}
How to fix it
Request the correct scope during login. In this case, add calendar.events.readonly. For example:
gcloud auth application-default login \
--scopes=\
https://www.googleapis.com/auth/cloud-platform,\
https://www.googleapis.com/auth/calendar.events.readonlyIf you also want to reflect the method’s scope in code, define the scope accordingly when constructing the credentials:
from google.auth import default as adc_default
from google.auth.transport.requests import Request as HttpRequest
from googleapiclient.discovery import build as api_build
PERMS = [
    "https://www.googleapis.com/auth/calendar.events.readonly"
]
creds_obj, proj_id = adc_default(scopes=PERMS, quota_project_id="my-project-id")
creds_obj.refresh(HttpRequest())
user_token = creds_obj.token
cal_api = api_build("calendar", "v3", credentials=creds_obj)
items = cal_api.events().list(
    calendarId="My Calendar Id",
    maxResults=10,
    singleEvents=True,
    orderBy="startTime"
).execute()Why this matters
Google APIs enforce authorization at the method level. Mixing up similar-looking scopes, for example calendars.readonly versus events.readonly, leads to puzzling 403 errors that aren’t obvious from the client’s local state. Matching the scope to the exact API method and verifying the active token with tokeninfo saves time and prevents brittle authentication flows.
Takeaways
Always align your OAuth scopes with the specific API methods you call. For Calendar API’s events.list, that means calendar.events.readonly. If you authenticated without it, re-run gcloud auth application-default login with the additional scope, and, if needed, reflect that scope when building the client. When in doubt, confirm the token’s scopes using the tokeninfo endpoint and cross-check the method’s Authorization section in the official API reference.
The article is based on a question from StackOverflow by Ian Burnette and an answer by DazWilkin.