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, 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. Discover the authorization server from the MCP server's metadata.
  2. 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.
  3. Your app generates a random code verifier and computes its SHA-256 hash (the code challenge).
  4. Your app redirects the user to the authorization endpoint with the code challenge.
  5. The user signs in and authorizes access; Productboard redirects back with an authorization code.
  6. 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.
  • Audience-bound tokens: Access tokens are issued for the MCP server, https://mcp.productboard.com (RFC 8707 resource indicator).

Endpoints and Discovery

All MCP OAuth endpoints live on https://mcp.productboard.com, which serves workspaces hosted in the US region.

EndpointURL
Protected Resource Metadata (RFC 9728)GET https://mcp.productboard.com/.well-known/oauth-protected-resource
Authorization Server Metadata (RFC 8414)GET https://mcp.productboard.com/.well-known/oauth-authorization-server
Dynamic Client Registration (RFC 7591)POST https://mcp.productboard.com/oauth2/register
AuthorizationGET https://mcp.productboard.com/oauth2/authorize
TokenPOST https://mcp.productboard.com/oauth2/token

The metadata and registration endpoints exist only on mcp.productboard.com. The authorization endpoint redirects the browser to https://app.productboard.com/oauth2/authorize, where the user signs in and sees the consent screen. The authorization and token endpoints are also served on app.productboard.com, but use the URLs advertised in the metadata.

Discovery flow:

  1. A request to the MCP server without a valid access token returns HTTP 401 with the header WWW-Authenticate: Bearer resource_metadata="https://mcp.productboard.com/.well-known/oauth-protected-resource".

  2. The protected resource metadata names the authorization server:

    {
      "resource": "https://mcp.productboard.com",
      "authorization_servers": ["https://mcp.productboard.com"],
      "bearer_methods_supported": ["header"]
    }
  3. The authorization server metadata lists the endpoints and supported features:

    {
      "issuer": "https://mcp.productboard.com",
      "authorization_endpoint": "https://mcp.productboard.com/oauth2/authorize",
      "token_endpoint": "https://mcp.productboard.com/oauth2/token",
      "registration_endpoint": "https://mcp.productboard.com/oauth2/register",
      "response_types_supported": ["code"],
      "grant_types_supported": ["authorization_code", "refresh_token"],
      "code_challenge_methods_supported": ["S256"],
      "token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"],
      "resource_parameter_supported": true
    }

    client_secret_basic and client_secret_post are listed so that clients which default to them can register. Registration always creates a public client and returns token_endpoint_auth_method: "none", so authenticate at the token endpoint with your client_id and PKCE only.


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://mcp.productboard.com/oauth2/register

Content-Type: application/json

Request body:

ParameterDescriptionRequired
redirect_urisArray of one or more redirect URIs. Each must be an https URI, an http loopback URI (localhost, 127.0.0.1, or [::1], any port), or a private-use URI scheme for native apps (for example, cursor://…). Wildcards and schemes such as javascript, data, or file are rejected.Yes
client_nameHuman-readable name of the application. Shown to users on the consent screen. If omitted, Productboard generates a name such as Dynamic Client 1a2b3c4d.No (recommended)
token_endpoint_auth_methodnone, client_secret_basic, or client_secret_post. Defaults to none. Whatever you send, the client is registered as a public client, no secret is issued, and the response returns none.No
client_uri, logo_uri, tos_uri, policy_uri, software_id, software_version, contactsOptional RFC 7591 client metadata. Stored and returned in the response.No

Any other fields, including scope, grant_types, and response_types, are ignored. See Access Scopes for how scopes are assigned.

Example request:

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

Successful response (201):

{
  "client_id": "hqAGVdAy9FZX5Ky5cBHUB2FshBdSO6eN75tWMX46ZZd",
  "client_id_issued_at": 1755600000,
  "client_name": "AwesomeMCP",
  "redirect_uris": ["http://localhost:8080/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "resource": "https://mcp.productboard.com"
}

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

  • Re-registering is safe. Registering again with the same client_name, redirect_uris (in the same order), and software_id returns the existing client instead of creating a new one. The response is still HTTP 201, with the original client_id and client_id_issued_at and the metadata stored at first registration: changed fields such as logo_uri or client_uri aren't updated. This only works when you send a client_name.
  • Unused registrations are cleaned up. If a registered client has no unrevoked tokens, it is deleted 7 days after it was last registered. If the authorization endpoint reports Invalid client_id., register again.

Rate limits: Registration is rate-limited to 60 requests per minute and 500 requests per day per remote IP. Exceeding a limit returns HTTP 429 with a Retry-After header (in seconds). Register once per installation and cache the client_id, rather than registering on every run.

Registration errors

Validation errors return HTTP 400:

Error codeWhen it happens
invalid_redirect_uriredirect_uris is missing or empty, or a URI is malformed, contains a wildcard, uses a forbidden scheme, or uses http on a non-loopback host.
invalid_client_metadataAnother field failed validation (for example, an unsupported token_endpoint_auth_method).

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


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's browser to the authorization endpoint, https://mcp.productboard.com/oauth2/authorize, with the following query parameters. Productboard redirects the browser to https://app.productboard.com/oauth2/authorize, where the user signs in and approves access on the consent screen.

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
resourceThe MCP server's resource identifier, https://mcp.productboard.com (RFC 8707). If sent, it must match (a trailing slash is ignored). If omitted, it defaults to the same value.No
stateAn arbitrary string returned unchanged in the response. Recommended for CSRF prevention.No
scopeIgnored. Productboard assigns the token's scopes itself — see Access Scopes.No

Example authorization URL:

https://mcp.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
  &resource=https%3A%2F%2Fmcp.productboard.com
  &state=abc123

Important: If you omit code_challenge or use any method other than S256, Productboard shows an authorization error instead of the consent screen. See Authorization errors.

To complete authorization, the user must be an active member of the workspace, the workspace must have MCP turned on, and the user's role must be allowed to use MCP (if the admin hasn't selected any roles, every role is allowed). Workspace admins control both settings — see Connecting your agent.

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://mcp.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
resourceIf sent, must be https://mcp.productboard.comNo

Note: No client_secret is needed. The client authenticates with its client_id alone (token_endpoint_auth_method: none), and the code_verifier proves you are the same client that initiated the authorization request.

Example request:

curl -X POST "https://mcp.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": "identity:read documents:read entities:read members:read teams:read",
  "created_at": 1692288000
}
FieldDescription
access_tokenBearer token (JWT format) for requests to the MCP server, https://mcp.productboard.com
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 scopes Productboard assigned, which depend on the user's role — see Access Scopes
created_atUNIX timestamp of token creation

Send the access token in the Authorization: Bearer YOUR_ACCESS_TOKEN header of your requests to the MCP server.


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://mcp.productboard.com/oauth2/token

ParameterDescriptionRequired
client_idYour application's Client IDYes
grant_typeAlways refresh_tokenYes
refresh_tokenThe refresh token from the token responseYes
resourceIf sent, must be https://mcp.productboard.comNo

Example request:

curl -X POST "https://mcp.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. Store the new refresh token right away: the one you just used stays valid for only 10 minutes, giving you time to retry if you experience network issues.

Warning: If you lose both the access and refresh tokens, or a workspace admin disconnects all MCP clients, the user must re-authorize your application.


Error Handling

Authorization errors

When an authorization request can't proceed, Productboard shows the user an Authorization error page (HTTP 401) instead of the consent screen. Your redirect_uri is not called.

CauseMessage shown
Missing code_challenge, or code_challenge_method other than S256"PKCE code_challenge is required for this application. Use S256 method."
Unknown or deleted client_id"Invalid client_id."
The user isn't an active member of the workspace they signed in to"Your account is not an active member of this workspace. Sign in to the workspace you want to authorize and try again."
MCP is turned off for the workspace, or the user's role isn't allowed to use MCP"MCP access is not enabled for this workspace, or your role is not allowed to authorize MCP applications."

These errors are returned to your redirect_uri instead:

ErrorWhen it happens
invalid_targetThe resource parameter doesn't match https://mcp.productboard.com, or is sent in array form (resource[]=…). Productboard checks it when the user approves, so this redirect comes after the consent screen. The redirect includes error, error_description, and state.
access_deniedThe user selected Deny on the consent screen.

Token exchange errors

ErrorWhen it happens
invalid_grantThe authorization code is invalid, expired, or already used, or the refresh token is invalid, expired, or revoked.
invalid_grantThe code_verifier does not match the code_challenge sent during authorization.
invalid_targetThe resource parameter doesn't match https://mcp.productboard.com (HTTP 400).
invalid_clientThe client_id is unknown, or the request included a client_secret — public clients have none (HTTP 401).
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 10 minutes after being used)Refresh token / New authorization request
Client registrationDeleted 7 days after it was last registered, if it has no unrevoked tokensRegister again

Access Scopes

MCP clients don't choose scopes. Productboard ignores scope at registration and at the authorization endpoint, and gives each MCP access token the scopes of the signed-in user's role. Custom roles get the scopes of the role they're based on.

RoleScopes
Maker with admin access, Makeridentity:read, documents:read, documents:write, entities:read, entities:write, fields:write, notes:read, notes:write, members:read, members:pii:read, teams:read, users:read, users:pii:read, companies:read
ContributorSame as Maker, without fields:write and entities:write
Vieweridentity:read, documents:read, entities:read, members:read, teams:read

members:pii:read and users:pii:read cover names and email addresses of teammates and end users.

Scopes are an upper limit, not a grant. Within them, what your client can see and do is still determined by the signed-in user's role permissions in Productboard and by the workspace's MCP settings. MCP tools that need a scope the token doesn't carry may be left out of tools/list or refused. The scopes per role can change as the MCP server gains tools, so read scope from the token response rather than hard-coding it. The consent screen lists what the client will be able to do on the user's behalf.


Best Practices

  • Discover endpoints from the metadata. Use the URLs in https://mcp.productboard.com/.well-known/oauth-authorization-server rather than hard-coding hosts.
  • Register once and cache the client_id. Registration is rate-limited; re-registering the same client returns the same client_id.
  • 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.
  • Send the resource parameter (https://mcp.productboard.com) on authorization and token requests.
  • Handle token refresh gracefully. Refresh the access token before it expires, and always store the newest refresh token.