Token Vault Endpoints

Manage third-party OAuth2 connections (Google, GitHub, Slack, etc.) on behalf of users. Bulwark stores the encrypted refresh token, runs the OAuth dance, and vends short-lived access tokens to authorized agents.

Refresh tokens are NEVER returned via the API. Only short-lived access tokens are exposed, and each retrieval refreshes the token if it's near expiry. The refresh token stays encrypted-at-rest in the vault.


Authentication

All management endpoints (/connections, /providers) require:

| Header | Description | |--------|-------------| | Authorization | Bearer <jwt> — Bulwark JWT (admin role required for create/delete). | | X-Bulwark-Tenant | Tenant UUID. |

The OAuth callback endpoint (/callback) is public — it's hit by the OAuth provider and authenticates via a signed state parameter, not headers.


List Available Providers

GET /api/v1/token-vault/providers

Returns the registry of OAuth2 providers Bulwark knows how to talk to (e.g. google, github, slack). Use this to populate provider pickers in admin tooling.

Response 200

{
  "providers": [
    {
      "name": "google",
      "display_name": "Google",
      "default_scopes": ["openid", "email", "profile"]
    }
  ]
}

Create Connection

POST /api/v1/token-vault/connections

Registers a new OAuth2 provider connection for the tenant. The client_id and client_secret are encrypted before storage. After this call, kick off the OAuth dance via the /authorize endpoint below.

Body

{
  "provider_name": "google",
  "client_id": "<oauth-client-id-from-google>",
  "client_secret": "<oauth-client-secret-from-google>",
  "scopes": ["openid", "email", "profile", "https://www.googleapis.com/auth/calendar.readonly"],
  "display_name": "Google Calendar (read-only)"
}

| Field | Required | Description | |-------|----------|-------------| | provider_name | Yes | Provider key from /providers. | | client_id | Yes | OAuth2 client ID issued by the provider. | | client_secret | Yes | OAuth2 client secret. Encrypted before storage. | | scopes | No | Override the provider's default scope set. | | display_name | No | Human-readable label for the admin UI. |

Response 201

{
  "id": "01j...",
  "tenant_id": "01j...",
  "provider_name": "google",
  "display_name": "Google Calendar (read-only)",
  "scopes": ["openid", "email", "profile", "https://www.googleapis.com/auth/calendar.readonly"],
  "has_token": false,
  "created_at": "2026-05-07T00:00:00Z"
}

has_token: false means the OAuth dance hasn't completed yet. Hit /authorize next to start it.


List Connections

GET /api/v1/token-vault/connections

Response 200

{
  "connections": [
    {
      "id": "01j...",
      "tenant_id": "01j...",
      "provider_name": "google",
      "display_name": "Google Calendar (read-only)",
      "scopes": ["openid", "email", "profile", "https://www.googleapis.com/auth/calendar.readonly"],
      "has_token": true,
      "token_expiry": "2026-05-07T01:00:00Z",
      "created_at": "2026-05-07T00:00:00Z"
    }
  ],
  "count": 1
}

Delete Connection

DELETE /api/v1/token-vault/connections/{id}

Removes the connection and its stored tokens. Cannot be undone.

Response 200

{ "status": "deleted" }

Start OAuth Dance

GET /api/v1/token-vault/connections/{id}/authorize

Redirects the browser to the provider's authorization URL. Bulwark builds the URL with the right client_id, redirect_uri, scope, and a signed state parameter that ties the eventual callback back to this connection.

This endpoint returns a 302 Found; the browser follows it to the provider, the user grants consent, and the provider redirects back to /api/v1/token-vault/callback with code + state.


OAuth Callback

GET /api/v1/token-vault/callback

Public endpoint. Hit by the OAuth provider after the user grants consent. Bulwark verifies the state HMAC, exchanges the authorization code for tokens, encrypts the refresh token, and stores both. On success returns a small success page.

You should NOT call this endpoint directly. It exists so the OAuth provider can redirect to it.


Get Access Token

GET /api/v1/token-vault/connections/{id}/token

Returns a fresh access token for the connection. If the cached token is near expiry, Bulwark refreshes it transparently using the stored refresh token before returning. The refresh token itself is NEVER returned.

Response 200

{
  "access_token": "<short-lived-provider-access-token>",
  "expires_at": "2026-05-07T01:00:00Z",
  "provider": "google"
}

Errors

  • 404 — connection not found, or has_token: false (OAuth dance never completed)
  • 503 — refresh failed (provider rejected the refresh token, e.g. user revoked access). Re-run the OAuth dance via /authorize.

Typical flow

  1. Admin calls POST /connections to register the provider's client_id / client_secret.
  2. Admin clicks "Connect" in the UI, which navigates to GET /connections/{id}/authorize.
  3. User is redirected to the provider, grants consent.
  4. Provider redirects to /callback with code + state. Bulwark exchanges and stores.
  5. Connection now has has_token: true.
  6. Agent code reads GET /connections/{id}/token whenever it needs a fresh access token to call the provider's API. Bulwark handles refresh internally.