> ## Documentation Index
> Fetch the complete documentation index at: https://developer.productboard.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth for MCP Clients (DCR)

Authenticate MCP (Model Context Protocol) clients using Dynamic Client Registration and the authorization code flow with PKCE. No client secret required.

> 📘 This page is for **MCP clients only**
>
> This guide documents the OAuth flow used by **MCP (Model Context Protocol) clients** that register themselves with Productboard at runtime. It is **not** a general REST API authentication method — for that, see [Authentication](https://developer.productboard.com/reference/authentication).
>
> If you just want to connect an existing agent (Claude Code, Cursor, …) to Productboard, you don't need anything here — see [Connecting your agent](https://developer.productboard.com/reference/mcp-install). This page is for engineers **building their own MCP client** who need to implement registration and sign-in by hand.

# OAuth for MCP Clients with PKCE

## Overview

MCP (Model Context Protocol) clients are applications that cannot securely store a client secret. They use **Dynamic Client Registration** to register themselves at runtime and **PKCE (Proof Key for Code Exchange)** instead of a client secret to secure the authorization code flow.

### When to use this flow

Use this flow when:

* You're building an **MCP client** that connects to Productboard on a user's behalf.
* Your client **registers itself programmatically** rather than being created manually in the admin UI.
* You need **user consent** and **refreshable access tokens**, without a client secret.

This flow is specific to MCP clients. For other use cases, pick the matching method from [Authentication](https://developer.productboard.com/reference/authentication): use the standard [OAuth 2.0 Authorization Code flow](oauth-authorization-code) for server-side apps that can store a secret, or the [Server-to-Server (JWT) Flow](oauth-server-to-server) for backend-to-backend automation.

### How it works (at a glance)

1. **Register** your application programmatically via the [Dynamic Client Registration endpoint](#registering-an-oauth-application) (RFC 7591). Public clients are registered at runtime by the client itself, not created manually in the Productboard admin UI.
2. Your app generates a random **code verifier** and computes its **SHA-256 hash** (the code challenge).
3. Your app redirects the user to the **authorization endpoint** with the code challenge.
4. The user authorizes access; Productboard redirects back with an **authorization code**.
5. Your app exchanges the code for an **access token**, sending the original code verifier for verification.

### Security model

* **No client secret:** Public clients do not receive or store a client secret. PKCE replaces the secret as the proof of the legitimate client.
* **S256 only:** Only the `S256` challenge method is accepted. The `plain` method is rejected.
* **One-time use:** Each code verifier/challenge pair is used exactly once.
* **Short-lived codes:** Authorization codes expire after 10 minutes.

***

## Registering an OAuth Application

Public clients are registered programmatically via the [OAuth 2.0 Dynamic Client Registration](https://www.rfc-editor.org/rfc/rfc7591) endpoint (RFC 7591). **There is no manual admin UI for public clients** — your application self-registers at runtime (typically at first launch or install) and receives a Client ID it can persist and reuse.

**Endpoint:** `POST https://app.productboard.com/oauth2/register`

**Content-Type:** `application/json`

**Request body:**

| Parameter                    | Description                                                                                          | Required |
| :--------------------------- | :--------------------------------------------------------------------------------------------------- | :------- |
| `redirect_uris`              | Array of redirect URIs. HTTPS is required except for `localhost` URIs used during local development. | Yes      |
| `client_name`                | Human-readable name of the application. Shown to users on the consent screen.                        | Yes      |
| `token_endpoint_auth_method` | Must be `none` (public client, PKCE instead of secret). If omitted, defaults to `none`.              | No       |
| `scope`                      | Space-separated list of requested scopes. If omitted, a default scope set is granted.                | No       |

**Example request:**

```bash
curl -X POST "https://app.productboard.com/oauth2/register" \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "AwesomeMCP",
    "redirect_uris": ["http://localhost:8080/callback"],
    "token_endpoint_auth_method": "none",
    "scope": "entities:read notes:read"
  }'
```

**Successful response (201):**

```json
{
  "client_id": "hqAGVdAy9FZX5Ky5cBHUB2FshBdSO6eN75tWMX46ZZd",
  "client_id_issued_at": 1755600000,
  "client_name": "AwesomeCLI",
  "redirect_uris": ["http://localhost:8080/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "entities:read notes:read"
}
```

Persist the returned `client_id`. No client secret is issued — public clients use PKCE in place of a secret.

> **Rate limits:** Registration is rate-limited to **5 requests per minute** and **50 requests per day** per remote IP. Register once per installation and cache the `client_id`, rather than registering on every run.

### Registration errors

| Error code                | When it happens                                                                                                               |
| :------------------------ | :---------------------------------------------------------------------------------------------------------------------------- |
| `invalid_redirect_uri`    | A redirect URI is missing, malformed, or uses a forbidden scheme.                                                             |
| `invalid_client_metadata` | Other request fields failed validation (e.g. missing `client_name`, unsupported `token_endpoint_auth_method`, unknown scope). |

The response also includes an `error_description` field with a human-readable explanation.

### Discovery

The registration endpoint is advertised by the [OAuth 2.0 Authorization Server Metadata](https://www.rfc-editor.org/rfc/rfc8414) document at `https://app.productboard.com/.well-known/oauth-authorization-server` as `registration_endpoint`.

***

## Integration Guide

### 1) Generate the PKCE Code Verifier and Challenge

Before starting the authorization flow, generate a cryptographically random **code verifier** and compute its **code challenge**.

**Requirements:**

* `code_verifier`: A random string, 43-128 characters, using unreserved characters (`[A-Za-z0-9._~-]`).
* `code_challenge`: The Base64-URL-encoded SHA-256 hash of the code verifier (no padding).

**JavaScript / Node.js:**

```javascript
const crypto = require('crypto');

// Generate code verifier (43-128 chars)
const codeVerifier = crypto.randomBytes(32).toString('base64url');

// Compute code challenge (S256)
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');
```

**Python:**

```python
import secrets
import hashlib
import base64

# Generate code verifier
code_verifier = secrets.token_urlsafe(32)

# Compute code challenge (S256)
digest = hashlib.sha256(code_verifier.encode('ascii')).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
```

**Ruby:**

```ruby
require 'securerandom'
require 'digest'
require 'base64'

# Generate code verifier
code_verifier = SecureRandom.urlsafe_base64(32)

# Compute code challenge (S256)
digest = Digest::SHA256.digest(code_verifier)
code_challenge = Base64.urlsafe_encode64(digest, padding: false)
```

Store the `code_verifier` securely in your app. You will need it when exchanging the authorization code for a token.

***

### 2) Get the Authorization Code

Redirect the user to the authorization endpoint with the following query parameters:

| Parameter               | Description                                                                                      | Required |
| :---------------------- | :----------------------------------------------------------------------------------------------- | :------- |
| client\_id              | Your application's Client ID                                                                     | Yes      |
| response\_type          | Always `code`                                                                                    | Yes      |
| redirect\_uri           | One of your registered redirect URIs (URL-encoded). Must be an exact match.                      | Yes      |
| code\_challenge         | The Base64-URL-encoded SHA-256 hash of your code verifier                                        | Yes      |
| code\_challenge\_method | Always `S256`                                                                                    | Yes      |
| scope                   | Space-separated list of requested scopes. If omitted, the application's default scopes are used. | No       |
| state                   | An arbitrary string returned unchanged in the response. Recommended for CSRF prevention.         | No       |

**Example authorization URL:**

```
https://app.productboard.com/oauth2/authorize
  ?client_id=YOUR_CLIENT_ID
  &response_type=code
  &redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &scope=entities%3Aread%20notes%3Aread
  &state=abc123
```

> **Important:** If you omit `code_challenge` or use `code_challenge_method=plain`, the request will be rejected with an error.

After the user authorizes access, they are redirected to your `redirect_uri` with:

| Parameter | Description                                    |
| :-------- | :--------------------------------------------- |
| code      | The authorization code (use within 10 minutes) |
| state     | The same value you sent (if provided)          |

Example callback: `https://your-app.com/callback?code=AUTH_CODE&state=abc123`

***

### 3) Exchange the Authorization Code for an Access Token

Make a POST request to the token endpoint to exchange the authorization code for an access token. Send the **code verifier** (not the challenge) so the server can verify it matches.

**Endpoint:** `POST https://app.productboard.com/oauth2/token`

**Content-Type:** `application/x-www-form-urlencoded`

| Parameter      | Description                                             | Required |
| :------------- | :------------------------------------------------------ | :------- |
| client\_id     | Your application's Client ID                            | Yes      |
| grant\_type    | Always `authorization_code`                             | Yes      |
| code           | The authorization code from the previous step           | Yes      |
| redirect\_uri  | The same redirect URI used in the authorization request | Yes      |
| code\_verifier | The original code verifier you generated in step 1      | Yes      |

> **Note:** No `client_secret` is needed. The `code_verifier` proves you are the same client that initiated the authorization request.

**Example request:**

```bash
curl -X POST "https://app.productboard.com/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=https://your-app.com/callback" \
  -d "code_verifier=YOUR_CODE_VERIFIER"
```

**Successful response (200):**

```json
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJraWQ...",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "4w4_-gHaOVixNuS_naAvqsRTsCuV7wWQgn0jXQYtUhs",
  "refresh_token_expires_in": 15552000,
  "scope": "entities:read notes:read",
  "created_at": 1692288000
}
```

| Field                       | Description                                        |
| :-------------------------- | :------------------------------------------------- |
| access\_token               | Bearer token for API requests (JWT format)         |
| token\_type                 | Always `Bearer`                                    |
| expires\_in                 | Seconds until the access token expires (24 hours)  |
| refresh\_token              | Token to obtain a new access token when it expires |
| refresh\_token\_expires\_in | Seconds until the refresh token expires (180 days) |
| scope                       | The granted scopes                                 |
| created\_at                 | UNIX timestamp of token creation                   |

***

### 4) Refresh the Access Token

When the access token expires, use the refresh token to obtain a new one. Public clients do not need a client secret for refresh requests.

**Endpoint:** `POST https://app.productboard.com/oauth2/token`

| Parameter      | Description                               | Required |
| :------------- | :---------------------------------------- | :------- |
| client\_id     | Your application's Client ID              | Yes      |
| grant\_type    | Always `refresh_token`                    | Yes      |
| refresh\_token | The refresh token from the token response | Yes      |

**Example request:**

```bash
curl -X POST "https://app.productboard.com/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=YOUR_REFRESH_TOKEN"
```

The response format is the same as the initial token exchange, with fresh access and refresh tokens.

> **Warning:** If you lose both the access and refresh tokens, the user must re-authorize your application.

***

## Error Handling

### Authorization errors

If the authorization request is missing `code_challenge` or uses an unsupported method:

| Error                         | Description                                                                             |
| :---------------------------- | :-------------------------------------------------------------------------------------- |
| Missing `code_challenge`      | Returns HTTP 401 with message: "PKCE code\_challenge is required for this application." |
| `code_challenge_method=plain` | Returns HTTP 400 with message: "The code challenge method is not supported."            |

### Token exchange errors

| Error             | When it happens                                                                    |
| :---------------- | :--------------------------------------------------------------------------------- |
| `invalid_grant`   | The authorization code is invalid, expired, or already used.                       |
| `invalid_grant`   | The `code_verifier` does not match the `code_challenge` sent during authorization. |
| `invalid_request` | Required parameters are missing.                                                   |

***

## Credential Expirations

| Credential         | Expiration                                   | How to refresh                            |
| :----------------- | :------------------------------------------- | :---------------------------------------- |
| Authorization code | 10 minutes (or immediately after being used) | New authorization request                 |
| Access token       | 24 hours                                     | Refresh token                             |
| Refresh token      | 180 days (or 60 minutes after being used)    | Refresh token / New authorization request |

***

## Access Scopes

Public client applications support the same scopes as other OAuth applications. See the [Access Scopes section](oauth-authorization-code#access-scopes) of the OAuth 2.0 Authorization Code documentation for the full list.

A Productboard user can authorize any scopes on the application, but their [role permissions](https://support.productboard.com/hc/en-us/articles/360056316294-Member-role-definitions#h_01H9QDFJE5867M4692E6AGEF2B) are enforced when the application calls the API on their behalf.

***

## Best Practices

* **Always use S256.** The `plain` challenge method is not supported.
* **Generate a new code verifier for each authorization request.** Never reuse verifiers.
* **Store the code verifier securely** until the token exchange is complete, then discard it.
* **Use the `state` parameter** to prevent CSRF attacks and maintain application state.
* **Request only the scopes you need.** Users see the requested permissions on the consent screen.
* **Handle token refresh gracefully.** Refresh the access token before it expires to avoid interruptions.