Skip to content

Latest commit

 

History

History
478 lines (371 loc) · 22.1 KB

File metadata and controls

478 lines (371 loc) · 22.1 KB

SPEC: VouchGate Guest Access Portal

VouchGate is a self-service guest access request portal for Microsoft Entra ID with manager approval. It is a lightweight alternative to Entitlement Management that requires no Governance (Entra ID P2 / Governance) licenses. Internal users request time-boxed guest (B2B) access; the requester's manager approves or denies; on approval VouchGate creates the guest and stamps a lifecycle expiry attribute that is later cleaned up by an external process.

This document is the authoritative functional specification. Implementation of the backend functions and the frontend SPA follows in later steps.


1. Objective

Enable any authorized internal user to request external guest access without a helpdesk ticket or a Governance license, while keeping a human (the requester's manager) in the approval loop and maintaining a complete audit trail.

Business Requirements

  • Self-service: requesters submit guest access requests through a portal, no ticket required.
  • Manager approval: every request is routed to the requester's manager (resolved server-side), with a configurable fallback approver group.
  • No Governance license: built only on Azure Static Web Apps, Azure Functions, Azure Table Storage and Microsoft Graph application permissions.
  • Time-boxed access: each request carries a requested duration (30 / 90 / 180 / 365 days); the approval expiry is stamped on the guest object.
  • External lifecycle cleanup: expiry enforcement is delegated to the EntraID Guest User LifeCycle Management project, which reads extensionAttribute13.
  • Auditability: every request and decision is persisted with actor, timestamps and source IP.

2. Architecture

2.1 Components

Component Technology Responsibility
Frontend Azure Static Web App (Standard) Single-file SPA (index.html with embedded CSS + JS), MSAL.js auth (Authorization Code + PKCE)
Backend Azure Functions (Node.js 24, v4 model) HTTP APIs, request/approval workflow, Graph calls
Identity Entra ID App Registration + Easy Auth Sign-in, JWT validation before function code runs
Graph access System-assigned Managed Identity Guest invitation, guest object update, manager lookup, mail
State Azure Table Storage VouchGateRequests (requests), VouchGateAudit (audit trail)
Monitoring Log Analytics + Application Insights Telemetry, traces and diagnostics

2.2 Data Flow

Requester ──▶ SPA ──▶ (Easy Auth JWT) ──▶ Function: submitRequest
                                              │
                                              ├─▶ resolve manager (Graph /users/{id}/manager)
                                              ├─▶ store request (VouchGateRequests)
                                              ├─▶ write audit (VouchGateAudit)
                                              └─▶ sendMail to approver (Graph, MAIL_SENDER)

Approver ──▶ notification mail link ──▶ SPA ──▶ (Easy Auth JWT) ──▶ Function: pendingApprovals
                                                                       │
Approver approves/denies ──▶ Function: decideRequest ──────────────────┤
                                              │
                    approve ─────────────────┤
                                              ├─▶ POST /invitations (Graph, User.Invite.All)
                                              ├─▶ set extensionAttribute13 / employeeType / sponsors
                                              ├─▶ store decision + write audit
                                              └─▶ sendMail to requester ("sharing possible")
                    deny ────────────────────┤
                                              ├─▶ store decision + write audit
                                              └─▶ sendMail to requester (reason)

3. Authentication & Authorization

3.1 Frontend (MSAL.js)

  • MSAL.js browser library using the Authorization Code flow with PKCE.
  • Configuration is injected at deploy time via authConfig.js (window.VOUCHGATE_CONFIG), generated by the deploy scripts and gitignored.
  • The SPA acquires an access token for the API scope api://{clientId}/access_as_user and sends it as a Bearer token to the backend.
  • No tokens or secrets are persisted server-side.

3.2 Backend (Easy Auth + Managed Identity)

  • Easy Auth (App Service Authentication V2) validates the Entra ID JWT before any function code runs; unauthenticated requests are rejected with 401 automatically (unauthenticatedClientAction: Return401).
  • The signed-in caller's identity (object ID, UPN, name) is read from the validated token / Easy Auth headers.
  • All Microsoft Graph calls use the Function App's system-assigned Managed Identity: there are no stored client secrets.

3.3 Graph API Permissions (Application Permissions)

Assigned to the Managed Identity by the deploy scripts:

Permission Why
User.Invite.All Create the guest via POST /invitations
User.ReadWrite.All Read the requester's manager; set extensionAttribute13, employeeType, sponsors on the guest
GroupMember.Read.All Verify fallback approver group membership (checkMemberGroups)
Mail.Send Send all notification mails as MAIL_SENDER

Delegated User.Read is admin-consented for interactive sign-in.

Least privilege for mail: Mail.Send as an application permission allows sending as any mailbox by default. It must be scoped to the MAIL_SENDER mailbox using an Exchange Online Application Access Policy (documented in docs/CONFIGURATION.md).


4. Requester Flow

  1. An internal user signs in with their Entra ID account.
  2. The user submits a guest access request with:
    • Guest email (required), validated against allowed/blocked domain lists.
    • Display name (required).
    • Company (optional).
    • Justification (required, minimum 10 characters).
    • Requested duration (required), one of 30, 90, 180, 365 days, capped by MAX_DURATION_DAYS.
  3. The backend resolves the approver server-side:
    • Primary: the requester's manager via Graph GET /users/{id}/manager.
    • Fallback: if no manager is set, the request is routed to the configured approver fallback group (APPROVER_FALLBACK_GROUP_ID); any member may approve.
  4. The request is stored in VouchGateRequests with status Pending, an audit record is written, and a notification mail is sent to the approver.

5. Approver Flow

  1. The approver receives a notification mail containing a link into the portal (the SPA). There are no unauthenticated magic links: the approver must sign in.
  2. After signing in, the approver sees the list of pending requests assigned to them (as manager, as a member of the fallback group, or, for escalated requests, as a fallback group member; see §5.1).
  3. The approver approves or denies each request, with an optional comment.
  4. The decision is validated server-side (the caller must be an authorized approver for that request) and recorded with a full audit entry that captures the authorization basis (approverType: manager / group / fallback-escalation).

5.1 Escalation

To prevent requests stalling on an unavailable manager, VouchGate escalates:

  • While a request is pending and older than ESCALATION_DAYS (default 5), members of the fallback approver group (APPROVER_FALLBACK_GROUP_ID) become additional valid approvers for it, including for manager-routed requests.
  • This is enforced server-side in both authorization paths: the pending list (GET /api/requests?view=pending) shows escalated requests to fallback group members, and the decision endpoint (POST /api/requests/{id}/decision) allows fallback group members to decide them. Eligibility is age-based and re-checked via Microsoft Graph on every call.
  • A daily timer function escalationReminder (scheduled by ESCALATION_REMINDER_CRON, default "0 0 7 * * *") sends one digest mail to the fallback group listing all pending requests that have passed ESCALATION_DAYS and are not yet escalated, then stamps escalatedAt on those requests so the digest is never sent twice for the same request. The mail is only stamped after a successful send, so a send failure is retried next run.
  • Decisions made through escalation are audited with approverType = fallback-escalation.

6. Approval Actions

6.1 On Approval

A Function performs, in order:

  1. Create the guest: POST /invitations (Graph, User.Invite.All) with invitedUserEmailAddress and invitedUserDisplayName. sendInvitationMessage is false: VouchGate does not send a redemption mail. The guest gains access the first time the requester shares a resource with them (for example, the SharePoint sharing notification), and redemption happens on first access. Existing directory users are matched guests only (userType eq 'Guest'); if the email belongs to an internal member account the approval is refused with 422 INTERNAL_USER (and submission is rejected early with 400).
  2. On the resulting guest user object, set:
    • extensionAttribute13 = approval expiry date (ISO 8601), computed as approval time + requested duration. This is the value the external lifecycle management reads. Expiry is never shortened (see 6.1.2).
    • employeeType = "VouchGate" (marks guests provisioned by this portal).
    • sponsors = the requester (Graph sponsors relationship / $ref).
  3. Update the request status to Approved, persist the decision and expiry, and write an audit record.
  4. Send a notification mail to the requester indicating that sharing with the guest is now possible.

Lifecycle guarantee: VouchGate must never set extensionAttribute15 = "ExcludeFromLCM". Setting it would exclude the guest from the external lifecycle cleanup and defeat the time-boxing.

6.1.1 Failure handling (retry model)

The approve path is ordered so it is safe to retry after a partial failure:

  • Guest creation (POST /invitations) and the lifecycle attribute patch (extensionAttribute13, employeeType) run before the request status is updated. If either fails, the request stays pending and can be retried.
  • Sponsor assignment is best-effort: a failure is logged as a warning and emitted as an Application Insights event (VouchGateSponsorAssignmentFailed), but it does not fail the approval.
  • The request status is written last, only after guest provisioning has completed. A Graph failure mid-flow therefore never leaves a request marked approved without a provisioned guest.
  • On a retry, the guest is detected as already existing, so the approved-existing path re-applies all attributes (expiry, employeeType and sponsor). This makes the operation self-healing: re-running a partially-failed approval converges to the correct end state.

6.1.2 Expiry is never shortened on re-approval

When the approval targets a guest that already exists (the approved-existing path), the Function first reads the guest's current onPremisesExtensionAttributes.extensionAttribute13. If that value is a valid ISO date later than the newly computed expiry (approval time + requested duration), the existing later date is kept and the shorter one is discarded. A new approval may extend access but never reduce it, because another requester may already rely on the longer period. Concretely:

  • Existing expiry later than the requested one: the existing date is preserved (the patch re-stamps the same later value, so the operation stays idempotent).
  • Existing expiry earlier than the requested one (or none stored): the newly computed, later expiry is applied, extending access.

The effective expiry that was applied and how it was determined (new, extended or kept-existing) are recorded in the audit entry (ExpiryDate, ExpiryDisposition). The requester's approval confirmation mail reflects the effective expiry and, when a longer existing period was kept, explains that the requested duration would have shortened it so the existing date was retained.

6.2 On Denial

  1. Update the request status to Denied, persist the decision (including the optional reason/comment) and write an audit record.
  2. Send a notification mail to the requester with the denial reason.

7. Data Storage

State is stored in Azure Table Storage (no database, no Governance license).

7.1 VouchGateRequests

Holds one entity per guest access request.

Field Notes
PartitionKey Requester object ID (efficient "my requests" queries)
RowKey Request ID (GUID)
RequesterId / RequesterUpn / RequesterName Who requested
GuestEmail / GuestDisplayName / GuestCompany Requested guest
Justification Free text (≥ 10 chars)
RequestedDurationDays 30 / 90 / 180 / 365
ApproverId / ApproverType Resolved approver (manager or group)
Status pending / approved / approved-existing / denied
DecisionComment Optional approver comment
ExpiryDate ISO 8601 expiry (set on approval, mirrors extensionAttribute13)
GuestObjectId Created guest's object ID (set on approval)
EscalatedAt ISO 8601 timestamp the escalation digest was sent (blank until escalated)
CreatedAt / DecidedAt Timestamps

7.2 VouchGateAudit

Append-only audit trail (see §8).


8. Audit Logging

8.1 Storage

Every request submission and every approval/denial writes an immutable record to the VouchGateAudit Azure Table.

8.2 Log Schema

Field Description
PartitionKey Request ID
RowKey Event timestamp + unique suffix
EventType Requested / Approved / ApprovedExisting / Denied
ActorId / ActorUpn / ActorName Who performed the action
ApproverType Authorization basis for a decision (manager / group / fallback-escalation)
RequesterId Original requester
GuestEmail Guest subject of the request
Comment Justification (request) or decision comment
SourceIp Caller IP (from X-Forwarded-For)
ExpiryDate Effective guest access expiry applied on approval (ISO 8601)
ExpiryDisposition How the expiry was determined: new / extended / kept-existing
Timestamp UTC ISO 8601

8.3 Retention

Audit records are retained per the storage account policy; they are never modified or deleted by the application.


9. Configuration

All runtime configuration is supplied as Function App application settings (see backend/local.settings.json.example and infra/modules/functionapp.bicep):

Setting Purpose
TENANT_ID Entra ID tenant
AUTH_CLIENT_ID App Registration client ID (Easy Auth audience)
TABLE_STORAGE_CONNECTION_STRING Storage account for both tables
REQUESTS_TABLE_NAME Default VouchGateRequests
AUDIT_TABLE_NAME Default VouchGateAudit
GRAPH_API_ENDPOINT https://graph.microsoft.com
JUSTIFICATION_MIN_LENGTH Minimum justification length (default 10)
MAX_DURATION_DAYS Maximum requestable duration (default 365)
ESCALATION_DAYS Pending age after which fallback group members may also approve (default 5)
ESCALATION_REMINDER_CRON NCRONTAB schedule for the escalation reminder timer (default 0 0 7 * * *)
APPROVER_FALLBACK_GROUP_ID Object ID of the fallback approver group
ALLOWED_GUEST_DOMAINS Comma-separated allow-list (empty = allow all)
BLOCKED_GUEST_DOMAINS Comma-separated block-list (empty = block none)
MAIL_SENDER Shared mailbox used as sender for all notifications

10. Mail / Notifications

  • All notification mails are sent via Graph sendMail as MAIL_SENDER using the Managed Identity (Mail.Send application permission).
  • Notifications sent:
    • To approver: new request awaiting decision, with a link into the portal. A manager approver is emailed directly. A fallback approver group is notified by resolving its transitive user members and placing them in BCC of a single message, so a plain (non-mail-enabled) security group works. If no member has a mail address the request is still created and a VouchGateApproverNotificationFailed telemetry event is emitted.
    • To requester (approved): sharing with the guest is now possible.
    • To requester (denied): request denied, including the reason.
    • To fallback group (escalation digest): one mail (member addresses in BCC) listing all requests pending longer than ESCALATION_DAYS, sent by the daily escalationReminder timer.
  • Mail.Send must be scoped to the MAIL_SENDER mailbox with an Exchange Online Application Access Policy (documented in docs/CONFIGURATION.md).

11. Frontend

11.1 Views

  • Requester view: guest access request form and the requester's own request history with status.
  • Approver view: list of pending requests assigned to the signed-in approver, with approve / deny actions and an optional comment field.

11.2 UX Requirements

  • Single-file SPA: index.html with embedded CSS and JS.
  • Light/dark theme with a toggle in the top bar; the default honors prefers-color-scheme; the choice is applied via data-theme="dark".
  • Client-side validation mirrors backend rules (email format, justification length, duration options) but is never authoritative.

11.3 CSS Theming

  • All colors, fonts, radii and shadows are defined as CSS custom properties in frontend/css/theme.css (the EphemGate branding system, re-headed for VouchGate), including a full [data-theme="dark"] block.
  • The SPA must consume only these custom properties: no color, font, radius or shadow value is hardcoded in index.html.

11.4 Custom Domain Support

  • Supported via Azure Static Web Apps (Standard tier); the optional customDomain Bicep parameter provisions the custom domain resource.

12. Backend

12.1 Azure Function App

  • Node.js 24, Azure Functions v4 programming model, Linux B1 App Service Plan.
  • Application Insights initialized at startup (src/index.js).
  • Shared libraries in src/lib/: auth.js (identity/JWT), graph.js (Managed Identity Graph client), telemetry.js, audit.js, requests.js (request store), approver.js (approver resolution + escalation), mailTemplates.js.

12.2 API Endpoints & timers

Method & Route Description
POST /api/requests Submit a new guest access request
GET /api/requests?view=mine List the signed-in requester's own requests
GET /api/requests?view=pending List pending requests the signed-in approver may decide (incl. escalated)
GET /api/my-approver Return the resolved approver's display name for the request form
POST /api/requests/{id}/decision Approve or deny a request (optional comment)
escalationReminder (timer) Daily digest of over-age pending requests to the fallback group

Both list views and the decision endpoint enforce authorization server-side: the approver is independently re-resolved (manager object-ID match, fallback group membership via checkMemberGroups, or age-based escalation); client role claims are never trusted. The decision endpoint is idempotent, a request that is not pending returns 409, and concurrent decisions are guarded with an ETag (If-Match) update.

Also referenced in lib/approver.js (approver resolution) and lib/requests.js (request store).


13. Infrastructure (Bicep)

13.1 Resources

  • Resource group (subscription-scope deployment).
  • Log Analytics Workspace + Application Insights (monitoring).
  • Storage account with VouchGateRequests and VouchGateAudit tables (storage).
  • Linux B1 App Service Plan (appServicePlan).
  • Static Web App (Standard) (staticwebapp).
  • Function App with system-assigned Managed Identity and Easy Auth (functionapp).

13.2 Parameters

projectName, location, swaLocation, resourceGroupName, customDomain, authClientId, approverFallbackGroupId, allowedGuestDomains, blockedGuestDomains, maxDurationDays, mailSender.

13.3 Naming Convention

All resources are prefixed with projectName: <project>-plan, <project>-law, <project>-ai, <project>-swa, <project>-func, and a globally-unique storage account name derived from projectName.


14. Security Requirements

  • Easy Auth enforced on all backend endpoints (Return401 for anonymous).
  • No stored secrets, Managed Identity for all Graph access.
  • Least-privilege Graph application permissions (§3.3).
  • Mail.Send scoped to MAIL_SENDER via Exchange Online Application Access Policy.
  • Server-side authorization: approvers can only decide requests actually assigned to them; requesters can only see their own requests.
  • Recommended access controls before go-live: Entra ID assignment enforcement (assign a dedicated security group) and Conditional Access (MFA, location, block legacy auth).

15. Lifecycle Integration

  • On approval VouchGate stamps extensionAttribute13 with the ISO 8601 expiry.
  • The external EntraID Guest User LifeCycle Management reads extensionAttribute13 and performs expiry cleanup (notification, disable, delete) outside of VouchGate.
  • VouchGate never sets extensionAttribute15 = "ExcludeFromLCM".

16. Out of Scope

  • Expiry enforcement / cleanup (handled by the external lifecycle project).
  • Multi-stage or multi-approver workflows beyond manager + fallback group.
  • Access reviews and recertification (use Entra ID Governance if required).
  • Automatic re-invitation or renewal of expired guests.