Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/joseluis-dev/harness-ai/llms.txt

Use this file to discover all available pages before exploring further.

The Auth Service (mcp-auth-service) is the single token authority for all of Harness AI. It is the only service in the harness that issues, validates, renews, and revokes OAuth 2.1 access tokens. Every component that needs to authenticate a caller — an MCP server, a CLI wrapper, an n8n webhook adapter, the runtime, or the admin panel — delegates that verification to this service through the auth-sdk shared library. There is no distributed auth, no per-MCP credential store, and no self-registration path. The service persists in its own isolated database (harness_auth_db), which is entirely separate from the application database (harness_db).

The Single-Issuer Guarantee

No MCP server, CLI wrapper, webhook adapter, or integration may issue its own tokens or implement its own token validation logic. Every Bearer token in the harness is issued by the Auth Service and validated through auth-sdk. Any component that bypasses this guarantee is an anti-pattern and will be rejected at code review.
This guarantee is what makes the harness auditable: every token issuance, use, denial, and revocation flows through a single log (audit_auth_events). If the Auth Service is down, no new tokens are issued and no sensitive operation proceeds.

OAuth 2.1 Client Credentials Flow

The harness uses the Client Credentials grant exclusively for service-to-service authentication. There is no Authorization Code flow, no implicit flow, and no device flow in the current implementation.
1

Client Requests Token

The client (MCP server, CLI wrapper, or integration) sends a POST to /oauth/token using HTTP Basic authentication. The client_id and client_secret travel in the Authorization: Basic header — never in the query string or the request body.
POST /oauth/token
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=sql:read
2

Auth Service Validates

The service verifies the client_id against active_secret_hash (or previous_secret_hash during a rotation grace window), checks that state=active, confirms that the requested scope is a subset of allowed_scopes, and reads risk_level_cap from oauth_clients. It never accepts a risk_level_cap from the incoming request.
3

JWT Issued

A signed JWT is emitted with kid, exp, iat, jti, sub=client_id, scope, and risk_level_cap. The token and an audit_auth_events{type="token.issued"} row are persisted atomically.
{
  "access_token": "eyJ…",
  "token_type":   "Bearer",
  "expires_in":   3600,
  "scope":        "sql:read"
}

Endpoints

MethodPathAuthDescription
POST/oauth/tokenHTTP Basic client_id:client_secretIssue an access token via Client Credentials.
GET/.well-known/jwks.jsonpublicPublic keys for local JWT verification by auth-sdk.
POST/oauth/introspectservice-to-serviceCheck the active/expired/revoked state of a token on demand.
POST/oauth/revokeservice-to-serviceRevoke a jti before its natural expiration.
Admin CRUD endpoints for clients, scopes, and policies (/admin/clients, /admin/scopes, /admin/policies) are planned for Phase 6 and are not part of the current implementation. Clients are provisioned exclusively through versioned database migrations and the bootstrap command.

harness_auth_db Schema

The Auth Service persists in harness_auth_db, which is a separate PostgreSQL database from harness_db. Connections, migrations, and backups are independent. The two databases must never share a connection string.
-- Client registry
oauth_clients (
  id                         UUID PRIMARY KEY,
  client_id                  TEXT UNIQUE NOT NULL,
  active_secret_hash         TEXT NOT NULL,           -- argon2id/bcrypt
  previous_secret_hash       TEXT,                    -- grace window during rotation
  previous_secret_expires_at TIMESTAMP,
  name                       TEXT NOT NULL,
  state                      TEXT NOT NULL,           -- active | suspended | revoked
  allowed_scopes             TEXT[],
  risk_level_cap             TEXT,                    -- ceiling enforced on all tokens
  created_at                 TIMESTAMP NOT NULL,
  rotated_at                 TIMESTAMP
);

-- Scope catalog
oauth_scopes (
  name        TEXT PRIMARY KEY,
  description TEXT,
  state       TEXT NOT NULL  -- active | deprecated
);

-- Issued tokens
oauth_tokens (
  jti        UUID PRIMARY KEY,
  client_id  TEXT REFERENCES oauth_clients(client_id),
  scope      TEXT NOT NULL,
  issued_at  TIMESTAMP NOT NULL,
  expires_at TIMESTAMP NOT NULL,
  revoked    BOOLEAN NOT NULL DEFAULT FALSE
);

-- Per-tool execution policies
oauth_policies (
  id             UUID PRIMARY KEY,
  tool_name      TEXT NOT NULL,   -- technical_name in tool_registry
  operation      TEXT,            -- NULL = applies to all operations
  required_scope TEXT NOT NULL,
  risk_level     TEXT NOT NULL,
  requires_human BOOLEAN NOT NULL DEFAULT FALSE
);

-- Tool registry (source of truth for tool resolution)
tool_registry      -- see architecture/07-tool-registry.md
tool_operations
permissions
agent_permissions

-- Authorization audit trail
audit_auth_events (
  id           BIGSERIAL PRIMARY KEY,
  type         TEXT NOT NULL,   -- token.issued | token.used | token.denied | token.revoked
  client_id    TEXT,
  scope        TEXT,
  tool_name    TEXT,
  operation    TEXT,
  decision     TEXT,            -- allow | deny | require_approval
  reason       TEXT,
  source_ip    INET,
  execution_id UUID,
  created_at   TIMESTAMP NOT NULL
);

-- Additional audit tables
tool_execution_logs
security_events

Client Lifecycle

Clients move through a one-way lifecycle. Only active clients can receive new tokens.
active → suspended → revoked
StateBehavior
activeCan request tokens. Existing valid tokens continue to work.
suspendedCannot request new tokens. Existing valid tokens may be honoured depending on policy (operator decision).
revokedCannot request tokens. All existing tokens are invalidated. The state is terminal — a revoked client cannot be reactivated; a new client must be created.

Secret Rotation

The Auth Service supports zero-downtime secret rotation through a dual-hash grace window. This allows operators to distribute a new secret to all consumers before the old secret is invalidated.
1

Generate New Secret

A new secret is generated outside the repository (via Dokploy/secret manager or the bootstrap command). The operator stores active_secret_hashhash(new_secret) and moves the old hash to previous_secret_hash with a previous_secret_expires_at window.
2

Grace Window

During the grace window, the Auth Service accepts tokens signed with either the active or previous secret. Consumers can rotate their stored secret at their own pace within the window.
3

Window Expires

When previous_secret_expires_at passes, previous_secret_hash is cleared. Any request using the old secret returns token.denied in audit_auth_events.
auth-sdk handles this transparently: it re-fetches tokens automatically when they are near expiration, so a well-behaved consumer never experiences an outage during rotation.

JWKS Rotation with kid

JWT tokens carry a kid (Key ID) header claim. The /.well-known/jwks.json endpoint publishes the current public key set. auth-sdk maintains a short-TTL cache of the JWKS and re-fetches it whenever it encounters an unknown kid. This allows the Auth Service to rotate signing keys without restarting any downstream service.
Key rotation does not require a service restart for any MCP, runtime worker, or integration. The auth-sdk detects the new kid on the first token it cannot verify locally, fetches the updated JWKS, and validates the token — all within a single request.

auth-sdk Shared Library

auth-sdk is available in both TypeScript (for the BFF and admin panel) and Python (for MCP servers, the runtime, and webhook adapters). It is the only approved mechanism for token verification in the harness.

Validation Flow

1. Decode the JWT header to extract `kid`.
2. Look up `kid` in the local JWKS cache (TTL: short, configurable).
3. If found: verify signature locally. No network call.
4. If not found: re-fetch /.well-known/jwks.json from the Auth Service.
5. If the Auth Service returns 429 or 5xx above the circuit-breaker threshold:
   degrade with a secure error — do not allow the request through.
6. If signature valid: check expiration, scope, and risk_level_cap.
7. Any denial is reported to the Auth Service as
   audit_auth_events{type="token.denied"}.

Python Dependency

# pyproject.toml
[project]
dependencies = [
  "auth-sdk = { path = \"../auth-sdk\" }",
]
The AuthClient, AuthContext, and AuthError public surface (landing in Phase 0.b PR-4) are the only approved entry points. Consumers must not reach into internal modules.

risk_level Decision Table

The Auth Service enforces risk_level ceilings as follows:
Operation risk_levelDefault decision
READ_ONLYallow if scope matches
READ_SENSITIVEallow if scope matches; reinforced audit
WRITEallow with write scope and risk_level_cap ≥ WRITE
DESTRUCTIVErequire_approval with destructive scope (Phase 9)
ADMINISTRATIVEallow only to clients with admin scope and risk_level_cap ≥ ADMINISTRATIVE
EXTERNAL_SIDE_EFFECTrequire_approval with external scope
Composite operations inherit the maximum risk_level of all their constituent actions. A read-sensitive + write composite is evaluated as WRITE and retains the reinforced audit of READ_SENSITIVE.

Admin-Only Client Creation

There is no public self-registration path. Clients are created exclusively through the admin panel (Phase 6) or versioned database migrations. The auth_clients_seed.yaml bootstrap file declares client_id, name, allowed scopes, risk_level_cap, and state — it never contains client_secret values or secret hashes. The first credential for each client is generated outside the repository and printed once by the bootstrap command.
The bootstrap sequence:
  1. Run the bootstrap command (outside the repo) to generate client_secret and compute active_secret_hash.
  2. Store only active_secret_hash in harness_auth_db. The plaintext secret is never persisted.
  3. Distribute the plaintext secret to the consuming service through the secret manager or Dokploy.
  4. Rotate the first admin credential immediately after bootstrap. The rotation is recorded as audit_auth_events{type="client.rotated"}.
For the full endpoint reference, see the Auth Contracts Reference.

Build docs developers (and LLMs) love