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.

If you just want to connect an existing agent (Claude Code, Cursor, …) to Productboard, you don't need anything here — see Connecting your agent. 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: use the standard OAuth 2.0 Authorization Code flow for server-side apps that can store a secret, or the Server-to-Server (JWT) Flow for backend-to-backend automation.

How it works (at a glance)

  1. Register your application programmatically via the Dynamic Client Registration endpoint (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 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:

ParameterDescriptionRequired
redirect_urisArray of redirect URIs. HTTPS is required except for localhost URIs used during local development.Yes
client_nameHuman-readable name of the application. Shown to users on the consent screen.Yes
token_endpoint_auth_methodMust be none (public client, PKCE instead of secret). If omitted, defaults to none.No
scopeSpace-separated list of requested scopes. If omitted, a default scope set is granted.No

Example request:

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):

{
  "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 codeWhen it happens
invalid_redirect_uriA redirect URI is missing, malformed, or uses a forbidden scheme.
invalid_client_metadataOther 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 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:

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:

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:

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:

ParameterDescriptionRequired
client_idYour application's Client IDYes
response_typeAlways codeYes
redirect_uriOne of your registered redirect URIs (URL-encoded). Must be an exact match.Yes
code_challengeThe Base64-URL-encoded SHA-256 hash of your code verifierYes
code_challenge_methodAlways S256Yes
scopeSpace-separated list of requested scopes. If omitted, the application's default scopes are used.No
stateAn 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:

ParameterDescription
codeThe authorization code (use within 10 minutes)
stateThe 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

ParameterDescriptionRequired
client_idYour application's Client IDYes
grant_typeAlways authorization_codeYes
codeThe authorization code from the previous stepYes
redirect_uriThe same redirect URI used in the authorization requestYes
code_verifierThe original code verifier you generated in step 1Yes

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

Example request:

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):

{
  "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
}
FieldDescription
access_tokenBearer token for API requests (JWT format)
token_typeAlways Bearer
expires_inSeconds until the access token expires (24 hours)
refresh_tokenToken to obtain a new access token when it expires
refresh_token_expires_inSeconds until the refresh token expires (180 days)
scopeThe granted scopes
created_atUNIX 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

ParameterDescriptionRequired
client_idYour application's Client IDYes
grant_typeAlways refresh_tokenYes
refresh_tokenThe refresh token from the token responseYes

Example request:

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:

ErrorDescription
Missing code_challengeReturns HTTP 401 with message: "PKCE code_challenge is required for this application."
code_challenge_method=plainReturns HTTP 400 with message: "The code challenge method is not supported."

Token exchange errors

ErrorWhen it happens
invalid_grantThe authorization code is invalid, expired, or already used.
invalid_grantThe code_verifier does not match the code_challenge sent during authorization.
invalid_requestRequired parameters are missing.

Credential Expirations

CredentialExpirationHow to refresh
Authorization code10 minutes (or immediately after being used)New authorization request
Access token24 hoursRefresh token
Refresh token180 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 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 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.