Skip to content

Authenticate integrations with personal API tokens - #682

Merged
vdolek merged 6 commits into
masterfrom
claude/project-thread-fyo2wf
Oct 30, 2026
Merged

vdolek merged 6 commits into
masterfrom
claude/project-thread-fyo2wf

Conversation

@EvilBrainsBot

@EvilBrainsBot EvilBrainsBot commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Requested by Martin · project thread

Summary

Personal API tokens for integrations and agents, PR 1 of 2; the MCP server follows stacked on it.

Changes

  • evc_ Bearer tokens with their own handler, picked by prefix ahead of the cookie; stored as SHA-256, read with the user row on every request.
  • 403 for a read-only token's writes and for any token on /api/auth/** or /api/api-tokens; 401 for an unknown, revoked or expired token.
  • At most 20 unexpired tokens per user; last use stamped at most once a minute; token id and name in the log scope.
  • /openapi/v1.json anonymous in every environment, with the Bearer scheme.
  • Settings split into /settings/labels, /settings/api-tokens and /settings/export under one layout; /settings redirects.
  • SDD-005, 006, 016, 019, README and the design export.

Deviations

  • A new token defaults to Jen čtení; the artboard shows Čtení i zápis checked.
  • .claude/rules and the run-app skill still describe auth correctly and stay unchanged.
  • ai-check did not run locally: no pwsh.

Screenshots

settings-before-1440
settings-before-390
settings-labels-1440
settings-labels-390
api-tokens-list-1440
api-tokens-list-390
api-tokens-new-modal-1440
api-tokens-new-modal-390
api-tokens-created-modal-1440
api-tokens-created-modal-390

Part of #673

https://claude.ai/code/session_01UovdganawgyqDHGsank6jS


Generated by Claude Code

@EvilBrainsBot
EvilBrainsBot requested a review from vdolek as a code owner October 30, 2026 18:58
@github-actions github-actions Bot added the agent-in-progress An agent is working on this pull request label Oct 30, 2026
@EvilBrainsBot EvilBrainsBot self-assigned this Oct 30, 2026
@github-actions github-actions Bot added the ci-failed CI failed on this pull request label Oct 30, 2026
@github-actions

Copy link
Copy Markdown

EvilBrainsBot pushed a commit that referenced this pull request Oct 30, 2026

Copy link
Copy Markdown
Member Author

Code Review

  • The history convention tests (DatabaseHistoryTests) excluded only RefreshTokens; they now treat ApiTokens as outside history too (bc4faf0).
  • The request completion log event now carries the API token id and name through Serilog's diagnostic context (1de60d5).

Generated by Claude Code

@EvilBrainsBot EvilBrainsBot removed the agent-in-progress An agent is working on this pull request label Oct 30, 2026
@EvilBrainsBot EvilBrainsBot added the waiting-for-agent Waiting for an agent to pick it up label Oct 30, 2026 — with Claude
@github-actions github-actions Bot removed the ci-failed CI failed on this pull request label Oct 30, 2026
@EvilBrainsBot EvilBrainsBot removed the waiting-for-agent Waiting for an agent to pick it up label Oct 30, 2026
@EvilBrainsBot EvilBrainsBot added the agent-done Waiting for the owner's review and merge label Oct 30, 2026 — with Claude

Copy link
Copy Markdown
Member Author

Code Review

  • Rework 87e6753 (201 without Location on token creation): /code-review medium changed nothing.

Generated by Claude Code

vdolek
vdolek previously approved these changes Oct 30, 2026
Exported from the approved canvas artboard; the settings navigation of
nastaveni.html gains the API tokeny entry. Export follows with #674.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UovdganawgyqDHGsank6jS
A token belongs to one user and acts as that user, tenant and role read
from the user row on every request. It is evc_ plus 256 random bits,
stored as its SHA-256 and found by a unique index, sent as a Bearer
header; a policy scheme picks its handler by the prefix, so a token
beside the access cookie wins.

A boundary after authentication keeps a token out of /api/auth/** and
token management (403) and refuses every write of a read-only token
(403). An unknown, revoked or expired token is 401. Revocation deletes
the row. The last use is stamped by a conditional update at most once a
minute. Creation locks the user row, so two at once cannot pass the
limit of 20 unexpired tokens.

The OpenAPI document is anonymous in every environment and declares the
Bearer scheme; Scalar stays Development only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UovdganawgyqDHGsank6jS
@vdolek
vdolek added this pull request to stack #689 October 30, 2026 20:54
Labels move to /settings/labels and the new API token section is
/settings/api-tokens; /settings redirects to the labels. Both pages share
SettingsLayout, whose navigation lists the sections, so a later section
is one entry and one page.

The token section lists name, scope, created, expiry and last use, marks
an expired token, creates a token with a name, a scope and a lifetime of
30, 90 or 365 days, shows the new token once with a copy button, and
revokes a token through the confirmation modal. A new token defaults to
read only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UovdganawgyqDHGsank6jS
ApiTokens carries the token hash and its last use moves every minute, so
its migration gives it no history trigger; the convention tests still
named RefreshTokens alone and failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UovdganawgyqDHGsank6jS
Serilog writes the completion event outside the token's log scope, so
the request's own record named no token. The diagnostic context adds
the token id and name to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UovdganawgyqDHGsank6jS
A token has no detail endpoint, so the created response carries no Location; SDD-005 names the exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UovdganawgyqDHGsank6jS
@vdolek
vdolek merged commit 05ff704 into master Oct 30, 2026
2 checks passed
@vdolek
vdolek deleted the claude/project-thread-fyo2wf branch October 30, 2026 21:49
vdolek pushed a commit that referenced this pull request Oct 30, 2026
<!-- claude-projects-attribution: {"github_login":"EvilBrainsBot"} -->
_Requested by **Martin** · [project
thread](https://claude.ai/code/project/chan_018WzBMUTj3Q3JGkpeCYsRag?thread=cmsg_018WzBMUTj3Q3JGkpeCYsRagTqdQm8Yo4J6YudBEHUPusZ)_

## Summary
An MCP server on `/mcp` for AI agents, authenticated by API token only,
with one-time links for file bytes. Stacked on #682, whose branch is the
base.

## Changes
- Stateless Streamable HTTP through the official SDK; anything but an
API token is 401, a foreign `Origin` 403; a read-only token lists and
calls only read tools.
- Tools for every SDD-005 resource with the contract DTOs, REST paging
and filters; errors are tool errors with the Problem Details title and
detail.
- `create_file_upload_link` / `create_file_download_link` on anonymous
`/api/file-links`: 256-bit secret stored hashed, 10 minutes, single use,
revoked with its token. The secret travels in the query string, which no
log records. The download link tool is a read tool.
- `read_file_text`: text formats, PDF, DOCX; 50,000-character pages; 30
MB limit.
- `/mcp` requests are logged like `/api`.
- SDD-023, SDDs 001/002/005/006/018, README, deploy.

## Risks
- The compose stack requires `EVILCASE_HOST`, which sets `AllowedHosts`.
- `CLAUDE.md` and the API rule still say every other path serves the
frontend: #685.
- If a token expires (rather than being revoked) within 10 minutes of
creating a file link, the link stays usable until it expires itself, and
SDD-023 only says that revoking a token ends its links.
- `dotnet r` could not run here (no pwsh, no .NET 8 runtime); format,
build and the changed test classes ran directly.

Closes #673

---
_Generated by [Claude
Code](https://claude.ai/code/session_01UovdganawgyqDHGsank6jS)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot removed the agent-done Waiting for the owner's review and merge label Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants