Skip to content

Commit 484fd04

Browse files
author
Nico Thomaier
committed
feat(auth): implement cookie-based authentication for SharePoint Online
- Added `CookieAuthProvider` to handle authentication using browser-session cookies. - Introduced `with_cookies` method in `AuthenticationContext`, `ClientContext`, and `SharePointRequest` for seamless integration. - Added examples for loading cookies from Playwright's `storage_state.json` and using them in authentication. - Updated documentation to include new authentication method and examples.
1 parent 8352da8 commit 484fd04

File tree

9 files changed

+251
-1
lines changed

9 files changed

+251
-1
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,9 @@ Pipfile.lock
8282

8383
# Self signed certificates
8484
examples/*.pem
85+
86+
# Playwright
87+
storage_state.json
88+
89+
# Cursor
90+
.cursorignore

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,29 @@ me = ctx.web.current_user.get().execute_query()
115115
print(me.login_name)
116116
```
117117

118+
#### 5. Browser session cookies (SharePoint Online)
119+
120+
Authenticate using cookies from a real browser session (e.g., Playwright). No Azure AD app registration required.
121+
122+
Usage:
123+
```python
124+
from office365.sharepoint.client_context import ClientContext
125+
126+
def cookie_source():
127+
# Return a dict with FedAuth/rtFa/SPOIDCRL values or an AuthCookies instance
128+
return {"FedAuth": "...", "rtFa": "...", "SPOIDCRL": "..."}
129+
130+
ctx = ClientContext("https://contoso.sharepoint.com/sites/demo").with_cookies(cookie_source, ttl_seconds=None)
131+
web = ctx.web.get().execute_query()
132+
print(web.title)
133+
```
134+
135+
Example: [auth_cookies.py](examples/sharepoint/auth_cookies.py)
136+
137+
Notes:
138+
- `ttl_seconds` is optional. Use it to periodically refresh cookies from your source if it’s cheap (e.g., file read).
139+
- Cookies are secrets. Do not log them; secure any storage (e.g., Playwright `storage_state.json`).
140+
118141
### Examples
119142

120143
There are **two approaches** available to perform API queries:
@@ -158,6 +181,7 @@ json = json.loads(response.content)
158181
web_title = json['d']['Title']
159182
print("Web title: {0}".format(web_title))
160183
```
184+
Tip: You can also authenticate `SharePointRequest` with cookies using `with_cookies(cookie_source, ttl_seconds=None)`.
161185

162186
For SharePoint-specific examples, see:
163187
📌 **[SharePoint examples guide](examples/sharepoint/README.md)**

examples/sharepoint/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,13 @@ This directory contains examples for SharePoint REST API v1
2424

2525
### Working with site
2626

27+
### Authentication using browser session cookies
28+
- **Authenticate with cookies**: [`../auth_cookies.py`](../auth_cookies.py)
29+
- Demonstrates loading `FedAuth`, `rtFa`, `SPOIDCRL` from Playwright `storage_state.json` and using `ClientContext.with_cookies(...)`.
30+
- Optional `ttl_seconds` parameter can periodically refresh cookies from the source.
31+
- **Capture cookies with Playwright (optional)**: [`./auth/capture_cookies_with_playwright.py`](./auth/capture_cookies_with_playwright.py)
32+
- Not a library dependency. Requires `pip install playwright` and `playwright install chromium`.
33+
- Launches a browser to log in, then saves `storage_state.json` which can be consumed by the cookie auth example.
34+
2735
---
2836

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
Acquire SharePoint Online browser-session cookies using Playwright and save them into
3+
storage_state.json (or a custom path), which can be consumed by examples/sharepoint/auth_cookies.py.
4+
5+
Requirements (not installed by the library):
6+
pip install playwright
7+
playwright install chromium
8+
9+
Usage:
10+
SP_SITE_URL="https://contoso.sharepoint.com/sites/demo" \
11+
PLAYWRIGHT_STORAGE_STATE="./storage_state.json" \
12+
HEADLESS=false \
13+
python examples/sharepoint/auth/capture_cookies_with_playwright.py
14+
15+
Notes:
16+
- The script opens a browser window. Complete the Microsoft login (including MFA) manually.
17+
- After login, return to the terminal and press Enter to persist cookies.
18+
- The resulting storage_state.json can be used by auth_cookies.py.
19+
"""
20+
21+
import os
22+
23+
from playwright.sync_api import sync_playwright
24+
25+
26+
def main() -> None:
27+
site_url = os.environ.get("SP_SITE_URL")
28+
if not site_url:
29+
raise SystemExit(
30+
"SP_SITE_URL is required, e.g. https://contoso.sharepoint.com/sites/demo"
31+
)
32+
33+
storage_state_path = os.environ.get(
34+
"PLAYWRIGHT_STORAGE_STATE", "./storage_state.json"
35+
)
36+
headless_env = os.environ.get("HEADLESS", "false").lower()
37+
headless = headless_env in ("1", "true", "yes")
38+
39+
with sync_playwright() as p:
40+
browser = p.chromium.launch(headless=headless)
41+
context = browser.new_context()
42+
page = context.new_page()
43+
page.goto(site_url)
44+
# Wait for network to be idle; login flow may redirect to Microsoft login pages
45+
page.wait_for_load_state("networkidle")
46+
47+
print(
48+
"\nA browser window is open. Complete the login (including MFA) if prompted."
49+
)
50+
input(
51+
"When the SharePoint page is fully loaded and you are authenticated, press Enter here to continue..."
52+
)
53+
54+
# Persist cookies and related state
55+
context.storage_state(path=storage_state_path)
56+
print(f"Saved Playwright storage state to: {storage_state_path}")
57+
58+
context.close()
59+
browser.close()
60+
61+
62+
if __name__ == "__main__":
63+
main()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import json
2+
import os
3+
from typing import Dict
4+
5+
from office365.sharepoint.client_context import ClientContext
6+
7+
8+
def load_cookies_from_storage_state(path: str) -> Dict[str, str]:
9+
"""Load cookies exported by Playwright storage_state.json and extract SPO cookies.
10+
11+
The library does not depend on Playwright; this is a helper to demonstrate usage.
12+
"""
13+
with open(path, "r", encoding="utf-8") as f:
14+
data = json.load(f)
15+
cookies = {}
16+
for c in data.get("cookies", []):
17+
name = c.get("name")
18+
if name in {"FedAuth", "rtFa", "SPOIDCRL"}:
19+
cookies[name] = c.get("value", "")
20+
return cookies
21+
22+
23+
if __name__ == "__main__":
24+
site_url = os.environ.get(
25+
"SP_SITE_URL", "https://contoso.sharepoint.com/sites/demo"
26+
)
27+
storage_state_path = os.environ.get(
28+
"PLAYWRIGHT_STORAGE_STATE", "./storage_state.json"
29+
)
30+
31+
def cookie_source():
32+
return load_cookies_from_storage_state(storage_state_path)
33+
34+
ctx = ClientContext(site_url).with_cookies(cookie_source)
35+
web = ctx.web.get().execute_query()
36+
print(f"Web title: {web.title}")

office365/runtime/auth/authentication_context.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from office365.azure_env import AzureEnvironment
99
from office365.runtime.auth.client_credential import ClientCredential
1010
from office365.runtime.auth.providers.acs_token_provider import ACSTokenProvider
11+
from office365.runtime.auth.providers.cookie_provider import CookieAuthProvider
1112
from office365.runtime.auth.providers.saml_token_provider import SamlTokenProvider
1213
from office365.runtime.auth.token_response import TokenResponse
1314
from office365.runtime.auth.user_credential import UserCredential
@@ -180,7 +181,6 @@ def with_access_token(self, token_func):
180181
"""
181182

182183
def _authenticate(request):
183-
184184
request_time = datetime.now(timezone.utc)
185185

186186
if self._cached_token is None or request_time > self._token_expires:
@@ -196,6 +196,23 @@ def _authenticate(request):
196196
self._authenticate = _authenticate
197197
return self
198198

199+
def with_cookies(self, cookie_source, ttl_seconds=None):
200+
# type: (Any, Any) -> "AuthenticationContext"
201+
"""
202+
Initializes authentication using browser-session cookies.
203+
204+
:param Any cookie_source: Callable returning Dict[str, str] or an AuthCookies instance.
205+
:param Any ttl_seconds: Optional max age for cached cookies before reloading from source.
206+
"""
207+
provider = CookieAuthProvider(cookie_source, ttl_seconds)
208+
209+
def _authenticate(request):
210+
# type: (RequestOptions) -> None
211+
provider.authenticate_request(request)
212+
213+
self._authenticate = _authenticate
214+
return self
215+
199216
def with_credentials(self, credentials):
200217
# type: (UserCredential | ClientCredential) -> "AuthenticationContext"
201218
"""
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from datetime import datetime, timedelta, timezone
2+
from typing import Callable, Dict, Optional, Union
3+
4+
from office365.runtime.auth.auth_cookies import AuthCookies
5+
from office365.runtime.auth.authentication_provider import AuthenticationProvider
6+
7+
8+
class CookieAuthProvider(AuthenticationProvider):
9+
"""Authentication provider that applies SharePoint Online browser-session cookies.
10+
11+
Accepts a cookie source callback or a prebuilt AuthCookies instance and sets
12+
the HTTP "Cookie" header on outgoing requests. Optionally, a TTL can be supplied
13+
to refresh cached cookies after a specified number of seconds.
14+
"""
15+
16+
def __init__(
17+
self,
18+
cookie_source: Union[Callable[[], Dict[str, str]], AuthCookies],
19+
ttl_seconds: Optional[int] = None,
20+
) -> None:
21+
super().__init__()
22+
self._cookie_source = cookie_source
23+
self._ttl_seconds = ttl_seconds
24+
self._cached_auth_cookies: Optional[AuthCookies] = None
25+
self._acquired_at: Optional[datetime] = None
26+
27+
def refresh(self) -> None:
28+
"""Clears the cached cookies so that the next request reacquires them."""
29+
self._cached_auth_cookies = None
30+
self._acquired_at = None
31+
32+
def _is_expired(self, now_utc: datetime) -> bool:
33+
if self._cached_auth_cookies is None:
34+
return True
35+
if self._ttl_seconds is None:
36+
return False
37+
if self._acquired_at is None:
38+
return True
39+
return now_utc >= self._acquired_at + timedelta(seconds=self._ttl_seconds)
40+
41+
def _acquire_from_source(self) -> AuthCookies:
42+
source = self._cookie_source
43+
if callable(source):
44+
result = source()
45+
if isinstance(result, AuthCookies):
46+
cookies = result
47+
elif isinstance(result, dict):
48+
cookies = AuthCookies(result)
49+
else:
50+
raise ValueError(
51+
"cookie_source must return Dict[str, str] or AuthCookies"
52+
)
53+
elif isinstance(source, AuthCookies):
54+
cookies = source
55+
else:
56+
raise ValueError(
57+
"cookie_source must be a Callable[[], Dict[str, str]] or AuthCookies"
58+
)
59+
60+
if not cookies.is_valid:
61+
raise ValueError("Provided cookies are not valid for SharePoint Online.")
62+
return cookies
63+
64+
def _ensure_cookies_cached(self) -> None:
65+
now_utc = datetime.now(timezone.utc)
66+
if self._is_expired(now_utc):
67+
cookies = self._acquire_from_source()
68+
self._cached_auth_cookies = cookies
69+
self._acquired_at = now_utc
70+
71+
def authenticate_request(self, request) -> None:
72+
"""Sets the Cookie header using cached or freshly acquired cookies."""
73+
self._ensure_cookies_cached()
74+
request.set_header("Cookie", self._cached_auth_cookies.cookie_header)

office365/sharepoint/client_context.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ def with_credentials(self, credentials):
185185
self.authentication_context.with_credentials(credentials)
186186
return self
187187

188+
def with_cookies(self, cookie_source, ttl_seconds=None):
189+
# type: (object, object) -> Self
190+
"""
191+
Initializes authentication using browser-session cookies.
192+
193+
:param object cookie_source: Callable returning Dict[str, str] or an AuthCookies instance.
194+
:param object ttl_seconds: Optional max age for cached cookies before reloading from source.
195+
"""
196+
self.authentication_context.with_cookies(cookie_source, ttl_seconds)
197+
return self
198+
188199
def execute_batch(self, items_per_batch=100, success_callback=None):
189200
# type: (int, Callable[[List[ClientObject|ClientResult]], None]) -> Self
190201
"""

office365/sharepoint/request.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ def with_credentials(self, credentials):
4949
self._auth_context.with_credentials(credentials)
5050
return self
5151

52+
def with_cookies(self, cookie_source, ttl_seconds=None):
53+
# type: (object, object) -> Self
54+
"""
55+
Initializes authentication using browser-session cookies.
56+
57+
:param object cookie_source: Callable returning Dict[str, str] or an AuthCookies instance.
58+
:param object ttl_seconds: Optional max age for cached cookies before reloading from source.
59+
"""
60+
self._auth_context.with_cookies(cookie_source, ttl_seconds)
61+
return self
62+
5263
def with_client_certificate(
5364
self,
5465
tenant,

0 commit comments

Comments
 (0)