A lightweight web application for Microsoft 365 administrators to quickly see which mailboxes a specific user has delegated access to in Exchange Online — the reverse of what the EAC shows ou[...]
Warning
This project was primarily created using AI-assisted development (Vibe Coding) and has been processed through multiple AI agents and automated stages to review for security vulnerabilities.
We do not provide any guarantee that this code is free of security vulnerabilities. Use it at your own risk, and ensure you perform your own security review before deploying it in a production or sensitive environment.
Responsible / confidential reporting of security vulnerabilities is welcome. Please report any discovered vulnerabilities privately — do not open a public issue. You can use GitHub's private vulnerability reporting to submit a report confidentially.
| Feature | Details |
|---|---|
| People Picker | Autocomplete user search via Microsoft Graph API |
| Full Access | Finds every mailbox the user can open and manage |
| Send As | Finds every mailbox the user can send email as |
| Send on Behalf | Finds every mailbox the user can send on behalf of |
| M365 Login | Azure AD authentication; access restricted to a configurable Entra ID group |
| Filter & Search | Filter results by permission type or mailbox name |
| Export CSV | One-click export of all results |
| Azure-ready | Runs on Azure App Service (free tier) or in Docker |
Browser
│
├─► Express (Node.js) ──► Microsoft Graph API (user search / autocomplete)
│ │
│ └──► PowerShell (ExchangeOnlineManagement) ──► Exchange Online
│
└─ Azure AD (MSAL) ──► Entra ID group check ──► allow / deny
Two Azure AD app registrations are required:
| Registration | Purpose | Auth type |
|---|---|---|
| Web App | User login + Graph API (People Picker) | Delegated (user signs in) |
| EXO App | PowerShell app-only access to Exchange Online | Application (certificate) |
- Node.js 18+ (for local dev or Azure App Service)
or Docker (for containerised deployment) - PowerShell Core 7+ (
pwsh) with theExchangeOnlineManagementmodule ≥ 3.0.0 - Two Azure AD app registrations (see below)
- An EXO certificate (PFX) for the Exchange Online app registration
- Azure Portal → Azure Active Directory → App registrations → New registration
- Name it, e.g.
EXO Tools. - Supported account type: Accounts in this organizational directory only (Single tenant).
- Redirect URI → Web →
https://<your-app>.azurewebsites.net/callback
(addhttp://localhost:5000/callbackfor local development as well) - Note the Application (client) ID and Directory (tenant) ID.
- Certificates & secrets → New client secret → copy the value immediately.
- API permissions → Add a permission → Microsoft Graph → Delegated:
User.ReadUser.ReadBasic.AllGroupMember.Read.All(only needed if you configureACCESS_GROUP_ID)
- Grant admin consent for the tenant.
- (Optional) Token configuration → Add groups claim → Security groups – adds the
groupsclaim to the ID token for a faster group membership check.
-
New registration → name it, e.g.
EXO Tools – EXO Access. -
Note the Application (client) ID.
-
API permissions → Add a permission → APIs my organization uses →
SearchOffice 365 Exchange Online→ Application permissions →
Exchange.ManageAsApp→ Add. -
Grant admin consent.
-
Assign the Exchange Administrator role to the app in Exchange Online:
# Connect as a Global / Exchange Administrator first Connect-ExchangeOnline -UserPrincipalName admin@yourtenant.onmicrosoft.com New-ManagementRoleAssignment ` -App "<EXO App client ID>" ` -Role "View-Only Organization Management" # For full read access you may also add: # -Role "Mailbox Search"
Minimum roles needed:
View-Only Organization Management(forGet-Mailbox,Get-User) andView-Only Recipients(forGet-RecipientPermission).
# Run in PowerShell on your local machine
# Generate a 2-year self-signed certificate
$cert = New-SelfSignedCertificate `
-Subject "CN=EXOTools" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-KeyExportPolicy Exportable `
-KeySpec Signature `
-KeyLength 2048 `
-KeyAlgorithm RSA `
-HashAlgorithm SHA256 `
-NotAfter (Get-Date).AddYears(2)
# Export the PUBLIC key (.cer) – upload this to the Azure AD app registration
Export-Certificate -Cert $cert -FilePath "exo-cert.cer"
# Export the PRIVATE key (.pfx) – place this on the web server, never commit it
$pwd = ConvertTo-SecureString -String "YourCertPassword" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath "exo-cert.pfx" -Password $pwdUpload exo-cert.cer to the EXO app registration under Certificates & secrets → Certificates.
Keep exo-cert.pfx secure – it goes on the server only.
cp .env.example .env
# Edit .env and fill in all values| Variable | Description |
|---|---|
SECRET_KEY |
Random string for session signing |
CLIENT_ID |
Web App registration client ID |
CLIENT_SECRET |
Web App client secret |
TENANT_ID |
Azure AD tenant ID |
REDIRECT_URI |
OAuth redirect URI (must match app registration) |
ACCESS_GROUP_ID |
Entra ID group Object ID (leave empty for all users) |
EXO_APP_ID |
EXO app registration client ID |
EXO_CERT_PATH |
Absolute path to the PFX file |
EXO_CERT_PASSWORD |
PFX password (empty if none) |
EXO_ORGANIZATION |
yourtenant.onmicrosoft.com |
PWSH_PATH |
Path to pwsh executable (default: pwsh) |
PS_TIMEOUT |
Seconds before the PS script times out (default: 600) |
# 1. Build and start
cp .env.example .env # fill in your values
mkdir -p certs
cp /path/to/exo-cert.pfx certs/exo.pfx
docker compose up -d
# 2. Open http://localhost:5000# Install Azure CLI if needed: https://aka.ms/install-az-cli
az login
# Create resource group and App Service plan (free tier)
az group create --name rg-exotools --location westeurope
az appservice plan create \
--name asp-exotools \
--resource-group rg-exotools \
--sku F1 \
--is-linux
# Create the web app (Node.js 22)
az webapp create \
--name exo-tools \
--resource-group rg-exotools \
--plan asp-exotools \
--runtime "NODE:22-lts"
# Set startup command
az webapp config set \
--name exo-tools \
--resource-group rg-exotools \
--startup-file "bash startup.sh"
# Upload environment variables (repeat for each variable)
az webapp config appsettings set \
--name exo-tools \
--resource-group rg-exotools \
--settings \
SECRET_KEY="..." \
CLIENT_ID="..." \
CLIENT_SECRET="..." \
TENANT_ID="..." \
REDIRECT_URI="https://exo-tools.azurewebsites.net/callback" \
EXO_APP_ID="..." \
EXO_ORGANIZATION="yourtenant.onmicrosoft.com" \
EXO_CERT_PATH="/home/site/wwwroot/certs/exo.pfx" \
EXO_CERT_PASSWORD="..."
# Deploy code
az webapp deploy \
--name exo-tools \
--resource-group rg-exotools \
--src-path . \
--type zip
# Upload the certificate via Kudu (replace values)
curl -X PUT \
"https://exo-tools.scm.azurewebsites.net/api/vfs/site/wwwroot/certs/exo.pfx" \
-u "<deployment-user>:<password>" \
--upload-file /path/to/exo-cert.pfxTip: For production, store the certificate in Azure Key Vault and retrieve it at startup, instead of uploading the PFX directly.
# Requires Node.js 18+ and PowerShell Core (pwsh) on PATH
npm install
cp .env.example .env # fill in values
mkdir -p certs
cp /path/to/exo-cert.pfx certs/exo.pfx
# Install the EXO PowerShell module (once)
pwsh -Command "Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force"
NODE_ENV=development node app.js
# Open http://localhost:5000- Navigate to the web app URL.
- Click Sign in with Microsoft 365 and authenticate with your admin account.
- In the User field, start typing a name or email address.
- Select the user from the autocomplete suggestions.
- Click Check Permissions.
- The app queries Exchange Online in the background (this may take several minutes on large tenants).
- Results appear in a table grouped by permission type.
- Use the type filter buttons or the search box to narrow results.
- Click Export CSV to download the full results.
| Operation | Complexity | Notes |
|---|---|---|
| Send As | O(1) – single EXO query | Fast; uses Get-RecipientPermission -Trustee |
| Full Access | O(n mailboxes) | Iterates every mailbox; slow on large tenants |
| Send on Behalf | O(n mailboxes) | Iterates every mailbox to check GrantSendOnBehalfTo |
For tenants with > 500 mailboxes, the check typically takes 5–15 minutes.
Increase PS_TIMEOUT (default 600 s) if you see timeouts.
- All traffic should be served over HTTPS (Azure App Service enforces this).
- The EXO certificate private key (PFX) never leaves the server.
- The
userPrincipalNameinput is validated against a strict regex before being passed to PowerShell. - Sessions are signed (HMAC) and marked
HttpOnly+SameSite=Lax. - Set
SESSION_COOKIE_SECURE=True(the default whenNODE_ENV=production) to enforce HTTPS-only cookies. - Restrict app access to authorised administrators via
ACCESS_GROUP_ID.
| Symptom | Likely cause | Fix |
|---|---|---|
| "PowerShell not found" | pwsh not on PATH |
Install PowerShell Core or set PWSH_PATH |
| "Failed to connect to Exchange Online" | Wrong cert / app ID | Check EXO_APP_ID, EXO_CERT_PATH, EXO_ORGANIZATION |
| "Access denied. Your account is not in the required group." | User not in ACCESS_GROUP_ID group |
Add user to the group or clear ACCESS_GROUP_ID |
| "Could not acquire token" | Wrong CLIENT_ID / CLIENT_SECRET |
Verify the app registration values |
| Timeout after 10 minutes | Very large tenant | Increase PS_TIMEOUT; consider off-hours scheduling |
| No users found in People Picker | Missing admin consent | Grant admin consent for User.ReadBasic.All |