DEV Community

Cover image for How We Stopped Fighting Enterprise Auth and Read Calendars With a URL
Praveen KG
Praveen KG

Posted on

How We Stopped Fighting Enterprise Auth and Read Calendars With a URL

Reading Microsoft 365 calendars from scripts in locked-down enterprise environments — without Graph API, without OAuth, without any authentication at all.


The Problem

We're building an on-call scheduling tool for our platform engineering team. One of its core features: automatically check who's out of office before assigning someone to the on-call rota. Sounds simple — read a calendar, find OOO events, done.

Except we work inside a large enterprise with a locked-down Microsoft 365 tenant. And reading a calendar programmatically turned out to be the hardest part of the entire project.

What we needed:

  • Read OOO/leave events from team members' Outlook calendars
  • Run it from a script on any engineer's laptop (Mac or Windows)
  • No manual token copying, no browser interactions, no IT tickets
  • Just: run a command, get OOO data

What we assumed: Microsoft Graph API. It's the modern, documented, supported way to read calendars. Every tutorial says "just call /me/calendar/getSchedule". We even identified it as the perfect endpoint — it supports batch queries (20 users per call), returns native out-of-office status, and works with both delegated and app-only permissions.

The API wasn't the problem. Getting a token was.

What We Tried (and Why Everything Failed)

Attempt 1: Azure CLI Token

The obvious approach. We already use az login for other tooling. Microsoft's docs say you can get a Graph token with:

az account get-access-token --resource https://graph.microsoft.com
Enter fullscreen mode Exit fullscreen mode

Result: Blocked. Our tenant's Continuous Access Evaluation (CAE) policies reject CLI-issued tokens for Graph API. The token generates fine but every API call returns 401.

Attempt 2: MSAL Device Code Flow

The "headless-friendly" OAuth flow. Display a code, user opens a browser, authorises, script gets a token.

app = msal.PublicClientApplication(client_id, authority=authority)
flow = app.initiate_device_flow(scopes=["Calendars.Read"])
Enter fullscreen mode Exit fullscreen mode

Result: Blocked. Tenant rejects with AADSTS65002 and AADSTS7000218. The app ID isn't pre-approved and our IT team doesn't approve custom app registrations for internal tools.

Attempt 3: Azure AD App Registration

The "proper" way — register an app in Azure AD, get admin consent for Calendars.ReadBasic, use client credentials flow.

Result: Not attempted. Our IT security team is unlikely to approve a custom app registration requesting calendar read permissions across the organisation. The approval process alone would take weeks, and the answer would probably be no.

Attempt 4: Graph Explorer Token (Manual)

Microsoft's Graph Explorer web tool gives you a token when you sign in. We could copy-paste it into the script.

Result: Works, but impractical. The token expires in ~1 hour. Every team lead or engineer running the tool would need to open Graph Explorer, sign in, copy the token, paste it into the terminal. That's not a tool — that's a chore.

Attempt 5: Browser Cookie Extraction

Our incident management integration works by extracting OAuth tokens from browser cookies using Python's browser_cookie3. Could we do the same for Graph tokens?

import browser_cookie3
cj = browser_cookie3.chrome()
Enter fullscreen mode Exit fullscreen mode

Result: Failed. Graph API access tokens aren't stored in cookies. Microsoft stores them in localStorage and sessionStorage, which browser_cookie3 can't access. We found 159 Microsoft cookies in Chrome — session cookies, SSO cookies, auth cookies — but none of them were Graph API access tokens.

Attempt 6: Headless Browser Automation (Playwright)

If the tokens are in localStorage, we'll use Playwright to launch a browser, inject our SSO cookies, navigate to Outlook, and extract the tokens via JavaScript.

context.add_cookies(chrome_cookies)  # 159 SSO cookies
page.goto("https://outlook.office.com/calendar")
token = page.evaluate("localStorage.getItem('msal.access_token')")
Enter fullscreen mode Exit fullscreen mode

Result: Failed. Microsoft's enterprise SSO is profile-bound. It's not just cookies — it's device certificates, MSAL cache, keychain integration, and Conditional Access Evaluation (CAE) device compliance checks. An isolated Playwright context with injected cookies doesn't satisfy any of these checks. The browser just shows a login page.

Attempt 7: Playwright with Persistent Profile

Launch Playwright with a fresh user data directory and hope the SSO cookies carry over from the system.

Result: Failed. A fresh profile has no SSO state. Microsoft redirects to the login page with no auto-SSO.

Attempt 8: Exchange Web Services (EWS)

The older API. Maybe it has a different, more permissive auth story?

Result: Failed differently. Our on-prem Exchange servers are reachable via Kerberos. But the user's mailbox is in Exchange Online, not on-prem. The org-relationship between on-prem and Exchange Online is misconfigured, so EWS returns "mailbox not found" even though Kerberos auth succeeds.

Attempt 9: Chrome DevTools Protocol

Connect to a running Chrome instance via the DevTools Protocol and extract localStorage directly.

Result: Not tested. Requires restarting Chrome with --remote-debugging-port=9222. Impractical for daily use — you'd have to close all Chrome tabs, relaunch with debug flags, then run the tool.


At this point, we'd spent several sessions across multiple days trying every documented and undocumented approach to reading a calendar from a script. The problem wasn't finding the right API — getSchedule is perfect. The problem was enterprise authentication: a layered defence of CAE, Conditional Access, device compliance, and profile-bound SSO that blocks every automated token acquisition method.

The Breakthrough: A Feature From 2010

While exploring Outlook's calendar sharing settings for an unrelated reason, we noticed something: Publish Calendar.

Outlook has had the ability to publish calendars as ICS (iCalendar) feeds since Exchange 2010. You go to Settings → Calendar → Shared calendars → Publish a calendar, and Outlook generates a URL like:

https://outlook.office365.com/owa/calendar/{calendarId}@domain.com/{secret}/calendar.ics
Enter fullscreen mode Exit fullscreen mode

We opened that URL in a browser. It downloaded an .ics file. We curl'd it from a terminal. It returned data. We tried it from a different machine, without being logged into anything. It returned data.

No authentication. No tokens. No OAuth. No cookies. No browser profile. Nothing.

Just a plain HTTP GET to a URL that returns standard iCalendar data with all the calendar events — including every team member's OOO events.

curl -s "https://outlook.office365.com/owa/calendar/.../calendar.ics"
Enter fullscreen mode Exit fullscreen mode

That's it. That's the entire "authentication" story.

Why This Works

Published calendar ICS feeds are a feature of Exchange, not of Azure AD or the Microsoft identity platform. They predate OAuth 2.0, Graph API, MSAL, and Conditional Access by years. The URL contains a cryptographic secret (the publishSecret path component) that acts as the access control — if you have the URL, you can read the calendar.

This means:

  • No tenant policy blocks it — CAE, Conditional Access, and device compliance don't apply because there's no authentication flow to evaluate
  • No app registration needed — there's no OAuth client involved
  • No token expiry — the URL is permanent until you unpublish
  • Works from anywhere — any machine, any OS, any CI pipeline, any curl command
  • Standard format — iCalendar (RFC 5545) is parseable by every calendar library in every language

The feed is live — every request returns the current calendar state, not a snapshot. When someone creates an OOO event, it appears in the feed within minutes.

Parsing the Data

The ICS feed returns standard VEVENT entries. OOO events typically have summaries like:

SUMMARY:Alex - OOO
SUMMARY:Sam - OOO
SUMMARY:Jordan - A/L
SUMMARY:Morgan - Holiday
SUMMARY:Riley - On leave
Enter fullscreen mode Exit fullscreen mode

With Python's icalendar library:

import urllib.request
from icalendar import Calendar

data = urllib.request.urlopen(ics_url).read()
cal = Calendar.from_ical(data)

for event in cal.walk():
    if event.name == 'VEVENT':
        summary = str(event.get('summary', ''))
        start = event.get('dtstart').dt
        end = event.get('dtend').dt
        # Match OOO patterns: "Name - OOO", "Name on leave", etc.
Enter fullscreen mode Exit fullscreen mode

We parsed our feed and extracted dozens of OOO events covering our full team — from a single URL, with zero authentication, in about 10 lines of Python.

The Approach: One Calendar, One URL

Our team follows the convention of sending OOO calendar events when they're going to be away. These events appear on the team lead's calendar (as accepted invites). So a single person's published calendar contains OOO data for the entire team.

The setup is:

  1. One person publishes their Outlook calendar (one-time, 30-second setup)
  2. The tool fetches the ICS URL (plain HTTP GET)
  3. Parses OOO events with regex pattern matching
  4. Feeds the OOO data into the on-call scheduling tool

For teams where OOO events don't naturally aggregate on one calendar, alternatives include:

  • Microsoft 365 Group: Create a group, everyone adds OOO events to the group calendar, publish the group calendar — one URL
  • Individual feeds: Each person publishes their calendar, tool fetches all URLs

The Irony

We spent days fighting the modern Microsoft identity stack — CAE, MSAL, Conditional Access, device compliance, profile-bound SSO, browser automation. The solution was a feature that Microsoft shipped in Exchange Server 2010, before any of those systems existed.

Published calendar feeds sit at a layer below the modern auth stack. They don't use Azure AD tokens, they don't trigger Conditional Access policies, they don't require device compliance. They're just... URLs.

Sometimes the answer isn't fighting through the front door. It's finding the side entrance that's been open for 16 years.

How to Set This Up

Publishing Your Calendar

  1. Go to outlook.office.com/calendar
  2. Settings (gear icon) → View all Outlook settings
  3. CalendarShared calendars
  4. Under Publish a calendar, select the calendar
  5. Choose Can view all details
  6. Click Publish
  7. Copy the ICS link

That's your permanent, zero-auth calendar feed URL.

Reading It

# From any machine, no login required
curl -s "https://outlook.office365.com/owa/calendar/.../calendar.ics" | head -50
Enter fullscreen mode Exit fullscreen mode

Parsing OOO Events

pip install icalendar
Enter fullscreen mode Exit fullscreen mode
import urllib.request
from icalendar import Calendar
from datetime import date

url = "https://outlook.office365.com/owa/calendar/.../calendar.ics"
data = urllib.request.urlopen(url).read()
cal = Calendar.from_ical(data)

for event in cal.walk():
    if event.name == "VEVENT":
        summary = str(event.get("summary", ""))
        if any(kw in summary.lower() for kw in ["ooo", "leave", "holiday", "sick"]):
            start = event.get("dtstart").dt
            end = event.get("dtend").dt
            print(f"{start}{end}: {summary}")
Enter fullscreen mode Exit fullscreen mode

Security Considerations

The published calendar URL contains a secret in its path. Anyone with the URL can read the calendar. Treat it like an API key:

  • Don't commit it to public repositories
  • Share it only with people who need it
  • Store it in environment variables or secret management
  • You can unpublish at any time to invalidate the URL

The URL doesn't grant write access — it's read-only. And it only exposes the published calendar, not the entire mailbox.

When This Approach Works

This approach is ideal when:

  • Your enterprise tenant blocks Graph API tokens from CLI tools (CAE/Conditional Access)
  • You can't get an Azure AD App Registration approved
  • You need zero-setup, zero-auth calendar reads from scripts or CI pipelines
  • Your team follows a convention of calendar-based OOO announcements
  • You're comfortable with a convention-dependent (not system-enforced) data source

It's less suitable when:

  • You need write access to calendars
  • You need real-time, sub-minute freshness (ICS feeds have a few minutes propagation delay)
  • Your organisation has disabled calendar publishing at the tenant level
  • You need calendar data from people who haven't published their calendars

Closing Thought

The best engineering solutions aren't always the most sophisticated ones. Graph API with getSchedule is technically the right answer — batch queries, native OOF detection, structured response. But "technically right" doesn't matter if you can't get a token.

A URL that returns a text file solved a problem that OAuth 2.0, MSAL, Playwright, and five different authentication strategies couldn't.


The author is a software engineering manager at a large enterprise retailer, building internal developer platform tooling.

Top comments (0)