---
updatedAt: 2026-04-16T11:32:22.000Z
agentTools:
  projectIndex: https://developer.productboard.com/llms.txt
---

# OAuth Server-to-Server (JWT)

Authenticate backend systems without user interaction. This flow enables server-to-server integrations with short-lived, signed tokens and optional user impersonation.

# OAuth Server-to-Server (JWT)

## 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 this flow 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)

1. **Register** an OAuth Application and **upload your public RSA key** (PEM).
2. Your server **builds and signs** a JWT (`RS256`) with required claims (`iss`, `sub`, `aud`, `exp`, `iat`; optional `scope`).
3. Your server **POSTs** the JWT to the token endpoint with grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`.
4. If valid, we return a **Bearer access token** (**300 s** TTL).
5. You call the API with `Authorization: Bearer <token>`.

### Key capabilities

* **Act-on-behalf-of a user:** Provide `sub` as 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:** `aud` must match the **token endpoint URL** to prevent token reuse against other services.
* **Tight time windows:** `iat` in the past; `exp` in 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 Server-to-Server 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 an OAuth Application

You can register and manage your OAuth Application at <https://app.productboard.com/oauth2/applications>. When creating a new application, select the **Server-to-Server** type and choose the workspace it will authenticate against. You need an active Productboard user account with admin access to the workspace.

***

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

* **OAuth Application (Client):** Your integration registered in Productboard. Identified by **Client ID** (`iss` in JWT). Stores your **public key** used to verify your JWT signature. Managed at `/oauth2/applications`.
* **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/token` endpoint. 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

1. Register an **OAuth Application** and upload your **public RSA key** (PEM).
2. Your backend creates a **JWT assertion** with required claims and signs it with **RS256**.
3. POST the assertion to the **Token Endpoint** using the grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`.
4. Receive a **Bearer access token** (valid **300 seconds**).
5. Call the API with `Authorization: Bearer <token>`.

## 3) Endpoints

### Token Endpoint

```text
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):**

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "users:read"
}
```

**Notes**

* `token_type` is always `Bearer`.
* `expires_in` is always `300` (seconds).
* `refresh_token` is **not** returned for this grant.

***

## 4) Certificate (Public Key) Upload

1. Generate an RSA key pair (min. 2048 bits).
2. Keep the private key secure on your server (never share it).
3. Upload the public key (PEM) to your OAuth Application settings.
4. 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 | `"alice@example.com"`                    |
| `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:**

```json
{
  "iss": "YOUR_CLIENT_ID",
  "sub": "user@example.com",
  "aud": "https://api.example.com/oauth2/token",
  "exp": 1692288000,
  "iat": 1692287940,
  "scope": "users:read"
}
```

## 6) End-to-End Example

```mermaid
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:**

```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:**

```ruby
require "jwt"
require "openssl"

private_key = OpenSSL::PKey::RSA.new(File.read("rsa_private_key.pem"))

payload = {
  iss: ENV["CLIENT_ID"],
  sub: "user@example.com",
  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 assertion
```

***

## 7) 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 `exp` within \~60 seconds and request the token just-in-time.
* **Clock Skew:** Ensure NTP is configured; `iat` must be in the past, `exp` in the future.
* **Key Management:** Protect your private key; rotate keys periodically.
* **Least Privilege:** Request only the minimal `scope` you need.
* **No Refresh Token:** This grant intentionally issues **no** refresh token. Re-assert as needed.
* **Audience Exact Match:** `aud` must exactly equal the token endpoint URL.
* **Tenant Boundaries:** `sub` must identify an **activated** user who is a **member** of the OAuth App’s space.