{"id":48451159,"url":"https://github.com/jesusfer/graph-webhooks-testbed","last_synced_at":"2026-04-06T20:30:51.312Z","repository":{"id":340588160,"uuid":"1166597016","full_name":"jesusfer/graph-webhooks-testbed","owner":"jesusfer","description":"A web app to test MS Graph webhooks","archived":false,"fork":false,"pushed_at":"2026-03-04T16:07:00.000Z","size":175,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-04T23:05:18.886Z","etag":null,"topics":["microsoft365","msgraph"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jesusfer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-25T11:51:43.000Z","updated_at":"2026-03-04T16:07:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jesusfer/graph-webhooks-testbed","commit_stats":null,"previous_names":["jesusfer/graph-webhooks-testbed"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/jesusfer/graph-webhooks-testbed","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesusfer%2Fgraph-webhooks-testbed","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesusfer%2Fgraph-webhooks-testbed/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesusfer%2Fgraph-webhooks-testbed/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesusfer%2Fgraph-webhooks-testbed/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jesusfer","download_url":"https://codeload.github.com/jesusfer/graph-webhooks-testbed/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesusfer%2Fgraph-webhooks-testbed/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31489348,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-06T17:22:55.647Z","status":"ssl_error","status_checked_at":"2026-04-06T17:22:54.741Z","response_time":112,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["microsoft365","msgraph"],"created_at":"2026-04-06T20:30:46.698Z","updated_at":"2026-04-06T20:30:51.299Z","avatar_url":"https://github.com/jesusfer.png","language":"TypeScript","readme":"# Graph Webhooks Testbed\n\nA web application for testing Microsoft Graph subscriptions and receiving webhook notifications.\n\n## Features\n\n- **MSAL Authentication** — Sign in with your Microsoft account using Entra ID Authorization Code flow with PKCE (`@azure/msal-browser`)\n- **Create Graph Subscriptions** — Create subscriptions to Microsoft Graph resources (e.g. `me/messages`) directly from the UI\n- **Webhook Receiver** — `POST /api/webhook` endpoint handles Graph validation handshakes and stores incoming notifications\n- **Notifications Dashboard** — View all received webhook notifications with timestamps; click through to see the full pretty-printed JSON body\n- **Azure Table Storage** — Subscriptions and notifications are persisted in Azure Storage Account tables\n\n## Prerequisites\n\n- Node.js 18+\n- An **Azure Storage Account** (or [Azurite](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite) for local dev)\n- An **Entra ID App Registration** with:\n  - Single-page application (SPA) redirect URI (e.g. `http://localhost:3000`)\n  - API permissions: `User.Read`, `Mail.Read` (or whatever resources you want to subscribe to)\n  - Expose an API: at least one scope to use for the backend API\n- It's recommended to have **two** app registrations, one for the client and one for the API. For this setup, the API app registration will only Expose an API with one scope and the client app will have the SPA redirect URI, Graph API permissions and backend API permissions. If the client app is preauthorized to use the backend app, users will not see a consent prompt.\n- A **publicly reachable URL** for the webhook endpoint (use [ngrok](https://ngrok.com/) or [VS Code port forwarding](https://code.visualstudio.com/docs/editor/port-forwarding) for local dev)\n\n## Setup\n\n1. **Install dependencies**\n\n    ```bash\n    npm install\n    ```\n\n2. **Configure environment** — Copy `.env.example` to `.env` and fill in your values:\n\n    ```bash\n    cp .env.example .env\n    ```\n\n    | Variable                          | Description                                                                       |\n    | --------------------------------- | ---------------------------------------------------------------------------       |\n    | `ENTRA_CLIENT_ID`                 | Entra ID app registration client ID                                               |\n    | `ENTRA_TENANT_ID`                 | Entra ID tenant ID                                                                |\n    | `ENTRA_REDIRECT_URI`              | SPA redirect URI (e.g. `http://localhost:3000`)                                   |\n    | `AZURE_STORAGE_CONNECTION_STRING` | Azure Storage connection string                                                   |\n    | `GRAPH_NOTIFICATION_URL`          | Public URL for webhook endpoint (e.g. `https://xxxx.ngrok.io/api/webhook`)        |\n    | `GRAPH_ENCRYPTION_CERTIFICATE`    | Base64-encoded X.509 certificate for rich notifications (optional)                |\n    | `GRAPH_ENCRYPTION_CERTIFICATE_ID` | Identifier for the encryption certificate (optional)                              |\n    | `GRAPH_ENCRYPTION_PFX`            | Base64-encoded PFX (PKCS#12) with private key for decrypting payloads (optional)  |\n    | `GRAPH_ENCRYPTION_PFX_PASSWORD`   | Password for the PFX file, leave empty if none (optional)                         |\n    | `API_AUDIENCE`                    | App ID URI used as the token audience (e.g. `api://\u003cclient-id\u003e`)                  |\n    | `API_SCOPE`                       | Full scope URI the frontend requests (e.g. `api://\u003cclient-id\u003e/user_access`)       |\n    | `SESSION_SECRET`                  | Random secret for Express sessions                                                |\n    | `PORT`                            | Server port (default: `3000`)                                                     |\n    | `TRUST_PROXY`                     | Number of reverse proxies between client and server (default: `1`)                |\n    | `RATE_LIMIT_WINDOW_MS`            | Rate limit window in milliseconds (default: `900000` / 15 min)                    |\n    | `RATE_LIMIT_MAX`                  | Max requests per IP per window (default: `100`)                                   |\n\n3. **Build**\n\n    ```bash\n    npm run build:all\n    ```\n\n4. **Start**\n\n    ```bash\n    npm start\n    ```\n\n## Development\n\nRun the backend and frontend watchers in parallel:\n\n```bash\nnpm run dev:watch\n```\n\nOr run the backend with ts-node:\n\n```bash\nnpm run dev\n```\n\n## Project Structure\n\n```\nsrc/\n  backend/\n    config.ts              — Environment config and app settings\n    server.ts              — Express server entry point\n    wsServer.ts            — WebSocket server for real-time broadcasting\n    @types/                — Custom type declarations\n    middleware/            — Express middleware (token validation)\n    storage/               — Azure Table Storage helpers\n    routes/                — API route handlers (webhook, subscriptions, etc.)\n    util/                  — Utilities (decryption, Graph client, validation)\n  frontend/\n    api.ts                 — Fetch wrapper for backend API calls\n    app.ts                 — Frontend entry point (auth state, auto-refresh)\n    auth.ts                — MSAL authentication and token acquisition\n    detailsPage.ts         — Notification detail view with JSON pretty-printing\n    graph.ts               — Fetch wrapper for Microsoft Graph API calls\n    router.ts              — Client-side routing\n    types.ts               — Shared TypeScript interfaces\n    websocket.ts           — Client-side WebSocket with auto-reconnect\n    appNotifications/      — App-only notification and subscription UI\n    delegatedNotifications/ — Delegated notification and subscription UI\npublic/\n  index.html               — Single-page application shell\n  redirect.html            - MSAL redirection page with bridge\n  js/app.js                — Bundled frontend (generated)\n```\n\n## How It Works\n\n1. User signs in via MSAL popup (Authorization Code + PKCE)\n2. User fills in a Graph resource and change type, clicks **Create Subscription**\n3. The frontend calls Microsoft Graph `POST /v1.0/subscriptions` with the user's access token\n4. Graph validates the webhook endpoint by sending a `validationToken` query parameter\n5. The backend responds with the token, completing the handshake\n6. When events occur, Graph sends notifications to `POST /api/webhook`\n7. The backend stores each notification in Azure Table Storage\n8. The dashboard shows all subscriptions and notifications for the signed-in user\n\n## Libraries quirks\n\n### MSAL bridge\n\nCopy msal-redirect-bridge to lib for development.\n\n```shell\ncp 'frontend/node_modules/@azure/msal-browser/lib/redirect-bridge/msal-redirect-bridge.js' frontend/public/lib/\n```\n\n\n## Architecture Diagram\n\n```\n       ┌── Microsoft Graph ──┐\n       │                     │\n       ▼                     ▼\n┌──────────────┐     ┌───────────────┐\n│ POST /webhook│     │POST /lifecycle│\n│  (no auth)   │     │  (no auth)    │\n└──────┬───────┘     └──────┬────────┘\n       │                     │\n       ▼                     ▼\n  findUserForSubscription()\n  validateNotificationTokens()\n  decryptNotificationContent()\n  insertNotification()\n  broadcast() ──────────────────► WebSocket clients\n       │\n       ▼\n Azure Table Storage\n  ┌─────────────┐  ┌──────────────┐\n  │Subscriptions│  │Notifications │\n  └─────────────┘  └──────────────┘\n       ▲\n       │\n  requireApiToken() ◄── Bearer token\n       │\n       ▼\n /api/delegated\n /api/app\n /api/notifications\n```\n\n## Security Considerations\n\n1. Webhook endpoints are unauthenticated (as required by Graph) but validate incoming data via validationTokens JWT checks and clientState matching.\n1. API endpoints require Entra ID Bearer tokens validated against JWKS.\n1. Input validation uses strict regex patterns via validateParams.ts to prevent injection.\n1. HTML escaping via escapeHtml prevents XSS when echoing validation tokens.\n1. Rate limiting is applied globally via express-rate-limit.\n1. Tenant verification on both webhook and lifecycle handlers ensures only notifications from the configured tenant are processed","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjesusfer%2Fgraph-webhooks-testbed","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjesusfer%2Fgraph-webhooks-testbed","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjesusfer%2Fgraph-webhooks-testbed/lists"}