JWT SSO Implementation
Developer-focused reference
This guide details how to implement JWT-based authentication for the Partner Fleet In-App Marketplace. Your application generates a signed JWT containing user and account context, and passes it to Partner Fleet via SSO. Partner Fleet uses the claims in the token to identify the user, personalize which listings they see (Audience Filtering), decide whether they can install (Permissions), and route them to the right destination on click.
Where the JWT fits in the setup
The JWT identifies the user and their account when the marketplace loads. Field names and values you include here must match the configuration in your Partner Fleet admin — specifically Settings > In-App Marketplace > Permissions and Audience Filtering. Coordinate exact strings with the team configuring those tabs.
Note about integration status:
Integration status (Active, Pending, Error, etc.) is not passed via the JWT. It is passed through the embed script's
integrationsarray when the iframe initializes. See the Embed Script Implementation guide for details. A future API route will provide an additional way to pass status — the embed script approach is the current canonical method.
Authentication flow
At a high level:
- Your end user navigates to the page in your application that hosts the embedded marketplace.
- Your frontend calls a backend endpoint to generate a fresh JWT for the current user.
- Your backend signs the JWT with the shared secret configured in Partner Fleet (Settings > In-App Marketplace > Developer Settings > JWT Identity Provider).
- Your frontend passes the JWT to the marketplace embed via the iframe initialization.
- Partner Fleet verifies the signature, decodes the claims, and renders the marketplace personalized for the user.
JWT Structure
The JWT payload requires specific fields for proper user and account identification:
{
// Required User Fields
"email": "[email protected]", // Unique identifier for the user
"ufn": "FirstName", // User's first name
"uln": "LastName", // User's last name
// Optional User Custom Fields
"fields": {
"user_type": "Admin" // Maps to your user-level permissions
},
// Required Account Information
"account": {
"external_id": "123456789", // Unique identifier for the account
"name": "Account Name", // Account display name
// Optional Account Custom Fields
"tier": "Enterprise", // Maps to account-level permissions
"region": "North America", // Used for audience filtering
"beta": "true", // Used for audience filtering
"can_see_hidden_listings": "true"
},
// Required JWT Expiration
"exp": 1234567890 // Unix timestamp (current time + 5 minutes)
}Where each field goes
- Top-level claims (
email,ufn,uln,exp): required on every JWT. fieldsobject: user-level custom values, used for user-level Permissions (for example,user_typefor the Role permission).accountobject: account-level values.external_idandnameare required. All other account-level custom values (tier,region,beta, etc.) go inside theaccountobject alongside them, not in thefieldsobject.
Field names and values must match your Partner Fleet admin configuration exactly. If you've created an audience filter field called Region with options North America, Europe, and Asia Pacific, the JWT must use one of those exact strings. Casing matters.
Technical Requirements
- Algorithm: HS256 (HMAC SHA-256). Other algorithms are not supported.
- Token Expiration: Set expiration to more than 5 minutes from generation time.
- Token Refresh: New token will be requested if within 5 minutes of expiration.
Important
Store your shared secret securely in your application's credentials system. Never expose the secret in client-side code.
Token refresh
The marketplace will request a new token when the current one is within 5 minutes of expiring. Implement your token-generation endpoint to be idempotent and fast — it's called multiple times per session.
Common pattern: cache the token for the user's session, regenerate when expiring soon, return immediately when valid.
Implementation Examples
Ruby
# Gemfile
# gem 'jwt'
class JwtAuthenticator
def self.decode(token, secret = Rails.application.credentials.jwt_shared_secret)
JWT.decode(token, secret, true, algorithm: 'HS256')[0]
rescue JWT::ExpiredSignature
nil # Handle expired token
rescue JWT::DecodeError
nil # Handle invalid token
end
def self.encode(payload, secret = Rails.application.credentials.jwt_shared_secret)
JWT.encode(payload, secret, 'HS256')
end
end
# Usage Example
def generate_marketplace_token
data = {
"email": current_user.email,
"ufn": current_user.first_name,
"uln": current_user.last_name,
"fields": {
"user_type": current_user.type
},
"account": {
"external_id": current_user.account.id,
"name": current_user.account.name,
"tier": current_user.account.tier,
"region": current_user.account.region
},
"exp": (Time.zone.now + 5.minutes).to_i
}
JwtAuthenticator.encode(data, ENV['PARTNER_FLEET_SECRET'])
endNode.js
// Package required: jsonwebtoken
// npm install jsonwebtoken
const jwt = require('jsonwebtoken');
class JwtAuthenticator {
static decode(token, secret) {
try {
return jwt.verify(token, secret);
} catch (error) {
if (error.name === 'TokenExpiredError') {
return null; // Handle expired token
}
return null; // Handle invalid token
}
}
static encode(payload, secret) {
return jwt.sign(payload, secret, { algorithm: 'HS256' });
}
}
// Usage Example
function generateMarketplaceToken(user, account) {
const data = {
email: user.email,
ufn: user.firstName,
uln: user.lastName,
fields: {
user_type: user.type
},
account: {
external_id: account.id,
name: account.name,
tier: account.tier,
region: account.region
},
exp: Math.floor(Date.now() / 1000) + 300 // Current time + 5 minutes
};
return JwtAuthenticator.encode(data, process.env.PARTNER_FLEET_SECRET);
}Python
# Package required: PyJWT
# pip install PyJWT
import os
import jwt
from time import time
from typing import Dict, Optional
class JwtAuthenticator:
@staticmethod
def decode(token: str, secret: str) -> Optional[Dict]:
try:
return jwt.decode(token, secret, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
return None # Handle expired token
except jwt.DecodeError:
return None # Handle invalid token
@staticmethod
def encode(payload: Dict, secret: str) -> str:
return jwt.encode(payload, secret, algorithm='HS256')
# Usage Example
def generate_marketplace_token(user, account):
data = {
"email": user.email,
"ufn": user.first_name,
"uln": user.last_name,
"fields": {
"user_type": user.type
},
"account": {
"external_id": str(account.id),
"name": account.name,
"tier": account.tier,
"region": account.region
},
"exp": int(time()) + 300 # Current time + 5 minutes
}
return JwtAuthenticator.encode(data, os.environ.get('PARTNER_FLEET_SECRET'))Best Practices
Security
- Store the shared secret securely.
- Never expose the secret in client-side code.
- Implement proper error handling for token validation.
Performance
- Only include necessary custom fields in the payload.
- Consider caching the token for repeated requests.
- Implement token refresh logic before expiration.
Error Handling
- Handle expired tokens gracefully.
- Provide clear error messages for invalid tokens.
- Implement retry logic for failed token generation.
Validation
Use the Test Tools tab in your Partner Fleet admin (Settings > In-App Marketplace > Test Tools) to validate your implementation without standing up a test embed. You can:
- Generate test tokens with arbitrary user/account properties to validate marketplace personalization.
- Decode tokens your backend generates to verify field names, values, and signatures.
See the Test Tools article for details.
Validation Steps
- Generate a test token with your implementation.
- Verify all required fields are present.
- Confirm the token can be decoded with your shared secret.
- Test token expiration handling.
- Verify custom fields are properly formatted.
Common Issues
Token rejected: signature invalid
Your application is signing with a different secret than the Partner Fleet secret. Verify the shared secret in Settings > In-App Marketplace > Developer Settings > JWT Identity Provider matches what your backend uses to sign.
End user sees no listings or wrong listings
Audience filter values in your JWT don't match the configured options. Use the Test Tools tab to decode a token and inspect the claim values. Field names and option values are case-sensitive.
Buttons don't appear correctly
Integration status data isn't being passed correctly. Status flows through the embed script's integrations array, not the JWT — see the Embed Script Implementation guide.
Restricted users don't see expected fallback
Permission field names or option labels in the JWT don't match the Permissions tab configuration. Casing matters: "Enterprise" and "enterprise" are different.
What's Next
- Embed Script Implementation — how to render the marketplace in your application and pass integration status.
- Buttons & Actions — configure how integration statuses map to buttons and destinations.
- Permissions — control who can install integrations.
- Audience Filtering — control which listings each audience sees.
- Test Tools — generate and decode JWT tokens for validation.
Updated 2 months ago

