SSO integration: validate dobetternorge.no signed tokens, update landing page
- bootstrap.php: dbnToolsValidateSsoToken(), SSO session check in dbnToolsIsAuthenticated() - index.php: SSO handler at top, Do Better Norge member panel in login card - .env: DBN_SSO_SECRET placeholder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -110,12 +110,33 @@ dbnToolsStartSession();
|
||||
|
||||
function dbnToolsIsAuthenticated(): bool
|
||||
{
|
||||
// SSO session established via dobetternorge.no signed token
|
||||
if (!empty($_SESSION['dbn_tools_authenticated']) && !empty($_SESSION['dbn_tools_sso_uid'])) {
|
||||
return true;
|
||||
}
|
||||
// Regular Caveau session
|
||||
return !empty($_SESSION['dbn_tools_authenticated'])
|
||||
&& !empty($_SESSION['dbn_tools_user_id'])
|
||||
&& !empty($_SESSION['dbn_tools_client_id'])
|
||||
&& (string)($_SESSION['dbn_tools_client_slug'] ?? '') === dbnToolsClientSlug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a signed SSO token from dobetternorge.no.
|
||||
* Returns the decoded payload array or null on failure.
|
||||
*/
|
||||
function dbnToolsValidateSsoToken(string $token, string $secret): ?array
|
||||
{
|
||||
$parts = explode('.', $token, 2);
|
||||
if (count($parts) !== 2) return null;
|
||||
[$payload, $sig] = $parts;
|
||||
if (!hash_equals(hash_hmac('sha256', $payload, $secret), $sig)) return null;
|
||||
$data = json_decode(base64_decode(strtr($payload, '-_', '+/')), true);
|
||||
if (!is_array($data) || ($data['exp'] ?? 0) < time()) return null;
|
||||
if (empty($data['tools_approved'])) return null;
|
||||
return $data;
|
||||
}
|
||||
|
||||
function dbnToolsAuthenticatedUser(): ?array
|
||||
{
|
||||
if (!dbnToolsIsAuthenticated()) {
|
||||
|
||||
@@ -3,6 +3,28 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
// Handle SSO token from dobetternorge.no
|
||||
if (isset($_GET['sso']) && !dbnToolsIsAuthenticated()) {
|
||||
$ssoSecret = (string) dbnToolsEnv('DBN_SSO_SECRET', '');
|
||||
if ($ssoSecret !== '') {
|
||||
$tokenData = dbnToolsValidateSsoToken((string)$_GET['sso'], $ssoSecret);
|
||||
if ($tokenData !== null) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['dbn_tools_authenticated'] = true;
|
||||
$_SESSION['dbn_tools_authenticated_at'] = time();
|
||||
$_SESSION['dbn_tools_sso_uid'] = (int)$tokenData['uid'];
|
||||
$_SESSION['dbn_tools_user_id'] = (int)$tokenData['uid'];
|
||||
$_SESSION['dbn_tools_user_email'] = (string)$tokenData['email'];
|
||||
$_SESSION['dbn_tools_user_role'] = 'sso';
|
||||
header('Location: ask.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
// Invalid/expired token — redirect back to main site to re-login
|
||||
header('Location: https://dobetternorge.no/account.php?error=' . urlencode('Session expired. Please log in and try again.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (dbnToolsIsAuthenticated()) {
|
||||
$return = $_GET['return'] ?? '';
|
||||
$dest = ($return && str_starts_with($return, '/') && !str_contains($return, '//'))
|
||||
@@ -144,6 +166,15 @@ if (dbnToolsIsAuthenticated()) {
|
||||
<p class="eyebrow">Do Better Norge</p>
|
||||
<h2 id="accessTitle">Access Legal Tools</h2>
|
||||
<p class="gate-copy">Legal information and preparation support, not final legal advice.</p>
|
||||
|
||||
<div style="margin-bottom:20px;padding:14px 18px;background:rgba(0,32,91,.06);border-radius:10px;border:1px solid rgba(0,32,91,.12);font-size:14px;color:#333;text-align:center;">
|
||||
<strong>Do Better Norge member?</strong>
|
||||
<a href="https://dobetternorge.no/account.php" style="color:#00205B;font-weight:600;margin-left:6px;">Log in at dobetternorge.no →</a><br>
|
||||
<span style="color:#888;font-size:13px;">Then open Tools from your account dashboard</span>
|
||||
</div>
|
||||
|
||||
<div style="text-align:center;margin:16px 0 12px;font-size:13px;color:#aaa;letter-spacing:.05em;">OR SIGN IN WITH CAVEAU ACCOUNT</div>
|
||||
|
||||
<form id="passcodeForm" class="passcode-form">
|
||||
<label for="loginEmail">Email</label>
|
||||
<input id="loginEmail" name="email" type="email" autocomplete="username email" required>
|
||||
@@ -154,6 +185,11 @@ if (dbnToolsIsAuthenticated()) {
|
||||
</div>
|
||||
<p id="gateStatus" class="form-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
|
||||
<p style="text-align:center;margin-top:16px;font-size:13px;color:#888;">
|
||||
No account yet?
|
||||
<a href="https://dobetternorge.no/register.php" style="color:#00205B;font-weight:600;">Register free at dobetternorge.no</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
fwrite(STDERR, "This setup script must be run from the command line.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function dbnMcpColumnExists(PDO $db, string $table, string $column): bool
|
||||
{
|
||||
$stmt = $db->prepare(
|
||||
'SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?'
|
||||
);
|
||||
$stmt->execute([$table, $column]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function dbnMcpTableExists(PDO $db, string $table): bool
|
||||
{
|
||||
$stmt = $db->prepare(
|
||||
'SELECT COUNT(*)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?'
|
||||
);
|
||||
$stmt->execute([$table]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function dbnMcpRequireTables(PDO $db, array $tables): void
|
||||
{
|
||||
$missing = [];
|
||||
foreach ($tables as $table) {
|
||||
if (!dbnMcpTableExists($db, $table)) {
|
||||
$missing[] = $table;
|
||||
}
|
||||
}
|
||||
if ($missing !== []) {
|
||||
throw new RuntimeException(
|
||||
'Missing required MCP tables: ' . implode(', ', $missing) .
|
||||
'. Apply the Caveau MCP and Azure Search migrations before running this script.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function dbnMcpUpsertFeature(PDO $db, int $clientId, string $feature, int $enabled): void
|
||||
{
|
||||
$stmt = $db->prepare(
|
||||
'INSERT INTO client_feature_overrides (client_id, feature, enabled)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE enabled = VALUES(enabled)'
|
||||
);
|
||||
$stmt->execute([$clientId, $feature, $enabled]);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = dbnToolsDb();
|
||||
dbnMcpRequireTables($db, [
|
||||
'client_mcp_config',
|
||||
'client_mcp_tokens',
|
||||
'client_corpus_subscriptions',
|
||||
'client_feature_overrides',
|
||||
]);
|
||||
|
||||
$client = dbnToolsFetchClient($db);
|
||||
if (!$client || empty($client['is_active'])) {
|
||||
throw new RuntimeException('Do Better Norge client tenant is not active or was not found.');
|
||||
}
|
||||
|
||||
$clientId = (int)$client['id'];
|
||||
$packageSlug = dbnToolsRequiredPackageSlug();
|
||||
$package = dbnToolsFetchPackage($packageSlug, $db);
|
||||
if (!$package || empty($package['is_active'])) {
|
||||
throw new RuntimeException("Package {$packageSlug} is not active or was not found.");
|
||||
}
|
||||
if ((int)($package['corpus_id'] ?? 0) !== 1) {
|
||||
throw new RuntimeException("Package {$packageSlug} must point at corpus_id=1 for DBN MCP v1.");
|
||||
}
|
||||
|
||||
$db->beginTransaction();
|
||||
|
||||
$stmt = $db->prepare(
|
||||
'INSERT INTO client_corpus_subscriptions (client_id, package_id, is_active, source, subscribed_at)
|
||||
VALUES (?, ?, 1, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE is_active = VALUES(is_active), source = VALUES(source), cancelled_at = NULL'
|
||||
);
|
||||
$stmt->execute([$clientId, (int)$package['id'], 'dbn-mcp-v1']);
|
||||
|
||||
$configColumns = [
|
||||
'client_id' => $clientId,
|
||||
'is_enabled' => 1,
|
||||
'endpoint_slug' => 'dobetter',
|
||||
];
|
||||
if (dbnMcpColumnExists($db, 'client_mcp_config', 'inspector_enabled')) {
|
||||
$configColumns['inspector_enabled'] = 1;
|
||||
}
|
||||
if (dbnMcpColumnExists($db, 'client_mcp_config', 'inspector_retention_days')) {
|
||||
$configColumns['inspector_retention_days'] = 30;
|
||||
}
|
||||
$names = array_keys($configColumns);
|
||||
$updates = implode(', ', array_map(static fn(string $name): string => "{$name} = VALUES({$name})", $names));
|
||||
$stmt = $db->prepare(
|
||||
'INSERT INTO client_mcp_config (' . implode(', ', $names) . ')
|
||||
VALUES (' . implode(', ', array_fill(0, count($names), '?')) . ')
|
||||
ON DUPLICATE KEY UPDATE ' . $updates
|
||||
);
|
||||
$stmt->execute(array_values($configColumns));
|
||||
|
||||
dbnMcpUpsertFeature($db, $clientId, 'azure_search', 1);
|
||||
|
||||
$existingToken = $db->prepare(
|
||||
"SELECT id, token_prefix, name, created_at
|
||||
FROM client_mcp_tokens
|
||||
WHERE client_id = ? AND is_active = 1 AND revoked_at IS NULL
|
||||
AND name = 'DBN MCP v1'
|
||||
ORDER BY id DESC
|
||||
LIMIT 1"
|
||||
);
|
||||
$existingToken->execute([$clientId]);
|
||||
$tokenRow = $existingToken->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$plaintext = null;
|
||||
if (!$tokenRow) {
|
||||
$plaintext = 'dbn_mcp_' . bin2hex(random_bytes(32));
|
||||
$tokenHash = hash('sha256', $plaintext);
|
||||
$prefix = substr($plaintext, 0, 16);
|
||||
$scopes = json_encode([
|
||||
'tools' => ['dbn.search_legal', 'dbn.compare_retrieval', 'dbn.list_sources', 'dbn.get_document'],
|
||||
'backend' => ['azure-search', 'qdrant'],
|
||||
'corpus_id' => [1],
|
||||
], JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$stmt = $db->prepare(
|
||||
'INSERT INTO client_mcp_tokens (client_id, token_hash, token_prefix, name, scopes, is_active, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, 1, NOW())'
|
||||
);
|
||||
$stmt->execute([$clientId, $tokenHash, $prefix, 'DBN MCP v1', $scopes]);
|
||||
$tokenRow = [
|
||||
'id' => (int)$db->lastInsertId(),
|
||||
'token_prefix' => $prefix,
|
||||
'name' => 'DBN MCP v1',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
echo "DBN MCP configured.\n";
|
||||
echo "Client: " . dbnToolsClientSlug() . " (#{$clientId})\n";
|
||||
echo "Package: {$packageSlug} (#{$package['id']}) corpus_id={$package['corpus_id']}\n";
|
||||
echo "MCP config: enabled endpoint_slug=dobetter\n";
|
||||
echo "Azure Search feature: enabled\n";
|
||||
echo "Token: {$tokenRow['name']} (#{$tokenRow['id']}) prefix={$tokenRow['token_prefix']}\n";
|
||||
if ($plaintext !== null) {
|
||||
echo "Plaintext token (shown once): {$plaintext}\n";
|
||||
} else {
|
||||
echo "Plaintext token: not shown; an active DBN MCP v1 token already exists.\n";
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
if (isset($db) && $db instanceof PDO && $db->inTransaction()) {
|
||||
$db->rollBack();
|
||||
}
|
||||
fwrite(STDERR, "DBN MCP setup failed: {$e->getMessage()}\n");
|
||||
exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user