Token Endpoint
Create a backend endpoint that generates signed JWT tokens for authentication
Provide Authentication Tokens
Round Two supports three methods for providing JWT tokens to the embed script. Choose the method that best fits your application architecture.
Quick Start
Before implementing, ask yourself:
-
What authentication system do you use?
- Server-side sessions/cookies → Use Option A (Server Endpoint)
- Client-side auth (Supabase, Auth0, Firebase) → Use Option B (Token Provider Function)
- Testing or advanced scenarios → Use Option C (Pre-Provided Token)
-
What's your deployment type?
- Traditional server/SSR → Option A
- Single Page Application (SPA) → Option B
- Need manual control → Option C
-
Do you have cookie/CORS issues?
- Yes → Option B (avoids cookie parsing and CORS complexity)
- No → Option A (simpler, standard approach)
Critical Requirements:
- JWT must include
iss(issuer) claim matching your Round Two configuration exactly (format:"[workspace]/[board]"in lowercase) - JWT must include
aud(audience) claim matching your Board ID - Token must be signed with RS256 algorithm using your RSA private key
- All required claims must be present:
sub,name,email,jti,iat,exp
Need help choosing? See the decision guide in each option below, or use the AI prompt in your Round Two embed settings for personalized guidance.
Option A: Server Endpoint (Recommended for Server-Side Auth)
Create a backend endpoint that returns a signed JWT token representing the currently logged-in user.
Endpoint Requirements
URL: /api/roundtwo/token (or your custom path - must match the script configuration)
Method: POST
Authentication: Must verify the user is logged into your application
Response: JSON with a token field containing a signed JWT
When to use: Best for traditional server-side applications with cookie-based or session-based authentication.
Example Implementation
const express = require('express');
const jwt = require('jsonwebtoken');
const fs = require('fs');
const app = express();
// Load private key (keep this secure!)
const privateKey = fs.readFileSync('private_key.pem', 'utf8');
// Middleware to verify user is authenticated
function authenticateUser(req, res, next) {
// Your authentication logic here
// Verify session, check JWT, etc.
const user = req.user; // Your authenticated user object
if (!user) {
return res.status(401).json({ error: 'Unauthorized' });
}
req.user = user;
next();
}
app.post('/api/roundtwo/token', authenticateUser, async (req, res) => {
try {
const user = req.user;
// Get workspace and board names (these form the issuer)
// Format: "[workspace]/[board]" in lowercase (e.g., "myworkspace/myboard")
// This must match exactly what's configured in Round Two embed settings
const issuer = 'yourworkspace/yourboard'; // Must be lowercase!
// Create token payload
const payload = {
iss: issuer,
aud: 'roundtwo',
sub: user.id, // Required: Your application's unique user ID
name: user.name || user.displayName, // Required: Display name (non-empty string)
email: user.email, // Required: Email address (valid format)
avatar_url: user.avatarUrl, // Optional: Avatar URL
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
iat: Math.floor(Date.now() / 1000),
};
// Sign token with private key
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
res.json({ token });
} catch (error) {
console.error('Token generation error:', error);
res.status(500).json({ error: 'Failed to generate token' });
}
});
from flask import Flask, request, jsonify
from functools import wraps
import jwt
import time
app = Flask(__name__)
# Load private key
with open('private_key.pem', 'r') as f:
private_key = f.read()
# Authentication decorator
def require_auth(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Your authentication logic here
user = get_current_user() # Your authentication function
if not user:
return jsonify({'error': 'Unauthorized'}), 401
return f(user, *args, **kwargs)
return decorated_function
@app.route('/api/roundtwo/token', methods=['POST'])
@require_auth
def generate_token(user):
try:
issuer = 'yourworkspace/yourboard' # Format: "[workspace]/[board]" in lowercase
now = int(time.time())
payload = {
'iss': issuer,
'aud': 'roundtwo',
'sub': user.id, # Required: unique user ID
'name': user.name, # Required: display name (non-empty string)
'email': user.email, # Required: valid email address format
'avatar_url': user.avatar_url, # Optional: avatar URL
'exp': now + 300, # 5 minutes
'iat': now,
}
token = jwt.encode(payload, private_key, algorithm='RS256')
return jsonify({'token': token})
except Exception as e:
return jsonify({'error': 'Failed to generate token'}), 500
Option B: Token Provider Function (Recommended for Client-Side Auth)
If you're using client-side authentication (Supabase, Auth0, Firebase, etc.) or cookies aren't reliable, provide a global function that the embed script can call.
Implementation
Define a global function before the Round Two embed script loads:
// Define this BEFORE the Round Two embed script loads
window.getRoundTwoToken = async () => {
// Use your existing auth system
const session = await supabase.auth.getSession();
if (!session.data.session) {
throw new Error('User not authenticated');
}
// Call your token endpoint with credentials
const response = await fetch('/api/roundtwo/token', {
method: 'POST',
credentials: 'include', // Include cookies
headers: {
'Authorization': `Bearer ${session.data.session.access_token}`,
},
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status}`);
}
const data = await response.json();
return data.token;
};
React/Next.js Example
// In your app initialization (e.g., _app.tsx or layout.tsx)
import { useEffect } from 'react';
import { supabase } from './lib/supabase';
export default function App() {
useEffect(() => {
// Define token provider function
window.getRoundTwoToken = async () => {
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Not authenticated');
const res = await fetch('/api/roundtwo/token', {
method: 'POST',
credentials: 'include',
headers: {
'Authorization': `Bearer ${session.access_token}`,
},
});
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const { token } = await res.json();
return token;
};
}, []);
return <YourApp />;
}
When to Use
- SPAs (Single Page Applications) with client-side authentication
- Cross-origin scenarios where cookies aren't reliable
- SameSite cookie restrictions preventing automatic cookie sending
- Modern auth providers like Supabase, Auth0, Firebase Auth
Benefits
- No need to parse cookies in proxy/Vite config
- Works seamlessly with client-side auth systems
- Avoids CORS credential issues
- More flexible for complex authentication flows
Option C: Pre-Provided Token (For Manual Token Management)
For advanced use cases where you manage tokens manually or need full control over the token lifecycle.
Implementation
Set a global variable before the Round Two embed script loads:
// Set this BEFORE the Round Two embed script loads
window.__ROUNDTWO_TOKEN__ = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...";
When to Use
- Advanced scenarios with custom token management
- Testing and development with pre-generated tokens
- Full control over token lifecycle and refresh
- Integration with existing token systems
Security Note
⚠️ Warning: Pre-provided tokens are static and don't refresh automatically. Ensure tokens are refreshed before expiration if using this method.
Token Structure
The JWT token must include specific claims:
{
"iss": "yourworkspace/yourboard",
"aud": "roundtwo",
"exp": 1234567890,
"iat": 1234567890,
"sub": "external-user-id",
"name": "User Name",
"email": "user@example.com",
"avatar_url": "https://example.com/avatar.jpg"
}
Note: name and email are required claims. avatar_url is optional.
Required Claims
iss(issuer): Automatically set to[Workspace Name]/[Board Title]in Round Two. Must match exactly.aud(audience): Must be exactly"roundtwo"(case-sensitive)exp(expiration): Token expiration timestamp (Unix epoch). Recommended: 5 minutes fromiatiat(issued at): Token creation timestamp (Unix epoch)sub(subject): Your application's unique user ID. This identifies the user in Round Twoname: User's display name (required) - Must be a non-empty string. Shown in feedback submissions and comments.email: User's email address (required) - Must be a valid email address format. Used for identity matching if user later signs up for Round Two.
Optional Claims
avatar_url: URL to user's avatar image (optional) - Displayed in feedback submissions and comments. Should be a publicly accessible HTTPS URL.
Signing with RSA (RS256)
Round Two only supports RSA public key (RS256) signing for security.
Generate Key Pair
For detailed instructions on generating and managing RSA keys, see our comprehensive guide: RSA Keys.
Quick reference:
# Generate private key (keep this secure!)
openssl genrsa -out private_key.pem 2048
# Generate public key
openssl rsa -in private_key.pem -pubout -out public_key.pem
Sign Tokens
const jwt = require('jsonwebtoken');
const fs = require('fs');
// Load private key
const privateKey = fs.readFileSync('private_key.pem', 'utf8');
const token = jwt.sign(
{
iss: 'yourworkspace/yourboard', // Format: "[workspace]/[board]" in lowercase
aud: 'roundtwo',
sub: user.id,
name: user.name,
email: user.email,
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
iat: Math.floor(Date.now() / 1000),
},
privateKey,
{ algorithm: 'RS256' }
);
Security Best Practices
-
Protect Your Private Key:
- Never commit private keys to version control
- Store in environment variables or secret management service
- Use different keys for production and staging
- Rotate keys periodically
-
Short Token Expiration:
- Keep tokens valid for 5 minutes or less
- Reduces risk if token is intercepted
- Forces regular re-authentication
-
Secure Token Endpoint:
- Require user authentication
- Implement rate limiting
- Log token generation for audit
- Monitor for suspicious activity
-
Validate User Session:
- Verify user is logged in before generating token
- Check user permissions if needed
- Ensure user data is current
Testing Your Implementation
Testing Option A (Server Endpoint)
Test your token endpoint:
# Test with authenticated request
curl -X POST https://your-app.com/api/roundtwo/token \
-H "Authorization: Bearer YOUR_SESSION_TOKEN" \
-H "Content-Type: application/json"
# Expected response:
# {
# "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
# }
Testing Option B (Token Provider Function)
Test in browser console:
// Test the function
window.getRoundTwoToken()
.then(token => console.log('Token:', token))
.catch(error => console.error('Error:', error));
Testing Option C (Pre-Provided Token)
Verify the token is set:
console.log('Token available:', !!window.__ROUNDTWO_TOKEN__);
Verify Token Structure
For all options, verify the token structure:
- Decode the JWT (without verification) to check claims
- Ensure all required claims are present
- Verify expiration is set correctly
- Check that issuer matches your Round Two configuration
Common Issues
- 404 on token endpoint → Endpoint not implemented or wrong path (Option A only)
- 401 "Invalid issuer" → Issuer in token doesn't match your Round Two configuration
- 401 "Invalid audience" → Audience in token doesn't match the board ID
- "Token provider function failed" → Check that
window.getRoundTwoTokenreturns a valid token string (Option B) - CORS errors → Ensure your token endpoint allows requests from your domain (Option A)
Note: Floating buttons only appear for authenticated users. If a user isn't authenticated (no valid JWT token), the floating button won't be displayed. Custom triggers will still work but will prompt for authentication when clicked.