This document describes the steps to create a custom eSHARE Collaborate application in Microsoft Entra (Azure AD) portal. At a high level, the steps involved are:
REQUIREMENT:
The Person performing these steps must have an administrator role assignment of Application Administator, Cloud Application Administator, or Global Administator in the destination M365 tenant.
Application Registration - Commercial Cloud
To register the custom App Registration for eSHARE, login to Microsoft Entra console (https://entra.microsoft.com or https://entra.microsoft.us) and navigate to ‘Applications‘ > ‘App Registrations‘.
In ‘App Registrations‘ page, click the ‘+ New registration‘ button in top menu bar.
Input a recognizable unique name for the custom eSHARE app and select the ‘Register‘ button at bottom of the page.
In a few moments, a shell application is created and ready for further configuration.
Alternate Method through PowerShell Script
You can also automate the Commercial Cloud installation steps by running a PowerShell script. Copy and paste the following script into PowerShell to perform the same configuration
Expand this block and copy the script below
# =============================================================================
# Create App Registration: eSHARE Collaborate
# - Single tenant
# - Web + SPA redirect URIs
# - API permissions: Graph, ARMS, MIP, Exchange Online, O365 Mgmt, SharePoint
#
# Usage:
# ./create_eshare_collaborate.ps1 -TenantId <tenant-guid>
# =============================================================================
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $TenantId,
# Optional: create a 10-year self-signed certificate and export the
# private key as a PEM .key file. The certificate is NOT uploaded to the
# app automatically — you must upload it manually via the Entra portal.
# If not specified, the script prompts interactively.
[switch] $CreateCertificate,
# Where the .cer / .crt / .key files are written when -CreateCertificate is on.
[string] $CertOutputDir = (Join-Path $PSScriptRoot 'certs')
)
$ErrorActionPreference = 'Stop'
$DisplayName = 'eSHARE Collaborate'
$WebRedirectUris = @(
'https://www.ncryptedcloud.com/oauth2_openid/consume/outlook',
'https://www.ncryptedcloud.com/oauth2_openid/consume/labels',
'https://www.ncryptedcloud.com/oauth2_openid/consume/personal_graphAPI',
'https://login.ncryptedcloud.com/oauth2_openid/consume/personal_graphAPI',
'https://n11d.com/oauth2_openid/consume/graphAPI',
'https://login.ncryptedcloud.com/oauth2_openid/consume/graphAPI',
'https://login.ncryptedcloud.com/oauth2_openid/consume/outlook',
'https://login.ncryptedcloud.com/cloudwebportal/onedrive/auth/endpoint/provisioning/',
'https://login.ncryptedcloud.com/cloudwebportal/onedrive/auth/endpoint/business/',
'https://login.ncryptedcloud.com/oauth2_openid/consume/adgraph',
'https://login.ncryptedcloud.com/oauth2_openid/consume/sharepoint_search'
)
$SpaRedirectUris = @(
'https://*.sharepoint.com/_forms/default.aspx'
)
# Permissions to request, grouped by resource API.
# Type = 'Application' (app role) or 'Delegated' (oauth2 scope).
$PermissionsByResource = @(
@{
ResourceName = 'Azure Rights Management Services'
ResourceAppId = '00000012-0000-0000-c000-000000000000'
Permissions = @(
@{ Name = 'Content.DelegatedReader'; Type = 'Application' },
@{ Name = 'user_impersonation'; Type = 'Delegated' }
)
},
@{
ResourceName = 'Microsoft Graph'
ResourceAppId = '00000003-0000-0000-c000-000000000000'
Permissions = @(
@{ Name = 'Directory.Read.All'; Type = 'Application' },
@{ Name = 'Files.ReadWrite.All'; Type = 'Application' },
@{ Name = 'Group.Read.All'; Type = 'Application' },
@{ Name = 'GroupMember.Read.All'; Type = 'Application' },
@{ Name = 'Sites.ReadWrite.All'; Type = 'Application' },
@{ Name = 'User.Read.All'; Type = 'Application' }
)
},
@{
ResourceName = 'Microsoft Information Protection Sync Service'
ResourceAppId = '870c4f2e-85b6-4d43-bdda-6ed9a579b725'
Permissions = @(
@{ Name = 'UnifiedPolicy.User.Read'; Type = 'Delegated' }
)
},
@{
ResourceName = 'Office 365 Exchange Online'
ResourceAppId = '00000002-0000-0ff1-ce00-000000000000'
Permissions = @(
@{ Name = 'Exchange.ManageAsApp'; Type = 'Application' }
)
},
@{
ResourceName = 'Office 365 Management APIs'
ResourceAppId = 'c5393580-f805-4401-95e8-94b7a6ef2fc2'
Permissions = @(
@{ Name = 'ActivityFeed.Read'; Type = 'Application' },
@{ Name = 'ActivityFeed.ReadDlp'; Type = 'Application' }
)
},
@{
ResourceName = 'SharePoint'
ResourceAppId = '00000003-0000-0ff1-ce00-000000000000'
Permissions = @(
@{ Name = 'Sites.Read.All'; Type = 'Application' }
)
}
)
# --- Ensure required modules ---
$requiredModules = @('Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications')
foreach ($m in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $m)) {
Write-Host "Installing module $m ..." -ForegroundColor Yellow
Install-Module -Name $m -Scope CurrentUser -Force -AllowClobber
}
}
Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
Import-Module Microsoft.Graph.Applications -ErrorAction Stop
# --- Connect to Microsoft Graph (reuse existing session if same tenant) ---
$existingCtx = Get-MgContext -ErrorAction SilentlyContinue
$weConnected = $false
if ($existingCtx -and $existingCtx.TenantId -eq $TenantId) {
Write-Host "Reusing existing Microsoft Graph connection (tenant $TenantId)." -ForegroundColor Green
} else {
try {
Connect-MgGraph -TenantId $TenantId -Scopes 'Application.ReadWrite.All','Directory.Read.All' -NoWelcome -ErrorAction Stop
Write-Host "Connected to Microsoft Graph (tenant $TenantId)." -ForegroundColor Green
$weConnected = $true
} catch {
Write-Host "Error: Failed to connect to Microsoft Graph: $_" -ForegroundColor Red
exit 1
}
}
# If we did not open the connection, shadow Disconnect-MgGraph with a no-op
# so the rest of the script (including error paths) does not tear down the
# caller's session.
if (-not $weConnected) {
function Disconnect-MgGraph { param() }
}
# --- Build RequiredResourceAccess by resolving permission IDs at runtime ---
$requiredResourceAccess = @()
foreach ($resource in $PermissionsByResource) {
$sp = Get-MgServicePrincipal -Filter "appId eq '$($resource.ResourceAppId)'" -ErrorAction Stop
if (-not $sp) {
Write-Host "Error: Service principal for '$($resource.ResourceName)' (appId $($resource.ResourceAppId)) not found in tenant. The resource API may not be provisioned. Run: New-MgServicePrincipal -AppId $($resource.ResourceAppId)" -ForegroundColor Red
Disconnect-MgGraph | Out-Null
exit 1
}
$resourceAccess = @()
foreach ($p in $resource.Permissions) {
if ($p.Type -eq 'Application') {
$role = $sp.AppRoles | Where-Object { $_.Value -eq $p.Name }
if (-not $role) {
Write-Host "Error: Application permission '$($p.Name)' not found on resource '$($resource.ResourceName)'." -ForegroundColor Red
Disconnect-MgGraph | Out-Null
exit 1
}
$resourceAccess += @{ id = $role.Id; type = 'Role' }
}
elseif ($p.Type -eq 'Delegated') {
$scope = $sp.Oauth2PermissionScopes | Where-Object { $_.Value -eq $p.Name }
if (-not $scope) {
Write-Host "Error: Delegated permission '$($p.Name)' not found on resource '$($resource.ResourceName)'." -ForegroundColor Red
Disconnect-MgGraph | Out-Null
exit 1
}
$resourceAccess += @{ id = $scope.Id; type = 'Scope' }
}
else {
Write-Host "Error: Unknown permission type '$($p.Type)' for '$($p.Name)'. Use 'Application' or 'Delegated'." -ForegroundColor Red
Disconnect-MgGraph | Out-Null
exit 1
}
}
$requiredResourceAccess += @{
resourceAppId = $sp.AppId
resourceAccess = $resourceAccess
}
}
# --- Authentication platforms ---
$Web = @{
RedirectUris = $WebRedirectUris
ImplicitGrantSettings = @{
EnableAccessTokenIssuance = $false
EnableIdTokenIssuance = $false
}
}
$Spa = @{ RedirectUris = $SpaRedirectUris }
# --- Create or update the application ---
$existing = Get-MgApplication -Filter "displayName eq '$DisplayName'" -ErrorAction SilentlyContinue
if ($existing) {
Write-Host "Application '$DisplayName' already exists (AppId: $($existing.AppId)). Updating it." -ForegroundColor Yellow
Update-MgApplication -ApplicationId $existing.Id `
-SignInAudience 'AzureADMyOrg' `
-Web $Web `
-Spa $Spa `
-RequiredResourceAccess $requiredResourceAccess `
-ErrorAction Stop
$app = Get-MgApplication -ApplicationId $existing.Id
} else {
$app = New-MgApplication `
-DisplayName $DisplayName `
-SignInAudience 'AzureADMyOrg' `
-Web $Web `
-Spa $Spa `
-RequiredResourceAccess $requiredResourceAccess `
-ErrorAction Stop
Write-Host "Created application '$DisplayName' (AppId: $($app.AppId))." -ForegroundColor Green
}
# --- Upload application logo (best-effort; skips gracefully if unavailable) ---
$LogoUrl = 'https://eshareimagereference.blob.core.windows.net/images/eSHARE%20Sign%20WhiteBg.png'
$logoTmp = Join-Path ([System.IO.Path]::GetTempPath()) "eshare-logo-$([guid]::NewGuid()).png"
try {
Write-Host "Downloading application logo from $LogoUrl ..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $LogoUrl -OutFile $logoTmp -UseBasicParsing -ErrorAction Stop
$sig = [System.IO.File]::ReadAllBytes($logoTmp) | Select-Object -First 4
if ($sig.Count -ge 4 -and $sig[0] -eq 0x89 -and $sig[1] -eq 0x50 -and $sig[2] -eq 0x4E -and $sig[3] -eq 0x47) {
Set-MgApplicationLogo -ApplicationId $app.Id -InFile $logoTmp -ContentType 'image/png' -ErrorAction Stop
Write-Host "Uploaded application logo." -ForegroundColor Green
} else {
Write-Host "Skipping logo upload: response from $LogoUrl is not a PNG." -ForegroundColor Yellow
}
} catch {
Write-Host "Skipping logo upload: $($_.Exception.Message)" -ForegroundColor Yellow
} finally {
if (Test-Path $logoTmp) { Remove-Item $logoTmp -ErrorAction SilentlyContinue }
}
# --- Optional: create a self-signed certificate and export key ---
if (-not $PSBoundParameters.ContainsKey('CreateCertificate')) {
while ($true) {
$a = Read-Host "Create a 10-year self-signed certificate for '$DisplayName'? You will need to upload it to the app manually. [y/n]"
switch ($a.ToString().Trim().ToLower()) {
'y' { $CreateCertificate = $true; break }
'n' { $CreateCertificate = $false; break }
default { Write-Host " Please enter 'y' or 'n'." -ForegroundColor Yellow; continue }
}
break
}
}
if ($CreateCertificate) {
$rsa = $null
$cert = $null
try {
$safeName = ($DisplayName -replace '[^\w\.\-]', '_')
$appCertDir = Join-Path $CertOutputDir $safeName
New-Item -ItemType Directory -Force -Path $appCertDir | Out-Null
$rsa = [System.Security.Cryptography.RSA]::Create(2048)
$req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new(
"CN=$DisplayName",
$rsa,
[System.Security.Cryptography.HashAlgorithmName]::SHA256,
[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
$notBefore = [DateTimeOffset]::UtcNow.AddMinutes(-5)
$notAfter = $notBefore.AddYears(10)
$cert = $req.CreateSelfSigned($notBefore, $notAfter)
$cerBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
$crtPath = Join-Path $appCertDir "$safeName.crt"
$keyPath = Join-Path $appCertDir "$safeName.key"
$crtPem = "-----BEGIN CERTIFICATE-----`n" +
([Convert]::ToBase64String($cerBytes, 'InsertLineBreaks')) +
"`n-----END CERTIFICATE-----`n"
[System.IO.File]::WriteAllText($crtPath, $crtPem)
$keyDer = $rsa.ExportPkcs8PrivateKey()
$keyPem = "-----BEGIN PRIVATE KEY-----`n" +
([Convert]::ToBase64String($keyDer, 'InsertLineBreaks')) +
"`n-----END PRIVATE KEY-----`n"
[System.IO.File]::WriteAllText($keyPath, $keyPem)
# Export password-protected PFX (PKCS#12) bundle.
$pfxPassword = -join ((48..57) + (65..90) + (97..122) + (33,35,37,42,64) | Get-Random -Count 32 | ForEach-Object { [char]$_ })
$pfxPath = Join-Path $appCertDir "$safeName.pfx"
$pfxPwdPath = Join-Path $appCertDir "$safeName-pfx-password.txt"
$pfxBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $pfxPassword)
[System.IO.File]::WriteAllBytes($pfxPath, $pfxBytes)
[System.IO.File]::WriteAllText($pfxPwdPath, $pfxPassword)
# Best-effort: tighten permissions on sensitive files.
try {
if ($IsWindows) {
foreach ($sensitivePath in @($keyPath, $pfxPath, $pfxPwdPath)) {
$acl = Get-Acl $sensitivePath
$acl.SetAccessRuleProtection($true, $false)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
'FullControl','Allow')
$acl.SetAccessRule($rule)
Set-Acl -Path $sensitivePath -AclObject $acl
}
} else {
& chmod 600 $keyPath $pfxPath $pfxPwdPath 2>$null
}
} catch { }
Write-Host "Created 10-year self-signed certificate (thumbprint: $($cert.Thumbprint))." -ForegroundColor Green
Write-Host " Public cert (PEM, .crt) : $crtPath" -ForegroundColor Green
Write-Host " Private key (PEM, .key) : $keyPath" -ForegroundColor Green
Write-Host " PFX bundle (.pfx) : $pfxPath" -ForegroundColor Green
Write-Host " PFX password (.txt) : $pfxPwdPath" -ForegroundColor Green
Write-Host "" -ForegroundColor Yellow
Write-Host " IMPORTANT: You must upload the .crt file to the app registration" -ForegroundColor Yellow
Write-Host " manually via Entra admin center > App registrations > Certificates & secrets." -ForegroundColor Yellow
} catch {
Write-Host "Error creating certificate: $_" -ForegroundColor Red
} finally {
if ($cert) { $cert.Dispose() }
if ($rsa) { $rsa.Dispose() }
}
} else {
Write-Host "Skipping certificate creation." -ForegroundColor Gray
}
# --- Summary ---
Write-Host ""
Write-Host "=========================================================="
Write-Host " App Registration Created / Updated"
Write-Host "=========================================================="
Write-Host " Display Name : $($app.DisplayName)"
Write-Host " Object Id : $($app.Id)"
Write-Host " Application (Client) Id : $($app.AppId)"
Write-Host " Sign-in Audience : $($app.SignInAudience)"
if ($app.Web.RedirectUris) {
Write-Host " Web Redirect URIs:"
$app.Web.RedirectUris | ForEach-Object { Write-Host " - $_" }
}
if ($app.Spa.RedirectUris) {
Write-Host " SPA Redirect URIs:"
$app.Spa.RedirectUris | ForEach-Object { Write-Host " - $_" }
}
Write-Host "=========================================================="
Write-Host ""
Write-Host "NOTE: Application permissions still require ADMIN CONSENT." -ForegroundColor Yellow
Write-Host "Grant consent in the Entra admin center, or via:" -ForegroundColor Yellow
Write-Host " https://login.microsoftonline.com/$TenantId/adminconsent?client_id=$($app.AppId)" -ForegroundColor Yellow
Write-Host ""
Disconnect-MgGraph | Out-Null
Modify Application Manifest - Commercial Cloud
requiredResourceAccess
In the newly registered application page, navigate to the ‘Manage‘ > ‘Manifest‘ tab.
In the applications’ manifest, look for ‘requiredResourceAccess‘.
.png?sv=2026-02-06&spr=https&st=2026-05-31T18%3A25%3A09Z&se=2026-05-31T19%3A02%3A09Z&sr=c&sp=r&sig=OG%2FgghdppTvSImHIg8yL88RAJCBAV9fi3pbqfv7Tdbg%3D)
Replace the above string (including the comma at the end) from line 53 to 63 with the below text.
"requiredResourceAccess": [
{
"resourceAppId": "00000003-0000-0ff1-ce00-000000000000",
"resourceAccess": [
{
"id": "d13f72ca-a275-4b96-b789-48ebcc4da984",
"type": "Role"
}
]
},
{
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
"type": "Scope"
},
{
"id": "7ab1d382-f21e-4acd-a863-ba3e13f7da61",
"type": "Role"
},
{
"id": "75359482-378d-4052-8f01-80520e7db3cd",
"type": "Role"
},
{
"id": "5b567255-7703-4780-807c-7be8301ae99b",
"type": "Role"
},
{
"id": "98830695-27a2-44f7-8c18-0c3ebc9698f6",
"type": "Role"
},
{
"id": "9492366f-7969-46a4-8d15-ed1a20078fff",
"type": "Role"
},
{
"id": "df021288-bdef-4463-88db-98f22de89214",
"type": "Role"
}
]
},
{
"resourceAppId": "870c4f2e-85b6-4d43-bdda-6ed9a579b725",
"resourceAccess": [
{
"id": "34f7024b-1bed-402f-9664-f5316a1e1b4a",
"type": "Scope"
}
]
},
{
"resourceAppId": "00000012-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "c9c9a04d-3b66-4ca8-a00c-fca953e2afd3",
"type": "Scope"
},
{
"id": "7f740376-647b-4ad7-9ff7-292af252707a",
"type": "Role"
}
]
},
{
"resourceAppId": "00000002-0000-0ff1-ce00-000000000000",
"resourceAccess": [
{
"id": "dc50a0fb-09a3-484d-be87-e023b12c6440",
"type": "Role"
}
]
},
{
"resourceAppId": "c5393580-f805-4401-95e8-94b7a6ef2fc2",
"resourceAccess": [
{
"id": "594c1fb6-4f81-4475-ae41-0c394909246c",
"type": "Role"
},
{
"id": "4807a72c-ad38-4250-94c9-4eabfe26cd55",
"type": "Role"
}
]
}
],Click the ‘Save‘ button in top menu bar.
web
In the same application manifest, look for line 146 to 149 and replace it with the below text:
"web": {
"homePageUrl": null,
"logoutUrl": null,
"redirectUris": [
"https://login.ncryptedcloud.com/oauth2_openid/consume/sharepoint_search",
"https://login.ncryptedcloud.com/oauth2_openid/consume/adgraph",
"https://login.ncryptedcloud.com/cloudwebportal/onedrive/auth/endpoint/business/",
"https://login.ncryptedcloud.com/cloudwebportal/onedrive/auth/endpoint/provisioning/",
"https://login.ncryptedcloud.com/oauth2_openid/consume/outlook",
"https://login.ncryptedcloud.com/oauth2_openid/consume/graphAPI",
"https://n11d.com/oauth2_openid/consume/graphAPI",
"https://login.ncryptedcloud.com/oauth2_openid/consume/personal_graphAPI",
"https://www.ncryptedcloud.com/oauth2_openid/consume/personal_graphAPI",
"https://www.ncryptedcloud.com/oauth2_openid/consume/labels",
"https://www.ncryptedcloud.com/oauth2_openid/consume/outlook"
],Click on ‘Save‘ button in top menu bar.
redirectUriSettings
In the same application manifest, look for line 166 to 167 and replace it with the below text:
"redirectUriSettings": [
{
"uri": "https://login.ncryptedcloud.com/oauth2_openid/consume/sharepoint_search",
"index": null
},
{
"uri": "https://login.ncryptedcloud.com/oauth2_openid/consume/adgraph",
"index": null
},
{
"uri": "https://login.ncryptedcloud.com/cloudwebportal/onedrive/auth/endpoint/business/",
"index": null
},
{
"uri": "https://login.ncryptedcloud.com/cloudwebportal/onedrive/auth/endpoint/provisioning/",
"index": null
},
{
"uri": "https://login.ncryptedcloud.com/oauth2_openid/consume/outlook",
"index": null
},
{
"uri": "https://login.ncryptedcloud.com/oauth2_openid/consume/graphAPI",
"index": null
},
{
"uri": "https://n11d.com/oauth2_openid/consume/graphAPI",
"index": null
},
{
"uri": "https://login.ncryptedcloud.com/oauth2_openid/consume/personal_graphAPI",
"index": null
},
{
"uri": "https://www.ncryptedcloud.com/oauth2_openid/consume/personal_graphAPI",
"index": null
},
{
"uri": "https://www.ncryptedcloud.com/oauth2_openid/consume/labels",
"index": null
},
{
"uri": "https://www.ncryptedcloud.com/oauth2_openid/consume/outlook",
"index": null
}
]
},Click on ‘Save‘ button in top menu bar.
spa
In the same application manifest, look for line 221 to 224 and replace it with the below text:
"spa": {
"redirectUris": [
"https://*.sharepoint.com/_forms/default.aspx"
]
}
}Click on ‘Save‘ button in top menu bar.
Application Registration - Government Cloud
Alternate Method through PowerShell Script
You can also automate the Government Cloud installation steps by running a PowerShell script. Copy and paste the below script into PowerShell to perform the same configuration
Expand this block and copy the script below
# =============================================================================
# Create App Registration: eSHARE Collaborate (GCC HIGH)
# - Single tenant
# - Web + SPA redirect URIs
# - API permissions: Graph, ARMS, MIP, Exchange Online, O365 Mgmt, SharePoint
#
# GCC High targets the US Government cloud (graph.microsoft.us /
# login.microsoftonline.us). Connects via Connect-MgGraph -Environment USGov.
#
# Usage:
# ./create_eshare_collaborate.ps1 -TenantId <tenant-guid>
# =============================================================================
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $TenantId,
# Optional: create a 10-year self-signed certificate and export the
# private key as a PEM .key file. The certificate is NOT uploaded to the
# app automatically — you must upload it manually via the Entra portal.
# If not specified, the script prompts interactively.
[switch] $CreateCertificate,
# Where the .cer / .crt / .key files are written when -CreateCertificate is on.
[string] $CertOutputDir = (Join-Path $PSScriptRoot 'certs')
)
$ErrorActionPreference = 'Stop'
$DisplayName = 'eSHARE Collaborate'
$GraphEnv = 'USGov' # GCC High
$AdminConsentHost = 'login.microsoftonline.us' # GCC High consent host
$WebRedirectUris = @(
'https://app.e-sharegov.us/oauth2_openid/consume/outlook',
'https://app.e-sharegov.us/oauth2_openid/consume/labels',
'https://app.e-sharegov.us/oauth2_openid/consume/personal_graphAPI',
'https://app.e-sharegov.us/oauth2_openid/consume/graphAPI',
'https://app.e-sharegov.us/cloudwebportal/onedrive/auth/endpoint/provisioning/',
'https://app.e-sharegov.us/cloudwebportal/onedrive/auth/endpoint/business/',
'https://app.e-sharegov.us/oauth2_openid/consume/adgraph',
'https://app.e-sharegov.us/oauth2_openid/consume/sharepoint_search'
)
$SpaRedirectUris = @(
'https://*.sharepoint.us/_forms/default.aspx'
)
# Resource AppIds are the same across commercial and GCC High clouds.
$PermissionsByResource = @(
@{
ResourceName = 'Azure Rights Management Services'
ResourceAppId = '00000012-0000-0000-c000-000000000000'
Permissions = @(
@{ Name = 'Content.DelegatedReader'; Type = 'Application' },
@{ Name = 'user_impersonation'; Type = 'Delegated' }
)
},
@{
ResourceName = 'Microsoft Graph'
ResourceAppId = '00000003-0000-0000-c000-000000000000'
Permissions = @(
@{ Name = 'Directory.Read.All'; Type = 'Application' },
@{ Name = 'Files.ReadWrite.All'; Type = 'Application' },
@{ Name = 'Group.Read.All'; Type = 'Application' },
@{ Name = 'GroupMember.Read.All'; Type = 'Application' },
@{ Name = 'Sites.ReadWrite.All'; Type = 'Application' },
@{ Name = 'User.Read.All'; Type = 'Application' }
)
},
@{
ResourceName = 'Microsoft Information Protection Sync Service'
ResourceAppId = '870c4f2e-85b6-4d43-bdda-6ed9a579b725'
Permissions = @(
@{ Name = 'UnifiedPolicy.User.Read'; Type = 'Delegated' }
)
},
@{
ResourceName = 'Office 365 Exchange Online'
ResourceAppId = '00000002-0000-0ff1-ce00-000000000000'
Permissions = @(
@{ Name = 'Exchange.ManageAsApp'; Type = 'Application' }
)
},
@{
ResourceName = 'Office 365 Management APIs'
ResourceAppId = 'c5393580-f805-4401-95e8-94b7a6ef2fc2'
Permissions = @(
@{ Name = 'ActivityFeed.Read'; Type = 'Application' },
@{ Name = 'ActivityFeed.ReadDlp'; Type = 'Application' }
)
},
@{
ResourceName = 'SharePoint'
ResourceAppId = '00000003-0000-0ff1-ce00-000000000000'
Permissions = @(
@{ Name = 'Sites.Read.All'; Type = 'Application' }
)
}
)
# --- Ensure required modules ---
$requiredModules = @('Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications')
foreach ($m in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $m)) {
Write-Host "Installing module $m ..." -ForegroundColor Yellow
Install-Module -Name $m -Scope CurrentUser -Force -AllowClobber
}
}
Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
Import-Module Microsoft.Graph.Applications -ErrorAction Stop
# --- Connect to Microsoft Graph (GCC High; reuse existing session if same tenant) ---
$existingCtx = Get-MgContext -ErrorAction SilentlyContinue
$weConnected = $false
if ($existingCtx -and $existingCtx.TenantId -eq $TenantId) {
Write-Host "Reusing existing Microsoft Graph connection (tenant $TenantId)." -ForegroundColor Green
} else {
try {
Connect-MgGraph -Environment $GraphEnv -TenantId $TenantId -Scopes 'Application.ReadWrite.All','Directory.Read.All' -NoWelcome -ErrorAction Stop
Write-Host "Connected to Microsoft Graph [$GraphEnv] (tenant $TenantId)." -ForegroundColor Green
$weConnected = $true
} catch {
Write-Host "Error: Failed to connect to Microsoft Graph: $_" -ForegroundColor Red
exit 1
}
}
if (-not $weConnected) {
function Disconnect-MgGraph { param() }
}
# --- Build RequiredResourceAccess by resolving permission IDs at runtime ---
$requiredResourceAccess = @()
foreach ($resource in $PermissionsByResource) {
$sp = Get-MgServicePrincipal -Filter "appId eq '$($resource.ResourceAppId)'" -ErrorAction Stop
if (-not $sp) {
Write-Host "Error: Service principal for '$($resource.ResourceName)' (appId $($resource.ResourceAppId)) not found in tenant. The resource API may not be provisioned. Run: New-MgServicePrincipal -AppId $($resource.ResourceAppId)" -ForegroundColor Red
Disconnect-MgGraph | Out-Null
exit 1
}
$resourceAccess = @()
foreach ($p in $resource.Permissions) {
if ($p.Type -eq 'Application') {
$role = $sp.AppRoles | Where-Object { $_.Value -eq $p.Name }
if (-not $role) {
Write-Host "Error: Application permission '$($p.Name)' not found on resource '$($resource.ResourceName)'." -ForegroundColor Red
Disconnect-MgGraph | Out-Null
exit 1
}
$resourceAccess += @{ id = $role.Id; type = 'Role' }
}
elseif ($p.Type -eq 'Delegated') {
$scope = $sp.Oauth2PermissionScopes | Where-Object { $_.Value -eq $p.Name }
if (-not $scope) {
Write-Host "Error: Delegated permission '$($p.Name)' not found on resource '$($resource.ResourceName)'." -ForegroundColor Red
Disconnect-MgGraph | Out-Null
exit 1
}
$resourceAccess += @{ id = $scope.Id; type = 'Scope' }
}
else {
Write-Host "Error: Unknown permission type '$($p.Type)' for '$($p.Name)'. Use 'Application' or 'Delegated'." -ForegroundColor Red
Disconnect-MgGraph | Out-Null
exit 1
}
}
$requiredResourceAccess += @{
resourceAppId = $sp.AppId
resourceAccess = $resourceAccess
}
}
# --- Authentication platforms ---
$Web = @{
RedirectUris = $WebRedirectUris
ImplicitGrantSettings = @{
EnableAccessTokenIssuance = $false
EnableIdTokenIssuance = $false
}
}
$Spa = @{ RedirectUris = $SpaRedirectUris }
# --- Create or update the application ---
$existing = Get-MgApplication -Filter "displayName eq '$DisplayName'" -ErrorAction SilentlyContinue
if ($existing) {
Write-Host "Application '$DisplayName' already exists (AppId: $($existing.AppId)). Updating it." -ForegroundColor Yellow
Update-MgApplication -ApplicationId $existing.Id `
-SignInAudience 'AzureADMyOrg' `
-Web $Web `
-Spa $Spa `
-RequiredResourceAccess $requiredResourceAccess `
-ErrorAction Stop
$app = Get-MgApplication -ApplicationId $existing.Id
} else {
$app = New-MgApplication `
-DisplayName $DisplayName `
-SignInAudience 'AzureADMyOrg' `
-Web $Web `
-Spa $Spa `
-RequiredResourceAccess $requiredResourceAccess `
-ErrorAction Stop
Write-Host "Created application '$DisplayName' (AppId: $($app.AppId))." -ForegroundColor Green
}
# --- Upload application logo (best-effort; skips gracefully if unavailable) ---
$LogoUrl = 'https://eshareimagereference.blob.core.windows.net/images/eSHARE%20Sign%20WhiteBg.png'
$logoTmp = Join-Path ([System.IO.Path]::GetTempPath()) "eshare-logo-$([guid]::NewGuid()).png"
try {
Write-Host "Downloading application logo from $LogoUrl ..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $LogoUrl -OutFile $logoTmp -UseBasicParsing -ErrorAction Stop
$sig = [System.IO.File]::ReadAllBytes($logoTmp) | Select-Object -First 4
if ($sig.Count -ge 4 -and $sig[0] -eq 0x89 -and $sig[1] -eq 0x50 -and $sig[2] -eq 0x4E -and $sig[3] -eq 0x47) {
Set-MgApplicationLogo -ApplicationId $app.Id -InFile $logoTmp -ContentType 'image/png' -ErrorAction Stop
Write-Host "Uploaded application logo." -ForegroundColor Green
} else {
Write-Host "Skipping logo upload: response from $LogoUrl is not a PNG." -ForegroundColor Yellow
}
} catch {
Write-Host "Skipping logo upload: $($_.Exception.Message)" -ForegroundColor Yellow
} finally {
if (Test-Path $logoTmp) { Remove-Item $logoTmp -ErrorAction SilentlyContinue }
}
# --- Optional: create a self-signed certificate and export key ---
if (-not $PSBoundParameters.ContainsKey('CreateCertificate')) {
while ($true) {
$a = Read-Host "Create a 10-year self-signed certificate for '$DisplayName'? You will need to upload it to the app manually. [y/n]"
switch ($a.ToString().Trim().ToLower()) {
'y' { $CreateCertificate = $true; break }
'n' { $CreateCertificate = $false; break }
default { Write-Host " Please enter 'y' or 'n'." -ForegroundColor Yellow; continue }
}
break
}
}
if ($CreateCertificate) {
$rsa = $null
$cert = $null
try {
$safeName = ($DisplayName -replace '[^\w\.\-]', '_')
$appCertDir = Join-Path $CertOutputDir $safeName
New-Item -ItemType Directory -Force -Path $appCertDir | Out-Null
$rsa = [System.Security.Cryptography.RSA]::Create(2048)
$req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new(
"CN=$DisplayName",
$rsa,
[System.Security.Cryptography.HashAlgorithmName]::SHA256,
[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
$notBefore = [DateTimeOffset]::UtcNow.AddMinutes(-5)
$notAfter = $notBefore.AddYears(10)
$cert = $req.CreateSelfSigned($notBefore, $notAfter)
$cerBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
$crtPath = Join-Path $appCertDir "$safeName.crt"
$keyPath = Join-Path $appCertDir "$safeName.key"
$crtPem = "-----BEGIN CERTIFICATE-----`n" +
([Convert]::ToBase64String($cerBytes, 'InsertLineBreaks')) +
"`n-----END CERTIFICATE-----`n"
[System.IO.File]::WriteAllText($crtPath, $crtPem)
$keyDer = $rsa.ExportPkcs8PrivateKey()
$keyPem = "-----BEGIN PRIVATE KEY-----`n" +
([Convert]::ToBase64String($keyDer, 'InsertLineBreaks')) +
"`n-----END PRIVATE KEY-----`n"
[System.IO.File]::WriteAllText($keyPath, $keyPem)
# Export password-protected PFX (PKCS#12) bundle.
$pfxPassword = -join ((48..57) + (65..90) + (97..122) + (33,35,37,42,64) | Get-Random -Count 32 | ForEach-Object { [char]$_ })
$pfxPath = Join-Path $appCertDir "$safeName.pfx"
$pfxPwdPath = Join-Path $appCertDir "$safeName-pfx-password.txt"
$pfxBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $pfxPassword)
[System.IO.File]::WriteAllBytes($pfxPath, $pfxBytes)
[System.IO.File]::WriteAllText($pfxPwdPath, $pfxPassword)
# Best-effort: tighten permissions on sensitive files.
try {
if ($IsWindows) {
foreach ($sensitivePath in @($keyPath, $pfxPath, $pfxPwdPath)) {
$acl = Get-Acl $sensitivePath
$acl.SetAccessRuleProtection($true, $false)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
'FullControl','Allow')
$acl.SetAccessRule($rule)
Set-Acl -Path $sensitivePath -AclObject $acl
}
} else {
& chmod 600 $keyPath $pfxPath $pfxPwdPath 2>$null
}
} catch { }
Write-Host "Created 10-year self-signed certificate (thumbprint: $($cert.Thumbprint))." -ForegroundColor Green
Write-Host " Public cert (PEM, .crt) : $crtPath" -ForegroundColor Green
Write-Host " Private key (PEM, .key) : $keyPath" -ForegroundColor Green
Write-Host " PFX bundle (.pfx) : $pfxPath" -ForegroundColor Green
Write-Host " PFX password (.txt) : $pfxPwdPath" -ForegroundColor Green
Write-Host "" -ForegroundColor Yellow
Write-Host " IMPORTANT: You must upload the .crt file to the app registration" -ForegroundColor Yellow
Write-Host " manually via Entra admin center > App registrations > Certificates & secrets." -ForegroundColor Yellow
} catch {
Write-Host "Error creating certificate: $_" -ForegroundColor Red
} finally {
if ($cert) { $cert.Dispose() }
if ($rsa) { $rsa.Dispose() }
}
} else {
Write-Host "Skipping certificate creation." -ForegroundColor Gray
}
# --- Summary ---
Write-Host ""
Write-Host "=========================================================="
Write-Host " App Registration Created / Updated [GCC HIGH]"
Write-Host "=========================================================="
Write-Host " Display Name : $($app.DisplayName)"
Write-Host " Object Id : $($app.Id)"
Write-Host " Application (Client) Id : $($app.AppId)"
Write-Host " Sign-in Audience : $($app.SignInAudience)"
if ($app.Web.RedirectUris) {
Write-Host " Web Redirect URIs:"
$app.Web.RedirectUris | ForEach-Object { Write-Host " - $_" }
}
if ($app.Spa.RedirectUris) {
Write-Host " SPA Redirect URIs:"
$app.Spa.RedirectUris | ForEach-Object { Write-Host " - $_" }
}
Write-Host "=========================================================="
Write-Host ""
Write-Host "NOTE: Application permissions still require ADMIN CONSENT." -ForegroundColor Yellow
Write-Host "Grant consent in the Entra admin center (GCC High), or via:" -ForegroundColor Yellow
Write-Host " https://$AdminConsentHost/$TenantId/adminconsent?client_id=$($app.AppId)" -ForegroundColor Yellow
Write-Host ""
Disconnect-MgGraph | Out-Null
Modify Application Manifest - Government Cloud
replyUrlsWithType
In the newly registered application page, navigate to the ‘Manage’ > ‘Manifest’ tab.
In the applications’ manifest, look for ‘replyUrlsWithType’.

Replace the above string (including the comma at the end) from line 41 with the below text.
"replyUrlsWithType": [
{
"url": "https://app.e-sharegov.us/oauth2_openid/consume/graphAPI",
"type": "Web"
},
{
"url": "https://app.e-sharegov.us/oauth2_openid/consume/labels",
"type": "Web"
},
{
"url": "https://app.e-sharegov.us/oauth2_openid/consume/sharepoint_search",
"type": "Web"
},
{
"url": "https://app.e-sharegov.us/oauth2_openid/consume/outlook",
"type": "Web"
},
{
"url": "https://*.sharepoint.us/_forms/default.aspx",
"type": "Spa"
},
{
"url": "https://app.e-sharegov.us/oauth2_openid/consume/personal_graphAPI",
"type": "Web"
},
{
"url": "https://app.e-sharegov.us/cloudwebportal/onedrive/auth/endpoint/provisioning/",
"type": "Web"
},
{
"url": "https://app.e-sharegov.us/cloudwebportal/onedrive/auth/endpoint/business/",
"type": "Web"
},
{
"url": "https://app.e-sharegov.us/oauth2_openid/consume/adgraph",
"type": "Web"
}
],Click the ‘Save‘ button in top menu bar.
requiredResourceAccess
In the same application manifest, look for line 79 to 89 and replace it with the below text:
"requiredResourceAccess": [
{
"resourceAppId": "00000003-0000-0ff1-ce00-000000000000",
"resourceAccess": [
{
"id": "d13f72ca-a275-4b96-b789-48ebcc4da984",
"type": "Role"
}
]
},
{
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
"type": "Scope"
},
{
"id": "7ab1d382-f21e-4acd-a863-ba3e13f7da61",
"type": "Role"
},
{
"id": "75359482-378d-4052-8f01-80520e7db3cd",
"type": "Role"
},
{
"id": "5b567255-7703-4780-807c-7be8301ae99b",
"type": "Role"
},
{
"id": "98830695-27a2-44f7-8c18-0c3ebc9698f6",
"type": "Role"
},
{
"id": "9492366f-7969-46a4-8d15-ed1a20078fff",
"type": "Role"
},
{
"id": "df021288-bdef-4463-88db-98f22de89214",
"type": "Role"
}
]
},
{
"resourceAppId": "870c4f2e-85b6-4d43-bdda-6ed9a579b725",
"resourceAccess": [
{
"id": "34f7024b-1bed-402f-9664-f5316a1e1b4a",
"type": "Scope"
}
]
},
{
"resourceAppId": "00000012-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "c9c9a04d-3b66-4ca8-a00c-fca953e2afd3",
"type": "Scope"
},
{
"id": "7f740376-647b-4ad7-9ff7-292af252707a",
"type": "Role"
}
]
},
{
"resourceAppId": "c5393580-f805-4401-95e8-94b7a6ef2fc2",
"resourceAccess": [
{
"id": "594c1fb6-4f81-4475-ae41-0c394909246c",
"type": "Role"
},
{
"id": "4807a72c-ad38-4250-94c9-4eabfe26cd55",
"type": "Role"
}
]
},
{
"resourceAppId": "00000002-0000-0ff1-ce00-000000000000",
"resourceAccess": [
{
"id": "dc50a0fb-09a3-484d-be87-e023b12c6440",
"type": "Role"
}
]
}
],Click the ‘Save‘ button in top menu bar.
Review API permissions and complete consent
REQUIREMENT:
Granting admin consent for permissions within an app registration must be performed by a Global Administator in the destination M365 tenant.
**Please consult your eSHARE Customer Success Manager before removing any API Permissions**
For the saved application, navigate to the ‘Manage‘ > ‘API permissions‘ tab.
Select ‘Grant admin consent for <tenant name>‘ in top of the API permissions table.
Confirm when prompted.

Upload Application Certificate
Option 1: Purchase a certificate from well-known certificate authority, extract the private and public portions of the certificate. Upload the public portion in Azure portal for the application and upload the private key in your eSHARE admin console (or provide the private key to your eSHARE admin).
Option 2: Create a self-signed certificate by following instructions available at https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-create-self-signed-certificate.
A short summary of the above instructions are below:
# Create a self-signed certificate in PowerShell
$mycert = "eShareApp"
$mycert = New-SelfSignedCertificate -DnsName "eShareApp" -Subject "CN=eShareApp" -CertStoreLocation "Cert:\CurrentUser\My" -NotAfter (Get-Date).AddYears(50) -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256# Extract .key file using OpenSSL
openssl pkcs12 -in eShareApp.pfx -nocerts -nodes -out eShareApp.key# Extract crt from pfx using OpenSSL
openssl pkcs12 -in eShareApp.pfx -clcerts -nokeys -out eShareApp.crt# Export certificate to .pfx file
$mycert | Export-PfxCertificate -FilePath eShareApp.pfx -Password $(ConvertTo-SecureString -String "myp@55W0rd" -AsPlainText -Force)When appropriate certificate portions are available, navigate to ‘Manage‘ > ‘Certificates & secrets’
In ‘Certificates’ tab, click on ‘Upload certificate’ button. Upload the CRT file from the above steps and click on ‘Add’ at bottom of the open pane.
Collect Items Required for eSHARE Tenant
M365 Tenant ID: Navigate to ‘Identity > Overview’ tab of the Entra admin console, copy the ‘Tenant ID’ and save it.
Application ID: Navigate to ‘Overview’ tab of the application, copy the ‘Application (client) ID’ and save it.
Certificate Thumbprint: For the certificate used during application registration process, copy the certificate thumbprint and save it.
Private Key: The private key (.key file from above steps) for the certificate uploaded in Entra portal for the application registration.