BowlingTracker.com SSO Integration Guide

This guide explains how to integrate your site or forum with BowlingTracker.com using JSON Web Tokens (JWT) signed with RSA-256. When a user clicks a link to BowlingTracker.com from your site, your server generates a short-lived signed JWT and appends it to the URL as a Token query-string parameter (or passes it in an Authorization: Bearer <token> HTTP header).

Prerequisites — contact BowlingTracker.com to receive:
  • Your assigned Issuer string (e.g. https://yoursite.com/forums)
  • Your RSA-2048 private key (PEM format, kept secret on your server)
  • Paid integrations can display your logo.

BowlingTracker.com stores only your public key for signature verification. We can generate a private key for you, if needed.

How It Works

  1. User clicks a BowlingTracker.com link on your site.
  2. Your server builds a JWT containing the user's identity claims, signs it with your RSA private key, and redirects the browser to:
    https://bowlingtracker.com/Home/Index?Token=<jwt>
  3. BowlingTracker.com validates the signature using your registered public key, reads the claims, and logs the user in automatically.

Required JWT Claims

ClaimDescriptionExample
issIssuer — your assigned identifier stringhttps://yoursite.com/forums
audAudience — must be exactly this valuehttps://bowlingtracker.com
nbfNot Before — recommend 15 minutes in the past to allow clock skewUnix timestamp
expExpiry — token must expire within 5–15 minutes of issueUnix timestamp
ExternalIdUnique, stable user ID on your system42
UsernameDisplay username on your systemJohnDoe

vBulletin 5.x Integration

vBulletin does not ship with a JWT library, so install one via Composer. The example below uses firebase/php-jwt, which is the most widely used PHP JWT library.

1. Install the library

Run this in the root of your vBulletin installation (where composer.json lives, or create one):

composer require firebase/php-jwt
2. Store your private key

Save your RSA private key PEM file outside your web root, e.g. /etc/ssl/private/bt_private.pem, and make it readable only by your web-server user. Never commit it to source control.

3. Create a vBulletin plugin or custom page

In vBulletin Admin → Plugin & Product Manager, add a new plugin hooked to global_start (or create a custom PHP page in /includes/). The snippet below can be dropped into either location.

<?php
// File: includes/bt_sso_redirect.php
// Hook: global_start  (or call directly from a custom navigation link)

require_once DIR . '/vendor/autoload.php';

use Firebase\JWT\JWT;

// ── Configuration ────────────────────────────────────────────────
define('BT_ISSUER',      'https://yoursite.com/forums');   // Your assigned issuer
define('BT_AUDIENCE',   'https://bowlingtracker.com');
define('BT_PRIVATE_KEY', file_get_contents('/etc/ssl/private/bt_private.pem'));
define('BT_REDIRECT',   'https://bowlingtracker.com/Home/Index');
// ─────────────────────────────────────────────────────────────────

// Only proceed if a vBulletin user is logged in.
if (!isset($vbulletin->userinfo['userid']) || $vbulletin->userinfo['userid'] == 0) {
    // Not logged in — send them to BowlingTracker without a token.
    header('Location: ' . BT_REDIRECT);
    exit;
}

$user       = $vbulletin->userinfo;
$now        = time();

$payload = [
    'iss'        => BT_ISSUER,
    'aud'        => BT_AUDIENCE,
    'nbf'        => $now - 900,          // 15 min clock-skew buffer
    'exp'        => $now + 300,          // 5 min expiry
    'ExternalId' => (string)$user['userid'],
    'Username'   => $user['username'],
];

$jwt = JWT::encode($payload, BT_PRIVATE_KEY, 'RS256');

header('Location: ' . BT_REDIRECT . '?Token=' . urlencode($jwt));
exit;
4. Add a navigation link

In vBulletin Admin → Navigation Manager, add a new link pointing to /includes/bt_sso_redirect.php (or whatever path you chose). Users who are already logged in to your forum will be silently authenticated on BowlingTracker.com.

vBulletin 4.x — the same PHP code works; just reference the global $vbulletin object the same way. Composer's autoload path may differ depending on where you ran composer install.

Simple Machines Forum (SMF 2.x) Integration

SMF uses a hook system. Install firebase/php-jwt via Composer, then register a hook to intercept navigation actions.

1. Install the library
composer require firebase/php-jwt
2. Create an SMF mod / hook file

Create Sources/BowlingTrackerSSO.php in your SMF installation:

<?php
// File: Sources/BowlingTrackerSSO.php

if (!defined('SMF')) die('No direct access...');

require_once(dirname __DIR__ . '/vendor/autoload.php');
use Firebase\JWT\JWT;

// ── Configuration ────────────────────────────────────────────────
define('BT_ISSUER',      'https://yoursite.com/forums');
define('BT_AUDIENCE',   'https://bowlingtracker.com');
define('BT_PRIVATE_KEY', file_get_contents('/etc/ssl/private/bt_private.pem'));
define('BT_REDIRECT',   'https://bowlingtracker.com/Home/Index');
// ─────────────────────────────────────────────────────────────────

/**
 * Called from a custom SMF action, e.g. ?action=bt_sso
 * Register in Modifications.php:
 *   $modSettings['integrate_actions'] .= ',bt_sso=BowlingTrackerSSO_Action';
 */
function BowlingTrackerSSO_Action()
{
    global $user_info;

    if ($user_info['is_guest']) {
        // Guest — redirect without token.
        redirectexit(BT_REDIRECT);
    }

    $now = time();

    $payload = [
        'iss'        => BT_ISSUER,
        'aud'        => BT_AUDIENCE,
        'nbf'        => $now - 900,
        'exp'        => $now + 300,
        'ExternalId' => (string)$user_info['id'],
        'Username'   => $user_info['name'],
    ];

    $jwt = JWT::encode($payload, BT_PRIVATE_KEY, 'RS256');

    redirectexit(BT_REDIRECT . '?Token=' . urlencode($jwt));
}
3. Register the action hook

In your Settings.php or a mod's install.php, add:

add_integration_function('integrate_actions', 'BowlingTrackerSSO_Action', false, '$sourcedir/BowlingTrackerSSO.php');
4. Add a menu item

Link your users to https://yoursite.com/forums/index.php?action=bt_sso. SMF will invoke the hook, generate the token, and redirect seamlessly.

SMF 2.1 uses the same hook API. For SMF 1.x, use $modSettings['integrate_actions'] string-append style instead of add_integration_function().

Generic PHP Integration

Works for any PHP 7.4+ application (Laravel, Symfony, CodeIgniter, plain PHP, etc.).

<?php
require 'vendor/autoload.php';
use Firebase\JWT\JWT;

$privateKey = file_get_contents('/etc/ssl/private/bt_private.pem');
$now        = time();

$payload = [
    'iss'        => 'https://yoursite.com/forums',
    'aud'        => 'https://bowlingtracker.com',
    'nbf'        => $now - 900,
    'exp'        => $now + 300,
    'ExternalId' => (string)$_SESSION['user_id'],
    'Username'   => $_SESSION['username'],
];

$jwt = JWT::encode($payload, $privateKey, 'RS256');
header('Location: https://bowlingtracker.com/Home/Index?Token=' . urlencode($jwt));
exit;

Node.js / Express Integration

// npm install jsonwebtoken
const jwt        = require('jsonwebtoken');
const fs         = require('fs');
const privateKey = fs.readFileSync('/etc/ssl/private/bt_private.pem');

// Express route: GET /bt-sso
app.get('/bt-sso', (req, res) => {
    if (!req.user) return res.redirect('https://bowlingtracker.com/Home/Index');

    const token = jwt.sign(
        {
            ExternalId: String(req.user.id),
            Username:   req.user.username,
        },
        privateKey,
        {
            algorithm : 'RS256',
            issuer    : 'https://yoursite.com/forums',
            audience  : 'https://bowlingtracker.com',
            notBefore : '-15m',
            expiresIn : '5m',
        }
    );
    res.redirect(`https://bowlingtracker.com/Home/Index?Token=${encodeURIComponent(token)}`);
});

Python / Django / Flask Integration

# pip install PyJWT cryptography
import jwt, time
from cryptography.hazmat.primitives import serialization

with open('/etc/ssl/private/bt_private.pem', 'rb') as f:
    private_key = serialization.load_pem_private_key(f.read(), password=None)

now = int(time.time())
payload = {
    'iss':        'https://yoursite.com/forums',
    'aud':        'https://bowlingtracker.com',
    'nbf':        now - 900,
    'exp':        now + 300,
    'ExternalId': str(request.user.id),
    'Username':   request.user.username,
}

token = jwt.encode(payload, private_key, algorithm='RS256')

# Flask example
from flask import redirect
return redirect(f'https://bowlingtracker.com/Home/Index?Token={token}')

ASP.NET Core Integration

Install Microsoft.IdentityModel.Tokens and System.IdentityModel.Tokens.Jwt via NuGet.

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;

// In your controller action:
public IActionResult BowlingTrackerSSO()
{
    if (!User.Identity?.IsAuthenticated ?? true)
        return Redirect("https://bowlingtracker.com/Home/Index");

    var pem        = System.IO.File.ReadAllText("/etc/ssl/private/bt_private.pem");
    var rsa        = RSA.Create();
    rsa.ImportFromPem(pem);
    var creds      = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);

    var claims = new[]
    {
        new Claim("ExternalId", User.FindFirstValue(ClaimTypes.NameIdentifier)!),
        new Claim("Username",   User.Identity!.Name!),
    };

    var token = new JwtSecurityToken(
        issuer:             "https://yoursite.com/forums",
        audience:           "https://bowlingtracker.com",
        claims:             claims,
        notBefore:          DateTime.UtcNow.AddMinutes(-15),
        expires:            DateTime.UtcNow.AddMinutes(5),
        signingCredentials: creds);

    var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
    return Redirect($"https://bowlingtracker.com/Home/Index?Token={Uri.EscapeDataString(tokenString)}");
}

WordPress Integration

Install firebase/php-jwt via Composer, then add a shortcode or a custom page template.

<?php
// Add to your theme's functions.php or a custom plugin.
require_once get_template_directory() . '/vendor/autoload.php';
use Firebase\JWT\JWT;

add_action('template_redirect', function () {
    if (!is_page('bowling-tracker-sso')) return;  // Only on a dedicated page slug.

    if (!is_user_logged_in()) {
        wp_redirect('https://bowlingtracker.com/Home/Index');
        exit;
    }

    $user       = wp_get_current_user();
    $now        = time();
    $privateKey = file_get_contents('/etc/ssl/private/bt_private.pem');

    $payload = [
        'iss'        => 'https://yoursite.com',
        'aud'        => 'https://bowlingtracker.com',
        'nbf'        => $now - 900,
        'exp'        => $now + 300,
        'ExternalId' => (string)$user->ID,
        'Username'   => $user->user_login,
    ];

    $jwt = JWT::encode($payload, $privateKey, 'RS256');
    wp_redirect('https://bowlingtracker.com/Home/Index?Token=' . urlencode($jwt));
    exit;
});

XenForo 2.x Integration

Create a simple XenForo add-on with a controller action.

<?php
// File: src/addons/YourVendor/BtSso/Pub/Controller/Sso.php
namespace YourVendor\BtSso\Pub\Controller;

use XF\Mvc\ParameterBag;
use XF\Pub\Controller\AbstractController;
use Firebase\JWT\JWT;

class Sso extends AbstractController
{
    public function actionIndex(ParameterBag $params)
    {
        $visitor = \XF::visitor();

        if (!$visitor->user_id) {
            return $this->redirect('https://bowlingtracker.com/Home/Index');
        }

        $privateKey = file_get_contents('/etc/ssl/private/bt_private.pem');
        $now        = time();

        $payload = [
            'iss'        => 'https://yoursite.com/forums',
            'aud'        => 'https://bowlingtracker.com',
            'nbf'        => $now - 900,
            'exp'        => $now + 300,
            'ExternalId' => (string)$visitor->user_id,
            'Username'   => $visitor->username,
        ];

        $jwt = JWT::encode($payload, $privateKey, 'RS256');
        return $this->redirect('https://bowlingtracker.com/Home/Index?Token=' . urlencode($jwt));
    }
}

Register the route bt-sso pointing to YourVendor\BtSso:Sso in your add-on's routes.php.

Generating Your RSA Key Pair

If you need to generate a new key pair, run the following OpenSSL commands on your server:

# 1. Generate the private key (keep this secret — never share it)
openssl genpkey -algorithm RSA -out bt_private.pem -pkeyopt rsa_keygen_bits:2048

# 2. Derive the public key (send this to BowlingTracker.com)
openssl rsa -pubout -in bt_private.pem -out bt_public.pem

Email bt_public.pem to us along with your desired Issuer string. We will register your public key and confirm your Issuer before tokens will be accepted.

Testing Your Integration

  1. Generate a token manually using the OpenSSL or language snippet above.
  2. Paste the JWT at jwt.io and verify the header shows "alg": "RS256", and the payload contains your expected claims.
  3. Append it to the URL and open it in a browser:
    https://bowlingtracker.com/Home/Index?Token=<your_jwt>
  4. You should be automatically logged in under your username.

Security Checklist

  • ✅ Private key stored outside the web root with restricted file permissions (chmod 600).
  • ✅ Token exp set to 5 minutes or less.
  • ✅ HTTPS enforced on your site so tokens are never transmitted in plaintext.
  • ✅ Never log or cache generated tokens.
  • ✅ Rotate your key pair immediately if it is ever compromised — contact us to update your registered public key.

Contact & Support

For registration, key exchange, or technical questions, contact us via the BowlingTracker.com forum or email sso@bowlingtracker.com.