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:

  1. 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)
  2. What's your deployment type?

    • Traditional server/SSR → Option A
    • Single Page Application (SPA) → Option B
    • Need manual control → Option C
  3. 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.

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

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 from iat
  • iat (issued at): Token creation timestamp (Unix epoch)
  • sub (subject): Your application's unique user ID. This identifies the user in Round Two
  • name: 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

  1. 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
  2. Short Token Expiration:

    • Keep tokens valid for 5 minutes or less
    • Reduces risk if token is intercepted
    • Forces regular re-authentication
  3. Secure Token Endpoint:

    • Require user authentication
    • Implement rate limiting
    • Log token generation for audit
    • Monitor for suspicious activity
  4. 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.getRoundTwoToken returns 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.

Next Steps

  • User Data - Set up username and email in tokens
  • Testing - Verify everything works correctly