diff --git a/.build/cspell-words.txt b/.build/cspell-words.txt index 3c5763413d..dcefa0f592 100644 --- a/.build/cspell-words.txt +++ b/.build/cspell-words.txt @@ -22,6 +22,7 @@ contoso CTMM Datacenter dcom +devicecode DMARC Dsamain DTLS @@ -171,4 +172,5 @@ Webex Weve wevtutil windir +wids Xlsb diff --git a/Hybrid/ConfigureExchangeHybridApplication/ConfigureExchangeHybridApplication.ps1 b/Hybrid/ConfigureExchangeHybridApplication/ConfigureExchangeHybridApplication.ps1 index 64e56b6dfe..983b2badb3 100644 --- a/Hybrid/ConfigureExchangeHybridApplication/ConfigureExchangeHybridApplication.ps1 +++ b/Hybrid/ConfigureExchangeHybridApplication/ConfigureExchangeHybridApplication.ps1 @@ -85,6 +85,12 @@ If you provide the thumbprint, the script searches and exports the certificate with the thumbprint provided from the local machines certificate store. If you provide the file path, the script uploads the certificate, which was specified. This parameter allows you to run granular configurations. Note that some of the tasks depend on others and can't be run alone. +.PARAMETER UseDeviceCodeFlow + Use this switch parameter to force the Graph API access token to be acquired via the OAuth 2.0 device code flow + instead of the default authorization code flow with PKCE. Windows Server Core is detected automatically and uses + the device code flow without this switch, so you only need it to force the device code flow on other browser-less hosts. + You will be asked to open a verification URL on another device and enter a user code to complete the sign-in. + Note that the device code flow may be blocked by Conditional Access policies in your tenant. .PARAMETER ScriptUpdateOnly This optional parameter allows you to only update the script without performing any other actions. .PARAMETER SkipVersionCheck @@ -227,6 +233,13 @@ param( [Parameter(Mandatory = $false, ParameterSetName = "Create")] [string]$CertificateInformation, + [Parameter(Mandatory = $false, ParameterSetName = "FullyConfigureExchangeHybridApplication")] + [Parameter(Mandatory = $false, ParameterSetName = "FirstPartyKeyCredentialsCleanup")] + [Parameter(Mandatory = $false, ParameterSetName = "Create")] + [Parameter(Mandatory = $false, ParameterSetName = "Delete")] + [Parameter(Mandatory = $false, ParameterSetName = "RemovePermissions")] + [switch]$UseDeviceCodeFlow, + [Parameter(Mandatory = $true, ParameterSetName = "ScriptUpdateOnly")] [switch]$ScriptUpdateOnly, @@ -249,6 +262,7 @@ begin { . $PSScriptRoot\..\..\Shared\Get-PSSessionDetails.ps1 . $PSScriptRoot\..\..\Shared\Get-ProtocolEndpointViaAutoDv2.ps1 . $PSScriptRoot\..\..\Shared\Show-Disclaimer.ps1 + . $PSScriptRoot\..\..\Shared\Test-IsServerCoreOperatingSystem.ps1 . $PSScriptRoot\..\..\Shared\ActiveDirectoryFunctions\Get-ExchangeOrganizationGuid.ps1 . $PSScriptRoot\..\..\Shared\AzureFunctions\Get-Consent.ps1 . $PSScriptRoot\..\..\Shared\AzureFunctions\Get-CloudServiceEndpoint.ps1 @@ -622,6 +636,14 @@ begin { $getGraphAccessTokenParams.Add("ClientId", $Script:CustomClientId) } + # Use the device code flow when it was explicitly requested via -UseDeviceCodeFlow, or when we detect a + # Windows Server Core installation, where no browser is available for the default authorization code flow. + if ($UseDeviceCodeFlow -or + (Test-IsServerCoreOperatingSystem)) { + Write-Verbose "The device code flow will be used to acquire the access token" + $getGraphAccessTokenParams.Add("UseDeviceCodeFlow", $true) + } + $graphAccessToken = Get-GraphAccessToken @getGraphAccessTokenParams if ($null -eq $graphAccessToken) { @@ -1384,7 +1406,8 @@ begin { $graphApiFeatureEnabledCount = 0 foreach ($o in $exchangeOnpremAsThirdPartyAppIdSettingOverrides) { - $match = [regex]::Match($o.Parameters, $settingOverridesEnabledRegex, "IgnoreCase") + $enabledParameter = @($o.Parameters) | Where-Object { $_ -match "^\s*Enabled\s*=" } | Select-Object -First 1 + $match = [regex]::Match([string]$enabledParameter, $settingOverridesEnabledRegex, "IgnoreCase") $featureIsEnabled = ($match.Success -and $match.Groups[1].Value -eq "true") $featureSettingOverrideValue = if (-not $match.Success) { "Unknown" } else { $match.Groups[1].Value } @@ -1398,7 +1421,8 @@ begin { } foreach ($o in $routeThroughMSGraphSettingOverrides) { - $match = [regex]::Match($o.Parameters, $settingOverridesEnabledRegex, "IgnoreCase") + $enabledParameter = @($o.Parameters) | Where-Object { $_ -match "^\s*Enabled\s*=" } | Select-Object -First 1 + $match = [regex]::Match([string]$enabledParameter, $settingOverridesEnabledRegex, "IgnoreCase") $featureIsEnabled = ($match.Success -and $match.Groups[1].Value -eq "true") $featureSettingOverrideValue = if (-not $match.Success) { "Unknown" } else { $match.Groups[1].Value } @@ -1525,7 +1549,7 @@ begin { Name = "EnableRouteThroughMSGraphFeature" Component = "SettingOverride" Section = "RouteThroughMSGraph" - Parameters = @("Enabled=true") + Parameters = @("Enabled=true", "EnabledForMailTips=true", "EnabledForAutomaticReplies=false") Reason = "Created by $($script:MyInvocation.MyCommand.Name) on $(Get-Date)" } # Execute the commands to create the new setting override and to refresh the variant configuration diff --git a/Security/src/CVE-2023-23397/CVE-2023-23397.ps1 b/Security/src/CVE-2023-23397/CVE-2023-23397.ps1 index 9919358b08..58d6d4dab1 100644 --- a/Security/src/CVE-2023-23397/CVE-2023-23397.ps1 +++ b/Security/src/CVE-2023-23397/CVE-2023-23397.ps1 @@ -71,6 +71,12 @@ Skip the first pass of -UseSearchFolders and just check the existing search folders for results. .PARAMETER TimeoutSeconds This optional parameter lets you specify the timeout value for the ExchangeService object. Defaults to 5 minutes. +.PARAMETER UseDeviceCodeFlow + This optional switch parameter forces the script to acquire the Graph API access token via the OAuth 2.0 device code flow + instead of the default authorization code flow with PKCE. Windows Server Core is detected automatically and uses the + device code flow without this switch, so you only need it to force the device code flow on other browser-less hosts. + You will be asked to open a verification URL on another device and enter a user code to complete the sign-in. + Note that the device code flow may be blocked by Conditional Access policies in your tenant. .EXAMPLE PS C:\> .\CVE-2023-23397.ps1 -CreateAzureApplication This will run the tool to create a new Azure application with required permissions @@ -183,7 +189,13 @@ param( [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [ValidateRange(1, 2147483)] - [int]$TimeoutSeconds = 300 + [int]$TimeoutSeconds = 300, + + [Parameter(Mandatory = $false, ParameterSetName = "CreateAzureApplication")] + [Parameter(Mandatory = $false, ParameterSetName = "DeleteAzureApplication")] + [Parameter(Mandatory = $false, ParameterSetName = "Audit")] + [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] + [switch]$UseDeviceCodeFlow ) dynamicparam { @@ -235,6 +247,7 @@ begin { . $PSScriptRoot\..\..\..\Shared\Get-NuGetPackage.ps1 . $PSScriptRoot\..\..\..\Shared\Invoke-ExtractArchive.ps1 . $PSScriptRoot\..\..\..\Shared\LoggerFunctions.ps1 + . $PSScriptRoot\..\..\..\Shared\Test-IsServerCoreOperatingSystem.ps1 . $PSScriptRoot\..\..\..\Shared\ActiveDirectoryFunctions\Test-ADCredentials.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-GraphAccessToken.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-CloudServiceEndpoint.ps1 @@ -627,8 +640,12 @@ begin { Enable-TrustAnyCertificateCallback } + # Use the device code flow when it was explicitly requested via -UseDeviceCodeFlow, or when we detect a + # Windows Server Core installation, where no browser is available for the default authorization code flow. + $useDeviceCodeFlow = $UseDeviceCodeFlow -or (Test-IsServerCoreOperatingSystem) + if ($CreateAzureApplication) { - $graphAccessToken = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint + $graphAccessToken = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint -UseDeviceCodeFlow:$useDeviceCodeFlow if ($null -eq $graphAccessToken) { Write-Host "Failed to acquire an access token - the script cannot continue" -ForegroundColor Red @@ -653,7 +670,7 @@ begin { } if ($DeleteAzureApplication) { - $graphAccessToken = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint + $graphAccessToken = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint -UseDeviceCodeFlow:$useDeviceCodeFlow if ($null -eq $graphAccessToken) { Write-Host "Failed to acquire an access token - the script cannot continue" -ForegroundColor Red @@ -726,7 +743,7 @@ begin { ([System.String]::IsNullOrEmpty($Organization)) -or ([System.String]::IsNullOrEmpty($CertificateThumbprint))) { # We need to query the Azure application information from the Azure AD if not explicitly provided via parameter - $azAccountsObject = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint + $azAccountsObject = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint -UseDeviceCodeFlow:$useDeviceCodeFlow if ($null -eq $azAccountsObject) { Write-Host "Failed to acquire an access token - the script cannot continue" -ForegroundColor Red diff --git a/Shared/AzureFunctions/Get-GraphAccessToken.ps1 b/Shared/AzureFunctions/Get-GraphAccessToken.ps1 index a0b9bd7949..5475c1fa3c 100644 --- a/Shared/AzureFunctions/Get-GraphAccessToken.ps1 +++ b/Shared/AzureFunctions/Get-GraphAccessToken.ps1 @@ -7,14 +7,23 @@ . $PSScriptRoot\..\ScriptUpdateFunctions\Invoke-WebRequestWithProxyDetection.ps1 <# - This function is used to get an access token for the Azure Graph API by using the OAuth 2.0 authorization code flow - with PKCE (Proof Key for Code Exchange). The OAuth 2.0 authorization code grant type, or auth code flow, - enables a client application to obtain authorized access to protected resources like web APIs. + This function is used to get an access token for the Azure Graph API. By default it uses the OAuth 2.0 + authorization code flow with PKCE (Proof Key for Code Exchange). The OAuth 2.0 authorization code grant type, + or auth code flow, enables a client application to obtain authorized access to protected resources like web APIs. The auth code flow requires a user-agent that supports redirection from the authorization server (the Microsoft identity platform) back to your application. More information about the auth code flow with PKCE can be found here: https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow#protocol-details + + For hosts without a browser (for example, Windows Server Core), the -UseDeviceCodeFlow switch can be used to + acquire the token via the OAuth 2.0 device authorization grant (device code flow) instead. In that flow the user + is asked to open a verification URL on another device and enter a user code to complete the sign-in, so no local + browser or redirect listener is required. Note that the device code flow is less phishing resistant than the auth + code flow and may be blocked by Conditional Access policies, so it should only be used when no browser is available. + + More information about the device code flow can be found here: + https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-device-code #> function Get-GraphAccessToken { [CmdletBinding()] @@ -26,10 +35,18 @@ function Get-GraphAccessToken { [string]$GraphApiUrl = "https://graph.microsoft.com", [Parameter(Mandatory = $false)] - [string]$ClientId = "1950a258-227b-4e31-a9cf-717495945fc2", # Well-known Microsoft Azure PowerShell application ID + [string]$ClientId = "4d2f5175-f06b-49e2-9f4a-8e614a8abc03", # Microsoft Exchange Hybrid Wizard application ID + + # The OpenID Connect scopes "openid" and "profile" must be requested explicitly so that an id_token is + # returned alongside the access_token. The id_token is required for the nonce replay check and to read the + # tenant id. ".default" requests all permissions that have already been consented for the app (required for + # a first party application, which cannot rely on dynamic consent / pre-authorization for a first party + # resource). + [Parameter(Mandatory = $false)] + [string]$Scope = "$($GraphApiUrl)/.default openid profile", [Parameter(Mandatory = $false)] - [string]$Scope = "$($GraphApiUrl)//AuditLog.Read.All Directory.AccessAsUser.All email openid profile" + [switch]$UseDeviceCodeFlow ) begin { @@ -112,6 +129,115 @@ function Get-GraphAccessToken { $connectionSuccessful = $false } process { + if ($UseDeviceCodeFlow) { + Write-Verbose "Device code flow was selected to acquire the access token" + Write-Host "Device code flow is intended for hosts without a browser (for example, Windows Server Core)." -ForegroundColor Yellow + Write-Host "Note: This flow may be blocked by Conditional Access policies in your tenant." -ForegroundColor Yellow + + # Request a device code from the Microsoft Azure Active Directory endpoint + $deviceCodeRequestParams = @{ + Uri = "$AzureADEndpoint/organizations/oauth2/v2.0/devicecode" + Method = "POST" + ContentType = "application/x-www-form-urlencoded" + Body = @{ + client_id = $ClientId + scope = $Scope + } + UseBasicParsing = $true + } + $deviceCodeResponse = Invoke-WebRequestWithProxyDetection -ParametersObject $deviceCodeRequestParams + + if (($null -eq $deviceCodeResponse) -or + ($deviceCodeResponse.StatusCode -ne 200)) { + Write-Host "Unable to acquire a device code from the Microsoft Azure Active Directory endpoint." -ForegroundColor Red + + return + } + + $deviceCode = $deviceCodeResponse.Content | ConvertFrom-Json + + # Display the user instructions returned by the endpoint (verification URL and user code) + Write-Host $deviceCode.message -ForegroundColor Cyan + + $pollingInterval = [int]$deviceCode.interval + $pollingStopwatch = New-Object System.Diagnostics.Stopwatch + $pollingStopwatch.Start() + + do { + Start-Sleep -Seconds $pollingInterval + + # Poll the token endpoint to check whether the user completed the sign-in. We use + # -ReturnErrorResponse so that the helper surfaces the HTTP 400 error body instead of swallowing + # it - the device code flow relies on the authorization_pending / slow_down error codes to drive + # the polling loop. Proxy detection is handled centrally by Invoke-WebRequestWithProxyDetection. + $redeemDeviceCodeParams = @{ + Uri = "$AzureADEndpoint/organizations/oauth2/v2.0/token" + Method = "POST" + ContentType = "application/x-www-form-urlencoded" + Body = @{ + client_id = $ClientId + grant_type = "urn:ietf:params:oauth:grant-type:device_code" + device_code = $deviceCode.device_code + } + UseBasicParsing = $true + } + $redeemDeviceCodeResponse = Invoke-WebRequestWithProxyDetection -ParametersObject $redeemDeviceCodeParams -ReturnErrorResponse + + if (($null -ne $redeemDeviceCodeResponse) -and + ($redeemDeviceCodeResponse.StatusCode -eq 200)) { + $tokens = $redeemDeviceCodeResponse.Content | ConvertFrom-Json + $idTokenPayload = (Convert-JsonWebTokenToObject $tokens.id_token).Payload + $connectionSuccessful = $true + + break + } + + $errorResponse = $null + + if (($null -ne $redeemDeviceCodeResponse) -and + (-not [System.String]::IsNullOrEmpty($redeemDeviceCodeResponse.Content))) { + $errorResponse = $redeemDeviceCodeResponse.Content | ConvertFrom-Json + } + + switch ($errorResponse.error) { + "authorization_pending" { + Write-Verbose "Authorization is pending - the user has not completed the sign-in yet" + } + "slow_down" { + Write-Verbose "Polling too fast - increasing the polling interval by 5 seconds" + $pollingInterval += 5 + } + "authorization_declined" { + Write-Host "The user declined the sign-in request." -ForegroundColor Red + + return + } + "expired_token" { + Write-Host "The device code has expired before the sign-in was completed." -ForegroundColor Red + + return + } + "access_denied" { + Write-Host "The sign-in request was denied." -ForegroundColor Red + + return + } + default { + Write-Host "Unable to redeem the device code for an access token." -ForegroundColor Red + Write-Verbose "Unexpected error: $($errorResponse.error) - $($errorResponse.error_description)" + + return + } + } + } while ($pollingStopwatch.Elapsed.TotalSeconds -lt [int]$deviceCode.expires_in) + + if (-not $connectionSuccessful) { + Write-Host "Timed out waiting for the device code sign-in to complete." -ForegroundColor Red + } + + return + } + $codeChallenge = $codeChallengeVerifier.CodeChallenge $codeVerifier = $codeChallengeVerifier.Verifier @@ -171,6 +297,15 @@ function Get-GraphAccessToken { if ($redeemAuthCodeResponse.StatusCode -eq 200) { $tokens = $redeemAuthCodeResponse.Content | ConvertFrom-Json + + # An id_token is only returned when the "openid" scope was requested. It is required to perform + # the nonce replay check and to read the tenant id, so fail clearly if it is missing. + if ([System.String]::IsNullOrEmpty($tokens.id_token)) { + Write-Host "No id_token was returned - make sure the 'openid' scope is part of the requested scope" -ForegroundColor Red + + return + } + $idTokenPayload = (Convert-JsonWebTokenToObject $tokens.id_token).Payload Write-Verbose "Script nonce: '$nonce' - Returned nonce: '$($idTokenPayload.nonce)'" diff --git a/Shared/GraphApiFunctions/Get-AzureSignedInUserInformation.ps1 b/Shared/GraphApiFunctions/Get-AzureSignedInUserInformation.ps1 index ff379eabb7..778a59de5d 100644 --- a/Shared/GraphApiFunctions/Get-AzureSignedInUserInformation.ps1 +++ b/Shared/GraphApiFunctions/Get-AzureSignedInUserInformation.ps1 @@ -1,45 +1,45 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -. $PSScriptRoot\..\AzureFunctions\Invoke-GraphApiRequest.ps1 +. $PSScriptRoot\..\AzureFunctions\Convert-JsonWebTokenToObject.ps1 <# .SYNOPSIS Retrieves information about the currently signed-in user and determines admin consent eligibility. .DESCRIPTION - This function queries the Microsoft Graph API to retrieve properties of the currently - signed-in user and their group/role memberships. It also determines whether the user - has sufficient privileges to grant Admin Consent for Azure AD applications. + This function reads properties of the currently signed-in user and their directory role + assignments directly from the claims of the provided access token. It also determines whether + the user has sufficient privileges to grant Admin Consent for Azure AD applications. The function performs the following operations: - 1. Queries the Graph API "me" endpoint to get the signed-in user's properties - 2. Queries the "me/memberOf" endpoint to get all group and role memberships + 1. Decodes the access token to read the signed-in user's object id ('oid' claim) + 2. Reads the user's directory role assignments ('wids' claim) 3. Checks if the user is a member of roles eligible to grant Admin Consent: - Global Administrator (62e90394-69f5-4237-9190-012177145e10) - Privileged Role Administrator (9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3) - 4. Returns a result object with user info, memberships, and consent eligibility + 4. Returns a result object with user info, role memberships, and consent eligibility + + Reading the information from the token claims avoids the need for the Microsoft Graph + "User.Read" and "Directory.Read.All" delegated permissions. This function is typically used as a prerequisite check before attempting to grant Admin Consent on Azure AD applications. .PARAMETER AzAccountsObject - The Azure accounts object containing authentication context (AccessToken) for Graph API calls. - -.PARAMETER GraphApiUrl - The Microsoft Graph API endpoint URL to use for API requests (e.g., "https://graph.microsoft.com"). + The Azure accounts object containing authentication context (AccessToken) whose claims are read. .OUTPUTS PSCustomObject with the following properties: - - UserInformation: The full user object from Graph API (id, displayName, mail, userPrincipalName, etc.) - - MemberOfInformation: List of groups and directory roles the user is a member of + - UserInformation: User object built from the token claims (id, displayName, userPrincipalName) + - MemberOfInformation: List of directory role template ids the user is assigned to ('wids' claim) - EligibleToGrantAdminConsent: Boolean indicating whether the user can grant Admin Consent (true if member of Global Administrator or Privileged Role Administrator) - Returns $null if either Graph API query fails. + Returns $null if the access token cannot be decoded. .EXAMPLE - $userInfo = Get-AzureSignedInUserInformation -AzAccountsObject $azContext -GraphApiUrl "https://graph.microsoft.com" + $userInfo = Get-AzureSignedInUserInformation -AzAccountsObject $azContext if ($userInfo.EligibleToGrantAdminConsent) { Write-Host "User $($userInfo.UserInformation.displayName) can grant Admin Consent" @@ -48,26 +48,28 @@ } .NOTES - Required Graph API permissions: - - User.Read (to read signed-in user information) - - Directory.Read.All (to read group/role memberships) - - API References: - - Get signed-in user: https://learn.microsoft.com/graph/api/user-get - - List memberOf: https://learn.microsoft.com/graph/api/user-list-memberof + The required information is read from the access token claims, so no Microsoft Graph + API permissions are required: + - oid: object id of the signed-in user + - wids: directory role template ids assigned to the signed-in user + + Note: The "wids" claim only contains directory role assignments (not security group + memberships). In rare overage scenarios (a very large number of role assignments) the + claim can be omitted; in that case the user is treated as not eligible to grant Admin + Consent and the caller can fall back to -AllowCreationWithoutConsentPermission. + + References: + - Access token claims reference: https://learn.microsoft.com/entra/identity-platform/access-token-claims-reference - Built-in roles: https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference - Admin consent overview: https://learn.microsoft.com/entra/identity/enterprise-apps/user-admin-consent-overview #> function Get-AzureSignedInUserInformation { param( [ValidateNotNullOrEmpty()] - $AzAccountsObject, - - [ValidateNotNullOrEmpty()] - $GraphApiUrl + $AzAccountsObject ) - Write-Verbose "Getting information for the signed-in user via Graph Api: $GraphApiUrl" + Write-Verbose "Getting information for the signed-in user from the access token claims" # Groups with permission to grant admin consent # Build-in roles: https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference @@ -79,35 +81,43 @@ function Get-AzureSignedInUserInformation { $memberOfListObject = New-Object System.Collections.Generic.List[object] - $getAzureSignedInUserBasicParams = @{ - AccessToken = $AzAccountsObject.AccessToken - GraphApiUrl = $GraphApiUrl - } - - # Gets the properties and relationship of the signed-in user - $getAzureSignedInUserResponse = Invoke-GraphApiRequest @getAzureSignedInUserBasicParams -Query "me" + # Decode the access token to read the signed-in user information from its claims. This avoids the need + # for the "User.Read" and "Directory.Read.All" Graph permissions + $tokenObject = Convert-JsonWebTokenToObject -Token $AzAccountsObject.AccessToken - if ($getAzureSignedInUserResponse.Successful -eq $false) { - Write-Verbose "Unable to query signed-in user information - please try again" + if ($null -eq $tokenObject) { + Write-Verbose "Unable to decode the access token - please try again" return } - # Gets the group membership of the signed-in user - $getAzureSignedInUserMemberOfResponse = Invoke-GraphApiRequest @getAzureSignedInUserBasicParams -Query "me/memberOf" + $tokenPayload = $tokenObject.Payload - if ($getAzureSignedInUserMemberOfResponse.Successful -eq $false) { - Write-Verbose "Unable to query signed-in user memberOf information - please try again" + # The "oid" claim is the object id of the signed-in user + if ([System.String]::IsNullOrEmpty($tokenPayload.oid)) { + Write-Verbose "The access token does not contain an 'oid' claim - unable to determine the signed-in user" return } - foreach ($group in $getAzureSignedInUserMemberOfResponse.Content.value) { - Write-Verbose "Adding group: '$($group.displayName)' to list" - $memberOfListObject.Add($group) + # The "wids" claim contains the directory role template ids assigned to the signed-in user. It may be + # absent if the user has no directory roles or in rare overage scenarios. + if ($null -ne $tokenPayload.wids) { + foreach ($roleTemplateId in $tokenPayload.wids) { + Write-Verbose "Adding directory role template id: '$roleTemplateId' to list" + $memberOfListObject.Add($roleTemplateId) + } + } else { + Write-Verbose "The access token does not contain a 'wids' claim - the user is treated as not eligible to grant Admin Consent" + } + + $userInformation = [PSCustomObject]@{ + id = $tokenPayload.oid + displayName = $tokenPayload.name + userPrincipalName = $tokenPayload.upn } return [PSCustomObject]@{ - UserInformation = $getAzureSignedInUserResponse.Content + UserInformation = $userInformation MemberOfInformation = $memberOfListObject - EligibleToGrantAdminConsent = ($groupsEligibleToGrantAdminConsent | Where-Object { $_ -in $memberOfListObject.roleTemplateId }).Count -ge 1 + EligibleToGrantAdminConsent = ($groupsEligibleToGrantAdminConsent | Where-Object { $_ -in $memberOfListObject }).Count -ge 1 } } diff --git a/Shared/GraphApiFunctions/New-ExchangeAzureApplication.ps1 b/Shared/GraphApiFunctions/New-ExchangeAzureApplication.ps1 index 346ad5e248..6c1e76ceb1 100644 --- a/Shared/GraphApiFunctions/New-ExchangeAzureApplication.ps1 +++ b/Shared/GraphApiFunctions/New-ExchangeAzureApplication.ps1 @@ -168,7 +168,7 @@ function New-ExchangeAzureApplication { } # Graph API call to get the current logged in user - we need this information to run the following Graph API calls - $getAzureSignedInUserInformation = Get-AzureSignedInUserInformation @graphApiBaseParams + $getAzureSignedInUserInformation = Get-AzureSignedInUserInformation -AzAccountsObject $AzAccountsObject if ($null -eq $getAzureSignedInUserInformation) { Write-Verbose "Unable to query the signed-in user information" diff --git a/Shared/GraphApiFunctions/Update-ExchangeAzureApplication.ps1 b/Shared/GraphApiFunctions/Update-ExchangeAzureApplication.ps1 index 67afa3e452..c6008e2def 100644 --- a/Shared/GraphApiFunctions/Update-ExchangeAzureApplication.ps1 +++ b/Shared/GraphApiFunctions/Update-ExchangeAzureApplication.ps1 @@ -160,7 +160,7 @@ function Update-ExchangeAzureApplication { } # Graph API call to get the current logged in user - we need this information to run the Admin Consent Graph API calls - $getAzureSignedInUserInformation = Get-AzureSignedInUserInformation @graphApiBaseParams + $getAzureSignedInUserInformation = Get-AzureSignedInUserInformation -AzAccountsObject $AzAccountsObject if ($null -eq $getAzureSignedInUserInformation) { Write-Verbose "Unable to query the signed-in user information" diff --git a/Shared/ScriptUpdateFunctions/Invoke-WebRequestWithProxyDetection.ps1 b/Shared/ScriptUpdateFunctions/Invoke-WebRequestWithProxyDetection.ps1 index 0a68b3ac52..d78d6d72b2 100644 --- a/Shared/ScriptUpdateFunctions/Invoke-WebRequestWithProxyDetection.ps1 +++ b/Shared/ScriptUpdateFunctions/Invoke-WebRequestWithProxyDetection.ps1 @@ -21,7 +21,11 @@ function Invoke-WebRequestWithProxyDetection { [Parameter(Mandatory = $false, ParameterSetName = "Default")] [string] - $OutFile + $OutFile, + + [Parameter(Mandatory = $false)] + [switch] + $ReturnErrorResponse ) Write-Verbose "Calling $($MyInvocation.MyCommand)" @@ -53,5 +57,19 @@ function Invoke-WebRequestWithProxyDetection { Invoke-WebRequest @params } catch { Write-VerboseErrorInformation + + # By default an HTTP error response (for example HTTP 400) is swallowed. When the caller opts in via + # -ReturnErrorResponse, surface the error response instead so flows that rely on the error body can + # inspect it. This is required by the OAuth 2.0 device code polling loop, which drives its loop based on + # the authorization_pending / slow_down error codes returned in the HTTP 400 body. The returned object + # exposes StatusCode and Content so the caller can treat success and error responses uniformly. + if ($ReturnErrorResponse -and + ($null -ne $_.Exception.Response)) { + Write-Verbose "Returning the error response because -ReturnErrorResponse was specified" + return [PSCustomObject]@{ + StatusCode = [int]$_.Exception.Response.StatusCode + Content = $_.ErrorDetails.Message + } + } } } diff --git a/Shared/Test-IsServerCoreOperatingSystem.ps1 b/Shared/Test-IsServerCoreOperatingSystem.ps1 new file mode 100644 index 0000000000..511116653c --- /dev/null +++ b/Shared/Test-IsServerCoreOperatingSystem.ps1 @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# + This function determines whether the script is running on a Windows Server Core installation. + Server Core does not include the components required to launch a browser, which some callers, like the OAuth 2.0 authorization code + flow depends on. It is detected via the "InstallationType" registry value, which is set to "Server Core" + on a Server Core installation. Any failure (for example, on a non-Windows host where the registry path does not exist) + is treated as "not Server Core" so that callers can safely fall back to the default authorization code flow. +#> +function Test-IsServerCoreOperatingSystem { + [CmdletBinding()] + [OutputType([bool])] + param () + + Write-Verbose "Calling $($MyInvocation.MyCommand)" + try { + $installationType = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name "InstallationType" -ErrorAction Stop).InstallationType + Write-Verbose "InstallationType registry value: '$installationType'" + + return ($installationType -eq "Server Core") + } catch { + Write-Verbose "Unable to determine the InstallationType - assuming this is not a Server Core installation" + + return $false + } +} diff --git a/docs/Hybrid/ConfigureExchangeHybridApplication.md b/docs/Hybrid/ConfigureExchangeHybridApplication.md index 6b91376b90..82a1f3bb04 100644 --- a/docs/Hybrid/ConfigureExchangeHybridApplication.md +++ b/docs/Hybrid/ConfigureExchangeHybridApplication.md @@ -65,6 +65,30 @@ The script will delete the application in Microsoft Entra ID without undoing any .\ConfigureExchangeHybridApplication.ps1 -DeleteApplication ``` +The script will fully configure the dedicated Exchange hybrid application with Graph API permissions only. When the `UseGraphApiOnly` parameter is used, EWS API permissions are not configured. Without this parameter, the script configures EWS API permissions by default and prompts whether to add Graph API permissions in addition. + +```powershell +.\ConfigureExchangeHybridApplication.ps1 -FullyConfigureExchangeHybridApplication -UseGraphApiOnly +``` + +In Split Execution Configuration Mode, you can also use the `UseGraphApiOnly` parameter when creating the application on a machine with Microsoft Graph API connectivity to configure Graph API permissions only. + +```powershell +.\ConfigureExchangeHybridApplication.ps1 -CreateApplication -UseGraphApiOnly -UpdateCertificate -CertificateMethod "File" -CertificateInformation "c:\temp\certificate.cer" +``` + +The script will remove the `full_access_as_app` EWS API permission from the dedicated Exchange hybrid application. Use this syntax if you don't use any features that require the EWS API and want to use Graph API permissions only. This removes both the admin consent (app role assignments) from all Service Principals and the permission entries from the application manifest. This command can be executed on a non-Exchange Server. + +```powershell +.\ConfigureExchangeHybridApplication.ps1 -RemoveApiPermissions "EWS" +``` + +The script will remove both the EWS and Graph API permissions from the dedicated Exchange hybrid application. Provide an array of API types to remove multiple permissions at once. This command can be executed on a non-Exchange Server. + +```powershell +.\ConfigureExchangeHybridApplication.ps1 -RemoveApiPermissions "EWS", "Graph" +``` + ## Parameters Parameter | Description @@ -74,6 +98,8 @@ CreateApplication | Use this switch parameter to create the application in Micro DeleteApplication | Use this switch parameter to delete an existing application in Microsoft Entra ID. Note that the script will only delete the application. The script doesn't undo any changes, e.g. to Auth Server objects and doesn't remove the Setting Override. This parameter allows you to run granular configurations. Note that some of the tasks depend on others and can't be run alone. UpdateCertificate | Use this switch parameter to upload the Auth Certificate to the application in Microsoft Entra ID. This parameter allows you to run granular configurations. Note that some of the tasks depend on others and can't be run alone. ConfigureAuthServer | Use this switch parameter to configure the Auth Server object. The script will add the appId of the newly created application to the `EvoSTS` or `EvoSTS - {Guid}` Auth Server object. This parameter allows you to run granular configurations. Note that some of the tasks depend on others and can't be run alone. +UseGraphApiOnly | Use this switch parameter to configure only Graph API permissions for the dedicated Exchange hybrid application. When this parameter is used, EWS API permissions will not be configured. If you do not use this parameter, the script will configure EWS API permissions by default, with an optional prompt to add Graph API permissions in addition. +RemoveApiPermissions | Use this parameter to remove specific API permissions from the dedicated Exchange hybrid application. Accepts an array of API types to remove. Valid values are: `EWS`, `Graph`. This removes both the admin consent (app role assignments) from all service principals and the permission entries from the application manifest. This is useful when you need to clean up permissions that are no longer needed. CustomAppId | Use this parameter to provide the Application (client) ID (also known as appId) of a custom application in Microsoft Entra ID. In most cases this parameter does not need to be used. TenantId | Use this parameter to provide the ID of your tenant in Microsoft Entra ID. In most cases this parameter does not need to be used. RemoteRoutingDomain | Use this parameter to provide the remote routing domain of your tenant in Microsoft Entra ID. In most cases this parameter does not need to be used. @@ -90,3 +116,4 @@ CertificateMethod | Use this parameter to specify the method which should be use CertificateInformation | Use this parameter to provide the thumbprint of the certificate that you want the script to export and upload or the file path to the certificate file, for example, `c:\temp\certificate.cer`. You don't need to use this parameter if `CertificateMethod` is set to `Automated`. If you provide the thumbprint, the script searches and exports the certificate with the thumbprint provided from the local machines certificate store. If you provide the file path, the script uploads the certificate, which was specified. This parameter allows you to run granular configurations. Note that some of the tasks depend on others and can't be run alone. ScriptUpdateOnly | This optional parameter allows you to only update the script without performing any other actions. SkipVersionCheck | This optional parameter allows you to skip the automatic version check and script update. +UseDeviceCodeFlow | Use this switch parameter to force the Graph API access token to be acquired via the OAuth 2.0 device code flow instead of the default authorization code flow with PKCE. Windows Server Core is detected automatically and uses the device code flow without this switch, so you only need it to force the device code flow on other browser-less hosts. You will be asked to open a verification URL on another device and enter a user code to complete the sign-in. Note that the device code flow may be blocked by Conditional Access policies in your tenant. diff --git a/docs/Security/CVE-2023-23397/index.md b/docs/Security/CVE-2023-23397/index.md index d6e3837ac3..ddb663ac9d 100644 --- a/docs/Security/CVE-2023-23397/index.md +++ b/docs/Security/CVE-2023-23397/index.md @@ -101,6 +101,7 @@ UseSearchFolders | This parameter causes the script to use deep-traversal search SearchFolderCleanup | This parameter cleans up any search folders left behind by the asynchronous search feature. It must be used together with the `UseSearchFolders` parameter. SkipSearchFolderCreation | This parameter skips the creation of search folders. It must be used together with the `UseSearchFolders` parameter. TimeoutSeconds | This optional parameter specifies the timeout on the EWS ExchangeService object. The default is 300 seconds (5 minutes). +UseDeviceCodeFlow | This optional switch parameter forces the script to acquire the Graph API access token via the OAuth 2.0 device code flow instead of the default authorization code flow with PKCE. Windows Server Core is detected automatically and uses the device code flow without this switch, so you only need it to force the device code flow on other browser-less hosts. You will be asked to open a verification URL on another device and enter a user code to complete the sign-in. Note that the device code flow may be blocked by Conditional Access policies in your tenant. #### Set Exchange Online Cloud Specific values: You can use the `AzureEnvironment` parameter to specify the cloud against which the script runs. By default, the script will run against the Global (worldwide) service. Supported values are: