Token Bearer pour les APIs Microsoft en PowerShell
Intro
Chaque API Microsoft (Graph, ARM, Azure DevOps, Key Vault, SharePoint…) exige un Bearer token dans le header HTTP de chaque requête. Ce token prouve l'identité de l'appelant et les droits associés. Deux contraintes s'ajoutent :
- Chaque API attend un token lié à sa propre ressource OAuth2 — un token Graph ne peut pas appeler ARM.
- Les tokens expirent (en général après 1 heure) et doivent être renouvelés.
Get-Faitza_Modul_Headers centralise cette logique. On lui passe un alias simple ("graph", "arm",
"azdevops"…) et elle retourne un hashtable de headers prêt à être passé à Invoke-RestMethod.
Le token est obtenu via Azure CLI, mis en cache en session, et renouvelé automatiquement avant expiration.
La fonction s'appuie sur Azure CLI pour récupérer le token. Une session authentifiée est nécessaire :
- En développement :
az login - Avec un Service Principal :
az login --service-principal -u <appId> -p <secret> --tenant <tenantId> - Sur une VM/runbook avec Managed Identity :
az login --identity
La fonction
Le module complet est dans Faitza_Function/Faitza_Modul/Faitza_Modul.ps1.
Il contient deux fonctions : Get-Faitza_Modul_Headers (tokens) et Connect-Faitza_Modul_ExchangeOnline (Exchange Online).
function Get-Faitza_Modul_Headers {
[CmdletBinding()]
param (
[string]$resource,
[string]$contenttype = "json",
[string]$tenant = "faitza",
[switch]$ForceNewToken
)
function Get-Faitza_Modul_Token ($resource, $tenant) {
Write-Host "$($PSStyle.Background.Red)>>> Get-Faitza_Modul_Headers <<<$($PSStyle.Reset)"
switch ($resource.ToLower()) {
"graph" { $resourceValue = "https://graph.microsoft.com" }
"purview" { $resourceValue = "b26e684c-5068-4120-a679-64a5d2c909d9" }
"arm" { $resourceValue = "https://management.azure.com" }
"azdevops" { $resourceValue = "499b84ac-1321-427f-aa17-267ca6975798" }
"exchange" { $resourceValue = "https://outlook.office365.com" }
"sharepoint" { $resourceValue = "https://$tenant.sharepoint.com" }
"keyvault" { $resourceValue = "https://vault.azure.net" }
"storage" { $resourceValue = "https://storage.azure.com" }
"loganalytics" { $resourceValue = "https://api.loganalytics.io" }
"security" { $resourceValue = "https://api.security.microsoft.com" }
"monitor" { $resourceValue = "https://monitor.azure.com" }
default {
if ($resource -match "management.azure.com") { $resourceValue = "https://management.azure.com" }
elseif ($resource.StartsWith("http") -or $resource.StartsWith("api://") -or $resource -match "^[a-f0-9]{8}-") {
$resourceValue = $resource
}
else { throw "Resource inconnue : $resource" }
}
}
try {
$azTokenJson = az account get-access-token --resource $resourceValue --output json | ConvertFrom-Json
$TokenBrut = $azTokenJson.accessToken
$unixEpoch = [DateTime]::new(1970, 1, 1, 0, 0, 0, 0, [DateTimeKind]::Utc)
$expiresOnUtc = $unixEpoch.AddSeconds($azTokenJson.expires_on)
$tzParis = [System.TimeZoneInfo]::FindSystemTimeZoneById("Romance Standard Time")
$expiresOnParis = [System.TimeZoneInfo]::ConvertTimeFromUtc($expiresOnUtc, $tzParis)
} catch {
throw "Erreur 'az account get-access-token' pour $resourceValue : $_"
}
try {
$payload = $TokenBrut.Split('.')[1]
$payload = $payload.Replace('-', '+').Replace('_', '/')
while (($payload.Length % 4) -ne 0) { $payload += "=" }
$TokenData = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload)) | ConvertFrom-Json
Write-Host "--------------------------------------------------"
if ($resource.ToLower() -eq "graph") {
if ($TokenData.roles) { Write-Host "Permissions App (Roles) : $($TokenData.roles -join ', ')" }
if ($TokenData.scp) { Write-Host "Permissions User (Scopes) : $($TokenData.scp)" }
} else {
Write-Host "Permissions App (Roles) : $resource"
Write-Host "> $resourceValue"
}
Write-Host "Token expires (Paris) : $($expiresOnParis.ToString('yyyy-MM-dd HH:mm:ss'))"
if ("api" -notin $resourceValue) {
Write-Host "App ID : $($TokenData.appid.Insert(1, [char]0x200B))"
}
Write-Host "--------------------------------------------------"
} catch {
Write-Host "Impossible de lire les permissions du token."
} finally {
Write-Host "$($PSStyle.Background.Red)<<< Get-Faitza_Modul_Headers >>>$($PSStyle.Reset)"
}
return @{
resource = $resourceValue
AccessToken = $TokenBrut
Expireson = $expiresOnParis
}
}
try {
if ([string]::IsNullOrWhiteSpace($resource)) { throw "Paramètre -resource manquant." }
$global:Faitza_token ??= @{}
$resKey = $resource.ToLower()
$needsNewToken = $ForceNewToken -or
-not $global:Faitza_token.ContainsKey($resKey) -or
$global:Faitza_token[$resKey].Expireson -lt (
[System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId(
(Get-Date).ToUniversalTime(), "Romance Standard Time"
).AddMinutes(5)
)
if ($needsNewToken) {
if ($ForceNewToken) {
Write-Host "$($PSStyle.Foreground.BrightYellow)Nettoyage du cache Azure CLI...$($PSStyle.Reset)"
@(
"C:\Users\svc_faitza_infra$\.Azure",
"C:\Users\svc_faitza_infra$\AppData\Local\.IdentityService"
) | Where-Object { Test-Path $_ } | ForEach-Object {
Remove-Item -Path $_ -Recurse -Force -ErrorAction SilentlyContinue
}
}
$global:Faitza_token[$resKey] = Get-Faitza_Modul_Token -resource $resource -tenant $tenant
}
$headers = @{
Authorization = "Bearer $($global:Faitza_token[$resKey].AccessToken)"
"Content-Type" = "application/$contenttype"
}
if ($resKey -eq "graph") { $headers["ConsistencyLevel"] = "eventual" }
if ($resKey -eq "purview") { $headers["X-AllowWithAADToken"] = "true" }
return $headers
} catch {
throw "Impossible d'obtenir un token pour '$resource' : $_"
}
}
Ressources supportées
Le paramètre -resource accepte des alias courts. La fonction les traduit vers la ressource OAuth2 attendue par Azure CLI.
Le paramètre -tenant (défaut : "faitza") est utilisé uniquement pour l'alias sharepoint.
# Alias disponibles
Get-Faitza_Modul_Headers -resource "graph" # → https://graph.microsoft.com
Get-Faitza_Modul_Headers -resource "arm" # → https://management.azure.com
Get-Faitza_Modul_Headers -resource "azdevops" # → 499b84ac-1321-427f-aa17-267ca6975798
Get-Faitza_Modul_Headers -resource "keyvault" # → https://vault.azure.net
Get-Faitza_Modul_Headers -resource "exchange" # → https://outlook.office365.com
Get-Faitza_Modul_Headers -resource "sharepoint" # → https://<tenant>.sharepoint.com
Get-Faitza_Modul_Headers -resource "storage" # → https://storage.azure.com
Get-Faitza_Modul_Headers -resource "loganalytics" # → https://api.loganalytics.io
Get-Faitza_Modul_Headers -resource "security" # → https://api.security.microsoft.com
Get-Faitza_Modul_Headers -resource "purview" # → GUID Purview (+ header X-AllowWithAADToken)
Get-Faitza_Modul_Headers -resource "monitor" # → https://monitor.azure.com
# Ressource personnalisée (URL, api://, ou GUID Entra)
Get-Faitza_Modul_Headers -resource "https://mon-api.example.com"
Get-Faitza_Modul_Headers -resource "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Deux headers supplémentaires sont ajoutés automatiquement selon la ressource :
- graph →
ConsistencyLevel: eventual— requis pour les requêtes$countet$searchsur Graph. - purview →
X-AllowWithAADToken: true— requis dans certains scénarios eDiscovery.
Pour le Content-Type, la valeur par défaut est application/json.
Certaines APIs (Azure DevOps PATCH, Graph batch) attendent un type différent :
# Créer / modifier un work item Azure DevOps — Content-Type json-patch+json obligatoire
$headers = Get-Faitza_Modul_Headers -resource "azdevops" -contenttype "json-patch+json"
Cache & ForceNewToken
Les tokens sont stockés dans $global:Faitza_token, un hashtable indexé par alias de ressource.
À chaque appel, la fonction vérifie si le token en cache expire dans moins de 5 minutes.
Si oui (ou s'il n'existe pas), elle en demande un nouveau.
$global:Faitza_token n'est pas persisté sur disque. À chaque ouverture de terminal, le cache repart à zéro.
C'est voulu : un access token ne doit pas être stocké durablement.
Le switch -ForceNewToken force le renouvellement même si le token en cache est encore valide.
Il efface aussi les dossiers .Azure et .IdentityService du compte de service
pour repartir d'une identité propre — utile quand Azure CLI a mis en cache des credentials corrompus.
# Forcer le renouvellement du token et vider le cache Azure CLI
$headers = Get-Faitza_Modul_Headers -resource "graph" -ForceNewToken
Exchange Online — cas particulier
Exchange Online ne fonctionne pas comme les autres APIs Microsoft.
Il n'existe pas d'endpoint REST pour les opérations d'administration Exchange (boîtes partagées, délégations, quotas…) :
une connexion au module ExchangeOnlineManagement est toujours obligatoire.
Connect-Faitza_Modul_ExchangeOnline (définie dans le même fichier Faitza_Modul.ps1)
gère cette connexion via un token Azure CLI ciblant la ressource outlook.office365.com —
distincte de la ressource Graph.
→ Article dédié : Connexion Exchange Online — pourquoi pas l'API REST ?
Aller plus loin
Get-Faitza_Modul_Headers est la brique de base de tous les modules Faitza_*.
Voici les articles qui l'utilisent en pratique :
-
Entra ID — utilisateurs, MFA, groupes via Microsoft Graph
— utilise
-resource "graph" -
SharePoint — fichiers et listes via Microsoft Graph
— utilise
-resource "graph"(ou-resource "sharepoint" -tenant "…") -
Azure — inventaire infra : VMs, NSG, Resource Graph, Key Vault
— utilise
-resource "arm"et-resource "keyvault" -
Azure DevOps — automatiser tickets et work items
— utilise
-resource "azdevops"avec-contenttype "json-patch+json" -
Intune — audit appareils, politiques et conformité
— utilise
-resource "graph"sur/deviceManagement/ -
Exports Excel et emails HTML via Microsoft Graph
— utilise
-resource "graph"pour/users/{id}/sendMail -
eDiscovery Microsoft 365 — exporter boîtes mail et OneDrive
— utilise
-resource "purview"
Pièges & bonnes pratiques
Un token obtenu avec -resource "graph" ne peut pas être utilisé pour appeler
management.azure.com — et vice versa. Chaque ressource OAuth2 génère un token
distinct. La fonction gère ce cloisonnement par alias.
À chaque nouveau token, la fonction décode le JWT et affiche les permissions (roles / scp),
l'App ID et la date d'expiration. C'est utile pour diagnostiquer immédiatement une erreur 403 :
si la permission manquante n'est pas dans la liste affichée, le problème vient du compte ou de l'application, pas du script.
En production (runbook, tâche planifiée), la session Azure CLI doit être pré-authentifiée
avant l'exécution du script — az login --identity sur une VM avec Managed Identity
ou az login --service-principal dans un pipeline.
// Commentaires
Aucun commentaire pour l'instant.