Skip to content

Commit f1055f0

Browse files
author
Vadim
committed
feat: add device code flow to GraphClient + example + update auth README
1 parent 2756781 commit f1055f0

5 files changed

Lines changed: 76 additions & 2 deletions

File tree

examples/auth/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ flowchart TD
1818
1919
C --> G{User present to interact?}
2020
G -->|Yes| H[Interactive auth\nsupports MFA, SSO]
21-
G -->|No, script or CI| I["ROPC (password grant)\nno MFA, legacy"]
21+
G -->|No| I{Has a browser?}
22+
I -->|Yes, visit URL| N[Device code flow\nheadless CLI, SSH,\nremote server]
23+
I -->|No browser at all| O["ROPC (password grant)\nno MFA, legacy"]
2224
2325
E --> J[with_certificate.py]
2426
F --> K[with_client_secret.py]
2527
H --> L[interactive.py]
26-
I --> M[with_user_creds.py]
28+
N --> P[with_device_flow.py]
29+
O --> M[with_user_creds.py]
2730
2831
style A fill:#1a73e8,color:#fff
2932
style B fill:#e8f0fe
@@ -43,6 +46,7 @@ flowchart TD
4346
| **Client secret** | `with_client_secret(client_id, secret)` | Daemons, cron jobs, CI/CD — app-only access || [`with_client_secret.py`](./with_client_secret.py) |
4447
| **Client certificate** | `with_certificate(client_id, thumbprint, key)` | Production daemons — app-only, no shared secret || [`with_client_cert.py`](./with_client_cert.py) |
4548
| **Interactive** | `with_token_interactive(client_id)` | Desktop apps, CLI tools — user signed-in || [`interactive.py`](./interactive.py) |
49+
| **Device code** | `with_device_flow(client_id)` | Headless CLI, SSH, remote servers — user visits a URL || [`with_device_flow.py`](./with_device_flow.py) |
4650
| **ROPC (password)** | `with_username_and_password(client_id, user, pass)` | Automated scripts — user context, no interactivity || [`with_user_creds.py`](./with_user_creds.py) |
4751
| **National cloud** | `AzureEnvironment.USGovernmentHigh` | GCC High, DoD, China — applies to any flow above | varies | [`gcc_high.py`](./gcc_high.py) |
4852

examples/auth/with_device_flow.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
Device code flow authentication — ideal for CLI tools and headless
3+
environments without a web browser.
4+
5+
The user authenticates by visiting a URL on another device (phone,
6+
laptop) and entering the displayed code. No interactive browser is
7+
needed on the machine running the script.
8+
9+
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
10+
"""
11+
12+
from office365.graph_client import GraphClient
13+
from tests import test_client_id, test_tenant
14+
15+
client = GraphClient(tenant=test_tenant).with_device_flow(test_client_id)
16+
me = client.me.get().execute_query()
17+
print(f"Authenticated as: {me.user_principal_name}")

office365/graph_client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,19 @@ def with_username_and_password(self, client_id: str, username: str, password: st
171171
self.pending_request().with_username_and_password(client_id, username, password)
172172
return self
173173

174+
def with_device_flow(self, client_id: str) -> Self:
175+
"""
176+
Initialize with device code flow authentication.
177+
178+
Useful for CLI tools and headless environments. The user authenticates
179+
by visiting a URL on another device and entering the displayed code.
180+
181+
Args:
182+
client_id: Application client ID
183+
"""
184+
self.pending_request().with_device_flow(client_id)
185+
return self
186+
174187
def with_transport(
175188
self,
176189
proxies: dict[str, str] | None = None,

office365/graph_request.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,22 @@ def with_username_and_password(self, client_id: str, username: str, password: st
110110
self._auth_context.with_username_and_password(client_id, username, password)
111111
return self
112112

113+
def with_device_flow(self, client_id: str) -> GraphRequest:
114+
"""
115+
Initialize with device code flow authentication.
116+
117+
Useful for CLI tools and headless environments. The user authenticates
118+
by visiting a URL on another device and entering the displayed code.
119+
120+
Args:
121+
client_id: The OAuth client ID of the calling application
122+
123+
Returns:
124+
self: Supports fluent method chaining
125+
"""
126+
self._auth_context.with_device_flow(client_id)
127+
return self
128+
113129
def authenticate_request(self, request: RequestOptions) -> None:
114130
"""
115131
Authenticate the request by adding a bearer token.

office365/runtime/auth/entra/authentication_context.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
from typing import Any, Callable, Dict, List, Optional
23

34
from typing_extensions import Self
@@ -46,6 +47,29 @@ def with_access_token(self, token_callback: Callable[[], Optional[Dict[str, Any]
4647
self._token_callback = token_callback
4748
return self
4849

50+
def with_device_flow(self, client_id: str) -> Self:
51+
"""Initializes the client via device code flow.
52+
53+
Useful for CLI tools and headless environments. The user authenticates
54+
by visiting a URL on another device and entering the displayed code.
55+
56+
Args:
57+
client_id: The OAuth client id of the calling application.
58+
"""
59+
import msal
60+
61+
app = msal.PublicClientApplication(client_id, authority=self.authority_url)
62+
63+
def _acquire_token():
64+
flow = app.initiate_device_flow(scopes=self._scopes)
65+
if "user_code" not in flow:
66+
raise ValueError(f"Failed to create device flow: {flow}")
67+
print(flow["message"])
68+
sys.stdout.flush()
69+
return app.acquire_token_by_device_flow(flow)
70+
71+
return self.with_access_token(_acquire_token)
72+
4973
def with_certificate(self, client_id: str, thumbprint: str, private_key: str):
5074
"""Initializes the confidential client with client certificate
5175

0 commit comments

Comments
 (0)