This guide explains how to integrate Single Sign-On (SSO) between your main website and the Central Tickets system, allowing users to seamlessly authenticate from the logout landing page.
When a user clicks "Sign In with [Your Company]" on the logout landing page:
https://yourwebsite.com/ticket-ssoThe logout page passes these URL parameters to your SSO endpoint:
redirect_url: Where to send the user after authentication (e.g., https://ticket.yourcompany.com/yourcompany/dashboard)callback_api: The API endpoint to call for authentication (e.g., https://tickets.flare99.com/api/auth/redirect)tenant: Your tenant slug (e.g., yourcompany) — must be passed as tenant in the POST body.X-API-Key / X-API-Secret: Required headers for the redirect endpoint.Create a file at https://yourwebsite.com/ticket-sso.php:
<?php
// Check if user is logged in
session_start();
if (!isset($_SESSION['user_id'])) {
// Redirect to login with return URL
$returnUrl = urlencode($_SERVER['REQUEST_URI']);
header("Location: /login?return=" . $returnUrl);
exit;
}
// Get user data from your session/database
$user = getUserById($_SESSION['user_id']); // Your user retrieval function
// Get parameters
$redirectUrl = $_GET['redirect_url'] ?? '';
$callbackApi = $_GET['callback_api'] ?? '';
$tenantSlug = $_GET['tenant'] ?? '';
// Validate parameters
if (empty($redirectUrl) || empty($callbackApi)) {
die('Invalid SSO request');
}
// Call ticket system API
$userData = [
'email' => $user['email'],
'name' => $user['name'],
'redirect_url' => $redirectUrl
];
// Include API credentials when calling the redirect endpoint
$headers = [
'X-API-Key' => getenv('TICKET_SYSTEM_API_KEY'),
'X-API-Secret' => getenv('TICKET_SYSTEM_API_SECRET'),
];
$ch = curl_init($callbackApi);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json'
],
CURLOPT_POSTFIELDS => json_encode($userData),
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HEADER => true
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Extract Location header from response
if ($httpCode === 302) {
preg_match('/Location: (.+)/', $response, $matches);
if (isset($matches[1])) {
$redirectTo = trim($matches[1]);
curl_close($ch);
header("Location: " . $redirectTo);
exit;
}
}
curl_close($ch);
die('Authentication failed');
?>
Route (routes/web.php):
Route::get('/ticket-sso', [SSOController::class, 'handleTicketSSO'])
->middleware('auth')
->name('ticket.sso');
Controller (app/Http/Controllers/SSOController.php):
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Auth;
class SSOController extends Controller
{
public function handleTicketSSO(Request $request)
{
// Validate user is logged in
if (!Auth::check()) {
return redirect('/login?return=' . urlencode($request->fullUrl()));
}
// Get parameters
$redirectUrl = $request->query('redirect_url');
$callbackApi = $request->query('callback_api');
$tenantSlug = $request->query('tenant');
// Validate parameters
if (!$redirectUrl || !$callbackApi || !$tenantSlug) {
abort(400, 'Invalid SSO request parameters');
}
// Get authenticated user
$user = Auth::user();
// Call ticket system API
try {
$response = Http::withHeaders([
'Accept' => 'application/json',
'Content-Type' => 'application/json',
])->post($callbackApi, [
'email' => $user->email,
'name' => $user->name,
'redirect_url' => $redirectUrl,
]);
// Check if response is a redirect
if ($response->redirect()) {
return redirect($response->header('Location'));
}
// If JSON response with redirect_url
$data = $response->json();
if (isset($data['redirect_url'])) {
return redirect($data['redirect_url']);
}
abort(500, 'Invalid API response');
} catch (\Exception $e) {
\Log::error('Ticket SSO failed', [
'error' => $e->getMessage(),
'user_id' => $user->id,
]);
return view('ticket-sso-error', [
'message' => 'Unable to authenticate with ticket system. Please try again.'
]);
}
}
}
Create a page at https://yourwebsite.com/ticket-sso.html:
<!DOCTYPE html>
<html>
<head>
<title>Signing you in...</title>
<style>
body {
font-family: system-ui;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 2rem;
border-radius: 1rem;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="spinner"></div>
<h1>Signing you in...</h1>
<p id="status">Authenticating with ticket system...</p>
</div>
<script>
// Get URL parameters
const urlParams = new URLSearchParams(window.location.search);
const redirectUrl = urlParams.get('redirect_url');
const callbackApi = urlParams.get('callback_api');
const tenantSlug = urlParams.get('tenant');
// Get user data from your authentication system
// This example assumes you have a function that returns logged-in user data
async function getUserData() {
// Replace with your actual user data retrieval
const response = await fetch('/api/user', {
credentials: 'include'
});
if (!response.ok) return null;
return await response.json();
}
async function authenticate() {
try {
// Get current user
const user = await getUserData();
if (!user) {
// Not logged in - redirect to login
window.location.href = '/login?return=' + encodeURIComponent(window.location.href);
return;
}
// Call ticket system API
const response = await fetch(callbackApi, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
email: user.email,
name: user.name,
redirect_url: redirectUrl
})
});
if (response.redirected) {
window.location.href = response.url;
} else {
const data = await response.json();
if (data.redirect_url) {
window.location.href = data.redirect_url;
} else {
throw new Error('No redirect URL received');
}
}
} catch (error) {
console.error('SSO Error:', error);
document.querySelector('.container').innerHTML = `
<h1 style="color: #e53e3e;">Authentication Failed</h1>
<p>Unable to sign you in. Please try again.</p>
<button onclick="history.back()" style="margin-top: 1rem; padding: 0.75rem 1.5rem; background: #667eea; color: white; border: none; border-radius: 0.5rem; cursor: pointer;">
Go Back
</button>
`;
}
}
// Start authentication
authenticate();
</script>
</body>
</html>
When your system creates a ticket on behalf of a user (server-to-server via the API), you can redirect that user directly to the newly created ticket — rather than their dashboard — by including a redirect_url in the SSO payload.
POST /api/v2/ticketsticket_url, sso_redirect.callback_url, and a jwt_payload templateredirect_url pointing to the ticket){callback_url}?token={jwt}curl -X POST https://tickets.flare99.com/api/v2/tickets \
-H "X-API-Key: tk_..." \
-H "X-API-Secret: ..." \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"name": "John Doe",
"external_user_id": "12345",
"title": "Login Issue",
"description": "Cannot login to the system",
"priority": "high"
}'
Response (201):
{
"success": true,
"data": {
"ticket": { "id": 42, "title": "Login Issue", "status": "open", ... },
"ticket_url": "https://tickets.yourcompany.com/tickets/42",
"user": {
"id": 7,
"name": "John Doe",
"email": "user@example.com",
"external_user_id": "12345"
},
"sso_redirect": {
"callback_url": "https://tickets.yourcompany.com/auth/callback",
"instructions": "Generate a JWT with the payload below...",
"jwt_payload": {
"email": "user@example.com",
"name": "John Doe",
"role": "user",
"redirect_url": "/tickets/42",
"iat": "(current unix timestamp)",
"exp": "(current unix timestamp + 120)",
"jti": "(unique id)"
}
}
}
}
Generate a JWT using the payload template from the API response, sign it with your jwt_secret, and redirect the user's browser to the callback URL.
PHP (Laravel) Example:
// After creating the ticket via API...
$apiResponse = $response->json();
$ssoRedirect = $apiResponse['data']['sso_redirect'];
// Build the JWT payload
$header = json_encode(['alg' => 'HS256', 'typ' => 'JWT']);
$payload = json_encode([
'email' => $ssoRedirect['jwt_payload']['email'],
'name' => $ssoRedirect['jwt_payload']['name'],
'role' => 'user',
'redirect_url' => $ssoRedirect['jwt_payload']['redirect_url'], // e.g. "/tickets/42"
'iat' => time(),
'exp' => time() + 120,
'jti' => uniqid('sso_', true),
]);
// Base64url encode
$headerB64 = rtrim(strtr(base64_encode($header), '+/', '-_'), '=');
$payloadB64 = rtrim(strtr(base64_encode($payload), '+/', '-_'), '=');
$signature = rtrim(strtr(base64_encode(
hash_hmac('sha256', "$headerB64.$payloadB64", $jwtSecret, true)
), '+/', '-_'), '=');
$token = "$headerB64.$payloadB64.$signature";
// Redirect the user's browser
return redirect("{$ssoRedirect['callback_url']}?token={$token}");
/api/auth/redirectIf you prefer the encrypted token approach, POST to /api/auth/redirect with the ticket path as redirect_url:
$ticketPath = $apiResponse['data']['sso_redirect']['jwt_payload']['redirect_url']; // "/tickets/42"
$response = Http::withHeaders([
'X-API-Key' => env('TICKET_API_KEY'),
'X-API-Secret' => env('TICKET_API_SECRET'),
])->post('https://tickets.flare99.com/api/auth/redirect', [
'email' => $user->email,
'name' => $user->name,
'tenant' => 'your-tenant-slug',
'redirect_url' => $ticketPath, // The ticket path
]);
// Follow the redirect
return redirect($response->header('Location'));
redirect_url Formats| Format | Example | Works? |
|---|---|---|
| Relative path | /tickets/42 |
✅ Yes (recommended) |
| Full URL (same host) | https://tickets.yourcompany.com/tickets/42 |
✅ Yes |
| External URL (allowlisted host) | https://yourcompany.com/some-page |
✅ Yes |
| External URL (unknown host) | https://evil.com/phish |
❌ Blocked |
Test with logged-in user:
Test with logged-out user:
Test SSO to specific ticket:
redirect_url set to /tickets/{id}https://yourwebsite.com/ticket-sso)/api/auth/redirect (with tenant in post body) is successfulredirect_url is included in the JWT payload (not just the API request body)/tickets/42 instead of a full URLredirect_url is validated: only internal paths and allowlisted external hosts are acceptedFor additional help, refer to:
TENANT_INTEGRATION_GUIDE.md - Complete integration documentation/integration route - Integration hub with downloadable examplestenant-examples/ directory