Skip to content

Commit e0509bf

Browse files
committed
In the name of SPEEEEEEEED
1 parent 89f8894 commit e0509bf

20 files changed

Lines changed: 1449 additions & 126 deletions

.github/instructions/auth-model.instructions.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ New-GraphGetRequest / New-ExoRequest / New-TeamsRequest / etc.
3030
3131
Get-GraphToken($tenantid, $scope, $AsApp)
3232
33-
├─ Check in-memory cache: $script:AccessTokens["{tenantid}-{scope}-{asApp}"]
33+
├─ Check process-wide .NET cache: [CIPP.CIPPTokenCache]::Lookup(key, 120)
3434
│ └─ Hit + not expired → return cached token
3535
3636
├─ Determine grant type:
@@ -43,7 +43,7 @@ New-GraphGetRequest / New-ExoRequest / New-TeamsRequest / etc.
4343
4444
└─ POST to login.microsoftonline.com/{tenantid}/oauth2/v2.0/token
4545
46-
└─ Cache result in $script:AccessTokens with expires_on
46+
└─ Cache result via [CIPP.CIPPTokenCache]::Store(key, json, expiresOn)
4747
```
4848

4949
The `-tenantid` parameter **drives token acquisition**, not just filtering. It determines which customer tenant the token is issued for.
@@ -116,10 +116,11 @@ Customer provides their own refresh token, stored in Key Vault per-tenant (keyed
116116

117117
## Token caching
118118

119-
Tokens are cached in `$script:AccessTokens` — a synchronized hashtable keyed by `{tenantid}-{scope}-{asApp}`.
119+
Tokens are cached in `[CIPP.CIPPTokenCache]` — a process-wide `ConcurrentDictionary` backed by a static .NET class in `Shared/CIPPHttp/CIPPHttpClient.cs`.
120120

121-
- **Per-runspace**: Not shared across Azure Functions instances
122-
- **Expiry-aware**: Checks `expires_on` (Unix timestamp) before returning cached token
121+
- **Process-wide**: Shared across all runspaces in the worker process (unlike the old `$script:AccessTokens` which was per-runspace)
122+
- **Cache key**: Built via `[CIPP.CIPPTokenCache]::BuildKey($tenantid, $scope, $asApp, $clientId, $grantType)`
123+
- **Expiry-aware**: `Lookup()` accepts a buffer (seconds) and returns `$false` for expired or soon-to-expire tokens
123124
- **Auto-refresh**: Expired tokens trigger automatic re-acquisition — no manual refresh needed
124125
- **Skip cache**: Pass `-SkipCache $true` to force a fresh token (rare, for debugging)
125126

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ SendNotifications/config.json
1313
Output/
1414
node_modules/.yarn-integrity
1515
yarn.lock
16+
Shared/CIPPHttp/obj/
1617

1718
# Cursor IDE
1819
.cursor/rules

Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TokenWarmupTimer.ps1

Lines changed: 0 additions & 32 deletions
This file was deleted.

Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,23 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $AppSecret, $refreshT
66
if (!$scope) { $scope = 'https://graph.microsoft.com/.default' }
77
if (!$tenantid) { $tenantid = $env:TenantID }
88

9-
# ── Fast path: return cached token immediately, skip all table lookups ──
10-
$TokenKey = '{0}-{1}-{2}' -f $tenantid, $scope, $asApp
11-
if ($SkipCache -ne $true -and $script:AccessTokens.$TokenKey -and [int](Get-Date -UFormat %s -Millisecond 0) -lt $script:AccessTokens.$TokenKey.expires_on) {
12-
$AccessToken = $script:AccessTokens.$TokenKey
13-
if ($ReturnRefresh) { return $AccessToken }
14-
return @{ Authorization = "Bearer $($AccessToken.access_token)" }
9+
$UseSharedTokenCache = ($SkipCache -ne $true) -and ($null -ne ('CIPP.CIPPTokenCache' -as [type]))
10+
11+
# ── Fast path: check shared .NET token cache before any table lookups ──
12+
if ($UseSharedTokenCache) {
13+
$CacheClientId = if ($AppID) { [string]$AppID } else { [string]$env:ApplicationID }
14+
$GrantType = if ($asApp -eq $true -or ($null -ne $AppID -and $null -ne $AppSecret)) { 'client_credentials' } else { 'refresh_token' }
15+
$SharedTokenCacheKey = [CIPP.CIPPTokenCache]::BuildKey([string]$tenantid, [string]$scope, [bool]$asApp, $CacheClientId, $GrantType)
16+
$SharedCacheEntry = [CIPP.CIPPTokenCache]::Lookup($SharedTokenCacheKey, 120)
17+
if ($SharedCacheEntry.Found -and -not [string]::IsNullOrWhiteSpace($SharedCacheEntry.TokenPayloadJson)) {
18+
try {
19+
$AccessToken = $SharedCacheEntry.TokenPayloadJson | ConvertFrom-Json -ErrorAction Stop
20+
if ($ReturnRefresh) { return $AccessToken }
21+
return @{ Authorization = "Bearer $($AccessToken.access_token)" }
22+
} catch {
23+
[CIPP.CIPPTokenCache]::Remove($SharedTokenCacheKey)
24+
}
25+
}
1526
}
1627

1728
# ── Slow path: need a new token — do table lookups + token acquisition ──
@@ -98,30 +109,31 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $AppSecret, $refreshT
98109
}
99110
}
100111

112+
# Rebuild cache key after credential loading (env vars may have been set by Get-CIPPAuthentication)
113+
if ($UseSharedTokenCache) {
114+
$CacheClientId = if ($AppID) { [string]$AppID } else { [string]$env:ApplicationID }
115+
$GrantType = if ($asApp -eq $true -or ($null -ne $AppID -and $null -ne $AppSecret)) { 'client_credentials' } else { 'refresh_token' }
116+
$SharedTokenCacheKey = [CIPP.CIPPTokenCache]::BuildKey([string]$tenantid, [string]$scope, [bool]$asApp, $CacheClientId, $GrantType)
117+
}
101118

102119
try {
103-
$TokenRequest = @{
104-
Method = 'POST'
105-
Uri = "https://login.microsoftonline.com/$($tenantid)/oauth2/v2.0/token"
106-
Body = $Authbody
107-
ErrorAction = 'Stop'
120+
$AccessToken = (Invoke-CIPPRestMethod -Method post -Uri "https://login.microsoftonline.com/$($tenantid)/oauth2/v2.0/token" -Body $Authbody -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop)
121+
if ($null -eq $AccessToken.expires_on -and $AccessToken.expires_in) {
122+
$ExpiresOn = [int](Get-Date -UFormat %s -Millisecond 0) + $AccessToken.expires_in
123+
Add-Member -InputObject $AccessToken -NotePropertyName 'expires_on' -NotePropertyValue $ExpiresOn -Force
108124
}
109-
if ($script:LoginWebSession) {
110-
$TokenRequest.WebSession = $script:LoginWebSession
111-
} else {
112-
$TokenRequest.SessionVariable = 'NewLoginSession'
113-
}
114-
$AccessToken = (Invoke-RestMethod @TokenRequest)
115-
if (!$script:LoginWebSession -and $NewLoginSession) {
116-
$script:LoginWebSession = $NewLoginSession
125+
126+
if ($UseSharedTokenCache -and $SharedTokenCacheKey) {
127+
try {
128+
$TokenPayloadJson = $AccessToken | ConvertTo-Json -Depth 20 -Compress
129+
[CIPP.CIPPTokenCache]::Store($SharedTokenCacheKey, $TokenPayloadJson, [int64]$AccessToken.expires_on)
130+
} catch {
131+
# Ignore shared cache write failures
132+
}
117133
}
118-
$ExpiresOn = [int](Get-Date -UFormat %s -Millisecond 0) + $AccessToken.expires_in
119-
Add-Member -InputObject $AccessToken -NotePropertyName 'expires_on' -NotePropertyValue $ExpiresOn
120-
if (!$script:AccessTokens) { $script:AccessTokens = [HashTable]::Synchronized(@{}) }
121-
$script:AccessTokens.$TokenKey = $AccessToken
122134

123-
if ($ReturnRefresh) { $header = $AccessToken } else { $header = @{ Authorization = "Bearer $($AccessToken.access_token)" } }
124-
return $header
135+
if ($ReturnRefresh) { return $AccessToken }
136+
return @{ Authorization = "Bearer $($AccessToken.access_token)" }
125137
} catch {
126138
# Track consecutive Graph API failures
127139
$TenantsTable = Get-CippTable -tablename Tenants

Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ function New-ExoBulkRequest {
9999
}
100100
$BatchBodyJson = ConvertTo-Json -InputObject $BatchBodyObj -Depth 10
101101
$BatchBodyJson = Get-CIPPTextReplacement -TenantFilter $tenantid -Text $BatchBodyJson
102-
$Results = Invoke-RestMethod $BatchURL -ResponseHeadersVariable responseHeaders -Method POST -Body $BatchBodyJson -Headers $Headers -ContentType 'application/json; charset=utf-8'
102+
$Results = Invoke-CIPPRestMethod $BatchURL -ResponseHeadersVariable responseHeaders -Method POST -Body $BatchBodyJson -Headers $Headers -ContentType 'application/json; charset=utf-8'
103103
foreach ($Response in $Results.responses) {
104104
$ReturnedData.Add($Response)
105105
}

Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function New-ExoRequest {
9898
if (!$Tenant.ComplianceUrl) {
9999
Write-Verbose "Getting Compliance URL for $($tenant.defaultDomainName)"
100100
$URL = "$Resource/adminapi/$ApiVersion/$($tenant.customerId)/EXOBanner('AutogenSession')?Version=$ModuleVersion"
101-
Invoke-RestMethod -ResponseHeadersVariable ComplianceHeaders -MaximumRedirection 0 -ErrorAction SilentlyContinue -Uri $URL -Headers $Headers -SkipHttpErrorCheck | Out-Null
101+
Invoke-CIPPRestMethod -ResponseHeadersVariable ComplianceHeaders -MaximumRedirection 0 -ErrorAction SilentlyContinue -Uri $URL -Headers $Headers -SkipHttpErrorCheck | Out-Null
102102
$RedirectedHost = ([System.Uri]($ComplianceHeaders.Location | Select-Object -First 1)).Host
103103
$RedirectedHostname = '{0}.ps.compliance.protection.outlook.com' -f ($RedirectedHost -split '\.' | Select-Object -First 1)
104104
$Resource = "https://$($RedirectedHostname)"
@@ -121,7 +121,7 @@ function New-ExoRequest {
121121
$Headers.CommandName = '*'
122122
$URL = "$Resource/adminapi/v1.0/$($tenant.customerId)/EXOModuleFile?Version=$ModuleVersion"
123123
Write-Verbose "GET [ $URL ]"
124-
return (Invoke-RestMethod -Uri $URL -Headers $Headers).value.exportedCmdlets -split ',' | Where-Object { $_ } | Sort-Object
124+
return (Invoke-CIPPRestMethod -Uri $URL -Headers $Headers).value.exportedCmdlets -split ',' | Where-Object { $_ } | Sort-Object
125125
}
126126

127127
if ($PSCmdlet.ParameterSetName -eq 'ExoRequest') {
@@ -140,7 +140,7 @@ function New-ExoRequest {
140140
ContentType = 'application/json; charset=utf-8'
141141
}
142142

143-
$Return = Invoke-RestMethod @ExoRequestParams -ResponseHeadersVariable ResponseHeaders
143+
$Return = Invoke-CIPPRestMethod @ExoRequestParams -ResponseHeadersVariable ResponseHeaders
144144
$URL = $Return.'@odata.nextLink'
145145
$Return
146146
} until ($null -eq $URL)

Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ function New-GraphBulkRequest {
4747
# Use select to create hashtables of id, method and url for each call
4848
$req['requests'] = ($Requests[$i..($i + 19)])
4949
$ReqBody = (ConvertTo-Json -InputObject $req -Compress -Depth 100)
50-
$Return = Invoke-RestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $ReqBody
50+
$Return = Invoke-CIPPRestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $ReqBody
5151
if ($Return.headers.'retry-after') {
5252
#Revist this when we are pushing this data into our custom schema instead.
5353
$headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp
54-
Invoke-RestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $ReqBody
54+
Invoke-CIPPRestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $ReqBody
5555
}
5656
$Return
5757
}

Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,27 +71,15 @@ function New-GraphGetRequest {
7171
ContentType = 'application/json; charset=utf-8'
7272
}
7373

74-
# Reuse WebSession for TCP/TLS connection pooling (PS 7.4+)
75-
if ($script:GraphWebSession) {
76-
$GraphRequest.WebSession = $script:GraphWebSession
77-
} else {
78-
$GraphRequest.SessionVariable = 'NewGraphSession'
79-
}
80-
8174
if ($ReturnRawResponse) {
8275
$GraphRequest.SkipHttpErrorCheck = $true
8376
$Data = Invoke-WebRequest @GraphRequest
8477
} else {
8578
$GraphRequest.ResponseHeadersVariable = 'ResponseHeaders'
86-
$Data = (Invoke-RestMethod @GraphRequest)
79+
$Data = (Invoke-CIPPRestMethod @GraphRequest)
8780
$script:LastGraphResponseHeaders = $ResponseHeaders
8881
}
8982

90-
# Store the WebSession for future calls in this runspace
91-
if (!$script:GraphWebSession -and $NewGraphSession) {
92-
$script:GraphWebSession = $NewGraphSession
93-
}
94-
9583
# If we reach here, the request was successful
9684
$RequestSuccessful = $true
9785

Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function New-GraphPOSTRequest {
4848
do {
4949
try {
5050
Write-Information "$($type.ToUpper()) [ $uri ] | tenant: $tenantid | attempt: $($RetryCount + 1) of $maxRetries"
51-
$ReturnedData = (Invoke-RestMethod -Uri $($uri) -Method $TYPE -Body $body -Headers $headers -ContentType $contentType -SkipHttpErrorCheck:$IgnoreErrors -ResponseHeadersVariable responseHeaders)
51+
$ReturnedData = (Invoke-CIPPRestMethod -Uri $($uri) -Method $TYPE -Body $body -Headers $headers -ContentType $contentType -SkipHttpErrorCheck:$IgnoreErrors -ResponseHeadersVariable responseHeaders)
5252
$RequestSuccessful = $true
5353
} catch {
5454
$ShouldRetry = $false

0 commit comments

Comments
 (0)