Last active
March 1, 2025 00:09
-
-
Save zjorz/ad253c009b080c91494e2a64981aca6b to your computer and use it in GitHub Desktop.
Creating An Application In Entra ID And Exchange Online To Be Used As A "Proxy" To Send Emails From On-Premises PowerShell Scripts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$tenantFQDN = "<TENANT NAME>.ONMICROSOFT.COM" # <= CONFIGURE THIS!!!!! | |
$appRegDisplayName = "<APPLICATION DISPLAY NAME>" # <= CONFIGURE THIS!!!!! | |
$credentialType = "<CREDENTIAL TYPE>" # "Secret" OR "Certificate" <= CONFIGURE THIS!!!!! | |
$lifetimeSecretInDays = 365 # <= CONFIGURE THIS!!!!! | |
$certCERFilePath = "<CERTIFICATE CER FILE PATH>" # <= CONFIGURE THIS!!!!! | |
$mailboxMailAddress = "<MAIL ADDRESS OF MAILBOX TO ALLOW TO SEND MAIL FROM>" # <= CONFIGURE THIS!!!!! | |
Invoke-Command -ArgumentList $tenantFQDN,$appRegDisplayName,$credentialType,$lifetimeSecretInDays,$certCERFilePath,$mailboxMailAddress -ScriptBlock { | |
Param ( | |
$tenantFQDN, | |
$appRegDisplayName, | |
$credentialType, | |
$lifetimeSecretInDays, | |
$certCERFilePath, | |
$mailboxMailAddress | |
) | |
<# | |
.SYNOPSIS | |
This PoSH Code Creates And Configures And Application In Both Entra ID And Exchange Online That Can Be Used By On-Premises PowerShell Scripts To Send E-mails | |
.DESCRIPTION | |
This PoSH code provides the following functions in the order listed: | |
- Authenticate and get an access token for MSFT Graph API | |
- Authenticate and get an access token for Office 365 Exchange Online | |
- Creation of a Application Registration in Entra ID WITHOUT any API permissions | |
- Creation of a Service Principal in Entra ID | |
- Addition of either a Client Secret or a certificate as client credentials | |
- Creation of a Service Principal in Office 365 Exchange Online referencing the Service Principal in Entra ID | |
- Creation of a management scope in Office 365 Exchange Online referencing scoping only the referenced mailbox to send mails from | |
- Creation of a management role assignement in Office 365 Exchange Online, granting Mail.Send to the Service Principal for the management scope | |
.NOTES | |
- By using this PowerShell code and its configuration, the PowerShell script will only be able to send mail from the mailbox referenced in the management scope | |
- The permissions in this scenario are configured on the Office 365 Exchange Online side using management scope and management role assignment. This is the | |
recommended way of doing this | |
- When configuring the permissions on the Entra ID side through API permission "Mail.Send" on the application registration , the application will be able to send | |
any mail from ANY mailbox in the organization. This is a security issues and very dangerous. With that in mind it IS NOT RECOMMENDED to do it like that! | |
- Requires => PSMSALNet Module (https://github.com/SCOMnewbie/PSMSALNet) to authenticate against Entra ID for all actions performed by this script/code | |
- Requires PowerShell v7.4 or higher | |
- For Entra ID Related Tasks/Actions..........: membership of the "Application Administrator", "Cloud Application Administrator" OR "Global Administrator" built-in role is required | |
- For Exchange Online Related Tasks/Actions...: membership of the "Exchange Administrator" OR "Global Administrator" built-in role is required | |
- The PowerShell code requires the permissions to execute all actions during the same run | |
- | |
#> | |
Write-Host "" | |
Write-Host "###############################################################################" -Foregroundcolor Yellow | |
Write-Host "### Creating A Send Mail Application In Entra ID ###" -Foregroundcolor Yellow | |
Write-Host "###############################################################################" -Foregroundcolor Yellow | |
################################################################ | |
### FUNCTIONS ### | |
################################################################ | |
# FUNCTION: Get The OpenID Configuration For The Specified Entra ID Tenant FQDN. | |
Function Get-MSFTApiOpenIDConfiguration { | |
<# | |
.SYNOPSIS | |
Get The OpenID Configuration For The Specified Entra ID Tenant FQDN. | |
.DESCRIPTION | |
Get The OpenID Configuration For The Specified Entra ID Tenant FQDN. | |
.PARAMETER tenantFQDN | |
The FQDN Of The Entra ID Tenant Being Targeted. | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE, ValueFromPipeline = $TRUE, ValueFromPipelineByPropertyName = $TRUE, HelpMessage = 'Please Specify The Entra ID Tenant FQDN')] | |
[ValidateNotNullOrEmpty()] | |
[String]$tenantFQDN | |
) | |
Begin { | |
# Specify The Tenant Specific Discovery Endpoint URL | |
$oidcConfigDiscoveryURL = "https://login.microsoftonline.com/$tenantFQDN/v2.0/.well-known/openid-configuration" | |
} | |
Process { | |
$oidcTenantConfig = Invoke-RestMethod -Uri $oidcConfigDiscoveryURL | |
Return $oidcTenantConfig | |
} | |
} | |
# FUNCTION: Get The Entra ID Tenant ID From The OIDC Configuration. | |
Function Get-MSFTTenantID { | |
<# | |
.SYNOPSIS | |
Get The Entra ID Tenant ID From The OIDC Configuration. | |
.DESCRIPTION | |
Get The Entra ID Tenant ID From The OIDC Configuration. | |
.PARAMETER oidcTenantConfig | |
The OIDC Configuration Of The Entra ID Tenant Being Targeted. | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE)] | |
[System.Management.Automation.PSObject]$oidcTenantConfig | |
) | |
Begin { | |
$tenantID = $null | |
} | |
Process { | |
$tenantID = $oidcTenantConfig.authorization_endpoint.Split("/")[3] | |
Return $tenantID | |
} | |
} | |
# FUNCTION: Create The Application Registration With Specific Details | |
Function New-GraphApiApplicationRegistration { | |
<# | |
.SYNOPSIS | |
Create The Application Registration With Specific Details | |
.DESCRIPTION | |
Create The Application Registration With Specific Details | |
.PARAMETER accessToken | |
The Access Token To Be Used To Execute The Operation | |
.PARAMETER appRegDisplayName | |
The Application Registration Display Name | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$accessToken, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$appRegDisplayName | |
) | |
Begin { | |
$requestHeader = $null | |
$requestBody = $null | |
$response = $null | |
} | |
Process { | |
# Define The Request Header To Execute The Required Action | |
$requestHeader = @{ | |
Authorization = "Bearer $accessToken" | |
} | |
Write-Verbose "Request Header...........................: $($requestHeader | Out-String)" | |
# Define The Request Body To Execute The Required Action | |
$requestBody = @{ | |
displayName = $appRegDisplayName | |
description = "Application Used To Send E-mail sing On-Prem PowerShell Scripts" | |
signInAudience = "AzureADMyOrg" | |
servicePrincipalLockConfiguration = @{"isEnabled" = $true; "allProperties" = $true} # App Instance Property Lock | |
web = @{"redirectUris" = @("http://localhost")} | |
} | |
Write-Verbose "Request Body.............................: $($requestBody | Out-String)" | |
# Get The Response From The Microsoft Graph | |
Try { | |
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://graph.microsoft.com/v1.0/applications" -Method POST -Headers $requestHeader -Body $($requestBody | ConvertTo-Json) -ContentType "application/json; charset=UTF-8" -ErrorAction Stop | |
Write-Verbose "Created The Application Registration '$($response.displayName)' Object Id '$($response.id)' And With App Id '$($response.appId)'" | |
} Catch { | |
If ($_.ErrorDetails.Message) { | |
$errorMessage = $_.ErrorDetails.Message | |
} Else { | |
$errorMessage = $_ | |
} | |
Throw $errorMessage | |
} | |
# Return | |
Return $response | |
} | |
} | |
# FUNCTION: Add A New Client Secret To An Existing Application Registration With A Specific Application Registration Object Id or A Specific Application Id. | |
Function New-GraphApiApplicationRegistrationClientSecret { | |
<# | |
.SYNOPSIS | |
Add A New Client Secret To An Existing Application Registration With A Specific Application Registration Object Id or A Specific Application Id. | |
.DESCRIPTION | |
Add A New Client Secret To An Existing Application Registration With A Specific Application Registration Object Id or A Specific Application Id. | |
.PARAMETER accessToken | |
The Access Token To Be Used To Execute The Operation | |
.PARAMETER applicationId | |
The Application ID Assigned And Configured On The Application Registration To Process | |
.PARAMETER applicationRegistrationObjectId | |
The Object Id Of The Application Registration To Process | |
.PARAMETER customLifetimeSecretInDays | |
The Custom Lifetime In Days For The New Secret To Be Added To The Existing Application Registration. When Nothing Is Specified The Default Will Be 1 Hour. | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$accessToken, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$applicationId, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$applicationRegistrationObjectId, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$customLifetimeSecretInDays | |
) | |
Begin { | |
# Making Sure The Correct Combination Of Parameters Are Used | |
If ([string]::IsNullOrEmpty($applicationId) -And [string]::IsNullOrEmpty($applicationRegistrationObjectId)) { | |
Write-Error "The Correct Combination Of Parameters Is Not Being Used" | |
Write-Host " * You Need To Either Specify '-applicationId' And Specify An Application Id Or '-applicationRegistrationObjectId' And Specify The Application Registration Object Id" -ForegroundColor red | |
Write-Host "" | |
BREAK | |
} | |
$internetIP = (Invoke-WebRequest -uri "http://ifconfig.me/ip").Content | |
$geoLocationInfo = Invoke-RestMethod -Method Get -Uri "http://ip-api.com/json/$internetIP" | |
$geoLocationTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById($($geoLocationInfo.timezone)) | |
$geoLocationDateTime = [System.TimeZoneInfo]::ConvertTimeFromUtc((Get-Date).ToUniversalTime(), $geoLocationTimeZone) | |
$defaultInHours = 1 | |
$startDate = $geoLocationDateTime # Start Date/Time Is Generated Based On The Internet Connected GeoLocation, And Not The Start/Time Of The Local Computer! | |
If ([string]::IsNullOrEmpty($customLifetimeSecretInDays)) { | |
$endDate = $startDate.AddHours($defaultInHours) | |
} Else { | |
$endDate = $startDate.AddDays($customLifetimeSecretInDays) | |
} | |
$requestHeader = $null | |
$requestBody = $null | |
$response = $null | |
} | |
Process { | |
# Define The Request Header To Execute The Required Action | |
$requestHeader = @{ | |
Authorization = "Bearer $accessToken" | |
} | |
Write-Verbose "Request Header...........................: $($requestHeader | Out-String)" | |
# Define The Request Body To Execute The Required Action | |
$requestBody = @{ | |
passwordCredential = @{ | |
customKeyIdentifier = $null; | |
displayName = "Client Secret (Generated $(Get-Date $startDate -Format 'yyyy-MM-dd HH:mm:ss'))"; | |
startDateTime = $(Get-Date $startDate -Format "yyyy-MM-ddTHH:mm:ss.0Z") | |
endDateTime = $(Get-Date $endDate -Format "yyyy-MM-ddTHH:mm:ss.0Z") | |
} | |
} | |
Write-Verbose "Request Body.............................: $($requestBody | ConvertTo-Json)" | |
# Get The Response From The Microsoft Graph | |
Try { | |
If (-not [string]::IsNullOrEmpty($applicationId)) { | |
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://graph.microsoft.com/v1.0/applications(appId='{$applicationId}')/addPassword" -Method POST -Headers $requestHeader -Body $($requestBody | ConvertTo-Json) -ContentType "application/json; charset=UTF-8" -ErrorAction Stop | |
Write-Verbose "Added A New Client Secret To The Application Registration With Application Id '$applicationId' With A Lifetime Of $(If ([string]::IsNullOrEmpty($customLifetimeSecretInDays)) {"$defaultInHours Hour(s)"} Else {"$customLifetimeSecretInDays Day(s)"})" | |
} | |
If (-not [string]::IsNullOrEmpty($applicationRegistrationObjectId)) { | |
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://graph.microsoft.com/v1.0/applications/$applicationRegistrationObjectId/addPassword" -Method POST -Headers $requestHeader -Body $($requestBody | ConvertTo-Json) -ContentType "application/json; charset=UTF-8" -ErrorAction Stop | |
Write-Verbose "Added A New Client Secret To The Application Registration With Object Id '$applicationRegistrationObjectId' With A Lifetime Of $(If ([string]::IsNullOrEmpty($customLifetimeSecretInDays)) {"$defaultInHours Hour(s)"} Else {"$customLifetimeSecretInDays Day(s)"})" | |
} | |
} Catch { | |
If ($_.ErrorDetails.Message) { | |
$errorMessage = $_.ErrorDetails.Message | |
} Else { | |
$errorMessage = $_ | |
If ($_ -like "*(401) Unauthorized*") { | |
Write-Verbose "Error: (401) Unauthorized - Not Authenticated, Or Access Token Has Expired" | |
} | |
If ($_ -like "*(404) Not Found*") { | |
Write-Verbose "Error: (404) Not Found - The Application Registration With $(If (-not [string]::IsNullOrEmpty($applicationId)) {"Application Id '$applicationId'"} ElseIf (-not [string]::IsNullOrEmpty($applicationRegistrationObjectId)) {"Object Id '$applicationRegistrationObjectId'"}) Does Not Exist In This Tenant" | |
} | |
} | |
Throw $errorMessage | |
} | |
# Return | |
Return $response | |
} | |
} | |
# FUNCTION: Add A Public Key Certificate To An Existing Application Registration With A Specific Application Registration Object Id or A Specific Application Id. | |
Function New-GraphApiApplicationRegistrationPublicKeyCertificate { | |
<# | |
.SYNOPSIS | |
Add A New Client Secret To An Existing Application Registration With A Specific Application Registration Object Id or A Specific Application Id. | |
.DESCRIPTION | |
Add A New Client Secret To An Existing Application Registration With A Specific Application Registration Object Id or A Specific Application Id. | |
.PARAMETER accessToken | |
The Access Token To Be Used To Execute The Operation | |
.PARAMETER applicationId | |
The Application ID Assigned And Configured On The Application Registration To Process | |
.PARAMETER applicationRegistrationObjectId | |
The Object Id Of The Application Registration To Process | |
.PARAMETER customKeyIdentifier | |
A 40-Character Binary Type That Can Be Used To Identify The Credential | |
(https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) | |
.PARAMETER displayName | |
The Friendly Name For The Key, With A Maximum Length Of 90 Characters. Longer Values Are Accepted But Shortened | |
(https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) | |
.PARAMETER endDateTime | |
The Date And Time At Which The Credential Expires (UTC time In ISO 8601 Format) | |
(https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) | |
.PARAMETER key | |
The Certificate's Raw Data In Byte Array Converted To Base64 String | |
(https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) | |
.PARAMETER keyId | |
The Unique Identifier (GUID) For The Key. | |
(https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) | |
.PARAMETER startDateTime | |
The Date And Time At Which The Credential Becomes Valid (UTC time In ISO 8601 Format) | |
(https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) | |
.PARAMETER type | |
The Type Of Key Credential: Symmetric, AsymmetricX509Cert. | |
(https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) | |
.PARAMETER usage | |
A String That Describes The Purpose For Which The Key Can Be Used: Verify | |
(https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$accessToken, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$applicationId, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$applicationRegistrationObjectId, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$customKeyIdentifier, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$displayName, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$endDateTime, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$key, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$keyId, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$startDateTime, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$type, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$usage | |
) | |
Begin { | |
# Making Sure The Correct Combination Of Parameters Are Used Targeting The Application | |
If ([string]::IsNullOrEmpty($applicationId) -And [string]::IsNullOrEmpty($applicationRegistrationObjectId)) { | |
Write-Error "The Correct Combination Of Parameters Is Not Being Used" | |
Write-Host " * You Need To Either Specify '-applicationId' And Specify An Application Id Or '-applicationRegistrationObjectId' And Specify The Application Registration Object Id" -ForegroundColor red | |
Write-Host "" | |
BREAK | |
} | |
# Making Sure The Correct Combination Of Parameters Are Used To Define A Key Credential | |
If ([string]::IsNullOrEmpty($customKeyIdentifier) -And [string]::IsNullOrEmpty($displayName) -And [string]::IsNullOrEmpty($endDateTime) -And [string]::IsNullOrEmpty($key) -And [string]::IsNullOrEmpty($keyId) -And [string]::IsNullOrEmpty($startDateTime) -And [string]::IsNullOrEmpty($type) -And [string]::IsNullOrEmpty($usage)) { | |
Write-Error "The Correct Combination Of Parameters Is Not Being Used" | |
Write-Host " * You Need To Specify '-customKeyIdentifier' And '-displayName' And '-endDateTime' And '-key' And '-keyId' And '-startDateTime' And '-type' And '-usage' And The Corresponding Values" -ForegroundColor red | |
Write-Host "" | |
BREAK | |
} | |
$requestHeader = $null | |
$requestBody = $null | |
$response = $null | |
} | |
Process { | |
# Define The Request Header To Execute The Required Action | |
$requestHeader = @{ | |
Authorization = "Bearer $accessToken" | |
} | |
Write-Verbose "Request Header...........................: $($requestHeader | Out-String)" | |
# Define The Request Body To Execute The Required Action | |
$requestBody = @( | |
[PSCustomObject]@{ | |
keyCredentials = @( | |
[PSCustomObject]@{ | |
customKeyIdentifier = $customKeyIdentifier | |
type = $type | |
usage = $usage | |
displayName = $displayName | |
startDateTime = $startDateTime | |
endDateTime = $endDateTime | |
key = $key | |
} | |
) | |
} | |
) | |
Write-Verbose "Request Body.............................: $($requestBody | ConvertTo-Json)" | |
# Get The Response From The Microsoft Graph | |
Try { | |
If (-not [string]::IsNullOrEmpty($applicationId)) { | |
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://graph.microsoft.com/v1.0/applications(appId='{$applicationId}')" -Method PATCH -Headers $requestHeader -Body $($requestBody | ConvertTo-Json) -ContentType "application/json; charset=UTF-8" -ErrorAction Stop | |
Write-Verbose "Added A New Public Key Certificate To The Application Registration With Application Id '$applicationId'" | |
} | |
If (-not [string]::IsNullOrEmpty($applicationRegistrationObjectId)) { | |
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://graph.microsoft.com/v1.0/applications/$applicationRegistrationObjectId" -Method PATCH -Headers $requestHeader -Body $($requestBody | ConvertTo-Json) -ContentType "application/json; charset=UTF-8" -ErrorAction Stop | |
Write-Verbose "Added A New Public Key Certificate To The Application Registration With Object Id '$applicationRegistrationObjectId'" | |
} | |
} Catch { | |
If ($_.ErrorDetails.Message) { | |
$errorMessage = $_.ErrorDetails.Message | |
} Else { | |
$errorMessage = $_ | |
If ($_ -like "*(401) Unauthorized*") { | |
Write-Verbose "Error: (401) Unauthorized - Not Authenticated, Or Access Token Has Expired" | |
} | |
If ($_ -like "*(404) Not Found*") { | |
Write-Verbose "Error: (404) Not Found - The Application Registration With $(If (-not [string]::IsNullOrEmpty($applicationId)) {"Application Id '$applicationId'"} ElseIf (-not [string]::IsNullOrEmpty($applicationRegistrationObjectId)) {"Object Id '$applicationRegistrationObjectId'"}) Does Not Exist In This Tenant" | |
} | |
} | |
Throw $errorMessage | |
} | |
# Return | |
Return $response | |
} | |
} | |
# FUNCTION: Get The Service Principal With A Specific Application ID Or Specific Service Principal Display Name | |
Function Get-GraphApiServicePrincipal { | |
<# | |
.SYNOPSIS | |
Get The Service Principal With A Specific Application ID Or Specific Service Principal Display Name | |
.DESCRIPTION | |
Get The Service Principal With A Specific Application ID Or Specific Service Principal Display Name | |
.PARAMETER accessToken | |
The Access Token To Be Used To Execute The Operation | |
.PARAMETER applicationId | |
The Application ID Assigned And Configured On The Service Principal To Process | |
.PARAMETER svcPrincDisplayName | |
The Application Display Name Assigned And Configured On The Service Principal To Process | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$accessToken, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$applicationId, | |
[Parameter(Mandatory = $FALSE)] | |
[string]$svcPrincDisplayName | |
) | |
Begin { | |
# Making Sure The Correct Combination Of Parameters Are Used | |
If ([string]::IsNullOrEmpty($applicationId) -And [string]::IsNullOrEmpty($svcPrincDisplayName)) { | |
Write-Error "The Correct Combination Of Parameters Is Not Being Used" | |
Write-Host " * You Need To Either Specify '-applicationId' And Specify An Application Id Or '-svcPrincDisplayName' And Specify The Service Principal Display Name" -ForegroundColor red | |
Write-Host "" | |
BREAK | |
} | |
$requestHeader = $null | |
$response = $null | |
} | |
Process { | |
# Define The Request Header To Execute The Required Action | |
$requestHeader = @{ | |
Authorization = "Bearer $accessToken" | |
} | |
Write-Verbose "Request Header...........................: $($requestHeader | Out-String)" | |
# Get The Response From The Microsoft Graph | |
Try { | |
If (-not [string]::IsNullOrEmpty($applicationId)) { | |
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId+eq+'$applicationId'" -Method GET -Headers $requestHeader -ErrorAction Stop | |
If ([string]::IsNullOrEmpty($($response.value | Out-String))) { | |
Write-Verbose "Unable To Find The Service Principal With App Id '$applicationId' Because It Does Not Exist In This Tenant" | |
} Else { | |
Write-Verbose "Found The Service Principal '$($response.value.displayName)' With Object Id '$($response.value.id)' And App Id '$($response.value.appId)'" | |
} | |
} | |
If (-not [string]::IsNullOrEmpty($svcPrincDisplayName)) { | |
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=displayName+eq+'$svcPrincDisplayName'" -Method GET -Headers $requestHeader -ErrorAction Stop | |
If ([string]::IsNullOrEmpty($($response.value | Out-String))) { | |
Write-Verbose "Unable To Find The Service Principal With Display Name '$svcPrincDisplayName' Because It Does Not Exist In This Tenant" | |
} Else { | |
Write-Verbose "Found The Service Principal '$($response.value.displayName)' With Object Id '$($response.value.id)' And App Id '$($response.value.appId)'" | |
} | |
} | |
} Catch { | |
If ($_.ErrorDetails.Message) { | |
$errorMessage = $_.ErrorDetails.Message | |
} Else { | |
$errorMessage = $_ | |
If ($_ -like "*(401) Unauthorized*") { | |
Write-Verbose "Error: (401) Unauthorized - Not Authenticated, Or Access Token Has Expired" | |
} | |
} | |
Throw $errorMessage | |
} | |
Return $response.value | |
} | |
} | |
# FUNCTION: Create The Service Principal With A Specific Application ID | |
Function New-GraphApiServicePrincipal { | |
<# | |
.SYNOPSIS | |
Create The Service Principal With A Specific Application ID | |
.DESCRIPTION | |
Create The Service Principal With A Specific Application ID | |
.PARAMETER accessToken | |
The Access Token To Be Used To Execute The Operation | |
.PARAMETER applicationId | |
The Application ID Assigned And Configured On The Service Principal | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$accessToken, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$applicationId | |
) | |
Begin { | |
$requestHeader = $null | |
$requestBody = $null | |
$response = $null | |
} | |
Process { | |
# Define The Request Header To Execute The Required Action | |
$requestHeader = @{ | |
Authorization = "Bearer $accessToken" | |
} | |
Write-Verbose "Request Header...........................: $($requestHeader | Out-String)" | |
# Define The Request Body To Execute The Required Action | |
$requestBody = @{ | |
appId = $applicationId | |
} | |
Write-Verbose "Request Body.............................: $($requestBody | Out-String)" | |
# Get The Response From The Microsoft Graph | |
Try { | |
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://graph.microsoft.com/v1.0/servicePrincipals" -Method POST -Headers $requestHeader -Body $($requestBody | ConvertTo-Json) -ContentType "application/json; charset=UTF-8" -ErrorAction Stop | |
Write-Verbose "Created The Service Principal '$($response.displayName)' With App Id '$($response.appId)' And Object Id '$($response.id)'" | |
} Catch { | |
If ($_.ErrorDetails.Message) { | |
$errorMessage = $_.ErrorDetails.Message | |
} Else { | |
$errorMessage = $_ | |
If ($_ -like "*(400) Bad Request*") { | |
Write-Verbose "Error: (400) Bad Request - The Application Registration With Application Id '$applicationId' Does Not Exist In This Tenant" | |
} | |
If ($_ -like "*(401) Unauthorized*") { | |
Write-Verbose "Error: (401) Unauthorized - Not Authenticated, Or Access Token Has Expired" | |
} | |
If ($_ -like "*(409) Conflict*") { | |
Write-Verbose "Error: (409) Conflict - The Service Principal With Application Id '$applicationId' Already Exists In This Tenant" | |
} | |
} | |
Throw $errorMessage | |
} | |
# Return | |
Return $response | |
} | |
} | |
# FUNCTION: Create The Service Principal With A Specific Application ID In Office 365 Exchange Online | |
Function New-EXOApiServicePrincipal { | |
<# | |
.SYNOPSIS | |
Create The Service Principal With A Specific Application ID In Office 365 Exchange Online | |
.DESCRIPTION | |
Create The Service Principal With A Specific Application ID In Office 365 Exchange Online | |
.PARAMETER accessToken | |
The Access Token To Be Used To Execute The Operation | |
.PARAMETER applicationName | |
The Application Name Assigned And Configured On The Service Principal In Entra ID | |
.PARAMETER applicationID | |
The Application ID Assigned And Configured On The Service Principal In Entra ID | |
.PARAMETER servicePrincipalObjectID | |
The Object ID From The Service Principal In Entra ID | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$accessToken, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$applicationName, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$applicationID, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$servicePrincipalObjectID | |
) | |
Begin { | |
$requestHeader = $null | |
$requestBody = $null | |
$response = $null | |
} | |
Process { | |
# Define The Request Header To Execute The Required Action | |
$requestHeader = @{ | |
Authorization = "Bearer $accessToken" | |
} | |
Write-Verbose "Request Header...........................: $($requestHeader | Out-String)" | |
# Define The Request Body To Execute The Required Action | |
$requestBody = [PSCustomObject]@{ | |
CmdletInput = [PSCustomObject]@{ | |
CmdletName = "New-ServicePrincipal" | |
Parameters = [PSCustomObject]@{ | |
ObjectId = $servicePrincipalObjectId | |
DisplayName = $applicationName | |
AppId = $applicationId | |
} | |
} | |
} | |
Write-Verbose "Request Body.............................: $($requestBody | Out-String)" | |
# Get The Response From The Office 365 Exchange Online API | |
Try { | |
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://outlook.office365.com/adminapi/beta/$tenantID/InvokeCommand" -Method POST -Headers $requestHeader -Body $($requestBody | ConvertTo-Json) -ContentType "application/json; charset=UTF-8" -ErrorAction Stop | |
Write-Verbose "Created The Exchange Online Service Principal '$applicationName' With App Id '$applicationId' And Object Id '$servicePrincipalObjectId'" | |
} Catch { | |
$err = $_.ErrorDetails.Message | ConvertFrom-Json | |
$errorMessage = $err.error.details.message | |
Throw $errorMessage | |
} | |
# Return | |
Return $response.value | |
} | |
} | |
# FUNCTION: Get The Mailbox Details In Office 365 Exchange Online | |
Function Get-EXOApiMailboxDetails { | |
<# | |
.SYNOPSIS | |
Get The Mailbox Details In Office 365 Exchange Online | |
.DESCRIPTION | |
Get The Mailbox Details In Office 365 Exchange Online | |
.PARAMETER accessToken | |
The Access Token To Be Used To Execute The Operation | |
.PARAMETER emailAddress | |
The E-mail Address Of The Mailbox To Get The Details For | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$accessToken, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$emailAddress | |
) | |
Begin { | |
$requestHeader = $null | |
$requestBody = $null | |
$response = $null | |
} | |
Process { | |
# Define The Request Header To Execute The Required Action | |
$requestHeader = @{ | |
Authorization = "Bearer $accessToken" | |
} | |
Write-Verbose "Request Header...........................: $($requestHeader | Out-String)" | |
# Define The Request Body To Execute The Required Action | |
$requestBody = [PSCustomObject]@{ | |
CmdletInput = [PSCustomObject]@{ | |
CmdletName = "Get-Mailbox" | |
Parameters = [PSCustomObject]@{ | |
Identity = $mailboxMailAddress | |
} | |
} | |
} | |
Write-Verbose "Request Body.............................: $($requestBody | Out-String)" | |
# Get The Response From The Office 365 Exchange Online API | |
Try { | |
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://outlook.office365.com/adminapi/beta/$tenantID/InvokeCommand" -Method POST -Headers $requestHeader -Body $($requestBody | ConvertTo-Json) -ContentType "application/json; charset=UTF-8" -ErrorAction Stop | |
Write-Verbose "Retrieved The Details From The Mailbox With E-mail Address '$mailboxMailAddress'" | |
} Catch { | |
$err = $_.ErrorDetails.Message | ConvertFrom-Json | |
$errorMessage = $err.error.details.message | |
Throw $errorMessage | |
} | |
# Return | |
Return $response.value | |
} | |
} | |
# FUNCTION: Create The Management Scope For A Specific Mailbox In Office 365 Exchange Online | |
Function New-EXOApiManagementScopeForMailbox { | |
<# | |
.SYNOPSIS | |
Create The Management Scope For A Specific Mailbox In Office 365 Exchange Online | |
.DESCRIPTION | |
Create The Management Scope For A Specific Mailbox In Office 365 Exchange Online | |
.PARAMETER accessToken | |
The Access Token To Be Used To Execute The Operation | |
.PARAMETER applicationName | |
The Application Name Assigned And Configured On The Service Principal In Entra ID | |
.PARAMETER mailboxGuid | |
The Guid Of The Mailbox To Create The Management Scope For | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$accessToken, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$applicationName, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$mailboxGuid | |
) | |
Begin { | |
$requestHeader = $null | |
$requestBody = $null | |
$response = $null | |
If ("RBAC-SCOPE-APP_$applicationName".Length -gt 64) { | |
$managementScopeName = "RBAC-SCOPE-APP_$applicationName".Substring(0,64) | |
} Else{ | |
$managementScopeName = "RBAC-SCOPE-APP_$applicationName" | |
} | |
} | |
Process { | |
# Define The Request Header To Execute The Required Action | |
$requestHeader = @{ | |
Authorization = "Bearer $accessToken" | |
} | |
Write-Verbose "Request Header...........................: $($requestHeader | Out-String)" | |
# Define The Request Body To Execute The Required Action | |
$requestBody = [PSCustomObject]@{ | |
CmdletInput = [PSCustomObject]@{ | |
CmdletName = "New-ManagementScope" | |
Parameters = [PSCustomObject]@{ | |
Name = $managementScopeName | |
RecipientRestrictionFilter = "Guid -eq '$mailboxGuid'" | |
} | |
} | |
} | |
Write-Verbose "Request Body.............................: $($requestBody | Out-String)" | |
# Get The Response From The Office 365 Exchange Online API | |
Try { | |
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://outlook.office365.com/adminapi/beta/$tenantID/InvokeCommand" -Method POST -Headers $requestHeader -Body $($requestBody | ConvertTo-Json) -ContentType "application/json; charset=UTF-8" -ErrorAction Stop | |
Write-Verbose "Created The Exchange Online Management Scope '$($response.value.Name)' With Filter '$($response.value.Filter)'" | |
} Catch { | |
$err = $_.ErrorDetails.Message | ConvertFrom-Json | |
$errorMessage = $err.error.details.message | |
Throw $errorMessage | |
} | |
# Return | |
Return $response.value | |
} | |
} | |
# FUNCTION: Create The Management Role Assignment For The RBAC Role And Scope In Office 365 Exchange Online | |
Function New-EXOApiManagementRoleAssignment { | |
<# | |
.SYNOPSIS | |
Create The Management Role Assignment For The RBAC Role And Scope In Office 365 Exchange Online | |
.DESCRIPTION | |
Create The Management Role Assignment For The RBAC Role And Scope In Office 365 Exchange Online | |
.PARAMETER accessToken | |
The Access Token To Be Used To Execute The Operation | |
.PARAMETER applicationName | |
The Application Name Assigned And Configured On The Service Principal In Entra ID | |
.PARAMETER applicationID | |
The Application ID Assigned And Configured On The Service Principal In Entra ID | |
.PARAMETER exoAppRole | |
The Office 365 Exchange Online Application Role (https://learn.microsoft.com/en-us/exchange/permissions-exo/application-rbac#supported-application-roles) | |
#> | |
[cmdletbinding()] | |
Param( | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$accessToken, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$applicationName, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$applicationID, | |
[Parameter(Mandatory = $TRUE)] | |
[ValidateNotNullOrEmpty()] | |
[string]$exoAppRole | |
) | |
Begin { | |
$requestHeader = $null | |
$requestBody = $null | |
$response = $null | |
If ("RBAC-ASSIGN-APP_$applicationName".Length -gt 64) { | |
$managementRoleAssignmentName = "RBAC-ASSIGN-APP_$applicationName".Substring(0,64) | |
} Else{ | |
$managementRoleAssignmentName = "RBAC-ASSIGN-APP_$applicationName" | |
} | |
If ("RBAC-SCOPE-APP_$applicationName".Length -gt 64) { | |
$managementScopeName = "RBAC-SCOPE-APP_$applicationName".Substring(0,64) | |
} Else{ | |
$managementScopeName = "RBAC-SCOPE-APP_$applicationName" | |
} | |
} | |
Process { | |
# Define The Request Header To Execute The Required Action | |
$requestHeader = @{ | |
Authorization = "Bearer $accessToken" | |
} | |
Write-Verbose "Request Header...........................: $($requestHeader | Out-String)" | |
# Define The Request Body To Execute The Required Action | |
$requestBody = [PSCustomObject]@{ | |
CmdletInput = [PSCustomObject]@{ | |
CmdletName = "New-ManagementRoleAssignment" | |
Parameters = [PSCustomObject]@{ | |
Name = $managementRoleAssignmentName | |
App = $applicationID | |
Role = $exoAppRole | |
CustomResourceScope = $managementScopeName | |
} | |
} | |
} | |
Write-Verbose "Request Body.............................: $($requestBody | Out-String)" | |
# Get The Response From The Office 365 Exchange Online API | |
Try { | |
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://outlook.office365.com/adminapi/beta/$tenantID/InvokeCommand" -Method POST -Headers $requestHeader -Body $($requestBody | ConvertTo-Json) -ContentType "application/json; charset=UTF-8" -ErrorAction Stop | |
Write-Verbose "Created The Exchange Online Management Role Assignment '$($response.value.Name)' For The Application '$applicationName' With Scope Definition '$($response.value.CustomResourceScope)' And Exo Role '$($response.value.Role)'" | |
} Catch { | |
$err = $_.ErrorDetails.Message | ConvertFrom-Json | |
$errorMessage = $err.error.details.message | |
Throw $errorMessage | |
} | |
# Return | |
Return $response.value | |
} | |
} | |
Clear-Host | |
################################################################ | |
### CONSTANTS ### | |
################################################################ | |
# If A Certificate Must Be Added To The Application, Let's Check The File Path Of The CER Has Been Specified | |
If (-not [String]::IsNullOrEmpty($certCERFilePath) -And $certCERFilePath -ne "<CERTIFICATE CER FILE PATH>") { | |
If (!(Test-Path $certCERFilePath)) { | |
Write-Host "" | |
Write-Host "The CER File '$certCERFilePath' DOES NOT Exist..." -Foregroundcolor Red | |
Write-Host "" | |
Write-Host " => Aborting Script..." -ForegroundColor Red | |
Write-Host "" | |
BREAK | |
} | |
} | |
################################################################ | |
### CHECKING REQUIREMENTS ### | |
################################################################ | |
# Due To The Use Of The PowerShell Module "PSMSALNet" For Authentication Against Entra ID, The Minimum PowerShell Version Is 7.4, So Let's Check If That Requirement Is Met. | |
If ($psVersionTable.PSVersion -lt [version]"7.4") { | |
Write-Host "" | |
Write-Host "This Script Depends On The PowerShell Module 'PSMSALNet' Which Has A minum Requirement Of PowerShell Version 7.4." -Foregroundcolor Red | |
Write-Host "" | |
Write-Host "Please Visit 'https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4'..." -Foregroundcolor Red | |
Write-Host "" | |
Write-Host "" | |
Write-Host " => Aborting Script..." -ForegroundColor Red | |
Write-Host "" | |
BREAK | |
} | |
# The Module "PSMSALNet" Is Used For Authentication Against Entra ID, So Let's Check If That Is Installed Or Not. If It Is Load The Most Recent Module | |
$moduleName = "PSMSALNet" | |
$psMSALNetModule = Get-Module -Name $moduleName -ListAvailable | |
If ([String]::IsNullOrEmpty($psMSALNetModule)) { | |
Write-Host "" | |
Write-Host "The $moduleName PowerShell Module IS NOT Installed" -ForegroundColor Red | |
Write-Host "" | |
Write-Host " => The $moduleName PowerShell Module Can Be Installed From An Elevated PowerShell Command Prompt By Running:" -ForegroundColor Red | |
Write-Host " - 'Install-Module $moduleName'" -ForegroundColor Red | |
Write-Host "" | |
Write-Host " => Aborting Script..." -ForegroundColor Red | |
Write-Host "" | |
BREAK | |
} Else { | |
$psMSALNetModuleVersionInstalled = $psMSALNetModule.Version | |
$psMSALNetModuleVersionAvailable = (Find-Module -Name $moduleName).Version | |
If ([version]$psMSALNetModuleVersionAvailable -gt $psMSALNetModuleVersionInstalled) { | |
Write-Host "" | |
Write-Host "A Newer Version Of The '$moduleName' PowerShell Module Is Available For Download/Install..." -ForegroundColor Red | |
Write-Host "" | |
} | |
} | |
If ($psMSALNetModule.count -gt 1) { | |
$latestVersionPSMSALNetModule = ($psMSALNetModule | Select-Object version | Sort-Object)[-1] | |
$psMSALNetModule = $psMSALNetModule | Where-Object {$_.version -eq $latestVersionMSFTGraphModule.version} | |
} | |
Try { | |
Import-Module $psMSALNetModule | |
} Catch { | |
$_ | |
} | |
################################################################ | |
### MAIN CODE ### | |
################################################################ | |
# Using The Tenant FQDN, Determine The Tenant ID | |
$oidcConfig = Get-MSFTApiOpenIDConfiguration -tenantFQDN $tenantFQDN | |
$tenantID = Get-MSFTTenantID -oidcTenantConfig $oidcConfig | |
If (!$([guid]::TryParse($tenantID, $([ref][guid]::Empty)))) { | |
Write-Host "" | |
Write-Host "Specified Tenant '$tenantFQDN' DOES NOT Exist..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host " => Aborting Script..." -ForegroundColor Red | |
Write-Host "" | |
BREAK | |
} | |
Write-Host "" | |
Write-Host "Tenant FQDN.........................: $tenantFQDN" | |
Write-Host "Tenant ID...........................: $tenantID" | |
Write-Host "" | |
# Authenticate And Get An Access Token For MSFT Graph API To Execute Actions In Entra ID | |
$clientId1 = "14d82eec-204b-4c2f-b7e8-296a70dab67e" # "Microsoft Graph Command Line Tools" | |
Write-Host "" | |
Write-Host "### Getting Access Token For Client App 'Microsoft Graph Command Line Tools' And The MSFT Graph API Resource In Entra ID Tenant '$tenantFQDN'..." -ForegroundColor Cyan | |
Write-Host "" | |
Try { | |
$entraTokenParametersHT1 = @{ | |
ClientId = $clientId1 | |
TenantId = $tenantID | |
RedirectUri = "http://localhost" | |
Resource = "GraphAPI" | |
Permissions = @("Directory.AccessAsUser.All") | |
verbose = $false | |
ErrorAction = "Stop" | |
} | |
$entraTokenMSGraphAPI = Get-EntraToken -PublicAuthorizationCodeFlow @entraTokenParametersHT1 | |
} Catch { | |
Write-Host "" | |
Write-Host " => Getting Access Token For Client App 'Microsoft Graph Command Line Tools' And The MSFT Graph API Resource In Entra ID Tenant '$tenantFQDN' Failed..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host " - Exception Type......: $($_.Exception.GetType().FullName)" -ForegroundColor Red | |
Write-Host " - Exception Message...: $($_.Exception.Message)" -ForegroundColor Red | |
Write-Host " - Error On Script Line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red | |
Write-Host "" | |
Write-Host " => Aborting Script..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host "" | |
Try { | |
Remove-Module $psMSALNetModule -ErrorAction SilentlyContinue | |
} Catch { | |
} | |
BREAK | |
} | |
$accessTokenMSGraphAPI = $entraTokenMSGraphAPI.AccessToken | |
# Authenticate And Get An Access Token For Office 365 Exchange Online To Execute Actions In Exchange Online | |
$clientId3 = "fb78d390-0c51-40cd-8e17-fdbfab77341b" # "Microsoft Exchange REST API Based Powershell" | |
Write-Host "" | |
Write-Host "### Getting Access Token For Client App 'Microsoft Exchange REST API Based Powershell' And The Office 365 Exchange Online Resource In Entra ID Tenant '$tenantFQDN'..." -ForegroundColor Cyan | |
Write-Host "" | |
Try { | |
$entraTokenParametersHT3 = @{ | |
ClientId = $clientId3 | |
TenantId = $tenantID | |
RedirectUri = "http://localhost" | |
Resource = "Custom" | |
CustomResource = "00000002-0000-0ff1-ce00-000000000000" # Resource ID For Office 365 Exchange Online (=> https://outlook.office365.com) | |
Permissions = @("AdminApi.AccessAsUser.All","FfoPowerShell.AccessAsUser.All","RemotePowerShell.AccessAsUser.All") | |
verbose = $false | |
ErrorAction = "Stop" | |
} | |
$entraTokenO365EXO = Get-EntraToken -PublicAuthorizationCodeFlow @entraTokenParametersHT3 | |
} Catch { | |
Write-Host "" | |
Write-Host " => Getting Access Token For Client App 'Microsoft Exchange REST API Based Powershell' And The Office 365 Exchange Online Resource Resource In Entra ID Tenant '$tenantFQDN' Failed..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host " - Exception Type......: $($_.Exception.GetType().FullName)" -ForegroundColor Red | |
Write-Host " - Exception Message...: $($_.Exception.Message)" -ForegroundColor Red | |
Write-Host " - Error On Script Line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red | |
Write-Host "" | |
Write-Host " => Aborting Script..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host "" | |
Try { | |
Remove-Module $psMSALNetModule -ErrorAction SilentlyContinue | |
} Catch { | |
} | |
BREAK | |
} | |
$accessTokenO365EXO = $entraTokenO365EXO.AccessToken | |
Write-Host "" | |
Write-Host "### Creating And Configuring The '$appRegDisplayName' App In Entra ID..." -ForegroundColor Cyan | |
Write-Host "" | |
# Creating The Application Registration In Entra ID | |
$eidAppReg = New-GraphApiApplicationRegistration -accessToken $accessTokenMSGraphAPI -appRegDisplayName $appRegDisplayName | |
If (-not [String]::IsNullOrEmpty($eidAppReg)) { | |
$eidAppRegObjectID = $eidAppReg.Id | |
$eidAppRegApplicationID = $eidAppReg.AppId | |
Write-Host " => Entra ID: Application Registration '$appRegDisplayName' Has Been Created Successfully..." -ForegroundColor Green | |
Write-Host "" | |
} Else { | |
Write-Host " => Entra ID: Application Registration '$appRegDisplayName' Failed To Be Created..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host "" | |
Try { | |
Remove-Module $psMSALNetModule -ErrorAction SilentlyContinue | |
} Catch { | |
} | |
BREAK | |
} | |
# Creating The Service Principal In Entra ID | |
Write-Host " => Waiting A Few Seconds Before Continuing..." -ForegroundColor Yellow | |
Write-Host "" | |
Start-Sleep -s 10 | |
$eidSvcPrinc = New-GraphApiServicePrincipal -accessToken $accessTokenMSGraphAPI -applicationId $eidAppRegApplicationID | |
If (-not [String]::IsNullOrEmpty($eidSvcPrinc)) { | |
$eidSvcPrincObjectID = $eidSvcPrinc.Id | |
$eidSvcPrincApplicationID = $eidSvcPrinc.AppId | |
Write-Host " => Entra ID: Service Principal '$appRegDisplayName' Has Been Created Successfully..." -ForegroundColor Green | |
Write-Host "" | |
} Else { | |
Write-Host " => Entra ID: Service Principal '$appRegDisplayName' Failed To Be Created..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host "" | |
Try { | |
Remove-Module $psMSALNetModule -ErrorAction SilentlyContinue | |
} Catch { | |
} | |
BREAK | |
} | |
# Creating A Client Secret For The App In Entra ID | |
If ($credentialType.ToUpper() -eq "SECRET") { | |
$responseNewAppRegClntScrt = New-GraphApiApplicationRegistrationClientSecret -accessToken $accessTokenMSGraphAPI -applicationId $eidAppRegApplicationID -customLifetimeSecretInDays $lifetimeSecretInDays | |
If ($responseNewAppRegClntScrt.StatusCode -eq 200) { | |
$eidClientSecret = $responseNewAppRegClntScrt.Content | ConvertFrom-Json | |
Write-Host " => Entra ID: Client Secret For Application Registration '$appRegDisplayName' Has Been Created..." -ForegroundColor Green | |
Write-Host "" | |
} Else { | |
Write-Host " => Entra ID: Client Secret For Application Registration '$appRegDisplayName' Failed To Be Created..." -ForegroundColor Red | |
Write-Host "" | |
} | |
} | |
# Uploading A Certificate For The App In Entra ID | |
If ($credentialType.ToUpper() -eq "CERTIFICATE") { | |
# Import The CER File Into A Certificate Object And Get The Base Details | |
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certCERFilePath) | |
$certFriendlyName = $cert.FriendlyName | |
$certSubjectName = $cert.Subject | |
$certThumbprint = $cert.Thumbprint | |
$certNotBefore = $cert.NotBefore | |
$certNotAfter = $cert.NotAfter | |
# Get The Certificate RAWData | |
$certRawData = $cert.GetRawCertData() | |
# Base64 Encode The Public Key | |
$certCERBase64 = [System.Convert]::ToBase64String($certRawData) | |
# Get The Custom Key Identifier | |
$certHash = $cert.GetCertHash() | |
$certCustomKeyIdentifier = [System.Convert]::ToBase64String($certHash) | |
# Get The NotBefore And NotAfter Dates Of The Certificate In The ISO8601Format | |
$certNotBeforeISO8601Format = (Get-Date $certNotBefore).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") | |
$certNotAfterISO8601Format = (Get-Date $certNotAfter).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") | |
# Define The Key ID | |
$keyID = (New-Guid).Guid | |
$responseNewAppRegCertificate = New-GraphApiApplicationRegistrationPublicKeyCertificate -accessToken $accessTokenMSGraphAPI -applicationId $eidAppRegApplicationID -customKeyIdentifier $certCustomKeyIdentifier -keyId $keyID -displayName $certSubjectName -startDateTime $certNotBeforeISO8601Format -endDateTime $certNotAfterISO8601Format -type "AsymmetricX509Cert" -usage "Verify" -key $certCERBase64 | |
If ($responseNewAppRegCertificate.StatusCode -eq 204) { | |
Write-Host " => Entra ID: Certificate For Application Registration '$appRegDisplayName' Was Uploaded Successfully..." -ForegroundColor Green | |
Write-Host "" | |
} Else { | |
Write-Host " => Entra ID: Certificate For Application Registration '$appRegDisplayName' Failed To Be Uploaded..." -ForegroundColor Red | |
Write-Host "" | |
} | |
} | |
# Creating The Service Principal In Office 365 Exchange Online | |
Write-Host " => Waiting A Few Seconds Before Continuing..." -ForegroundColor Yellow | |
Write-Host "" | |
Start-Sleep -s 30 | |
$exoSvcPrinc = New-EXOApiServicePrincipal -accessToken $accessTokenO365EXO -applicationName $appRegDisplayName -applicationID $eidAppRegApplicationID -servicePrincipalObjectID $eidSvcPrincObjectID | |
If (-not [String]::IsNullOrEmpty($exoSvcPrinc)) { | |
$exoSvcPrincName = $exoSvcPrinc.DisplayName | |
$exoSvcPrincGuid = $exoSvcPrinc.Guid | |
Write-Host " => Office 365 Exchange Online: Service Principal '$exoSvcPrincName' Has Been Created Successfully..." -ForegroundColor Green | |
Write-Host "" | |
} Else { | |
Write-Host " => Office 365 Exchange Online: Service Principal For '$appRegDisplayName' Failed To Be Created..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host "" | |
Try { | |
Remove-Module $psMSALNetModule -ErrorAction SilentlyContinue | |
} Catch { | |
} | |
BREAK | |
} | |
# Getting The Mailbox Details For Which The Application Requires Access | |
$mbxDetails = Get-EXOApiMailboxDetails -accessToken $accessTokenO365EXO -emailAddress $mailboxMailAddress | |
If (-not [String]::IsNullOrEmpty($mbxDetails)) { | |
$mbxDisplayName = $mbxDetails.DisplayName | |
$mbxUserPrincipalName = $mbxDetails.UserPrincipalName | |
$mbxGuid = $mbxDetails.Guid | |
Write-Host " => Retrieve The Mailbox Details For The Mailbox '$mbxDisplayName' With E-mail Address '$mbxUserPrincipalName'..." -ForegroundColor Green | |
Write-Host "" | |
} Else { | |
Write-Host " => Failed To Retrieve The Mailbox Details For The Mailbox With E-mail Address '$mailboxMailAddress'..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host "" | |
Try { | |
Remove-Module $psMSALNetModule -ErrorAction SilentlyContinue | |
} Catch { | |
} | |
BREAK | |
} | |
# Creating The Management Scope For The Mailbox In Office 365 Exchange Online | |
$exoMgmtScope = New-EXOApiManagementScopeForMailbox -accessToken $accessTokenO365EXO -applicationName $appRegDisplayName -mailboxGuid $mbxGuid | |
If (-not [String]::IsNullOrEmpty($exoMgmtScope)) { | |
$exoMgmtScopeName = $exoMgmtScope.Name | |
Write-Host " => Office 365 Exchange Online: Management Scope '$exoMgmtScopeName' Has Been Created Successfully..." -ForegroundColor Green | |
Write-Host "" | |
} Else { | |
Write-Host " => Office 365 Exchange Online: Management Scope For '$appRegDisplayName' Failed To Be Created..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host "" | |
Try { | |
Remove-Module $psMSALNetModule -ErrorAction SilentlyContinue | |
} Catch { | |
} | |
BREAK | |
} | |
# Creating The Management Role Assignment For The Mailbox In Office 365 Exchange Online | |
$exoMgmtScopeAssignment = New-EXOApiManagementRoleAssignment -accessToken $accessTokenO365EXO -applicationName $appRegDisplayName -applicationID $eidAppRegApplicationID -exoAppRole "Application Mail.Send" | |
If (-not [String]::IsNullOrEmpty($exoMgmtScopeAssignment)) { | |
$exoMgmtRoleAssignmentName = $exoMgmtScopeAssignment.Name | |
Write-Host " => Office 365 Exchange Online: Management Role Assignment '$exoMgmtRoleAssignmentName' Has Been Created Successfully..." -ForegroundColor Green | |
Write-Host "" | |
} Else { | |
Write-Host " => Office 365 Exchange Online: Management Role Assignment For '$appRegDisplayName' Failed To Be Created..." -ForegroundColor Red | |
Write-Host "" | |
Write-Host "" | |
Try { | |
Remove-Module $psMSALNetModule -ErrorAction SilentlyContinue | |
} Catch { | |
} | |
BREAK | |
} | |
# Displaying The Required Data For The App In Entra ID | |
Write-Host "### Displaying The Required Data For The App In Entra ID..." -ForegroundColor Cyan | |
Write-Host "" | |
Write-Host " => Tenant FQDN....................: '$tenantFQDN' (Required For 'Reset-KrbTgt-Password-For-RWDCs-And-RODCs.xml')" -ForegroundColor Yellow | |
Write-Host " => Tenant ID......................: '$tenantID'" -ForegroundColor Yellow | |
Write-Host " => Application Name...............: '$appRegDisplayName'" -ForegroundColor Yellow | |
Write-Host " => Service Principal Object ID....: '$eidSvcPrincObjectID'" -ForegroundColor Yellow | |
Write-Host " => App Registration Object ID.....: '$eidAppRegObjectID'" -ForegroundColor Yellow | |
Write-Host " => App Registration App ID........: '$eidAppRegApplicationID' (Required For 'Reset-KrbTgt-Password-For-RWDCs-And-RODCs.xml')" -ForegroundColor Yellow | |
Write-Host " => Service Principal Guid (EXO)...: '$exoSvcPrincGuid'" -ForegroundColor Yellow | |
Write-Host " => Mailbox Name...................: '$mbxDisplayName'" -ForegroundColor Yellow | |
Write-Host " => Mailbox Mail Address...........: '$mailboxMailAddress'" -ForegroundColor Yellow | |
Write-Host " => Mailbox Guid...................: '$mbxGuid'" -ForegroundColor Yellow | |
Write-Host " => Management Scope Name..........: '$exoMgmtScopeName'" -ForegroundColor Yellow | |
Write-Host " => Mgmt Role Assignment Name......: '$exoMgmtRoleAssignmentName'" -ForegroundColor Yellow | |
If ($credentialType.ToUpper() -eq "SECRET") { | |
Write-Host " => Credential Type................: SECRET" -ForegroundColor Yellow | |
Write-Host " => Credential Key ID..............: '$($eidClientSecret.keyId)' (Required For 'Reset-KrbTgt-Password-For-RWDCs-And-RODCs.xml')" -ForegroundColor Yellow | |
Write-Host " => Client Secret..................: '$($eidClientSecret.SecretText)' (Required For 'Reset-KrbTgt-Password-For-RWDCs-And-RODCs.xml')" -ForegroundColor Yellow | |
Write-Host " => Lifetime Client Secret.........: '$lifetimeSecretInDays Days'" -ForegroundColor Yellow | |
Write-Host " => Start Date.....................: '$($eidClientSecret.StartDateTime)'" -ForegroundColor Yellow | |
Write-Host " => End Date.......................: '$($eidClientSecret.EndDateTime)'" -ForegroundColor Yellow | |
Write-Host "" | |
Write-Host "WARNING: Make Sure To Store The Client Secret Specified Above In A Secure Location Like E.g. A Password Vault And Restrict Access To That Credential!!!" -ForegroundColor Red | |
} | |
Write-Host "" | |
If ($credentialType.ToUpper() -eq "CERTIFICATE") { | |
Write-Host " => Credential Type................: CERTIFICATE" -ForegroundColor Yellow | |
Write-Host " => Credential Display Name........: '$certSubjectName' (Required For 'Reset-KrbTgt-Password-For-RWDCs-And-RODCs.xml')" -ForegroundColor Yellow | |
Write-Host " => Friendly Name..................: '$certFriendlyName'" -ForegroundColor Yellow | |
Write-Host " => Subject Name...................: '$certSubjectName'" -ForegroundColor Yellow | |
Write-Host " => Thumbprint.....................: '$certThumbprint'" -ForegroundColor Yellow | |
Write-Host " => 'Not Before' Date (yyyy-MM-dd).: '$(Get-Date $certNotBefore -Format "yyyy-MM-dd HH:mm:ss")'" -ForegroundColor Yellow | |
Write-Host " => 'Not After' Date (yyyy-MM-dd)..: '$(Get-Date $certNotAfter -Format "yyyy-MM-dd HH:mm:ss")'" -ForegroundColor Yellow | |
} | |
Write-Host "" | |
### | |
# THE END OF THE SCRIPT | |
### | |
Try { | |
Remove-Module $psMSALNetModule -ErrorAction SilentlyContinue | |
} Catch { | |
$_ | |
} | |
Write-Host "" | |
Write-Host " +++ DONE +++ " -ForegroundColor Cyan | |
Write-Host "" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment