Authenticate backend systems without user interaction. This flow enables server-to-server integrations with short-lived, signed tokens and optional user impersonation.
Beta notice
Productboard REST API 2.0 is currently in Beta. Use it for experimentation, prototyping, and early integrations. Please note that individual endpoints may still change before the public release. We’re actively looking for Feedback & Help – let us know what works and what doesn’t. For critical production systems, continue using the Productboard REST API v1.0.
OAuth 2.0 JWT Bearer Flow
Overview
Many enterprise integrations need server-to-server authentication without any interactive user login. The JWT Bearer Grant (RFC 7523) lets your backend exchange a client-signed JWT for a short-lived OAuth 2.0 Bearer access token. You sign a JWT with your private key; we verify it with the public key you uploaded to your OAuth Application and return a time-boxed access token you can use to call our API.
Why we offer this
- Enterprise-grade trust: Key-based trust between your system and Productboard—no long-lived shared secrets.
- User attribution: Request tokens on behalf of a specific user in your space (via
sub) so actions are authorized and auditable. - Just-in-time access: Tokens are short-lived (5 min) and no refresh tokens are issued—simply re-assert as needed for strong replay resistance.
- Least privilege: Request only the scopes you need; we issue a token limited to your app’s allowed subset.
When to use this flow
Use the JWT Bearer Grant when:
- Your integration runs entirely on the server (no browser/user consent screen).
- You need strong coupling between systems (stable, automated jobs, backend-to-backend pipelines).
- You must act on behalf of individual users in your Productboard space (e.g., sync items as a specific user for correct permissions and audit).
If you require end-user sign-in or interactive consent, use a standard OAuth authorization_code flow instead.
How it works (at a glance)
- Register an OAuth Application and upload your public RSA key (PEM).
- Your server builds and signs a JWT (
RS256) with required claims (iss,sub,aud,exp,iat; optionalscope). - Your server POSTs the JWT to the token endpoint with grant type
urn:ietf:params:oauth:grant-type:jwt-bearer. - If valid, we return a Bearer access token (300 s TTL).
- You call the API with
Authorization: Bearer <token>.
Key capabilities
- Act-on-behalf-of a user: Provide
subas the email of an activated member of the same space as your OAuth App. - Fine-grained scopes: Request a subset of your application’s scopes via
scope; we return the intersection. - Short-lived tokens: Access tokens for this grant type default to 5 minutes.
- No refresh tokens: This flow does not mint refresh tokens. Re-send a new JWT assertion when needed.
Security model
- Asymmetric crypto: You keep your private key; we store your public key on the OAuth Application and use it to verify signatures.
- Exact audience:
audmust match the token endpoint URL to prevent token reuse against other services. - Tight time windows:
iatin the past;expin the near future; issued tokens live 300 s to reduce replay risk. - Key rotation: Upload a new public key, start signing with the new private key, then remove the old key after cutover.
- No shared secrets for this flow: For JWT Bearer apps we don’t issue client secrets; the certificate is the trust anchor.
Prerequisites
- An OAuth Application in Productboard with your public RSA key (PEM) uploaded.
- A user in your space to act on behalf of (
sub). The user must be activated and a member of the same space.
Note on customers without their own certificate This flow requires you to provide your public key; we do not generate or host keys on your behalf.
Typical use cases
- CRM/ITSM connectors (nightly syncs, ticket ↔ feature linkage) that run unattended and attribute changes to a real user.
- Data pipelines performing scheduled exports/imports with scoped, expiring tokens.
- Internal automations where your platform calls Productboard as a specific user to respect permissions and audit.
Registering a Connected App
You can register and manage your Connected App at https://app.productboard.com/oauth2/connected_apps. You need an active Productboard user account to access this page.
Integration Guide
Purpose: Enable server-to-server authentication using a client-signed JWT (RFC 7523) to obtain a short-lived OAuth 2.0 Bearer access token for calling the API.
1) Terminology (Token Dictionary)
- Connected App: Your application that integrates with our API.
- OAuth Application (Client): Your integration registered in our system. Identified by Client ID (
issin JWT). Stores your public key used to verify your JWT signature. - JWT (Assertion): A signed JSON Web Token you create and send to the token endpoint as
assertion. Signed with your private key using RS256. - Access Token: Short-lived token returned by our
/oauth2/tokenendpoint. Type is Bearer; lifetime 300s (5 min); no refresh token is issued for this flow. - Scopes: Space-separated permissions such as
users:read. The requested scope must be a subset of scopes granted to your OAuth Application; otherwise it’s trimmed to the allowed subset. - Resource Owner: A valid, activated user who is a member of the same space as the OAuth Application. Their email appears in
sub.
2) High-Level Flow
- Register an OAuth Application and upload your public RSA key (PEM).
- Your backend creates a JWT assertion with required claims and signs it with RS256.
- POST the assertion to the Token Endpoint using the grant type
urn:ietf:params:oauth:grant-type:jwt-bearer. - Receive a Bearer access token (valid 300 seconds).
- Call the API with
Authorization: Bearer <token>.
3) Endpoints
Token Endpoint
POST {APP_DEFAULT_URL}/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<your-signed-jwt>Successful Response (200):
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 300,
"scope": "users:read"
}Notes
token_typeis alwaysBearer.expires_inis always300(seconds).refresh_tokenis not returned for this grant.
4) Certificate (Public Key) Upload
- Generate an RSA key pair (min. 2048 bits).
- Keep the private key secure on your server (never share it).
- Upload the public key (PEM) to your OAuth Application settings.
- Rotate keys when needed:
- Upload the new public key.
- Start signing JWTs with the new private key.
- Remove the old key after traffic fully migrates.
Requirements
- Signature algorithm: RS256 (RSA with SHA-256).
- Public key must be a valid PEM.
5) JWT Requirements
Required Claims
| Claim | Name | Rules | Example |
|---|---|---|---|
iss | Issuer | Must equal your OAuth Application Client ID | "9b3c...client-id" |
sub | Subject | Must be the email of an activated user who is a member of the same space as the OAuth Application | "[email protected]" |
aud | Audience | Must exactly match the Token Endpoint URL | "https://api.example.com/oauth2/token" |
exp | Expiration time (UTC seconds). | Must be in the future, keep short (≤ 60s). | 1692288000 |
iat | Issued-at time (UTC seconds). | Must be in the past. | 1692287940 |
Optional Claim
| Claim | Description | Example |
|---|---|---|
scope | Space-separated scopes; must be a subset of the app’s allowed scopes. If omitted, the app’s default scopes are used. | "users:read users_pii:read" |
Example Payload:
{
"iss": "YOUR_CLIENT_ID",
"sub": "[email protected]",
"aud": "https://api.example.com/oauth2/token",
"exp": 1692288000,
"iat": 1692287940,
"scope": "users:read"
}6) End-to-End Example
sequenceDiagram
participant Client as Client Application
participant Authz as Authorization Server (STS)
participant Gateway as API Gateway
participant Service as Service
%% 1. Client obtains or builds JWT assertion
Client-->>Client: Issue JWT
%% 2. Exchange JWT for access token
Client->>Authz: POST /oauth2/token
Authz-->>Authz: Validate JWT signature & claims
Authz-->>Gateway: Register access token
Authz-->>Client: 200 OK { "access_token": "...", ... }
%% 3. Call protected API
Client->>Gateway: GET api.pb.com/resource Authorization: Bearer <token>
Gateway-->>Gateway: Validate access token
Gateway->>Service: GET pb-service/resource X-Headers
Service-->>Gateway: 200 OK data
Gateway-->>Client: 200 OK data
6.1 Create and Sign the JWT (RS256)
Bash:
# Generate RSA key pair (2048 bits)
#!/bin/bash
# This script regenerates the RSA key pair for JWT testing
# It ensures the keys are properly formatted and match each other
set -e
echo "Generating a fresh RSA key pair for testing..."
openssl genrsa -out temp_private_key.pem 2048
openssl rsa -in temp_private_key.pem -pubout -out temp_public_key.pem
# Copy files without any modifications to ensure formatting is preserved
cp temp_private_key.pem rsa_private_key.pem
cp temp_public_key.pem rsa_public_key.pem
# Clean up temporary files
rm temp_private_key.pem
rm temp_public_key.pem
# Verify the key pair works together
echo "Verifying keys match..."
KEY_MATCH=$(openssl rsa -in rsa_private_key.pem -noout -modulus | openssl md5)
PUB_MATCH=$(openssl rsa -in rsa_public_key.pem -pubin -noout -modulus | openssl md5)
if [ "$KEY_MATCH" = "$PUB_MATCH" ]; then
echo "SUCCESS: Keys match properly: $KEY_MATCH"
else
echo "ERROR: Keys don't match!"
echo "Private key modulus: $KEY_MATCH"
echo "Public key modulus: $PUB_MATCH"
fi
echo "Keys generated at:"
echo "- rsa_private_key.pem"
echo "- rsa_public_key.pem"Ruby:
require "jwt"
require "openssl"
private_key = OpenSSL::PKey::RSA.new(File.read("rsa_private_key.pem"))
payload = {
iss: ENV["CLIENT_ID"],
sub: "[email protected]",
aud: "#{ENV['APP_DEFAULT_URL']}/oauth2/token",
exp: (Time.now + 60).to_i,
iat: Time.now.to_i,
scope: "users:read"
}
assertion = JWT.encode(payload, private_key, "RS256")
puts assertion7) Error Handling
The token endpoint returns 400 for validation failures. Possible error values:
| error | error_description | When it happens |
|---|---|---|
jwt_bearer_missing_assertion | JWT Bearer assertion is missing. | assertion param not provided. |
jwt_bearer_invalid | JWT Bearer token is invalid. | Generic failure. |
jwt_bearer_invalid_signature | JWT Bearer token has an invalid signature. | Signature can’t be verified with your uploaded public key. |
jwt_bearer_expired | JWT Bearer token has expired. | exp in the past. |
jwt_bearer_invalid_issuer | JWT Bearer token has an invalid issuer. | iss not equal to your OAuth App Client ID, or app not found. |
jwt_bearer_invalid_audience | JWT Bearer token has an invalid audience. | aud not equal to the token endpoint URL. |
jwt_bearer_invalid_user | JWT Bearer token subject does not match a valid user. | sub email not found/activated/not in the same space. |
8) Security & Best Practices
- Short TTL: Keep
expwithin ~60 seconds and request the token just-in-time. - Clock Skew: Ensure NTP is configured;
iatmust be in the past,expin the future. - Key Management: Protect your private key; rotate keys periodically.
- Least Privilege: Request only the minimal
scopeyou need. - No Refresh Token: This grant intentionally issues no refresh token. Re-assert as needed.
- Audience Exact Match:
audmust exactly equal the token endpoint URL. - Tenant Boundaries:
submust identify an activated user who is a member of the OAuth App’s space.
