Self-service guest access requests for Microsoft Entra ID with manager approval. A lightweight alternative to Entitlement Management. No Governance licenses required.
Guest access with a sponsor. Nothing gets in without a vouch.
Built on Azure Static Web Apps + Azure Functions, secured with Entra ID. Microsoft Graph is called via a system-assigned Managed Identity, so there are no stored secrets.
- π Self-service requests: internal users request time-boxed guest (B2B) access without a helpdesk ticket
- β Manager approval: the requester's manager is resolved server-side via Microsoft Graph, with a configurable fallback approver group
- β« Approval escalation: if a request stays pending past a threshold, fallback approver group members can also approve it, and a daily digest reminds them
- β±οΈ Time-boxed access: choose 30 / 90 / 180 / 365 days; the approval expiry is stamped on the guest as
extensionAttribute13 - π¬ Portal-only approvals: approvers get a notification mail that links into the portal; no unauthenticated magic links
- π§Ύ Full audit trail: every request and decision is written to Azure Table Storage with actor, timestamps and source IP
- π Managed Identity: no stored secrets for Graph access
- π‘οΈ Easy Auth: Entra ID JWT validated by Azure before your code runs; invalid or missing tokens rejected with 401 automatically
- π¨ CSS theming: full white-labeling via CSS custom properties, with a light/dark theme toggle
- β»οΈ Lifecycle-friendly: expiry cleanup is delegated to an external lifecycle project; VouchGate never excludes its guests from it
- π Custom domain: supported via Azure Static Web Apps (Standard tier)
- π€ An internal user signs in with their Entra ID account
- π They submit a guest access request (guest email, display name, optional company, justification, duration)
- π§ The backend resolves the manager (
/users/{id}/manager), or falls back to a configured approver group if no manager is set - π¨ The approver receives a notification mail with a link into the portal
- π The approver signs in, reviews pending requests, and approves or denies (with an optional comment)
- πͺ On approval, a Function creates the guest (
POST /invitations) and stampsextensionAttribute13(expiry),employeeType = "VouchGate"andsponsors(the requester); the requester is notified that sharing is now possible - β On denial, the requester is notified with the reason
- π§Ύ Every step is written to the audit table
Approvers use the same portal URL as everyone else. There is no separate admin area or admin login.
- π After signing in, an Approver Inbox tab appears automatically whenever
there are pending requests assigned to the signed-in user, either as the
resolved manager of the requester, or as a fallback approver group
member once a request has escalated (pending past
ESCALATION_DAYS). If no requests are assigned, the tab is not shown. - π The link in the notification mail (
/?request={id}) is a convenience deep link that opens and highlights that specific request. It is not the only way in: approvers can always open the portal directly and use the Approver Inbox tab to see every request assigned to them.
β οΈ If "Assignment required" is enabled on the app, approvers must also be assigned. Managers (and fallback approver group members) sign in to the same app as requesters, so if only requesters are assigned, managers cannot sign in to approve. Assign a group that covers both requesters and approvers, or add the approvers' group to the app assignment. See Restricting access.
VouchGate v1 has no admin dashboard. The full audit trail is stored in the
VouchGateAudit table in the deployment's storage account, with one record
per request submission and per approval/denial (actor, approverType, guest
email, comment, source IP and timestamp). View it through the Azure Portal
(Storage account β Storage browser β Tables β VouchGateAudit) or
Azure Storage Explorer.
| Sign-in | Request form |
|---|---|
![]() |
![]() |
| My requests | Approver inbox |
|---|---|
![]() |
![]() |
| Tool | Min. Version | Install |
|---|---|---|
| Azure CLI | 2.50 | aka.ms/installazurecli |
| Node.js | 24 LTS | nodejs.org, includes npm |
| Static Web Apps CLI | latest | npm install -g @azure/static-web-apps-cli |
| Exchange Online PowerShell | latest | Install-Module ExchangeOnlineManagement (for the mail-scoping step) |
| Scope | Role |
|---|---|
| Azure subscription | Contributor |
| Entra ID tenant | Application Administrator (or Global Administrator) |
| Entra ID tenant | Privileged Role Administrator |
| Exchange Online | Exchange Administrator (to scope Mail.Send, see below) |
π‘ A Global Administrator covers the Entra roles by default. The Exchange Administrator role is required for the mandatory
Mail.Sendscoping step.
1. Install prerequisites (once):
# macOS (Homebrew)
brew install azure-cli node
npm install -g @azure/static-web-apps-cli2. Clone and deploy:
git clone https://github.com/daniel-fraubaum/VouchGate.git
cd VouchGate
chmod +x infra/deploy.sh
./infra/deploy.sh --project vouchgate-prod \
--mail-sender guest-access@contoso.com \
--fallback-group "Guest Access Approvers"1. Install prerequisites (once):
- π΅ Azure CLI, download and run the MSI installer
- π’ Node.js 24 LTS, download and run the installer (includes npm)
- π¦ Static Web Apps CLI:
npm install -g @azure/static-web-apps-cli
2. Clone and deploy:
git clone https://github.com/daniel-fraubaum/VouchGate.git
cd VouchGate
.\infra\deploy.ps1 -Project vouchgate-prod `
-MailSender guest-access@contoso.com `
-FallbackGroup "Guest Access Approvers"βΉοΈ On a first deployment both
--mail-sender/-MailSenderand--fallback-group/-FallbackGroupare required. The fallback approver group approves requests from users who have no manager, so the portal is not functional for those users without it. A group display name or object ID is accepted; the script resolves and validates it viaaz ad group show.
The script asks for confirmation, then handles everything end-to-end (~10 minutes):
| Step | Action |
|---|---|
| 1οΈβ£ | Creates (or reuses) the Entra ID App Registration, sets the identifier URI + access_as_user scope |
| 2οΈβ£ | Deploys all Azure infrastructure via Bicep (single pass) |
| 3οΈβ£ | Assigns User.Invite.All, User.ReadWrite.All, Mail.Send, GroupMember.Read.All to the Managed Identity |
| 4οΈβ£ | Deploys the backend (Azure Functions) |
| 5οΈβ£ | Generates frontend/authConfig.js from deployment outputs |
| 6οΈβ£ | Deploys the frontend (Azure Static Web App) |
| 7οΈβ£ | Registers the Static Web App URL as a redirect URI on the App Registration |
| 8οΈβ£ | Grants admin consent for User.Read |
β³ Allow a few minutes after deployment before the portal is fully functional. Graph API role assignments can take 2-5 minutes to propagate in Entra ID. If you see
403/ permission errors right after deployment, simply wait and refresh.
See docs/deployment.md for re-deploys, partial deploys, custom domains and troubleshooting.
| Resource | Details |
|---|---|
| π¦ Resource Group | rg-<projectName> |
| π Log Analytics + Application Insights | Telemetry and custom events |
| πΎ Storage Account | Runtime storage + VouchGateRequests & VouchGateAudit tables |
| βοΈ App Service Plan | Linux B1 |
| π Azure Static Web App | Standard tier (SPA frontend) |
| β‘ Azure Function App | Node.js 24, system-assigned Managed Identity |
| π Entra ID App Registration | MSAL user login + Easy Auth JWT validation |
All prices are approximate, based on West Europe, low/idle usage. Billed in USD by Azure.
| Resource | Tier | ~$/month |
|---|---|---|
| βοΈ App Service Plan | Linux B1 | ~$13 |
| π Azure Static Web App | Standard | ~$9 |
| πΎ Storage Account | LRS, minimal usage | ~$1 |
| π Log Analytics + App Insights | Pay-per-use, minimal ingestion | ~$1 |
| β‘ Function App | Runs on B1 plan (no extra charge) | included |
| π Entra ID App Registration | Free tier | included |
| Total | ~$24/month |
β οΈ Security requirement, do this before going live. By default, any user in your Entra ID tenant can log in to the portal. Enable assignment enforcement immediately after deployment.
- π’ Entra ID β Enterprise Applications β search for your app (e.g.
vouchgate-prod-vouchgate-portal) - βοΈ Properties β set "Assignment required?" to Yes β Save
- π₯ Users and groups β Add assignment β select your security group (e.g.
SG-VouchGate-Requesters)
Unassigned users receive AADSTS50105 from Entra ID at sign-in, the portal and backend never see the request.
β οΈ MFA cannot be enforced from application code. The only secure enforcement is via Entra ID Conditional Access.
Create a Conditional Access policy targeting your app:
- Entra ID β Security β Conditional Access β + New policy
- Users: your VouchGate security group
- Target resources: select your App Registration
- Grant: βοΈ Require multi-factor authentication
- Enable policy: On β Create
Entra ID then enforces MFA at token issuance, before the portal or backend sees anything.
π¨
Mail.Sendis an application permission that is tenant-wide by default. Without further restriction, VouchGate's Managed Identity could send email as any mailbox in your tenant. You must restrict it to the sender shared mailbox (MAIL_SENDER) using an Exchange Online Application Access Policy.
Minimal example (full walkthrough in docs/deployment.md):
Connect-ExchangeOnline -Organization contoso.onmicrosoft.com
# Mail-enabled security group containing ONLY the sender mailbox
New-DistributionGroup -Name "VouchGate Mail Senders" -Type Security `
-PrimarySmtpAddress "vouchgate-mail-senders@contoso.com" `
-Members "guest-access@contoso.com"
# Restrict the Managed Identity to that group only
New-ApplicationAccessPolicy `
-AppId "<managed-identity-app-id>" `
-PolicyScopeGroupId "vouchgate-mail-senders@contoso.com" `
-AccessRight RestrictAccess `
-Description "Restrict VouchGate to the guest-access shared mailbox"
# Verify: sender = Granted, any other mailbox = Denied
Test-ApplicationAccessPolicy -Identity "guest-access@contoso.com" -AppId "<managed-identity-app-id>"
Test-ApplicationAccessPolicy -Identity "ceo@contoso.com" -AppId "<managed-identity-app-id>"βΉοΈ The Application ID is the Managed Identity's service principal appId (named after the Function App), not the portal App Registration. Policy changes can take up to ~30 minutes to take effect.
VouchGate does not delete or disable guests itself. On approval it stamps each guest with:
extensionAttribute13= approval expiry date (ISO 8601)employeeType="VouchGate"sponsors= the requester
Expiry enforcement (notify β disable β delete) is handled externally by
EntraID Guest User LifeCycle Management,
which reads extensionAttribute13 and acts on it. This clean handoff keeps
VouchGate focused on the request/approval workflow.
VouchGate never sets
extensionAttribute15 = "ExcludeFromLCM", so every guest it provisions remains subject to the external lifecycle cleanup.
VouchGate produces three complementary logging layers.
1. Business audit trail. Every request, approval, denial and escalated
decision is written to the VouchGateAudit table in the storage account,
capturing the requester, guest, justification, decision, approverType
(manager / group / fallback-escalation), timestamps and caller IP. This
answers "why does this guest exist and who vouched for them" and is viewable via
the Azure Portal or Storage Explorer. Table Storage has no automatic
retention: entries are kept until deleted, which is intended for audit
purposes. Organizations with retention requirements should export or clean up the
table according to their own policy.
2. Technical telemetry. Application Insights captures requests, dependencies
(including Microsoft Graph call durations) and custom events, currently
VouchGateRequestSubmitted, VouchGateRequestDecided,
VouchGateSponsorAssignmentFailed, VouchGateEscalationReminder and
VouchGateApproverNotificationFailed. Default retention is 90 days. This layer is
for operations and troubleshooting.
3. Entra ID audit log. Guest invitations and user patches performed by the
Managed Identity appear in the Entra ID audit log with the Function App service
principal as the actor. This shows what VouchGate did at the directory level
but not on whose behalf, so cross-reference it with the VouchGateAudit table for
the business context: every invitation in the Entra log should correspond to an
approved request in VouchGateAudit.
See docs/architecture.md for the audit column reference and an example Application Insights query.
βββ infra/
β βββ deploy.sh β Full deployment (Bash, macOS/Linux/WSL)
β βββ deploy.ps1 β Full deployment (PowerShell, Windows)
β βββ main.bicep β Bicep template (subscription scope)
β βββ bicepconfig.json β Bicep linting rules
β βββ main.parameters.example.json β Parameters template
β βββ modules/
β βββ monitoring.bicep β Log Analytics + Application Insights
β βββ storage.bicep β Storage Account + requests & audit tables
β βββ appServicePlan.bicep β Linux App Service Plan (B1)
β βββ staticwebapp.bicep β Frontend Static Web App
β βββ functionapp.bicep β Backend Function App + Easy Auth
βββ frontend/
β βββ index.html β Single-page SPA (CSS + JS fully embedded)
β βββ authConfig.example.js β Template, copy to authConfig.js and fill in
β βββ staticwebapp.config.json β SWA routing + security headers
β βββ logo.svg β App logo (gate + vouch checkmark)
β βββ favicon.svg β Favicon
β βββ css/
β βββ theme.css β Branding overrides (colors, fonts, radii)
βββ backend/
β βββ host.json
β βββ package.json
β βββ eslint.config.js β ESLint 9 flat config
β βββ src/
β βββ index.js β Entry point (boots telemetry + registers functions)
β βββ functions/
β β βββ submitRequest.js β POST /api/requests
β β βββ listRequests.js β GET /api/requests?view=mine|pending
β β βββ myApprover.js β GET /api/my-approver
β β βββ decideRequest.js β POST /api/requests/{id}/decision
β β βββ escalationReminder.js β daily timer (escalation digest)
β βββ lib/
β βββ auth.js β Easy Auth + JWKS JWT verification
β βββ graph.js β Microsoft Graph (DefaultAzureCredential)
β βββ requests.js β VouchGateRequests store (ETag concurrency)
β βββ audit.js β VouchGateAudit append-only writer
β βββ approver.js β approver resolution + escalation
β βββ telemetry.js β Application Insights custom events
β βββ mailTemplates.js β Outlook-safe HTML mail templates
βββ docs/
βββ architecture.md β Architecture overview + auth flow
βββ deployment.md β Detailed deployment guide
βββ CONFIGURATION.md β All config values explained
βββ images/ β Logo + screenshots
Note on App Registration: Created and configured by the deploy scripts via
az ad appCLI, not via a Bicep module.
- π Deployment Guide, detailed steps, mail scoping, re-deploy, troubleshooting
- ποΈ Architecture, components, auth flow, resilience model
- βοΈ Configuration Reference, all config values and where they come from
- π Functional Spec, the full functional specification
MIT, see LICENSE



