Skip to content

Commit 3d77335

Browse files
Fix Graph scope WAM routing and billing expansion (#43)
* Fix Graph scope WAM routing and billing expansion * Fix legacy portal setters and PS5 broker tests * Remove legacy MFA setter and add WAM examples * Address Graph scope review feedback * Preserve Graph refresh state for scoped requests * Avoid prompting during silent Graph scope refresh * Tighten WAM and admin consent edge cases * Tighten admin consent reviewer handling
1 parent 0fccbdb commit 3d77335

33 files changed

Lines changed: 1443 additions & 257 deletions

COMMANDS.MD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
| Get-O365AzureGroupSecurity | Set-O365AzureGroupSecurity | |
3838
| Get-O365AzureGroupSelfService | Set-O365AzureGroupSelfService | |
3939
| Get-O365AzureLicenses | | |
40-
| Get-O365AzureMultiFactorAuthentication | Set-O365AzureMultiFactorAuthentication | Set cmd not working |
40+
| Get-O365AzureMultiFactorAuthentication | | Legacy MFA write endpoint removed by Microsoft |
4141
| Get-O365AzureProperties | Set-O365AzureProperties | |
4242
| Get-O365AzurePropertiesSecurity | Set-O365AzurePropertiesSecurity | |
4343
| Get-O365AzureTenantSKU | | |

Examples/ConnectO365AdminWam.ps1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Import-Module .\O365Essentials.psd1 -Force
2+
3+
$TenantId = '00000000-0000-0000-0000-000000000000'
4+
$Credential = Get-Credential -UserName 'admin@contoso.com' -Message 'Enter the account to use as the WAM login hint'
5+
6+
$Headers = Connect-O365Admin -UseWam -Credential $Credential -Tenant $TenantId -ForceRefresh -GraphScope 'Policy.Read.All' -Verbose
7+
8+
Get-O365AzureConditionalAccessLocation -Headers $Headers -Verbose
Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
Import-Module .\O365Essentials.psd1 -Force
1+
Import-Module .\O365Essentials.psd1 -Force
22

33
if (-not $Credentials) {
4-
$Credentials = Get-Credential
4+
$Credentials = Get-Credential -Message 'Enter the account to use as the WAM login hint'
55
}
6-
# This makes a connection to Office 365 tenant, using credentials
7-
# keep in mind that if there's an MFA you would be better left without Credentials and just let it prompt you
8-
$null = Connect-O365Admin -Verbose -Credential $Credentials
96

10-
Get-O365AzureMultiFactorAuthentication -Verbose
7+
# Use WAM for MFA-aware interactive sign-in. -ForceRefresh is helpful when Windows
8+
# has cached the wrong account and you need the account picker again.
9+
$null = Connect-O365Admin -Verbose -UseWam -Credential $Credentials -ForceRefresh
1110

12-
# Those cmdlets don't seem to work, not sure why
13-
Set-O365AzureMultiFactorAuthentication -Verbose -AccountLockoutDurationMinutes 5 -AccountLockoutCounterResetMinutes 15 -AccountLockoutDenialsToTriggerLockout 10 -WhatIf
14-
Set-O365AzureMultiFactorAuthentication -Verbose -EnableFraudAlert $false -WhatIf
11+
Get-O365AzureMultiFactorAuthentication -Verbose
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
function ConvertTo-O365AdminConsentReviewer {
2+
<#
3+
.SYNOPSIS
4+
Converts admin consent reviewer objects to the Graph update payload shape.
5+
#>
6+
[cmdletbinding()]
7+
param(
8+
[object[]] $Reviewer
9+
)
10+
11+
foreach ($Item in @($Reviewer)) {
12+
if (-not $Item) {
13+
continue
14+
}
15+
16+
if ($Item -is [string]) {
17+
$Query = $Item -replace '^/v1\.0/', '/'
18+
[ordered] @{
19+
query = $Query
20+
queryType = 'MicrosoftGraph'
21+
}
22+
continue
23+
}
24+
25+
if ($Item -is [System.Collections.IDictionary]) {
26+
if ($Item.Contains('query')) {
27+
$Query = $Item['query'] -replace '^/v1\.0/', '/'
28+
[ordered] @{
29+
query = $Query
30+
queryType = if ($Item.Contains('queryType') -and $Item['queryType']) { $Item['queryType'] } else { 'MicrosoftGraph' }
31+
}
32+
}
33+
continue
34+
}
35+
36+
if ($Item.PSObject.Properties.Name -contains 'query') {
37+
$Query = $Item.query -replace '^/v1\.0/', '/'
38+
[ordered] @{
39+
query = $Query
40+
queryType = if ($Item.PSObject.Properties.Name -contains 'queryType' -and $Item.queryType) { $Item.queryType } else { 'MicrosoftGraph' }
41+
}
42+
}
43+
}
44+
}

Private/Get-O365BrokerAccessToken.ps1

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,28 @@ function Get-O365BrokerAccessToken {
88
[Parameter(Mandatory, ParameterSetName = 'Resource')][string] $ResourceUrl,
99
[Parameter(Mandatory, ParameterSetName = 'Scope')][string] $Scope,
1010
[string] $Tenant = 'organizations',
11-
[string] $ClientId = '1950a258-227b-4e31-a9cf-717495945fc2',
11+
[string] $ClientId,
1212
[string] $Account,
1313
[switch] $ForcePrompt
1414
)
1515

16+
if ([string]::IsNullOrWhiteSpace($ClientId)) {
17+
$ScopeParts = if ($PSCmdlet.ParameterSetName -eq 'Scope') {
18+
@($Scope -split '\s+' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and $_ -notin 'offline_access', 'openid', 'profile', 'email' })
19+
} else {
20+
@()
21+
}
22+
$NonGraphScopeParts = @($ScopeParts | Where-Object { $_ -match '://' -and $_ -notlike 'https://graph.microsoft.com/*' })
23+
$UseGraphClient = $PSCmdlet.ParameterSetName -eq 'Scope' -and $ScopeParts.Count -gt 0 -and $NonGraphScopeParts.Count -eq 0
24+
$ClientId = if ($UseGraphClient) {
25+
# Microsoft Graph PowerShell public client. Its broker redirect URI is WAM-capable for delegated Graph scopes.
26+
'14d82eec-204b-4c2f-b7e8-296a70dab67e'
27+
} else {
28+
# Azure PowerShell public client works with the Microsoft 365 admin, portal, Teams, and ARM resource audiences.
29+
'1950a258-227b-4e31-a9cf-717495945fc2'
30+
}
31+
}
32+
1633
if ($PSEdition -ne 'Core' -or $PSVersionTable.PSVersion -lt [version] '7.4') {
1734
throw 'MSAL WAM broker authentication requires PowerShell 7.4 or newer.'
1835
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
function Set-O365EditableSettingValue {
2+
<#
3+
.SYNOPSIS
4+
Updates a Microsoft 365 admin setting wrapper when the setting is editable.
5+
#>
6+
[cmdletbinding()]
7+
param(
8+
[AllowNull()] $Setting,
9+
[Parameter(Mandatory)] $Value,
10+
[Parameter(Mandatory)][string] $Name
11+
)
12+
13+
if (-not $Setting) {
14+
Write-Warning -Message "Set-O365EditableSettingValue - Setting '$Name' was not found."
15+
return $false
16+
}
17+
18+
if ($Setting.PSObject.Properties.Name -contains 'EnableEditing' -and -not $Setting.EnableEditing) {
19+
Write-Warning -Message "Set-O365EditableSettingValue - Setting '$Name' is not editable in this tenant."
20+
return $false
21+
}
22+
23+
if ($Setting.PSObject.Properties.Name -contains 'Value') {
24+
$Setting.Value = $Value
25+
return $true
26+
}
27+
28+
Write-Warning -Message "Set-O365EditableSettingValue - Setting '$Name' does not expose a Value property."
29+
$false
30+
}

Private/Test-O365GraphScope.ps1

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
function Test-O365GraphScope {
2+
<#
3+
.SYNOPSIS
4+
Checks whether granted Graph scopes satisfy a command requirement.
5+
#>
6+
[cmdletbinding()]
7+
param(
8+
[string[]] $GrantedScope,
9+
[string[]] $RequiredScope
10+
)
11+
12+
$RequiredGroups = @(
13+
foreach ($Scope in @($RequiredScope)) {
14+
foreach ($Part in ($Scope -split '\s+')) {
15+
if (-not [string]::IsNullOrWhiteSpace($Part) -and $Part -notin 'offline_access', 'openid', 'profile', 'email') {
16+
, @($Part.Trim() -split '\|' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
17+
}
18+
}
19+
}
20+
)
21+
22+
if ($RequiredGroups.Count -eq 0) {
23+
return $true
24+
}
25+
26+
$Granted = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
27+
foreach ($Scope in @($GrantedScope)) {
28+
foreach ($Part in ($Scope -split '\s+')) {
29+
if (-not [string]::IsNullOrWhiteSpace($Part)) {
30+
[void] $Granted.Add($Part.Trim())
31+
}
32+
}
33+
}
34+
35+
foreach ($RequiredGroup in $RequiredGroups) {
36+
$Matched = $false
37+
foreach ($Scope in @($RequiredGroup)) {
38+
if ($Granted.Contains($Scope)) {
39+
$Matched = $true
40+
break
41+
}
42+
}
43+
if (-not $Matched) {
44+
return $false
45+
}
46+
}
47+
48+
$true
49+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
function Update-AuthorizationState {
2+
[cmdletbinding()]
3+
param(
4+
[System.Collections.IDictionary] $Target,
5+
[System.Collections.IDictionary] $Source,
6+
[string[]] $Key
7+
)
8+
9+
if (-not $Target -or -not $Source -or [object]::ReferenceEquals($Target, $Source)) {
10+
return
11+
}
12+
13+
$KeysToUpdate = if ($Key) { $Key } else { @($Source.Keys) }
14+
foreach ($Name in $KeysToUpdate) {
15+
if ($Source.Contains($Name)) {
16+
$Target[$Name] = $Source[$Name]
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)