API Authentication
CVD Portal provides two authentication methods for programmatic access: an OAuth 2.1 authorization server for autonomous agents and external applications, and static bearer API keys for direct scripts and CI/CD pipelines. Both methods enforce tenant isolation and role-based access control.
OAuth 2.1 Authorization Server
The platform operates a standards-compliant OAuth 2.1 authorization server. Agents obtain short-lived, scope-limited access tokens bound to an authenticated user and company.
Protocol characteristics
- Grant types:
authorization_codewith PKCE, andrefresh_token. - Mandatory PKCE: Proof Key for Code Exchange (RFC 7636) is mandatory for all clients. The
code_challenge_methodmust beS256. Theplainmethod is rejected. - Dynamic Client Registration: Clients can self-register using RFC 7591 at
/api/oauth/register. - Single-use codes: An authorization code gives tokens one time only. A second exchange of the same code fails with
invalid_grant, also when the two requests arrive at the same time. - Token rotation: Refresh tokens rotate on every use. Exchanging a refresh token issues a new access token and a new refresh token while revoking the previous one.
- Refresh token reuse detection: Use each refresh token one time only. When a client presents a refresh token that a rotation replaced, the platform revokes all live access and refresh tokens that came from the same authorization, and answers
invalid_grant(RFC 9700 section 4.14.2). The platform cannot tell if the client or an attacker sent the old token, so it ends the authorization. The app must ask for consent again, and the event is in the audit log asOAUTH_TOKEN_REVOKED. A refresh token that expired, or that was revoked for a different reason, getsinvalid_grantand ends nothing. Two requests that arrive at the same time with the same refresh token give tokens to one request only. The other request getsinvalid_grant, and the authorization stays live. A client that loses the response of a refresh must not send the old refresh token again. It must ask for consent again. - Token storage: All tokens and authorization codes are stored hashed (SHA-256) at rest. Plaintext credentials are never saved in the database.
Endpoints
| Endpoint | Path | Protocol | Purpose |
|---|---|---|---|
| Authorization | /api/oauth/authorize | RFC 6749 / OAuth 2.1 | Authorization code request with PKCE S256 |
| Token | /api/oauth/token | RFC 6749 / OAuth 2.1 | Code exchange and refresh token rotation |
| Dynamic registration | /api/oauth/register | RFC 7591 | Client registration |
| Introspection | /api/oauth/introspect | RFC 7662 | Query token active status and metadata |
| Revocation | /api/oauth/revoke | RFC 7009 | Invalidate access and refresh tokens |
Connected apps: see and revoke authorizations
Go to Settings > Connected Apps to see each OAuth client that has access to your organisation. The page shows one row for each app and user. A row shows:
- The app name and the hosts that get its authorizations, for example "Agent A, sends to agent.example.com".
- The user who authorized the app.
- The scopes the app holds.
- The date of authorization.
- The date the last token expires.
The app chooses its own name, and CVD Portal does not verify it. An app can use the name of a different app. The hosts come from the redirect URIs that the app registered. CVD Portal sends an authorization only to a registered redirect URI, so the hosts tell you which server got the access. Examine the hosts before you trust the name. The consent page shows the same host.
- An app with more than one host shows all of them, for example "sends to a.example.com or b.example.com".
- Two redirect URIs on one host show as one host.
- A loopback redirect shows its scheme, for example
http://127.0.0.1:8765. A native app redirect shows only its scheme, for examplecom.example.app:.
The revoke confirmation uses the same words.
The page shows only live authorizations. An authorization is live when one or more of its access or refresh tokens is not revoked and not expired. The date of authorization is the date of consent. It does not change when the app gets new tokens with its refresh token.
Click Revoke to end an authorization at once. The platform revokes all access and refresh tokens that the app holds for that user, and each authorization code that the app did not exchange yet. A token refresh or code exchange that is in progress at the same time also fails, so the app gets no new token. The app must ask for consent again to reconnect. Each revoke writes an OAUTH_TOKEN_REVOKED event to the audit log.
- An admin can revoke each authorization in the organisation.
- A member can revoke only the authorizations that the member gave.
- Auditor, demo and partner drill-in sessions are read-only.
An app can also revoke its own tokens through the revocation endpoint.
Discovery metadata
Clients can automatically discover endpoints and capabilities using standard metadata documents:
- Authorization Server Metadata (RFC 8414):
https://cvdportal.com/.well-known/oauth-authorization-server - Protected Resource Metadata (RFC 9728):
https://cvdportal.com/.well-known/oauth-protected-resource
Environment variables
The authorization server supports the following configuration options:
| Variable | Default | Description |
|---|---|---|
OAUTH_ACCESS_TOKEN_TTL_SECONDS | 3600 (1 hour) | Lifetime of issued access tokens in seconds |
OAUTH_REFRESH_TOKEN_TTL_SECONDS | 2592000 (30 days) | Lifetime of issued refresh tokens in seconds |
Scope Taxonomy & Enforcement
Every MCP tool and version 1 REST endpoint enforces a specific scope requirement.
Scopes
| Scope | Grants | Endpoints & Tools |
|---|---|---|
read | Read-only compliance data, submissions, products, and SBOMs | GET /v1/compliance/*, GET /v1/products, GET /v1/sbom, GET /v1/vulnerabilities/*, GET /v1/jobs/*, POST /v1/jobs (classification), get_getting_started, get_compliance_status, generate_csaf_advisory, assess_response, get_next_deadline, get_next_task, get_job_status, submit_job, ask_expert, get_conformity_gaps, get_release_gate |
submissions:write | Submit vulnerability reports | POST /v1/vulnerabilities, submit_vulnerability |
submissions:triage | Triage and progress vulnerability reports | MCP submission tools, see MCP Server |
submissions:communicate | Draft messages to reporters for approval | draft_reporter_message |
products:write | Create and update products | POST /v1/products |
sbom:write | Push SBOMs from CI/CD | POST /v1/sbom |
sarif:write | Push SARIF pipeline scan findings | POST /v1/scan/sarif |
gate:write | Store release gate verdicts from CI/CD | POST /v1/compliance/release-gate/runs |
webhooks:write | Register webhook callbacks | POST /v1/webhooks/register |
gdpr:export | Export company data archive | GET /v1/gdpr/export |
forge:submissions:read | Read submissions for Jira sync | GET /v1/forge/submissions |
article14:prepare | Prepare Article 14 notification dossiers | POST /v1/jobs, submit_job |
advisory:draft | Draft CSAF and OpenVEX advisories | POST /v1/jobs, submit_job |
conformity:prepare | Prepare conformity assessment & import prefill | POST /v1/jobs, submit_job |
Enforcement rules
- OAuth tokens: Strictly limited to the consented scopes. Access to an endpoint or tool outside granted scopes returns HTTP 403 with an RFC 6750 challenge header:
WWW-Authenticate: Bearer error="insufficient_scope", scope="<required>". - Registered scopes: An OAuth client gets only the scopes that it registered at
/api/oauth/register. The platform removes each other requested scope at the authorization request, at consent, at code exchange and at refresh, and writes anOAUTH_SCOPE_DENIEDaudit event. A request that names no scope gets the registered scopes. A request with no registered scope gets the errorinvalid_scope. An OAuth token never has an empty scope list. - Legacy API keys: A legacy key with an empty scope list maintains unrestricted access for backward compatibility. A key with specified scopes enforces those scopes.
- Plan gates: Scope checks and plan tier gates operate independently. A caller must hold the required scope and the company subscription must permit the feature.
- ADMIN-only calls:
POST /v1/webhooks/register,POST /v1/products,POST /v1/sbom,GET /v1/gdpr/export,POST /v1/compliance/assessmentsand everyPOST /v1/jobstype exceptCLASSIFY_PRODUCTneed the ADMIN role, as in the dashboard. With an OAuth token, the platform reads the role of the token's user in the token's company at each call. That is the account role in the user's own company, or the membership role in a client company where the user holds a seat. A MEMBER gets HTTP 403 even when the token has the scope. A plain API key belongs to the company, so its scopes alone decide.
Human Approval Queue for Irreversible Actions
Autonomous agents can prepare drafts and assemble packages, but cannot execute binding or outward-facing actions directly.
Every irreversible action initiated by an agent enqueues a PendingApproval record in the company workspace:
- Article 14 stage filing: Marking early warnings or full notifications as filed on the ENISA Single Reporting Platform (SRP).
- Security advisory publication: Transitioning draft advisories to published status.
- Declaration of Conformity issuance: Issuing and signing the EU Declaration of Conformity (
ISSUE_DOC). Agents assemble draft declarations, but cannot issue them directly. - Messages to reporters: Sending a message that an agent drafted for the vulnerability reporter (
SEND_REPORTER_MESSAGE).
A company administrator reviews, approves, or rejects pending actions on the Agent Approval Queue page at /settings/approvals. The Settings menu does not show a link to this page. Open the URL directly. Approvals execute idempotently through the canonical execution path. Webhooks notify administrators when an approval request arrives (approval.requested).
Asynchronous Job Layer
Long operations execute through background jobs to prevent HTTP timeout failures.
Operations
The following operations run asynchronously through the job system:
- Product classification. Analyzes product description text against CRA Annex III and Annex IV categories (
CLASSIFY_PRODUCT). - Artifact drafting. Drafts CRA technical file artifacts using the assessment model chain (
DRAFT_ARTIFACT). - Import prefill. Extracts conformity model fields from product documentation with per-field provenance and confidence flags (
IMPORT_PREFILL). Fields with confidence below threshold are flagged for human review. - Server auto-enrichment. Closes enrichable gaps automatically from the fixed risk methodology and the stored SBOM (
AUTO_ENRICH). The result lists candidate standards for the product category incandidateStandards. These are suggestions only. The job never records a standard as applied, and no CRA harmonised standard is cited in the Official Journal yet. The SBOM summary states the component count, how many components OSV.dev can check, and the date of the last complete check. It says so when the current SBOM was not checked. With no SBOM, the job writes no SBOM summary. - Dossier assembly. Compiles the Annex VII technical documentation index and drafts the Annex V Declaration of Conformity (
ASSEMBLE_DOSSIER). Does not issue the declaration.
Endpoints and tools
| Interface | Method / Tool | Description |
|---|---|---|
| REST | POST /api/v1/jobs | Submit a new background job. Returns HTTP 202 with job ID. |
| REST | GET /api/v1/jobs/[id] | Query current job status, progress, and result. |
| REST | GET /api/v1/compliance/gaps | Query structured conformity gaps and missing evidence. |
| MCP | submit_job | Enqueue a new background job. |
| MCP | get_job_status | Query job status and result. |
| MCP | get_conformity_gaps | Query structured conformity gaps across requirements. |
| MCP | ask_expert | Consult the CRA regulatory expert assistant. |
Idempotency
Requests can include an idempotencyKey string. When a submit request carries an existing key for the company, the platform returns the existing job record without starting duplicate work.
Signed completion push
Callers can provide an optional callbackUrl and callbackSecret. When the worker finishes processing the job, the server sends an HTTP POST request to the callback URL.
The push notification includes the following headers:
X-CVD-Timestamp: Time of the push event, in milliseconds since the Unix epoch.X-CVD-Signature-256: Signature string in formatt=<timestamp>,v1=<hmac-sha256>. The server sends this header only when you supply acallbackSecret.
The server validates all callback URLs against Server-Side Request Forgery rules before saving and before dispatch. The push delivery operates in non-blocking mode with exponential retry backoff.
Regulatory Expert Assistant (MCP ask_expert)
Autonomous agents can consult the CRA regulatory expert tool (ask_expert) to resolve compliance ambiguities during the conformity run.
- Scope:
read. - Advisory nature: The tool provides advisory guidance only. It never claims that a Declaration of Conformity or Article 14 filing has occurred.
- Grounded answers: Responses cite official European Union legal sources including Regulation (EU) 2024/2847 and applicable harmonized standards.
- Structured needs: When the expert requires missing product or architecture details to answer definitively, it returns a structured
needobject with the target field, rationale, and question for human clarification.
Legacy Bearer API Keys
Administrators can provision static API keys in the dashboard under Settings → API Keys. A general API key needs the Enterprise plan. Below Enterprise, you can create three kinds of limited key. A key with only the read scope needs the Reporting plan or higher, and works with the Slack app, the MCP server and the next-deadline and next-task endpoints. A key made only of MCP scopes and the gate:write scope needs the Compliance plan or higher. It works with the full MCP server and the two release gate endpoints. It does not open the rest of the REST API. A key with only forge: scopes works with the Jira app.
API keys are intended for server-to-server workflows and CI/CD integrations. Each key carries an optional scope list and an optional IP allowlist. Keys are transmitted via the standard HTTP header:
Authorization: Bearer <your-api-key>
Keys are stored as SHA-256 hashes. In the event of compromise, an administrator can immediately revoke or rotate the key in the dashboard.