Version: 1.0
Audience: Internal Development Team
Last Updated: October 27, 2025
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β External Company Website β
β (e.g., asknchat.com) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. User Authentication System β
β 2. JavaScript Integration Code β
β 3. contactSupport() Function β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ
β POST: /api/auth/redirect/{tenant}
β Payload: {email, name, redirect_url}
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Ticket System (tickets.flare99.com) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. AuthRedirectController β
β - Validates tenant β
β - Generates encrypted token (2-min TTL) β
β - Stores JTI in Redis (replay prevention) β
β 2. Returns 302 Redirect β
β Location: https://ticket.{tenant}.com/auth/callback?token=xxxβ
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ
β 302 Redirect with encrypted token
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tenant Subdomain (ticket.asknchat.com) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. AuthCallbackController β
β - Decrypts token β
β - Validates expiry & JTI β
β - Creates/finds user β
β - Establishes session β
β 2. Redirects to dashboard β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Location: app/Http/Controllers/AuthRedirectController.php
Responsibilities:
Location: app/Http/Controllers/AuthCallbackController.php
Responsibilities:
Location: app/Http/Middleware/IdentifyTenantByDomain.php
Responsibilities:
Location: app/Http/Middleware/HandleCors.php
Responsibilities:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 1: User Clicks "Submit Ticket" on External Site β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β JavaScript: contactSupport() β
β - Checks window.authUser exists β
β - Creates POST form with email/name β
β - Submits to tickets.flare99.com/api/auth/redirect/{tenant} β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 2: AuthRedirectController Processing β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββ΄βββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββ ββββββββββββββββ
β Validate β β Generate β
β Tenant ββββββββββββββββββββΆβ Token β
β Active β β (AES-256) β
ββββββββββββββ ββββββββ¬ββββββββ
β
βΌ
βββββββββββββββββββ
β Store JTI β
β in Redis β
β (5 min TTL) β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Return 302 Redirect β
β Location: https://ticket.asknchat.com/auth/callback?token=xxx β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 3: Browser Follows Redirect β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 4: AuthCallbackController Processing β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββ΄βββββββββββββββββ¬ββββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββ ββββββββββββ ββββββββββββ
β Decrypt β β Check β β Verify β
β Token ββββββββββββββββββββΆβ Expiry ββββββΆβ JTI β
ββββββββββββββ ββββββββββββ ββββββ¬ββββββ
β
βΌ
βββββββββββββββββ
β Find/Create β
β User β
βββββββββ¬ββββββββ
β
βΌ
βββββββββββββββββ
β Establish β
β Session β
βββββββββ¬ββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 5: Redirect to Final Destination β
β Location: https://ticket.asknchat.com/dashboard β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Encrypted Payload:
[
'tenant_id' => 123,
'email' => 'user@example.com',
'name' => 'John Doe',
'iat' => 1698360000, // Issued at timestamp
'exp' => 1698360120, // Expiry timestamp (2 min)
'jti' => 'uuid-v4-string', // Unique token ID
'redirect_url' => 'https://ticket.asknchat.com/dashboard'
]
Encryption:
APP_KEY from .envCrypt::encryptString()CREATE TABLE tenants (
id BIGINT UNSIGNED PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
domain VARCHAR(255) UNIQUE,
api_token VARCHAR(255),
api_enabled BOOLEAN DEFAULT TRUE,
status ENUM('active', 'suspended') DEFAULT 'active',
created_at TIMESTAMP,
updated_at TIMESTAMP,
INDEX idx_slug (slug),
INDEX idx_domain (domain),
INDEX idx_status (status)
);
CREATE TABLE users (
id BIGINT UNSIGNED PRIMARY KEY,
tenant_id BIGINT UNSIGNED,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
password VARCHAR(255),
role ENUM('master', 'admin', 'agent', 'user') DEFAULT 'user',
status ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
UNIQUE KEY unique_email_per_tenant (email, tenant_id),
INDEX idx_tenant (tenant_id),
INDEX idx_email (email),
INDEX idx_role (role)
);
CREATE TABLE tenant_domains (
id BIGINT UNSIGNED PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
domain VARCHAR(255) NOT NULL UNIQUE,
is_primary BOOLEAN DEFAULT FALSE,
is_verified BOOLEAN DEFAULT FALSE,
verification_token VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
INDEX idx_tenant (tenant_id),
INDEX idx_domain (domain)
);
Controller: AuthRedirectController@redirectToTickets
Middleware:
throttle:auth_redirect (60 requests/hour per IP)VerifyCsrfToken middleware)Request Validation:
[
'email' => 'required|email|max:255',
'name' => 'required|string|max:255',
'redirect_url' => 'nullable|url|max:500'
]
Response:
302 Redirect to tenant subdomain with tokenImplementation:
public function redirectToTickets(Request $request, string $tenantSlug)
{
// Validate request
$validated = $request->validate([
'email' => 'required|email|max:255',
'name' => 'required|string|max:255',
'redirect_url' => 'nullable|url|max:500'
]);
// Find tenant
$tenant = Tenant::where('slug', $tenantSlug)
->where('status', 'active')
->firstOrFail();
// Generate token
$token = $this->authRedirectService->generateToken(
$tenant,
$validated['email'],
$validated['name'],
$validated['redirect_url'] ?? null
);
// Build redirect URL
$domain = $tenant->domain ?? "ticket.{$tenant->slug}.com";
$callbackUrl = "https://{$domain}/auth/callback?token={$token}";
return redirect()->away($callbackUrl);
}
Controller: AuthCallbackController@callback
Middleware:
identify.tenant.by.domain (resolves tenant from Host header)Request Parameters:
token (query parameter) - Encrypted authentication tokenResponse:
302 Redirect to dashboard or specified URLImplementation:
public function callback(Request $request)
{
$token = $request->query('token');
if (!$token) {
abort(400, 'Invalid authentication token');
}
// Decrypt and validate token
$payload = $this->authRedirectService->validateToken($token);
// Find or create user
$user = User::firstOrCreate(
[
'email' => $payload['email'],
'tenant_id' => $payload['tenant_id']
],
[
'name' => $payload['name'],
'role' => 'user',
'status' => 'active'
]
);
// Login user
Auth::login($user);
$request->session()->regenerate();
// Store external redirect URL
if (!empty($payload['redirect_url'])) {
$request->session()->flash('external_redirect_url', $payload['redirect_url']);
}
// Redirect to destination
$redirectUrl = $payload['redirect_url'] ?? route('tenant.dashboard', $user->tenant->slug);
return redirect($redirectUrl);
}
Encryption:
// Generate encrypted token
$payload = json_encode([
'tenant_id' => $tenant->id,
'email' => $email,
'name' => $name,
'iat' => time(),
'exp' => time() + 120, // 2 minutes
'jti' => Str::uuid()->toString(),
'redirect_url' => $redirectUrl
]);
$encryptedToken = Crypt::encryptString($payload);
Validation:
// Decrypt and validate
try {
$decrypted = Crypt::decryptString($token);
$payload = json_decode($decrypted, true);
// Check expiry
if ($payload['exp'] < time()) {
throw new \Exception('Token expired');
}
// Check JTI (replay prevention)
$jtiKey = "auth_jti:{$payload['jti']}";
if (Cache::has($jtiKey)) {
throw new \Exception('Token already used');
}
// Mark JTI as used
Cache::put($jtiKey, true, 300); // 5 minutes
} catch (\Exception $e) {
abort(400, 'Invalid token');
}
Configuration: config/app.php
'throttle:auth_redirect' => [
'limit' => 60,
'decay_minutes' => 60,
'by_ip' => true
]
Implementation:
Route::post('/api/auth/redirect/{tenant}', [AuthRedirectController::class, 'redirectToTickets'])
->middleware('throttle:60,1') // 60 requests per minute
->name('auth.redirect');
Whitelist Configuration: config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => [
'https://asknchat.com',
'https://ticket.asknchat.com',
'https://tickets.flare99.com',
],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];
Cookie Configuration: config/session.php
return [
'driver' => 'database',
'lifetime' => 120,
'expire_on_close' => false,
'encrypt' => false,
'cookie' => env('SESSION_COOKIE', 'laravel_session'),
'path' => '/',
'domain' => env('SESSION_DOMAIN', null),
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'none', // Required for cross-domain
];
Command:
php artisan tenant:create \
--name="Company Name" \
--slug="companyslug" \
--domain="companyname.com"
ticket.company.com CNAME tickets.flare99.comtenant_domains tableCommand:
php artisan tenant:domain:add companyslug ticket.company.com --primary
config/cors.phpManual Edit: config/cors.php
'allowed_origins' => [
'https://company.com',
'https://ticket.company.com',
// ... existing origins
],
Email Template:
Subject: Welcome to Flare99 Ticket System
Hi [Company Name],
Your ticket system integration is ready!
Tenant Slug: companyslug
API Endpoint: Redirect API Route
Dashboard: https://ticket.company.com
Please see attached integration guide for implementation steps.
Support: integration@flare99.com
/api/auth/redirect/{tenant} endpointQuery:
SELECT
t.name,
COUNT(DISTINCT u.id) as total_users,
COUNT(CASE WHEN u.created_at > NOW() - INTERVAL 30 DAY THEN 1 END) as new_users_30d,
COUNT(CASE WHEN u.last_login_at > NOW() - INTERVAL 7 DAY THEN 1 END) as active_users_7d
FROM tenants t
LEFT JOIN users u ON t.id = u.tenant_id
WHERE t.status = 'active'
GROUP BY t.id, t.name
ORDER BY total_users DESC;
Laravel Telescope: Enable for detailed monitoring
php artisan telescope:install
php artisan migrate
Log Query:
# Check failed auth attempts
tail -f storage/logs/laravel.log | grep "Invalid token"
# Check rate limiting
tail -f storage/logs/laravel.log | grep "Too Many Requests"
Symptoms: 500 errors on /api/auth/redirect/{tenant}
Diagnostic Steps:
# Check Laravel logs
tail -f storage/logs/laravel.log
# Check tenant exists
php artisan tinker
>>> Tenant::where('slug', 'companyslug')->first()
# Check Redis connectivity
redis-cli ping
Common Causes:
Symptoms: "Invalid token" errors on callback
Diagnostic Steps:
// Test token decryption
$token = 'encrypted_token_string';
try {
$decrypted = Crypt::decryptString($token);
$payload = json_decode($decrypted, true);
dd($payload);
} catch (\Exception $e) {
dd($e->getMessage());
}
Common Causes:
Symptoms: Browser console shows CORS errors
Diagnostic Steps:
# Check CORS config
php artisan config:show cors
# Check middleware
php artisan route:list --name=auth.redirect
# Test CORS headers
curl -H "Origin: https://company.com" \
-H "Access-Control-Request-Method: POST" \
-X OPTIONS \
https://tickets.flare99.com/api/auth/redirect/company
Common Causes:
https://github.com/yourorg/ticket-systemtenant-examples/ directoryEXTERNAL_COMPANY_INTEGRATION_GUIDE.mdhttps://tickets.flare99.com/masterhttps://tickets.flare99.com/telescopehttps://tickets.flare99.com/docs/apiDocument Version: 1.0
Last Review: October 27, 2025
Next Review: January 27, 2026