$value){ if(strpos($key, 'HTTP_') === 0){ $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); $headers[$name] = $value; } } return $headers; } function auth_header_value($name){ $headers = auth_headers(); foreach($headers as $key => $value){ if(strtolower($key) === strtolower($name)) return $value; } return null; } function auth_json_response($statusCode, $payload){ http_response_code($statusCode); header('Content-Type: application/json; charset=utf-8'); echo json_encode($payload); exit; } function base64url_encode($data){ return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } function base64url_decode($data){ return base64_decode(strtr($data, '-_', '+/')); } function jwt_issue($claims, $ttlSeconds = 86400){ $now = time(); $payload = array_merge([ 'iss' => 'webgis', 'iat' => $now, 'exp' => $now + max(60, intval($ttlSeconds)), ], $claims); $header = ['alg' => 'HS256', 'typ' => 'JWT']; $encodedHeader = base64url_encode(json_encode($header)); $encodedPayload = base64url_encode(json_encode($payload)); $signature = hash_hmac('sha256', $encodedHeader . '.' . $encodedPayload, JWT_SECRET, true); return $encodedHeader . '.' . $encodedPayload . '.' . base64url_encode($signature); } function jwt_verify($token){ if(!$token || strpos($token, '.') === false) return null; $parts = explode('.', $token); if(count($parts) !== 3) return null; list($h, $p, $s) = $parts; $header = json_decode(base64url_decode($h), true); $payload = json_decode(base64url_decode($p), true); if(!$header || !$payload) return null; if(!isset($header['alg']) || $header['alg'] !== 'HS256') return null; $expected = base64url_encode(hash_hmac('sha256', $h . '.' . $p, JWT_SECRET, true)); if(!hash_equals($expected, $s)) return null; if(isset($payload['exp']) && time() > intval($payload['exp'])) return null; return $payload; } function auth_basic_credentials(){ $header = auth_header_value('Authorization'); if(!$header && isset($_SERVER['PHP_AUTH_USER'])){ return [$_SERVER['PHP_AUTH_USER'], isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '']; } if(!$header || stripos($header, 'Basic ') !== 0) return [null, null]; $decoded = base64_decode(substr($header, 6)); if($decoded === false || strpos($decoded, ':') === false) return [null, null]; list($user, $pass) = explode(':', $decoded, 2); return [$user, $pass]; } function auth_internal_cipher(){ return 'aes-256-gcm'; } function auth_internal_key(){ return hash('sha256', INTERNAL_AUTH_KEY, true); } function internal_auth_issue($claims, $ttlSeconds = 300){ $payload = array_merge([ 'iss' => 'webgis-internal', 'iat' => time(), 'exp' => time() + max(30, intval($ttlSeconds)), 'nonce' => bin2hex(random_bytes(8)) ], $claims); $plaintext = json_encode($payload); $iv = random_bytes(12); $tag = ''; $ciphertext = openssl_encrypt($plaintext, auth_internal_cipher(), auth_internal_key(), OPENSSL_RAW_DATA, $iv, $tag); if($ciphertext === false) return null; return base64url_encode($iv . $tag . $ciphertext); } function internal_auth_verify($token){ if(!$token) return null; $raw = base64url_decode($token); if($raw === false || strlen($raw) < 28) return null; $iv = substr($raw, 0, 12); $tag = substr($raw, 12, 16); $ciphertext = substr($raw, 28); $plaintext = openssl_decrypt($ciphertext, auth_internal_cipher(), auth_internal_key(), OPENSSL_RAW_DATA, $iv, $tag); if($plaintext === false) return null; $payload = json_decode($plaintext, true); if(!$payload) return null; if(isset($payload['exp']) && time() > intval($payload['exp'])) return null; return $payload; } function get_auth_context(){ start_session_if_needed(); global $conn; $context = ['user' => null, 'mode' => null]; $internalToken = auth_header_value('X-Internal-Auth'); if($internalToken){ $internal = internal_auth_verify($internalToken); if($internal){ $context['mode'] = 'internal'; $context['user'] = [ 'id' => isset($internal['sub']) ? intval($internal['sub']) : 0, 'email' => isset($internal['email']) ? $internal['email'] : 'internal', 'name' => isset($internal['name']) ? $internal['name'] : 'Internal', 'organization' => isset($internal['organization']) ? $internal['organization'] : 'Internal', 'role' => isset($internal['role']) ? $internal['role'] : 'admin', 'status' => 'active' ]; return $context; } } $bearer = auth_header_value('Authorization'); if($bearer && stripos($bearer, 'Bearer ') === 0){ $payload = jwt_verify(trim(substr($bearer, 7))); if($payload && isset($payload['sub'])){ $uid = intval($payload['sub']); if($uid > 0 && $conn){ $st = $conn->prepare('SELECT id, email, name, organization, role, status FROM users WHERE id = ? LIMIT 1'); if($st){ $st->bind_param('i', $uid); $st->execute(); $r = $st->get_result(); $row = $r ? $r->fetch_assoc() : null; $st->close(); if($row){ $context['mode'] = 'jwt'; $context['user'] = $row; return $context; } } } } } list($basicUser, $basicPass) = auth_basic_credentials(); if($basicUser !== null){ if($basicUser === BASIC_API_USER && hash_equals(BASIC_API_PASS, (string)$basicPass)){ $context['mode'] = 'basic'; $context['user'] = ['id' => 0, 'email' => 'api', 'name' => 'API', 'organization' => 'API', 'role' => 'admin', 'status' => 'active']; return $context; } if($conn){ $st = $conn->prepare('SELECT id, email, name, organization, role, status, password_hash FROM users WHERE email = ? LIMIT 1'); if($st){ $st->bind_param('s', $basicUser); $st->execute(); $r = $st->get_result(); $row = $r ? $r->fetch_assoc() : null; $st->close(); if($row && password_verify((string)$basicPass, $row['password_hash'])){ unset($row['password_hash']); $context['mode'] = 'basic'; $context['user'] = $row; return $context; } } } } if(isset($_SESSION['jwt'])){ $payload = jwt_verify($_SESSION['jwt']); if($payload && isset($payload['sub']) && $conn){ $uid = intval($payload['sub']); $st = $conn->prepare('SELECT id, email, name, organization, role, status FROM users WHERE id = ? LIMIT 1'); if($st){ $st->bind_param('i', $uid); $st->execute(); $r = $st->get_result(); $row = $r ? $r->fetch_assoc() : null; $st->close(); if($row){ $context['mode'] = 'jwt'; $context['user'] = $row; return $context; } } } } return $context; } function get_current_user_info(){ $context = get_auth_context(); return $context['user']; } function require_auth($allowedModes = ['jwt','basic','internal']){ $context = get_auth_context(); $user = $context['user']; if(!$user){ auth_json_response(401, ['success'=>false,'error'=>'unauthenticated']); } if(isset($user['status']) && $user['status'] !== 'active'){ auth_json_response(403, ['success'=>false,'error'=>'account_not_active']); } if($allowedModes && $context['mode'] && !in_array($context['mode'], (array)$allowedModes, true)){ auth_json_response(403, ['success'=>false,'error'=>'forbidden']); } return $user; } function require_login(){ return require_auth(['jwt','basic','internal']); } function require_bearer_login(){ $context = get_auth_context(); if(!$context['user'] || $context['mode'] !== 'jwt'){ auth_json_response(401, ['success'=>false,'error'=>'bearer_required']); } if(isset($context['user']['status']) && $context['user']['status'] !== 'active'){ auth_json_response(403, ['success'=>false,'error'=>'account_not_active']); } return $context['user']; } function require_role($roles){ $user = require_login(); if(!in_array($user['role'], (array)$roles, true)){ auth_json_response(403, ['success'=>false,'error'=>'forbidden']); } return $user; } function auth_current_mode(){ $context = get_auth_context(); return $context['mode']; } function auth_issue_user_token($userId, $role, $extra = [], $ttlSeconds = 86400){ return jwt_issue(array_merge(['sub' => intval($userId), 'role' => $role], $extra), $ttlSeconds); } function auth_set_session_jwt($token){ start_session_if_needed(); $_SESSION['jwt'] = $token; } function auth_clear_session(){ start_session_if_needed(); foreach(['jwt','user_id'] as $key){ if(isset($_SESSION[$key])) unset($_SESSION[$key]); } }