Skip to content

Commit c181f37

Browse files
committed
Reduce default delegated auth scopes
1 parent 52110e2 commit c181f37

9 files changed

Lines changed: 79 additions & 22 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ Customer admin consent:
125125
teams auth consent-url --tenant-id <customer-tenant-id-or-domain> --output json
126126
```
127127

128+
This prints a Microsoft identity platform v2 admin-consent URL with an explicit
129+
`scope` parameter for the CLI's default delegated Graph scopes.
130+
128131
For enterprises that require their own app registration, configure BYO mode:
129132

130133
```toml
@@ -175,6 +178,9 @@ teams auth login
175178
# Device code flow with OSO's public client app
176179
teams auth login --device-code
177180

181+
# Channel message reads require a broader Graph scope that often needs admin approval
182+
teams auth login --device-code --scopes "User.Read ChannelMessage.Read.All offline_access"
183+
178184
# Browser-based login with a customer-owned app
179185
teams auth login --client-id <client-id> --tenant-id <tenant-id>
180186

@@ -204,6 +210,12 @@ teams auth login --client-credentials \
204210

205211
Tokens are cached in the OS keyring — subsequent commands reuse the session without re-authentication.
206212

213+
Default delegated login asks for chat, channel-send, discovery, user lookup,
214+
and presence scopes. It intentionally does not request
215+
`ChannelMessage.Read.All`, because Microsoft marks that delegated scope as
216+
admin-consent required. Use `--scopes` or a customer-owned app when a workflow
217+
needs channel message reads.
218+
207219
**Credential resolution order**: CLI flags > environment variables > config file profiles.
208220

209221
### Why not import Teams client tokens?

docs/auth-implementation-plan.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,13 @@ Recommended initial delegated scopes:
135135
- `Team.ReadBasic.All`
136136
- `Channel.ReadBasic.All`
137137
- `ChannelMessage.Send`
138-
- `ChannelMessage.Read.All`
139138
- `Chat.ReadWrite`
140139
- `ChatMessage.Send`
141140
- `ChatMessage.Read`
142141
- `User.ReadBasic.All`
143142
- `Presence.Read.All`
144143

145-
Implementation note: split these into documented scope presets before release. The current default scope string is broad and convenient for testing, but commercial onboarding should explain exactly why each scope exists and allow lower-scope profiles where possible.
144+
Implementation note: keep the default scope string below known admin-consent-required delegated scopes where possible. `ChannelMessage.Read.All` is needed for channel message reads, but it should be requested explicitly with `--scopes` or through a customer-owned app because Microsoft marks it as admin-consent required.
146145

147146
### 2. Customer BYO Public Client App
148147

docs/auth.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,13 @@ teams auth consent-url --tenant-id <tenant-id-or-domain> --output json
6161
For the default app, this prints a URL like:
6262

6363
```text
64-
https://login.microsoftonline.com/<tenant>/adminconsent?client_id=fba1b5d0-fdd0-4fe2-9729-9ccdc38f9595
64+
https://login.microsoftonline.com/<tenant>/v2.0/adminconsent?client_id=fba1b5d0-fdd0-4fe2-9729-9ccdc38f9595&scope=...&redirect_uri=...
6565
```
6666

67+
The `scope` parameter is explicit so admin consent matches the CLI's default
68+
delegated Graph scopes instead of every static permission configured on the app
69+
registration.
70+
6771
Use a concrete tenant ID or verified tenant domain for customer onboarding. `organizations` is useful for sign-in discovery, but a customer admin consent link should normally target the customer's tenant explicitly.
6872

6973
## Current delegated permissions
@@ -76,15 +80,20 @@ offline_access
7680
Team.ReadBasic.All
7781
Channel.ReadBasic.All
7882
ChannelMessage.Send
79-
ChannelMessage.Read.All
8083
Chat.ReadWrite
8184
ChatMessage.Send
8285
ChatMessage.Read
8386
User.ReadBasic.All
8487
Presence.Read.All
8588
```
8689

87-
These permissions cover the current read/write message, team/channel discovery, chat, user lookup, and presence smoke tests. Future features may need additional consent.
90+
These permissions cover the current chat read/write, channel-send, team/channel discovery, user lookup, and presence smoke tests. The default does not include `ChannelMessage.Read.All` because Microsoft marks that delegated Graph scope as admin-consent required. Add it explicitly when a workflow needs channel message reads:
91+
92+
```bash
93+
teams auth login --device-code --scopes "User.Read ChannelMessage.Read.All offline_access"
94+
```
95+
96+
Future features may need additional consent.
8897

8998
## Login options
9099

docs/faq.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,14 @@ offline_access
4444
Team.ReadBasic.All
4545
Channel.ReadBasic.All
4646
ChannelMessage.Send
47-
ChannelMessage.Read.All
4847
Chat.ReadWrite
4948
ChatMessage.Send
5049
ChatMessage.Read
5150
User.ReadBasic.All
5251
Presence.Read.All
5352
```
5453

55-
Customers should review these during admin consent. Future features may require additional permissions.
54+
The default avoids `ChannelMessage.Read.All` because Microsoft marks that delegated Graph scope as admin-consent required. Customers that need channel message reads should grant it explicitly with `--scopes` or through a customer-owned app. Future features may require additional permissions.
5655

5756
## Why did `team list` return no teams?
5857

src/auth/auth_code_pkce.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ use std::sync::Mutex;
88
use tokio::sync::oneshot;
99

1010
use super::token::MsTokenResponse;
11+
use crate::config::{DEFAULT_DELEGATED_SCOPES, DEFAULT_REDIRECT_URI};
1112
use crate::error::{Result, TeamsError};
1213

13-
const DEFAULT_SCOPES: &str = "User.Read Team.ReadBasic.All Channel.ReadBasic.All ChannelMessage.Send ChannelMessage.Read.All Chat.ReadWrite ChatMessage.Send ChatMessage.Read User.ReadBasic.All Presence.Read.All offline_access";
14-
const REDIRECT_URI: &str = "http://localhost:8400/callback";
15-
1614
fn random_urlsafe_bytes(len: usize) -> Result<String> {
1715
let mut bytes = vec![0u8; len];
1816
getrandom::fill(&mut bytes)
@@ -37,7 +35,7 @@ pub async fn authenticate(
3735
tenant_id: &str,
3836
scopes: Option<&str>,
3937
) -> Result<MsTokenResponse> {
40-
let scopes = scopes.unwrap_or(DEFAULT_SCOPES);
38+
let scopes = scopes.unwrap_or(DEFAULT_DELEGATED_SCOPES);
4139
let (verifier, challenge) = generate_pkce()?;
4240

4341
let state = random_urlsafe_bytes(16)?;
@@ -51,7 +49,7 @@ pub async fn authenticate(
5149
&state={state}\
5250
&code_challenge={challenge}\
5351
&code_challenge_method=S256",
54-
urlencoding::encode(REDIRECT_URI),
52+
urlencoding::encode(DEFAULT_REDIRECT_URI),
5553
urlencoding::encode(scopes),
5654
);
5755

@@ -150,7 +148,7 @@ pub async fn authenticate(
150148
("grant_type", "authorization_code"),
151149
("client_id", client_id),
152150
("code", &code),
153-
("redirect_uri", REDIRECT_URI),
151+
("redirect_uri", DEFAULT_REDIRECT_URI),
154152
("code_verifier", &verifier),
155153
("scope", scopes),
156154
])

src/auth/device_code.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ use serde::Deserialize;
33
use std::time::Duration;
44

55
use super::token::MsTokenResponse;
6+
use crate::config::DEFAULT_DELEGATED_SCOPES;
67
use crate::error::{Result, TeamsError};
78

8-
const DEFAULT_SCOPES: &str = "User.Read Team.ReadBasic.All Channel.ReadBasic.All ChannelMessage.Send ChannelMessage.Read.All Chat.ReadWrite ChatMessage.Send ChatMessage.Read User.ReadBasic.All Presence.Read.All offline_access";
9-
109
#[derive(Debug, Deserialize)]
1110
struct DeviceCodeResponse {
1211
device_code: String,
@@ -40,7 +39,7 @@ pub async fn authenticate(
4039
tenant_id: &str,
4140
scopes: Option<&str>,
4241
) -> Result<MsTokenResponse> {
43-
let scopes = scopes.unwrap_or(DEFAULT_SCOPES);
42+
let scopes = scopes.unwrap_or(DEFAULT_DELEGATED_SCOPES);
4443
let http = Client::new();
4544

4645
// Step 1: Request device code

src/cli/auth.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,31 @@ fn token_warnings(claims: Option<&auth::token::TokenClaims>) -> Vec<String> {
110110
warnings
111111
}
112112

113+
fn graph_admin_consent_scopes(scopes: &str) -> String {
114+
scopes
115+
.split_whitespace()
116+
.map(|scope| {
117+
if scope.starts_with("https://")
118+
|| matches!(scope, "openid" | "profile" | "email" | "offline_access")
119+
{
120+
scope.to_string()
121+
} else {
122+
format!("https://graph.microsoft.com/{scope}")
123+
}
124+
})
125+
.collect::<Vec<_>>()
126+
.join(" ")
127+
}
128+
129+
fn delegated_admin_consent_url(client_id: &str, tenant_id: &str) -> String {
130+
let scopes = graph_admin_consent_scopes(config::DEFAULT_DELEGATED_SCOPES);
131+
format!(
132+
"https://login.microsoftonline.com/{tenant_id}/v2.0/adminconsent?client_id={client_id}&scope={}&redirect_uri={}",
133+
urlencoding::encode(&scopes),
134+
urlencoding::encode(config::DEFAULT_REDIRECT_URI)
135+
)
136+
}
137+
113138
pub async fn run(
114139
cmd: AuthCommand,
115140
config: &ConfigFile,
@@ -190,13 +215,13 @@ pub async fn run(
190215
config::resolve_delegated_client_id(client_id.as_deref(), profile, config)?;
191216
let tenant_id =
192217
config::resolve_delegated_tenant_id(tenant_id.as_deref(), profile, config);
193-
let url = format!(
194-
"https://login.microsoftonline.com/{tenant_id}/adminconsent?client_id={client_id}"
195-
);
218+
let url = delegated_admin_consent_url(&client_id, &tenant_id);
196219
let msg = serde_json::json!({
197220
"admin_consent_url": url,
198221
"client_id": client_id,
199222
"tenant_id": tenant_id,
223+
"scope": config::DEFAULT_DELEGATED_SCOPES,
224+
"redirect_uri": config::DEFAULT_REDIRECT_URI,
200225
});
201226
output::print_success(format, &msg, start);
202227
Ok(())
@@ -220,12 +245,15 @@ pub async fn run(
220245
let token = auth::resolve_token(profile).ok();
221246
let claims = token.as_ref().and_then(|t| t.unverified_claims());
222247
let warnings = token_warnings(claims.as_ref());
248+
let admin_consent_url = delegated_admin_consent_url(&client_id, &tenant_id);
223249
let msg = serde_json::json!({
224250
"profile": profile,
225251
"auth_app": auth_app,
226252
"client_id": client_id,
227253
"tenant_id": tenant_id,
228-
"admin_consent_url": format!("https://login.microsoftonline.com/{tenant_id}/adminconsent?client_id={client_id}"),
254+
"admin_consent_url": admin_consent_url,
255+
"default_delegated_scopes": config::DEFAULT_DELEGATED_SCOPES,
256+
"redirect_uri": config::DEFAULT_REDIRECT_URI,
229257
"authenticated": token.is_some(),
230258
"warnings": warnings,
231259
"token": token.as_ref().map(|t| serde_json::json!({

src/config.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use crate::error::{Result, TeamsError};
88

99
pub const OSO_PUBLIC_CLIENT_ID: &str = "fba1b5d0-fdd0-4fe2-9729-9ccdc38f9595";
1010
pub const DEFAULT_DELEGATED_TENANT_ID: &str = "organizations";
11+
pub const DEFAULT_DELEGATED_SCOPES: &str = "User.Read Team.ReadBasic.All Channel.ReadBasic.All ChannelMessage.Send Chat.ReadWrite ChatMessage.Send ChatMessage.Read User.ReadBasic.All Presence.Read.All offline_access";
12+
pub const DEFAULT_REDIRECT_URI: &str = "http://localhost:8400/callback";
1113

1214
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1315
pub struct ConfigFile {
@@ -404,6 +406,13 @@ auth_flow = "device-code"
404406
);
405407
}
406408

409+
#[test]
410+
fn default_delegated_scopes_avoid_admin_required_channel_read() {
411+
assert!(DEFAULT_DELEGATED_SCOPES.contains("ChatMessage.Send"));
412+
assert!(DEFAULT_DELEGATED_SCOPES.contains("ChannelMessage.Send"));
413+
assert!(!DEFAULT_DELEGATED_SCOPES.contains("ChannelMessage.Read.All"));
414+
}
415+
407416
#[test]
408417
fn delegated_auth_byo_requires_client_id() {
409418
let mut config = ConfigFile::default();

tests/cli.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@ fn auth_consent_url_uses_oso_default_client_id() {
6464
.success()
6565
.stdout(
6666
predicate::str::contains("fba1b5d0-fdd0-4fe2-9729-9ccdc38f9595")
67-
.and(predicate::str::contains("adminconsent"))
68-
.and(predicate::str::contains("organizations")),
67+
.and(predicate::str::contains("v2.0/adminconsent"))
68+
.and(predicate::str::contains("scope="))
69+
.and(predicate::str::contains("redirect_uri="))
70+
.and(predicate::str::contains("ChatMessage.Send"))
71+
.and(predicate::str::contains("organizations"))
72+
.and(predicate::str::contains("ChannelMessage.Read.All").not()),
6973
);
7074
}
7175

0 commit comments

Comments
 (0)