11 KiB
Ghi chú tránh lặp lại sai sót — Entra App + Microsoft Graph app-only cho SharePoint Ingestion
1) Tóm tắt sự cố đã gặp
Hiện tượng
- Lấy token thành công bằng
client_credentials. - Token có các claim hợp lệ kiểu app-only:
aud = https://graph.microsoft.comidtyp = app
- Nhưng gọi các API Graph như:
GET /sites/{hostname}GET /sites/{hostname}:/{server-relative-path}đều bị 401 Unauthorized.
Nguyên nhân gốc
App Registration đã được cấp Delegated permissions thay vì Application permissions.
Cụ thể lúc xảy ra lỗi, app đang có kiểu quyền như:
Files.Read.All→ DelegatedSites.Read.All→ DelegatedUser.Read→ Delegated
Trong khi luồng đang dùng là:
- OAuth 2.0 client credentials flow
- app-only / daemon / service-to-service
- không có user đăng nhập interactive
=> Kết quả: token app-only không có roles, nên Microsoft Graph không chấp nhận token cho các API cần application permissions.
2) Bài học rút ra (phải kiểm tra trước khi test Graph)
Quy tắc số 1
Nếu dùng client credentials flow thì bắt buộc phải cấp quyền ở dạng:
- Microsoft Graph → Application permissions
Không dùng Delegated permissions cho bài toán ingestion app-only.
Quy tắc số 2
Sau khi sửa permission trong Entra App:
- Bấm Grant admin consent
- Lấy token mới hoàn toàn
- Decode token và kiểm tra
rolestrước khi gọi Graph
Quy tắc số 3
Nếu token app-only không có roles, thì không test Graph tiếp.
Phải quay lại kiểm tra:
- Permission type có phải Application không
- Có phải Microsoft Graph không
- Đã Grant admin consent chưa
- Có cần đợi propagation vài phút không
3) Cấu hình đúng cho PoC hiện tại
Tối thiểu cần có trong Entra App
Microsoft Graph → Application permissions
Sites.Read.AllFiles.Read.All
Không cần cho app-only ingestion hiện tại
User.Read(Delegated)- Interactive login
- Redirect URI cho user login
4) Checklist fail-fast trước khi test Graph
Checklist nhanh
- App dùng client credentials flow
- API permissions nằm dưới Microsoft Graph
- Permission type là Application
- Có
Sites.Read.All - Có
Files.Read.All - Đã Grant admin consent
- Đã lấy token mới sau khi sửa permission
- Token decode ra có
roles roleschứa ít nhất:Sites.Read.AllFiles.Read.All
Nếu một trong các dòng trên fail, không chạy tiếp phần test Graph site/drive.
5) Script test hoàn chỉnh (đã thêm guard để tránh lặp lỗi)
Script này làm đúng 4 việc:
- Lấy token mới
- Decode token
- Kiểm tra
rolestrước- Chỉ khi roles đúng mới test Graph site endpoints
$ErrorActionPreference = "Stop"
# ==========================================
# Điền thông tin tại đây
# ==========================================
$TenantId = "<TENANT_ID_GUID>"
$ClientId = "<CLIENT_ID_GUID>"
$ClientSecret = "<CLIENT_SECRET_VALUE>"
$SharePointHost = "285pdg.sharepoint.com"
$SitePath = "/sites/poc_system"
# ==========================================
function Get-GraphToken {
param(
[string]$TenantId,
[string]$ClientId,
[string]$ClientSecret
)
$tokenUrl = "https://login.microsoftonline.com/" + $TenantId + "/oauth2/v2.0/token"
$body = @{
client_id = $ClientId
client_secret = $ClientSecret
scope = "https://graph.microsoft.com/.default"
grant_type = "client_credentials"
}
return Invoke-RestMethod `
-Method Post `
-Uri $tokenUrl `
-ContentType "application/x-www-form-urlencoded" `
-Body $body
}
function Decode-JwtPayload {
param([string]$Jwt)
$parts = $Jwt.Split('.')
if ($parts.Length -lt 2) {
throw "JWT không hợp lệ."
}
$payload = $parts[1]
switch ($payload.Length % 4) {
2 { $payload += '==' }
3 { $payload += '=' }
}
$payload = $payload.Replace('-', '+').Replace('_', '/')
$bytes = [System.Convert]::FromBase64String($payload)
$json = [System.Text.Encoding]::UTF8.GetString($bytes)
return $json | ConvertFrom-Json
}
function Invoke-GraphGet {
param(
[string]$Url,
[string]$AccessToken,
[string]$Label
)
Write-Host ""
Write-Host ("=== " + $Label + " ===") -ForegroundColor Cyan
Write-Host $Url -ForegroundColor DarkGray
$headers = @{
Authorization = "Bearer " + $AccessToken
}
try {
$resp = Invoke-WebRequest `
-Method Get `
-Uri $Url `
-Headers $headers `
-UseBasicParsing
Write-Host "OK" -ForegroundColor Green
Write-Host ("HTTP " + [int]$resp.StatusCode) -ForegroundColor Green
if ($resp.Content) {
$json = $resp.Content | ConvertFrom-Json
$json | ConvertTo-Json -Depth 10
return $json
}
return $null
}
catch {
Write-Host "FAILED" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
if ($_.Exception.Response -ne $null) {
$resp = $_.Exception.Response
Write-Host ""
Write-Host "Status code:" -ForegroundColor Yellow
try {
Write-Host ([int]$resp.StatusCode)
}
catch {
Write-Host "Không đọc được StatusCode"
}
Write-Host ""
Write-Host "WWW-Authenticate:" -ForegroundColor Yellow
try {
Write-Host $resp.Headers["WWW-Authenticate"]
}
catch {
Write-Host "Không có WWW-Authenticate header"
}
Write-Host ""
Write-Host "Response body:" -ForegroundColor Yellow
try {
$stream = $resp.GetResponseStream()
$reader = New-Object System.IO.StreamReader($stream)
$bodyText = $reader.ReadToEnd()
Write-Host $bodyText
}
catch {
Write-Host "Không đọc được response detail."
}
}
return $null
}
}
# =======================================================
# 1) Lấy token mới
# =======================================================
$tokenResponse = Get-GraphToken `
-TenantId $TenantId `
-ClientId $ClientId `
-ClientSecret $ClientSecret
$accessToken = $tokenResponse.access_token
Write-Host "TOKEN OK" -ForegroundColor Green
Write-Host ("token_type : " + $tokenResponse.token_type)
Write-Host ("expires_in : " + $tokenResponse.expires_in)
# =======================================================
# 2) Decode token + kiểm tra claims quan trọng
# =======================================================
$claims = Decode-JwtPayload -Jwt $accessToken
Write-Host ""
Write-Host "=== TOKEN CLAIMS ===" -ForegroundColor Cyan
Write-Host ("aud : " + $claims.aud)
Write-Host ("appid : " + $claims.appid)
if ($claims.PSObject.Properties.Name -contains "idtyp") {
Write-Host ("idtyp : " + $claims.idtyp)
}
$roles = @()
if ($claims.PSObject.Properties.Name -contains "roles") {
$roles = @($claims.roles)
Write-Host "roles:" -ForegroundColor Yellow
$roles | ForEach-Object { Write-Host (" - " + $_) }
}
else {
Write-Host "roles: <KHÔNG CÓ>" -ForegroundColor Red
}
# =======================================================
# 3) Guard bắt buộc: nếu không có roles đúng thì stop luôn
# =======================================================
$hasSitesRead = $roles -contains "Sites.Read.All"
$hasFilesRead = $roles -contains "Files.Read.All"
if (-not $hasSitesRead) {
Write-Host ""
Write-Host "STOP: Token chưa có role 'Sites.Read.All'." -ForegroundColor Red
Write-Host "=> Kiểm tra lại Microsoft Graph -> Application permissions -> Sites.Read.All -> Grant admin consent." -ForegroundColor Yellow
return
}
if (-not $hasFilesRead) {
Write-Host ""
Write-Host "WARNING: Token chưa có role 'Files.Read.All'." -ForegroundColor Yellow
Write-Host "=> Site test có thể pass, nhưng drive/delta về sau sẽ fail." -ForegroundColor Yellow
}
# =======================================================
# 4) TEST 1 - GET /sites/{hostname}
# =======================================================
$rootByHostUrl = "https://graph.microsoft.com/v1.0/sites/" + $SharePointHost + "?`$select=id,webUrl"
$rootSite = Invoke-GraphGet `
-Url $rootByHostUrl `
-AccessToken $accessToken `
-Label "TEST 1 - GET /sites/{hostname}"
# =======================================================
# 5) TEST 2 - GET /sites/{hostname}:/{server-relative-path}
# =======================================================
$siteRef = $SharePointHost + ":" + $SitePath
$resolveSiteUrl = "https://graph.microsoft.com/v1.0/sites/" + $siteRef + "?`$select=id,displayName,webUrl"
$resolvedSite = Invoke-GraphGet `
-Url $resolveSiteUrl `
-AccessToken $accessToken `
-Label "TEST 2 - GET /sites/{hostname}:/{server-relative-path}"
# =======================================================
# 6) Summary
# =======================================================
Write-Host ""
Write-Host "=== SUMMARY ===" -ForegroundColor Cyan
if ($null -ne $rootSite) {
Write-Host "TEST 1: PASS" -ForegroundColor Green
}
else {
Write-Host "TEST 1: FAIL" -ForegroundColor Red
}
if ($null -ne $resolvedSite) {
Write-Host "TEST 2: PASS" -ForegroundColor Green
}
else {
Write-Host "TEST 2: FAIL" -ForegroundColor Red
}
6) Kỳ vọng đúng sau khi sửa quyền
Token claim đúng
aud : https://graph.microsoft.com
appid : <app-id>
idtyp : app
roles:
- Sites.Read.All
- Files.Read.All
Test site đúng
TEST 1 - GET /sites/{hostname}→ PASSTEST 2 - GET /sites/{hostname}:/{server-relative-path}→ PASS
7) Ghi chú vận hành nội bộ
Khi nào được đi tiếp sang ingestion skeleton?
Chỉ khi đạt đủ 3 điều kiện:
- Token có
roles TEST 1passTEST 2pass
Nếu token vẫn không có roles
- Kiểm tra lại Type của permission có phải Application không
- Kiểm tra lại permission có nằm dưới Microsoft Graph không
- Kiểm tra đã bấm Grant admin consent chưa
- Đợi vài phút rồi lấy token mới lần nữa
8) Tên lỗi nội bộ để nhớ
Sai lầm đã gặp: “Dùng
client_credentialsnhưng lại cấp Delegated permissions trong App Registration.”
Cách nhớ nhanh
- App-only → Application permissions
- Có user login → Delegated permissions