Connected Apps / Dynamic Client Registration
Register OAuth 2.0 clients dynamically and manage them via the admin API.
Dynamic Client Registration
POST /oauth2/register
Register a new OAuth 2.0 client following RFC 7591. This endpoint is used by applications that need to become first-class OAuth clients within Bulwark — receiving their own client_id and client_secret — so that agents can authenticate on their behalf.
Headers
X-Bulwark-Tenant: <tenant-id>
Tenant opt-in required
This endpoint is unauthenticated — reachable with only the X-Bulwark-Tenant header (or a resolved publishable key) — so it is disabled by default for every tenant. A tenant must explicitly enable it first:
PUT /api/v1/settings
{
"auth_methods": {
"dynamic_client_registration": true
}
}
Until a tenant sets auth_methods.dynamic_client_registration: true, every POST /oauth2/register for that tenant — and any request naming an unknown or non-active tenant — returns:
403 Forbidden
{"error": "dynamic client registration is disabled for this tenant"}
Enable this only for tenants that need it (for example, to support MCP clients that rely on DCR per RFC 7591). Leaving it off is safe: it has no effect on the admin-authenticated POST /api/v1/oauth2/clients path below, which is unaffected by this setting.
Clients registered via DCR are also flagged as unverified on Bulwark's hosted consent screen (unless an admin later marks them trusted) — the user sees an explicit warning and the redirect host their authorization will be sent to, since a DCR client's registration is not admin-reviewed.
Body
{
"client_name": "Acme Data Exporter",
"redirect_uris": ["https://app.acme.com/auth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "read:customers write:reports",
"token_endpoint_auth_method": "client_secret_basic",
"application_type": "web",
"contacts": ["[email protected]"],
"logo_uri": "https://app.acme.com/logo.png",
"client_uri": "https://app.acme.com"
}
| Field | Required | Description |
|-------|----------|-------------|
| client_name | Yes | Human-readable application name |
| redirect_uris | Yes (for authorization_code) | Allowed redirect URIs |
| grant_types | No | Defaults to ["authorization_code"]. Also supports client_credentials. |
| response_types | No | Defaults to ["code"] |
| scope | No | Requested OAuth scopes |
| token_endpoint_auth_method | No | client_secret_basic, client_secret_post, or none |
Response 201
{
"client_id": "bwk_...",
"client_secret": "...",
"client_name": "Acme Data Exporter",
"redirect_uris": ["https://app.acme.com/auth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "read:customers write:reports",
"token_endpoint_auth_method": "client_secret_basic",
"client_id_issued_at": 1743292800,
"registration_access_token": "...",
"registration_client_uri": "/oauth2/register/bwk_..."
}
Important: Both client_secret and registration_access_token are returned only once. Store them securely.
The registration_access_token (RAT) is used to manage this client via RFC 7592 endpoints. The token is stored as a hash and cannot be recovered — if lost, you must update the client via the admin API.
Client Configuration Management (RFC 7592)
Once a client is registered via DCR, you can manage it using the registration access token.
Get Client Configuration
GET /oauth2/register/{client_id}
Retrieve the client's current configuration using the registration access token.
Headers
Authorization: Bearer <registration_access_token>
Response 200
{
"client_id": "bwk_...",
"client_name": "Acme Data Exporter",
"redirect_uris": ["https://app.acme.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "openid profile email",
"token_endpoint_auth_method": "client_secret_basic",
"client_id_issued_at": 1743292800
}
Note: The registration_access_token is not returned in GET responses — it is only provided during initial registration and after updates.
Update Client Configuration
PUT /oauth2/register/{client_id}
Update the client configuration. A new registration access token is issued, and the old one is invalidated.
Headers
Authorization: Bearer <registration_access_token>Content-Type: application/json
Body
{
"client_name": "Updated Name",
"redirect_uris": ["https://newdomain.com/callback"],
"grant_types": ["authorization_code", "refresh_token"]
}
Response 200
{
"client_id": "bwk_...",
"client_name": "Updated Name",
"redirect_uris": ["https://newdomain.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "openid profile email",
"token_endpoint_auth_method": "client_secret_basic",
"client_id_issued_at": 1743292800,
"registration_access_token": "...",
"registration_client_uri": "/oauth2/register/bwk_..."
}
Important: The old registration access token is now invalid. Use the new one for future management operations.
Delete Client
DELETE /oauth2/register/{client_id}
Soft-delete the client. All active tokens issued to this client are invalidated.
Headers
Authorization: Bearer <registration_access_token>
Response 204
No content. The client is marked as deleted and can no longer be used.
List Clients (Admin)
GET /api/v1/oauth2/clients
Headers
Authorization: Bearer <apiKey>X-Bulwark-Tenant: <tenant-id>
Response 200
{
"clients": [
{
"id": "01j...",
"client_id": "bwk_...",
"name": "Acme Data Exporter",
"redirect_uris": ["https://app.acme.com/auth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"scopes": ["openid", "profile", "read:customers", "write:reports"],
"is_public": false,
"token_endpoint_auth_method": "client_secret_basic",
"status": "active",
"created_at": "2026-03-30T00:00:00Z",
"updated_at": "2026-03-30T00:00:00Z"
}
],
"count": 1
}
Get Client (Admin)
GET /api/v1/oauth2/clients/{id}
Retrieve a single OAuth 2.0 client by its internal row ID. The tenant is verified server-side — clients that belong to another tenant return 404.
Headers
Authorization: Bearer <apiKey>X-Bulwark-Tenant: <tenant-id>
Path Parameters
| Parameter | Description |
|-----------|-------------|
| id | Internal row ID of the client (the id field, not client_id) |
Response 200
{
"id": "01j...",
"client_id": "bwk_...",
"name": "Acme Data Exporter",
"description": "Exports customer data to the Acme warehouse",
"redirect_uris": ["https://app.acme.com/auth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"scopes": ["openid", "profile", "read:customers", "write:reports"],
"is_public": false,
"token_endpoint_auth_method": "client_secret_basic",
"status": "active",
"created_at": "2026-03-30T00:00:00Z",
"updated_at": "2026-03-30T00:00:00Z"
}
description is omitted when empty. client_secret is never returned by this endpoint.
Response 404
{ "error": "client not found" }
Create Client (Admin)
POST /api/v1/oauth2/clients
Create a client directly without an initial access token. Equivalent to DCR but requires admin credentials.
Headers
Authorization: Bearer <apiKey>X-Bulwark-Tenant: <tenant-id>
Body
Same schema as POST /oauth2/register.
Response 201
Same schema as the DCR 201 response.
Revoke Client
DELETE /api/v1/oauth2/clients/{id}
Immediately revokes the client and all tokens issued to it. Active sessions authenticated via this client will be terminated.
Headers
Authorization: Bearer <apiKey>X-Bulwark-Tenant: <tenant-id>
Response 200
{
"status": "deleted"
}