Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,29 @@ NITRO_PORT=3000
# the server creates it `0750` and writes JSONL files `0640`. Operators
# MUST configure rotation/retention (logrotate or find -mtime) — see
# docs/DEPLOYMENT.md "Monitoring & logs" and docs/SECURITY-AUDIT.md.
# A webhook-only (Phase-1) deploy NEVER writes here — leave commented
# until the OAuth flow (Phase-3) is in scope.
# A webhook-only deploy NEVER writes here — leave commented until OAuth
# is enabled (`NUXT_BITRIX24_OAUTH_ENABLED=true`).
# NUXT_AUDIT_DIR=/data/audit

# Bitrix24 OAuth 2.0 / multi-tenant (Phase-3, opt-in). The full surface
# (install/callback routes, token store, refresh logic) is staged across
# PR-2a..PR-2c — see `docs/OAUTH-DESIGN.md`. Until then this flag should
# stay `false`: with `=true` AND no OAuth wiring landed, every tool call
# refuses to dispatch (loud failure, not silent fallback). With `=false`
# (default) tools transparently keep using the webhook singleton.
# Enable only AFTER PR-2c merges and you have:
# Bitrix24 OAuth 2.0 / multi-tenant (opt-in). The full surface is LANDED
# and end-to-end live behind this flag — see `docs/OAUTH-DESIGN.md` and the
# "OAuth 2.0 multi-tenant" section of `docs/DEPLOYMENT.md`. With `=true`,
# each end user authorises via `/api/oauth/install` and every REST call runs
# under THAT user's Bitrix24 identity (no shared service user). With `=false`
# (the default) tools transparently keep using the webhook singleton — a
# webhook-only fork sees zero behaviour change.
#
# ⚠ MIGRATION — READ BEFORE FLIPPING THE FLAG. When NUXT_BITRIX24_OAUTH_ENABLED
# =true, NUXT_MCP_AUTH_TOKEN is BYPASSED on /mcp: the endpoint stops accepting
# the legacy shared token and accepts ONLY a per-user OAuth Bearer minted via
# /api/oauth/install → /api/oauth/callback. Every connected client (Claude /
# Cursor / Windsurf) must swap its `Authorization: Bearer <NUXT_MCP_AUTH_TOKEN>`
# header for its own per-user Bearer BEFORE you enable the flag, or it gets a
# 401 with `WWW-Authenticate: Bearer error="invalid_token", errorCode="…",
# error_description="…"`.
# Rollback is just `=false` + restart — the SQLite store stays on disk.
#
# Enable only after you have:
# 1. Registered a Bitrix24 Marketplace application (CLIENT_ID/SECRET below).
# 2. Persistent volume mounted at the directory holding `oauth.sqlite`
# (docker-compose.yml already declares `bx24_data:/data`).
Expand All @@ -107,7 +119,8 @@ NITRO_PORT=3000
# docker-compose `bx24_data:/data` volume satisfies this by default. If
# you omit `_DB_DIR` AND don't mount a volume at `/data`, the DB lands in
# the container's ephemeral layer and is lost on restart — a silent
# data-loss trap; PR-5 operator docs will warn explicitly.
# data-loss trap: on the next restart every minted Bearer is gone and all
# users must re-run `/api/oauth/install`. Always mount a persistent volume.
#
# `_ADMIN_TOKEN` gates GET /api/oauth/_health (operator-tier counts
# endpoint). It is DELIBERATELY separate from NUXT_MCP_AUTH_TOKEN — the
Expand All @@ -116,7 +129,8 @@ NITRO_PORT=3000
# `location /api/oauth/_health { allow <ops-cidr>; deny all; }` block).
# Set it to an `openssl rand -hex 32` value if you can't isolate the
# route at the network layer. The route fails closed: with neither set,
# a non-localhost request gets 503 NOT-CONFIGURED.
# a non-localhost request gets 503 NOT-CONFIGURED. Once `_ADMIN_TOKEN` is
# set the Bearer is required uniformly — even a localhost request needs it.
#
# SCALING CAVEAT: the OAuth token cache + refresh state are PROCESS-LOCAL
# (single-instance design, see docs/OAUTH-DESIGN.md §5). Running 2+
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to this project are documented here. Format follows [Keep a

### Added

- **Auth — OAuth 2.0 multi-tenant (opt-in, off by default).** A staged rollout (#209, #210, #213, #216, #217) lands a per-user OAuth flow behind `NUXT_BITRIX24_OAUTH_ENABLED`: each end user authorises via `/api/oauth/install → /api/oauth/callback`, receives a per-user Bearer, and every Bitrix24 REST call runs under *their own* identity (no shared service user). Tokens are stored sha256-hashed in a SQLite store on the `bx24_data:/data` volume; an audit-first JSONL log records every credential mutation; `/api/oauth/_health` (gated by a separate `NUXT_BITRIX24_OAUTH_ADMIN_TOKEN`) exposes operator-tier counts. With the flag **off** (the default) behaviour is byte-identical to the webhook-only path — existing deployments are unaffected. **Migration note:** when the flag is on, `NUXT_MCP_AUTH_TOKEN` is bypassed on `/mcp` (the endpoint accepts only per-user Bearers) — migrate clients before flipping it. Operator guide: [`docs/DEPLOYMENT.md` → OAuth 2.0 multi-tenant](./docs/DEPLOYMENT.md#oauth-20-multi-tenant-opt-in); design + threat model: [`docs/OAUTH-DESIGN.md`](./docs/OAUTH-DESIGN.md).
- **CI**: `docker-smoke` job builds the production `Dockerfile`, boots two containers — one with a fresh `openssl rand -hex 32` Bearer (port `3000`), one with the `replace-with-secure-token` placeholder (port `3001`, in parallel — avoids the kernel-port-reuse race a `docker rm -f` + same-port re-run hits on busy runners) — and pins the externally-observable HTTP contract on every PR. Assertions: `/api/health` → `200 {"status":"ok"}`, container runs as non-root (Dockerfile `USER node` regression guard), `/mcp` → `401` without an `Authorization` header, `401` with a wrong length-matched Bearer (forces the comparator to look at content, not just length), non-`401`/`403`/`503` with the configured Bearer (auth passed), and `503` on the placeholder-token boot (pins the "copied-but-not-configured" gate). Closes the bring-up + Bearer-auth slice of issue #131 — the self-hosted HTTP path had never been booted in CI.
- `scripts/verify-deployment.sh` — operator-runnable version of the same smoke check, intended for use on a staging host (or production, post-promotion) since it makes no Bitrix24 REST call. **TLS verification is on by default** — pass `--insecure` only for self-signed staging hosts. Token is read from `$NUXT_MCP_AUTH_TOKEN` by default (so it never appears in `/proc/<pid>/cmdline` on shared hosts); `--token <value>` and `--token-stdin` are also accepted. Strict `jq -e '.status == "ok"'` body predicate when `jq` is on PATH, substring match otherwise. Hints distinguish `502/503/504` (proxy reaches an unhealthy upstream) from `000` (TLS / DNS / firewall) so the operator debugs the right layer first. Linked from [`docs/DEPLOYMENT.md`](./docs/DEPLOYMENT.md#verifying-your-deployment).
- `.env.example`: documented (commented-out) `NUXT_AUDIT_DIR` — the OAuth-only audit-log directory knob (`server/utils/audit-log.ts`, default `/data/audit/`) was readable in code but missing from the template, a drift caught by the deploy-path audit.
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ A starter template for building Model Context Protocol (MCP) servers on top of B
|---|---|---|
| **A non-technical Bitrix24 operator** (HR, accountant, foreman) on a single workstation | **DXT bundle** → [Desktop Extension](#desktop-extension--claude-desktop-one-file-two-clicks) | One file, two clicks. No terminal, no port, no Bearer. Webhook stored in the OS keychain. Локализованный гайд: [`INSTALL.ru.md`](./mcp-stdio/INSTALL.ru.md) · em PT-BR: [`INSTALL.pt-BR.md`](./mcp-stdio/INSTALL.pt-BR.md). |
| **A developer** running an AI agent on your laptop (Claude Code / Cursor / Claude Desktop) | **Local HTTP** → [Local MCP](#local-mcp--your-own-machine-claude-desktop-cursor-claude-code-cline) | `pnpm start`, point the client at `localhost:3000`. No public domain. Stays inside your machine. |
| **A team / SaaS deploying for many users** | **Docker production** → [Remote MCP](#remote-mcp--production-server-claudeai-web) | Public URL with TLS, Bearer-protected `/mcp`, GHCR image, GitHub-Actions deploy + rollback. |
| **A team / SaaS deploying for many users** | **Docker production** → [Remote MCP](#remote-mcp--production-server-claudeai-web) | Public URL with TLS, Bearer-protected `/mcp`, GHCR image, GitHub-Actions deploy + rollback. Optional **per-user [OAuth 2.0](#multi-tenant-oauth-20--per-user-identity-opt-in)** so each user acts under their own Bitrix24 identity. |

The three paths share **the same tool code** — same files in `server/mcp/tools/**`, same auth model, same logger redaction. Only the transport and packaging differ.

Expand All @@ -42,7 +42,7 @@ Off-the-shelf Bitrix24 MCP servers are either toy demos or vendor-locked. This p

> **Create the webhook under a dedicated service user**, not a real employee's account. The webhook inherits the creator's permissions for every call, so binding it to a personal account ties the integration to that person's role, department visibility, and tenure — anyone who leaves the company or loses rights silently breaks the MCP. Grant the service user the **minimum rights the tool set actually needs** (admin only if you need cross-user task visibility and want to avoid "task not found" / `ACCESS_DENIED` surprises on entities a non-admin user happens not to see).
>
> This is a webhook-era trade-off only. When the template moves to **OAuth 2.0** in a future release, each end user logs in with their own Bitrix24 account and every REST call is executed under that user's identity and permissions — the service-user shortcut goes away, and access becomes per-user by design.
> This is a webhook-era trade-off only. The template now also ships **OAuth 2.0 multi-tenant** auth (opt-in, behind `NUXT_BITRIX24_OAUTH_ENABLED`): each end user logs in with their own Bitrix24 account and every REST call is executed under that user's identity and permissions — the service-user shortcut goes away, and access becomes per-user by design. It stays **off by default**, so webhook-only deployments are unaffected. See [`docs/OAUTH-DESIGN.md`](./docs/OAUTH-DESIGN.md) and the [OAuth 2.0 multi-tenant](./docs/DEPLOYMENT.md#oauth-20-multi-tenant-opt-in) operator guide.

```bash
git clone https://github.com/bitrix24/templates-mcp.git
Expand Down Expand Up @@ -120,6 +120,12 @@ The 8 task-mutation tools above (`start_task` / `pause_task` / `complete_task` /

For production deployment, see [`docs/REVERSE-PROXY.md`](./docs/REVERSE-PROXY.md) — covers nginx-proxy (the default), Caddy, plain nginx + certbot, and Traefik. Pick whichever your hosting provider already runs.

#### Multi-tenant OAuth 2.0 — per-user identity (opt-in)

The Remote setup above shares one `NUXT_MCP_AUTH_TOKEN` and runs every call under one webhook service user — right for a single team. For a **multi-user / SaaS** deployment, flip on OAuth (`NUXT_BITRIX24_OAUTH_ENABLED=true`) and each end user authorises once at `https://<your-mcp>/api/oauth/install?portal=<theirportal>`, then pastes the per-user Bearer they receive into their connector. Every REST call then runs under *that* user's Bitrix24 identity and permissions — no shared service user.

> ⚠️ **When the flag is on, `NUXT_MCP_AUTH_TOKEN` is bypassed on `/mcp`** — the endpoint accepts only per-user OAuth Bearers. Migrate every connected client to its own Bearer **before** flipping the flag. It stays **off by default**, so existing webhook deployments are unaffected. Full operator guide: [`docs/DEPLOYMENT.md` → OAuth 2.0 multi-tenant](./docs/DEPLOYMENT.md#oauth-20-multi-tenant-opt-in); design + threat model: [`docs/OAUTH-DESIGN.md`](./docs/OAUTH-DESIGN.md).

### Local MCP — your own machine (Claude Desktop, Cursor, Claude Code, Cline, …)

No public domain or TLS required. Run the same Nuxt build on `localhost` and point any HTTP-MCP-capable AI client at it.
Expand Down
6 changes: 4 additions & 2 deletions docker-compose.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ services:
# and point Node at it. Leave unset for cloud portals (the Mozilla CA
# list bundled with Node 22 covers every `*.bitrix24.<tld>` certificate).
NODE_EXTRA_CA_CERTS: ${NODE_EXTRA_CA_CERTS:-}
# Audit log + OAuth scaffolding (Phase-3, opt-in). The flag stays off
# by default — webhook flow is unchanged. See docs/OAUTH-DESIGN.md.
# Audit log + OAuth multi-tenant (opt-in). The flag stays off by
# default — webhook flow is unchanged. See docs/DEPLOYMENT.md →
# "OAuth 2.0 multi-tenant".
NUXT_AUDIT_DIR: ${NUXT_AUDIT_DIR:-/data/audit}
NUXT_BITRIX24_OAUTH_ENABLED: ${NUXT_BITRIX24_OAUTH_ENABLED:-false}
NUXT_BITRIX24_OAUTH_CLIENT_ID: ${NUXT_BITRIX24_OAUTH_CLIENT_ID:-}
NUXT_BITRIX24_OAUTH_CLIENT_SECRET: ${NUXT_BITRIX24_OAUTH_CLIENT_SECRET:-}
NUXT_BITRIX24_OAUTH_REDIRECT_URL: ${NUXT_BITRIX24_OAUTH_REDIRECT_URL:-}
NUXT_BITRIX24_OAUTH_SCOPE: ${NUXT_BITRIX24_OAUTH_SCOPE:-user,task}
NUXT_BITRIX24_OAUTH_DB_DIR: ${NUXT_BITRIX24_OAUTH_DB_DIR:-/data}
NUXT_BITRIX24_OAUTH_ADMIN_TOKEN: ${NUXT_BITRIX24_OAUTH_ADMIN_TOKEN:-}
# Persistent storage for the audit log + OAuth token store. Required
# whenever NUXT_BITRIX24_OAUTH_ENABLED=true; harmless (just empty) for
# webhook-only forks. The named volume survives `docker compose down`
Expand Down
15 changes: 9 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,23 @@ services:
LETSENCRYPT_HOST: ${LETSENCRYPT_HOST}
LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}
NUXT_AUDIT_DIR: ${NUXT_AUDIT_DIR:-/data/audit}
# OAuth 2.0 scaffolding — empty/false by default; webhook-only forks
# see no behaviour change. Enable per docs/OAUTH-DESIGN.md once
# PR-2c lands and the Marketplace app is registered.
# OAuth 2.0 multi-tenant — empty/false by default; webhook-only forks
# see no behaviour change. Enable per docs/DEPLOYMENT.md → "OAuth 2.0
# multi-tenant" once the Marketplace app is registered.
NUXT_BITRIX24_OAUTH_ENABLED: ${NUXT_BITRIX24_OAUTH_ENABLED:-false}
NUXT_BITRIX24_OAUTH_CLIENT_ID: ${NUXT_BITRIX24_OAUTH_CLIENT_ID:-}
NUXT_BITRIX24_OAUTH_CLIENT_SECRET: ${NUXT_BITRIX24_OAUTH_CLIENT_SECRET:-}
NUXT_BITRIX24_OAUTH_REDIRECT_URL: ${NUXT_BITRIX24_OAUTH_REDIRECT_URL:-}
NUXT_BITRIX24_OAUTH_SCOPE: ${NUXT_BITRIX24_OAUTH_SCOPE:-user,task}
NUXT_BITRIX24_OAUTH_DB_DIR: ${NUXT_BITRIX24_OAUTH_DB_DIR:-/data}
# Gates GET /api/oauth/_health. Leave empty for localhost-only access;
# set to an `openssl rand -hex 32` value to token-gate the route.
NUXT_BITRIX24_OAUTH_ADMIN_TOKEN: ${NUXT_BITRIX24_OAUTH_ADMIN_TOKEN:-}
networks:
- proxy-net
# Persistent storage for the audit log (`/data/audit/*.jsonl`, written
# by `server/utils/audit-log.ts`) and the OAuth token store
# (`oauth.sqlite`, lands in PR-2b). Without this volume both are lost
# (`oauth.sqlite`). Without this volume both are lost
# on container recreate — silently breaking compliance/forensics and,
# once OAuth is on, every installed tenant. Webhook-only forks gain
# durable audit logs as a side-effect. The named volume survives
Expand All @@ -58,8 +61,8 @@ services:
# compliance you MUST copy them out first:
# docker cp bx24-template-mcp:/data/audit ./audit-backup-$(date +%F)
# Then `docker compose pull && docker compose up -d`. If you have no
# earlier audit history (fresh install, or audit logger never ran
# because the only call site lands in PR-2b), no action needed.
# earlier audit history (fresh install, or a webhook-only deploy where
# the audit logger never ran), no action needed.
volumes:
- bx24_data:/data
healthcheck:
Expand Down
Loading
Loading