From 1598bdacd0ad203578c8ce1e166b46d51507c8ac Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 11 Jul 2025 13:50:44 +0200 Subject: [PATCH 01/27] remove content-type header, already set by php-solid-auth --- www/idp/index.php | 1 - 1 file changed, 1 deletion(-) diff --git a/www/idp/index.php b/www/idp/index.php index 01126d0..d1cd410 100644 --- a/www/idp/index.php +++ b/www/idp/index.php @@ -30,7 +30,6 @@ Server::respond($response); break; case "/.well-known/openid-configuration": - header("Content-type: application/json"); $authServer = Server::getAuthServer(); $response = $authServer->respondToOpenIdMetadataRequest(); Server::respond($response); From b8833d7b2c45d2683032f80ce6ae3911b83e4ce0 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 11 Jul 2025 14:20:03 +0200 Subject: [PATCH 02/27] Move logic away from idp/index.html --- lib/Api/Accounts.php | 286 +++++++++++++++++++++++++++++++++ lib/Api/Solid.php | 207 ++++++++++++++++++++++++ www/idp/index.php | 369 +++---------------------------------------- 3 files changed, 519 insertions(+), 343 deletions(-) create mode 100644 lib/Api/Accounts.php create mode 100644 lib/Api/Solid.php diff --git a/lib/Api/Accounts.php b/lib/Api/Accounts.php new file mode 100644 index 0000000..45c5510 --- /dev/null +++ b/lib/Api/Accounts.php @@ -0,0 +1,286 @@ + $_POST['email'] + ]; + + $verifyToken = User::saveVerifyToken('verify', $verifyData); + Mailer::sendVerify($verifyToken); + + $responseData = "OK"; + header("HTTP/1.1 201 Created"); + header("Content-type: application/json"); + echo json_encode($responseData, JSON_PRETTY_PRINT); + } + + public static function respondToAccountNew() { + $verifyToken = User::getVerifyToken($_POST['confirm']); + if (!$verifyToken) { + error_log("Could not read verify token"); + header("HTTP/1.1 400 Bad Request"); + exit(); + } + if ($verifyToken['email'] !== $_POST['email']) { + error_log("Verify token does not match email"); + header("HTTP/1.1 400 Bad Request"); + exit(); + } + if (User::userEmailExists($_POST['email'])) { + error_log("Account already exists"); + header("HTTP/1.1 400 Bad Request"); + exit(); + } + if (!$_POST['password'] === $_POST['repeat_password']) { + error_log("Password repeat does not match"); + header("HTTP/1.1 400 Bad Request"); + exit(); + } + + $newUser = [ + "email" => $_POST['email'], + "password" => $_POST['password'] + ]; + + $createdUser = User::createUser($newUser); + if (!$createdUser) { + error_log("Failed to create user"); + header("HTTP/1.1 400 Bad Request"); + exit(); + } + Mailer::sendAccountCreated($createdUser); + + $responseData = array( + "webId" => $createdUser['webId'] + ); + header("HTTP/1.1 201 Created"); + header("Content-type: application/json"); + Session::start($_POST['email']); + echo json_encode($responseData, JSON_PRETTY_PRINT); + } + + public static function respondToAccountResetPassword() { + if (!User::userEmailExists($_POST['email'])) { + header("HTTP/1.1 200 OK"); // Return OK even when user is not found; + header("Content-type: application/json"); + echo json_encode("OK"); + exit(); + } + $verifyData = [ + 'email' => $_POST['email'] + ]; + + $verifyToken = User::saveVerifyToken('passwordReset', $verifyData); + Mailer::sendResetPassword($verifyToken); + header("HTTP/1.1 200 OK"); + header("Content-type: application/json"); + echo json_encode("OK"); + } + + public static function respondToAccountChangePassword() { + $verifyToken = User::getVerifyToken($_POST['token']); + if (!$verifyToken) { + header("HTTP/1.1 400 Bad Request"); + exit(); + } + $result = User::setUserPassword($verifyToken['email'], $_POST['newPassword']); + if (!$result) { + header("HTTP/1.1 400 Bad Request"); + exit(); + } + header("HTTP/1.1 200 OK"); + header("Content-type: application/json"); + echo json_encode("OK"); + } + + public static function respondToAccountDelete() { + if (!User::userEmailExists($_POST['email'])) { + header("HTTP/1.1 200 OK"); // Return OK even when user is not found; + header("Content-type: application/json"); + echo json_encode("OK"); + exit(); + } + $verifyData = [ + 'email' => $_POST['email'] + ]; + + $verifyToken = User::saveVerifyToken('deleteAccount', $verifyData); + Mailer::sendDeleteAccount($verifyToken); + header("HTTP/1.1 200 OK"); + header("Content-type: application/json"); + echo json_encode("OK"); + } + + public static function respondToAccountDeleteConfirm() { + $verifyToken = User::getVerifyToken($_POST['token']); + if (!$verifyToken) { + header("HTTP/1.1 400 Bad Request"); + exit(); + } + User::deleteAccount($verifyToken['email']); + header("HTTP/1.1 200 OK"); + header("Content-type: application/json"); + echo json_encode("OK"); + } + + public static function respondToLogin() { + $failureCount = IpAttempts::getAttemptsCount($_SERVER['REMOTE_ADDR'], "login"); + if ($failureCount > 5) { + header("HTTP/1.1 400 Bad Request"); + exit(); + } + if (User::checkPassword($_POST['username'], $_POST['password'])) { + Session::start($_POST['username']); + if (!isset($_POST['redirect_uri']) || $_POST['redirect_uri'] === '') { + header("Location: /dashboard/"); + exit(); + } + header("Location: " . urldecode($_POST['redirect_uri'])); // FIXME: Do we need to harden this? + } else { + IpAttempts::logFailedAttempt($_SERVER['REMOTE_ADDR'], "login", time() + 3600); + header("Location: /login/"); + } + } + + public static function respondToRegister() { + $postData = file_get_contents("php://input"); + $clientData = json_decode($postData, true); + if (!isset($clientData)) { + header("HTTP/1.1 400 Bad request"); + return; + } + $parsedOrigin = parse_url($clientData['redirect_uris'][0]); + $origin = $parsedOrigin['scheme'] . '://' . $parsedOrigin['host']; + if (isset($parsedOrigin['port'])) { + $origin .= ":" . $parsedOrigin['port']; + } + + + $generatedClientId = md5(random_bytes(32)); + $generatedClientSecret = md5(random_bytes(32)); + + $clientData['client_id_issued_at'] = time(); + $clientData['client_id'] = $generatedClientId; + $clientData['client_secret'] = $generatedClientSecret; + $clientData['origin'] = $origin; + ClientRegistration::saveClientRegistration($clientData); + + $client = ClientRegistration::getRegistration($generatedClientId); + + $responseData = array( + 'redirect_uris' => $client['redirect_uris'], + 'client_id' => $client['client_id'], + 'client_secret' => $client['client_secret'], + 'response_types' => array('code'), + 'grant_types' => array('authorization_code', 'refresh_token'), + 'application_type' => $client['application_type'] ?? 'web', + 'client_name' => $client['client_name'] ?? $client['client_id'], + 'id_token_signed_response_alg' => 'RS256', + 'token_endpoint_auth_method' => 'client_secret_basic', + 'client_id_issued_at' => $client['client_id_issued_at'], + 'client_secret_expires_at' => 0 + ); + header("HTTP/1.1 201 Created"); + header("Content-type: application/json"); + echo json_encode($responseData, JSON_PRETTY_PRINT); + } + + public static function respondToSharing() { + $clientId = $_POST['client_id']; + $userId = $user['userId']; + if ($_POST['consent'] === 'true') { + User::allowClientForUser($clientId, $userId); + } + $returnUrl = urldecode($_POST['returnUrl']); + header("Location: $returnUrl"); + } + + public static function respondToToken() { + $authServer = Server::getAuthServer(); + $tokenGenerator = Server::getTokenGenerator(); + + $requestFactory = new \Laminas\Diactoros\ServerRequestFactory(); + $request = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); + $requestBody = $request->getParsedBody(); + + $grantType = isset($requestBody['grant_type']) ? $requestBody['grant_type'] : null; + $clientId = isset($requestBody['client_id']) ? $requestBody['client_id'] : null; + switch ($grantType) { + case "authorization_code": + $code = $requestBody['code']; + $codeInfo = $tokenGenerator->getCodeInfo($code); + $userId = $codeInfo['user_id']; + if (!$clientId) { + $clientId = $codeInfo['client_id']; + } + break; + case "refresh_token": + $refreshToken = $requestBody['refresh_token']; + $tokenInfo = $tokenGenerator->getCodeInfo($refreshToken); // FIXME: getCodeInfo should be named 'decrypt' or 'getInfo'? + $userId = $tokenInfo['user_id']; + if (!$clientId) { + $clientId = $tokenInfo['client_id']; + } + break; + default: + $userId = false; + break; + } + + $httpDpop = $request->getServerParams()['HTTP_DPOP']; + + $response = $authServer->respondToAccessTokenRequest($request); + + if (isset($userId)) { + $response = $tokenGenerator->addIdTokenToResponse( + $response, + $clientId, + $userId, + ($_SESSION['nonce'] ?? ''), + Server::getKeys()['privateKey'], + $httpDpop + ); + } + + Server::respond($response); + } + } diff --git a/lib/Api/Solid.php b/lib/Api/Solid.php new file mode 100644 index 0000000..0870631 --- /dev/null +++ b/lib/Api/Solid.php @@ -0,0 +1,207 @@ +respondToJwksMetadataRequest(); + Server::respond($response); + } + + public static function respondToWellKnownOpenIdConfiguration() { + $authServer = Server::getAuthServer(); + $response = $authServer->respondToOpenIdMetadataRequest(); + Server::respond($response); + } + + public static function respondToAuthorize() { + $clientId = $_GET['client_id']; + $getVars = $_GET; + + if (!isset($getVars['grant_type'])) { + $getVars['grant_type'] = 'implicit'; + } + $getVars['scope'] = "openid" ; + $getVars['response_type'] = "token"; + + $requestedResponseTypes = explode(" ", ($_GET['response_type'] ?? '')); + foreach ($requestedResponseTypes as $responseType) { + if ($responseType == "code") { + $getVars['response_type'] = "code"; + } + } + + $keys = Server::getKeys(); + if (isset($_GET['request'])) { + $jwtConfig = \Lcobucci\JWT\Configuration::forSymmetricSigner( + new \Lcobucci\JWT\Signer\Rsa\Sha256(), + \Lcobucci\JWT\Signer\Key\InMemory::plainText($keys['privateKey'] + )); + + if (isset($_GET['nonce'])) { + $_SESSION['nonce'] = $_GET['nonce']; + } else if (isset($_GET['request'])) { + $token = $jwtConfig->parser()->parse($_GET['request']); + $_SESSION['nonce'] = $token->claims()->get('nonce'); + } + + if (!isset($getVars["redirect_uri"])) { + if (isset($token)) { + $getVars['redirect_uri'] = $token->claims()->get("redirect_uri"); + } + } + } + + $requestFactory = new \Laminas\Diactoros\ServerRequestFactory(); + $request = $requestFactory->fromGlobals($_SERVER, $getVars, $_POST, $_COOKIE, $_FILES); + + $authServer = Server::getAuthServer(); + + $approval = false; + // check clientId approval for the user + if (in_array($clientId, ($user['allowedClients'] ?? []))) { + $approval = true; + } else { + $clientRegistration = ClientRegistration::getRegistration($clientId); + if (in_array($clientRegistration['origin'], TRUSTED_APPS)) { + $approval = true; + } + } + + if (!$approval) { + header('Location: ' . BASEURL . '/sharing/' . "?" . http_build_query( + array( + "returnUrl" => urlencode($_SERVER["REQUEST_URI"]), + "client_id" => $clientId, + "redirect_uri" => $getVars['redirect_uri'] + ) + )); + exit(); + } + + $webId = "https://id-" . $user['userId'] . "." . BASEDOMAIN . "/#me"; + $user = new \Pdsinterop\Solid\Auth\Entity\User(); + $user->setIdentifier($webId); + + $response = $authServer->respondToAuthorizationRequest($request, $user, $approval); + + $tokenGenerator = Server::getTokenGenerator(); + + $response = $tokenGenerator->addIdTokenToResponse( + $response, + $clientId, + $webId, + $_SESSION['nonce'] ?? '', + Server::getKeys()["privateKey"] + ); + + Server::respond($response); + } + + public static function respondToRegister() { + $postData = file_get_contents("php://input"); + $clientData = json_decode($postData, true); + if (!isset($clientData)) { + header("HTTP/1.1 400 Bad request"); + return; + } + $parsedOrigin = parse_url($clientData['redirect_uris'][0]); + $origin = $parsedOrigin['scheme'] . '://' . $parsedOrigin['host']; + if (isset($parsedOrigin['port'])) { + $origin .= ":" . $parsedOrigin['port']; + } + + + $generatedClientId = md5(random_bytes(32)); + $generatedClientSecret = md5(random_bytes(32)); + + $clientData['client_id_issued_at'] = time(); + $clientData['client_id'] = $generatedClientId; + $clientData['client_secret'] = $generatedClientSecret; + $clientData['origin'] = $origin; + ClientRegistration::saveClientRegistration($clientData); + + $client = ClientRegistration::getRegistration($generatedClientId); + + $responseData = array( + 'redirect_uris' => $client['redirect_uris'], + 'client_id' => $client['client_id'], + 'client_secret' => $client['client_secret'], + 'response_types' => array('code'), + 'grant_types' => array('authorization_code', 'refresh_token'), + 'application_type' => $client['application_type'] ?? 'web', + 'client_name' => $client['client_name'] ?? $client['client_id'], + 'id_token_signed_response_alg' => 'RS256', + 'token_endpoint_auth_method' => 'client_secret_basic', + 'client_id_issued_at' => $client['client_id_issued_at'], + 'client_secret_expires_at' => 0 + ); + header("HTTP/1.1 201 Created"); + header("Content-type: application/json"); + echo json_encode($responseData, JSON_PRETTY_PRINT); + } + + public static function respondToSharing() { + $clientId = $_POST['client_id']; + $userId = $user['userId']; + if ($_POST['consent'] === 'true') { + User::allowClientForUser($clientId, $userId); + } + $returnUrl = urldecode($_POST['returnUrl']); + header("Location: $returnUrl"); + } + + public static function respondToToken() { + $authServer = Server::getAuthServer(); + $tokenGenerator = Server::getTokenGenerator(); + + $requestFactory = new \Laminas\Diactoros\ServerRequestFactory(); + $request = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); + $requestBody = $request->getParsedBody(); + + $grantType = isset($requestBody['grant_type']) ? $requestBody['grant_type'] : null; + $clientId = isset($requestBody['client_id']) ? $requestBody['client_id'] : null; + switch ($grantType) { + case "authorization_code": + $code = $requestBody['code']; + $codeInfo = $tokenGenerator->getCodeInfo($code); + $userId = $codeInfo['user_id']; + if (!$clientId) { + $clientId = $codeInfo['client_id']; + } + break; + case "refresh_token": + $refreshToken = $requestBody['refresh_token']; + $tokenInfo = $tokenGenerator->getCodeInfo($refreshToken); // FIXME: getCodeInfo should be named 'decrypt' or 'getInfo'? + $userId = $tokenInfo['user_id']; + if (!$clientId) { + $clientId = $tokenInfo['client_id']; + } + break; + default: + $userId = false; + break; + } + + $httpDpop = $request->getServerParams()['HTTP_DPOP']; + + $response = $authServer->respondToAccessTokenRequest($request); + + if (isset($userId)) { + $response = $tokenGenerator->addIdTokenToResponse( + $response, + $clientId, + $userId, + ($_SESSION['nonce'] ?? ''), + Server::getKeys()['privateKey'], + $httpDpop + ); + } + + Server::respond($response); + } + } diff --git a/www/idp/index.php b/www/idp/index.php index d1cd410..7757fd5 100644 --- a/www/idp/index.php +++ b/www/idp/index.php @@ -7,13 +7,8 @@ require_once(__DIR__ . "/../../vendor/autoload.php"); use Pdsinterop\PhpSolid\Middleware; - use Pdsinterop\PhpSolid\Server; - use Pdsinterop\PhpSolid\ClientRegistration; - use Pdsinterop\PhpSolid\User; - use Pdsinterop\PhpSolid\Session; - use Pdsinterop\PhpSolid\Mailer; - use Pdsinterop\PhpSolid\IpAttempts; - use Pdsinterop\PhpSolid\JtiStore; + use Pdsinterop\PhpSolid\Api\Account; + use Pdsinterop\PhpSolid\Api\Solid; $request = explode("?", $_SERVER['REQUEST_URI'], 2)[0]; $method = $_SERVER['REQUEST_METHOD']; @@ -25,122 +20,24 @@ switch ($request) { case "/jwks": case "/jwks/": - $authServer = Server::getAuthServer(); - $response = $authServer->respondToJwksMetadataRequest(); - Server::respond($response); + Solid::respondToJwks(); break; case "/.well-known/openid-configuration": - $authServer = Server::getAuthServer(); - $response = $authServer->respondToOpenIdMetadataRequest(); - Server::respond($response); + Solid::respondToWellKnownOpenIdConfiguration(); break; case "/authorize": case "/authorize/": - $user = User::getUser(Session::getLoggedInUser()); - if (!$user) { - header("Location: /login/?redirect_uri=" . urlencode($_SERVER['REQUEST_URI'])); - exit(); - } - - $clientId = $_GET['client_id']; - $getVars = $_GET; - - if (!isset($getVars['grant_type'])) { - $getVars['grant_type'] = 'implicit'; - } - $getVars['scope'] = "openid" ; - $getVars['response_type'] = "token"; - - $requestedResponseTypes = explode(" ", ($_GET['response_type'] ?? '')); - foreach ($requestedResponseTypes as $responseType) { - if ($responseType == "code") { - $getVars['response_type'] = "code"; - } - } - - $keys = Server::getKeys(); - if (isset($_GET['request'])) { - $jwtConfig = \Lcobucci\JWT\Configuration::forSymmetricSigner( - new \Lcobucci\JWT\Signer\Rsa\Sha256(), - \Lcobucci\JWT\Signer\Key\InMemory::plainText($keys['privateKey'] - )); - - if (isset($_GET['nonce'])) { - $_SESSION['nonce'] = $_GET['nonce']; - } else if (isset($_GET['request'])) { - $token = $jwtConfig->parser()->parse($_GET['request']); - $_SESSION['nonce'] = $token->claims()->get('nonce'); - } - - if (!isset($getVars["redirect_uri"])) { - if (isset($token)) { - $getVars['redirect_uri'] = $token->claims()->get("redirect_uri"); - } - } - } - - $requestFactory = new \Laminas\Diactoros\ServerRequestFactory(); - $request = $requestFactory->fromGlobals($_SERVER, $getVars, $_POST, $_COOKIE, $_FILES); - - $authServer = Server::getAuthServer(); - - $approval = false; - // check clientId approval for the user - if (in_array($clientId, ($user['allowedClients'] ?? []))) { - $approval = true; - } else { - $clientRegistration = ClientRegistration::getRegistration($clientId); - if (in_array($clientRegistration['origin'], TRUSTED_APPS)) { - $approval = true; - } - } - - if (!$approval) { - header('Location: ' . BASEURL . '/sharing/' . "?" . http_build_query( - array( - "returnUrl" => urlencode($_SERVER["REQUEST_URI"]), - "client_id" => $clientId, - "redirect_uri" => $getVars['redirect_uri'] - ) - )); - exit(); - } - - $webId = "https://id-" . $user['userId'] . "." . BASEDOMAIN . "/#me"; - $user = new \Pdsinterop\Solid\Auth\Entity\User(); - $user->setIdentifier($webId); - - $response = $authServer->respondToAuthorizationRequest($request, $user, $approval); - - $tokenGenerator = Server::getTokenGenerator(); - - $response = $tokenGenerator->addIdTokenToResponse( - $response, - $clientId, - $webId, - $_SESSION['nonce'] ?? '', - Server::getKeys()["privateKey"] - ); - - Server::respond($response); + Account::requireLoggedInUser(); + Solid::respondToAuthorize(); break; case "/dashboard": case "/dashboard/": - $user = User::getUser(Session::getLoggedInUser()); - if (!$user) { - header("Location: /login/"); - exit(); - } - echo "Logged in as " . $user['webId']; + Account::requireLoggedInUser(); + Account::respondToDashboard(); break; case "/logout": case "/logout/": - $user = User::getUser(Session::getLoggedInUser()); - if ($user) { - session_destroy(); - } - header("Location: /login/"); - exit(); + Account::respondToLogout(); break; case "/login/password": case "/login/password/": @@ -163,11 +60,7 @@ break; case "/sharing": case "/sharing/": - $user = User::getUser(Session::getLoggedInUser()); - if (!$user) { - header("Location: /login/"); - exit(); - } + Account::requireLoggedInUser(); include_once(FRONTENDDIR . "generated.html"); break; case '/session': @@ -185,264 +78,48 @@ switch ($request) { case "/api/accounts/verify": case "/api/accounts/verify/": - $verifyData = [ - 'email' => $_POST['email'] - ]; - - $verifyToken = User::saveVerifyToken('verify', $verifyData); - Mailer::sendVerify($verifyToken); - - $responseData = "OK"; - header("HTTP/1.1 201 Created"); - header("Content-type: application/json"); - echo json_encode($responseData, JSON_PRETTY_PRINT); + Account::respondToAccountVerify(); break; case "/api/accounts/new": case "/api/accounts/new/": - $verifyToken = User::getVerifyToken($_POST['confirm']); - if (!$verifyToken) { - error_log("Could not read verify token"); - header("HTTP/1.1 400 Bad Request"); - exit(); - } - if ($verifyToken['email'] !== $_POST['email']) { - error_log("Verify token does not match email"); - header("HTTP/1.1 400 Bad Request"); - exit(); - } - if (User::userEmailExists($_POST['email'])) { - error_log("Account already exists"); - header("HTTP/1.1 400 Bad Request"); - exit(); - } - if (!$_POST['password'] === $_POST['repeat_password']) { - error_log("Password repeat does not match"); - header("HTTP/1.1 400 Bad Request"); - exit(); - } - - $newUser = [ - "email" => $_POST['email'], - "password" => $_POST['password'] - ]; - - $createdUser = User::createUser($newUser); - if (!$createdUser) { - error_log("Failed to create user"); - header("HTTP/1.1 400 Bad Request"); - exit(); - } - Mailer::sendAccountCreated($createdUser); - - $responseData = array( - "webId" => $createdUser['webId'] - ); - header("HTTP/1.1 201 Created"); - header("Content-type: application/json"); - Session::start($_POST['email']); - echo json_encode($responseData, JSON_PRETTY_PRINT); + Account::respondToAccountNew(); break; case "/api/accounts/reset-password": case "/api/accounts/reset-password/": - if (!User::userEmailExists($_POST['email'])) { - header("HTTP/1.1 200 OK"); // Return OK even when user is not found; - header("Content-type: application/json"); - echo json_encode("OK"); - exit(); - } - $verifyData = [ - 'email' => $_POST['email'] - ]; - - $verifyToken = User::saveVerifyToken('passwordReset', $verifyData); - Mailer::sendResetPassword($verifyToken); - header("HTTP/1.1 200 OK"); - header("Content-type: application/json"); - echo json_encode("OK"); + Account::respondToAccountResetPassword(); break; case "/api/accounts/change-password": case "/api/accounts/change-password/": - $verifyToken = User::getVerifyToken($_POST['token']); - if (!$verifyToken) { - header("HTTP/1.1 400 Bad Request"); - exit(); - } - $result = User::setUserPassword($verifyToken['email'], $_POST['newPassword']); - if (!$result) { - header("HTTP/1.1 400 Bad Request"); - exit(); - } - header("HTTP/1.1 200 OK"); - header("Content-type: application/json"); - echo json_encode("OK"); + Account::respondToChangePassword(); break; case "/api/accounts/delete": case "/api/accounts/delete/": - if (!User::userEmailExists($_POST['email'])) { - header("HTTP/1.1 200 OK"); // Return OK even when user is not found; - header("Content-type: application/json"); - echo json_encode("OK"); - exit(); - } - $verifyData = [ - 'email' => $_POST['email'] - ]; - - $verifyToken = User::saveVerifyToken('deleteAccount', $verifyData); - Mailer::sendDeleteAccount($verifyToken); - header("HTTP/1.1 200 OK"); - header("Content-type: application/json"); - echo json_encode("OK"); + Account::respondToAccountDelete(); break; case "/api/accounts/delete/confirm": case "/api/accounts/delete/confirm/": - $verifyToken = User::getVerifyToken($_POST['token']); - if (!$verifyToken) { - header("HTTP/1.1 400 Bad Request"); - exit(); - } - User::deleteAccount($verifyToken['email']); - header("HTTP/1.1 200 OK"); - header("Content-type: application/json"); - echo json_encode("OK"); + Account::respondToAccountDeleteConfirm(); break; case "/login/password": case "/login/password/": - $failureCount = IpAttempts::getAttemptsCount($_SERVER['REMOTE_ADDR'], "login"); - if ($failureCount > 5) { - header("HTTP/1.1 400 Bad Request"); - exit(); - } - if (User::checkPassword($_POST['username'], $_POST['password'])) { - Session::start($_POST['username']); - if (!isset($_POST['redirect_uri']) || $_POST['redirect_uri'] === '') { - header("Location: /dashboard/"); - exit(); - } - header("Location: " . urldecode($_POST['redirect_uri'])); // FIXME: Do we need to harden this? - } else { - IpAttempts::logFailedAttempt($_SERVER['REMOTE_ADDR'], "login", time() + 3600); - header("Location: /login/"); - } + Account::respondToLogin(); break; case "/register": case "/register/": - $postData = file_get_contents("php://input"); - $clientData = json_decode($postData, true); - if (!isset($clientData)) { - header("HTTP/1.1 400 Bad request"); - return; - } - $parsedOrigin = parse_url($clientData['redirect_uris'][0]); - $origin = $parsedOrigin['scheme'] . '://' . $parsedOrigin['host']; - if (isset($parsedOrigin['port'])) { - $origin .= ":" . $parsedOrigin['port']; - } - - - $generatedClientId = md5(random_bytes(32)); - $generatedClientSecret = md5(random_bytes(32)); - - $clientData['client_id_issued_at'] = time(); - $clientData['client_id'] = $generatedClientId; - $clientData['client_secret'] = $generatedClientSecret; - $clientData['origin'] = $origin; - ClientRegistration::saveClientRegistration($clientData); - - $client = ClientRegistration::getRegistration($generatedClientId); - - $responseData = array( - 'redirect_uris' => $client['redirect_uris'], - 'client_id' => $client['client_id'], - 'client_secret' => $client['client_secret'], - 'response_types' => array('code'), - 'grant_types' => array('authorization_code', 'refresh_token'), - 'application_type' => $client['application_type'] ?? 'web', - 'client_name' => $client['client_name'] ?? $client['client_id'], - 'id_token_signed_response_alg' => 'RS256', - 'token_endpoint_auth_method' => 'client_secret_basic', - 'client_id_issued_at' => $client['client_id_issued_at'], - 'client_secret_expires_at' => 0 - ); - header("HTTP/1.1 201 Created"); - header("Content-type: application/json"); - echo json_encode($responseData, JSON_PRETTY_PRINT); + Solid::respondToRegister(); break; case "/api/sharing": case "/api/sharing/": - $user = User::getUser(Session::getLoggedInUser()); - if (!$user) { - header("HTTP/1.1 400 Bad request"); - } else { - $clientId = $_POST['client_id']; - $userId = $user['userId']; - if ($_POST['consent'] === 'true') { - User::allowClientForUser($clientId, $userId); - } - $returnUrl = urldecode($_POST['returnUrl']); - header("Location: $returnUrl"); - } + Solid::respondToSharing(); break; case "/token": case "/token/": - $authServer = Server::getAuthServer(); - $tokenGenerator = Server::getTokenGenerator(); - - $requestFactory = new \Laminas\Diactoros\ServerRequestFactory(); - $request = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); - $requestBody = $request->getParsedBody(); - - $grantType = isset($requestBody['grant_type']) ? $requestBody['grant_type'] : null; - $clientId = isset($requestBody['client_id']) ? $requestBody['client_id'] : null; - switch ($grantType) { - case "authorization_code": - $code = $requestBody['code']; - $codeInfo = $tokenGenerator->getCodeInfo($code); - $userId = $codeInfo['user_id']; - if (!$clientId) { - $clientId = $codeInfo['client_id']; - } - break; - case "refresh_token": - $refreshToken = $requestBody['refresh_token']; - $tokenInfo = $tokenGenerator->getCodeInfo($refreshToken); // FIXME: getCodeInfo should be named 'decrypt' or 'getInfo'? - $userId = $tokenInfo['user_id']; - if (!$clientId) { - $clientId = $tokenInfo['client_id']; - } - break; - default: - $userId = false; - break; - } - - $httpDpop = $request->getServerParams()['HTTP_DPOP']; - - $response = $authServer->respondToAccessTokenRequest($request); - - if (isset($userId)) { - $response = $tokenGenerator->addIdTokenToResponse( - $response, - $clientId, - $userId, - ($_SESSION['nonce'] ?? ''), - Server::getKeys()['privateKey'], - $httpDpop - ); - } - - Server::respond($response); + Solid::respondToToken(); break; default: header($_SERVER['SERVER_PROTOCOL'] . " 404 Not found"); break; } - if (!file_exists(CLEANUP_FILE) || (filemtime(CLEANUP_FILE) < time())) { - touch(CLEANUP_FILE, time() + 3600); - User::cleanupTokens(); - IpAttempts::cleanupAttempts(); - JtiStore::cleanupJti(); - } break; case "OPTIONS": break; @@ -451,4 +128,10 @@ header($_SERVER['SERVER_PROTOCOL'] . " 405 Method not allowed"); break; } + if (!file_exists(CLEANUP_FILE) || (filemtime(CLEANUP_FILE) < time())) { + touch(CLEANUP_FILE, time() + 3600); + User::cleanupTokens(); + IpAttempts::cleanupAttempts(); + JtiStore::cleanupJti(); + } \ No newline at end of file From 2304531ee779ebdd38cb73e5383762fb05b1422a Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 11 Jul 2025 14:23:42 +0200 Subject: [PATCH 03/27] restore cleanup deps --- www/idp/index.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/www/idp/index.php b/www/idp/index.php index 7757fd5..bd9ca70 100644 --- a/www/idp/index.php +++ b/www/idp/index.php @@ -10,6 +10,10 @@ use Pdsinterop\PhpSolid\Api\Account; use Pdsinterop\PhpSolid\Api\Solid; + use Pdsinterip\PhpSolid\User; + use Pdsinterip\PhpSolid\IpAttempts; + use Pdsinterip\PhpSolid\JtiStore; + $request = explode("?", $_SERVER['REQUEST_URI'], 2)[0]; $method = $_SERVER['REQUEST_METHOD']; From 5bb231a7680f24b6eed4a015ac6cd3ba84d806ad Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 11 Jul 2025 14:29:11 +0200 Subject: [PATCH 04/27] rename accounts to account --- lib/Api/{Accounts.php => Account.php} | 2 +- www/idp/index.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename lib/Api/{Accounts.php => Account.php} (99%) diff --git a/lib/Api/Accounts.php b/lib/Api/Account.php similarity index 99% rename from lib/Api/Accounts.php rename to lib/Api/Account.php index 45c5510..22b0360 100644 --- a/lib/Api/Accounts.php +++ b/lib/Api/Account.php @@ -8,7 +8,7 @@ use Pdsinterop\PhpSolid\Mailer; use Pdsinterop\PhpSolid\IpAttempts; - class Accounts { + class Account { public static function requireLoggedInUser() { $user = User::getUser(Session::getLoggedInUser()); if (!$user) { diff --git a/www/idp/index.php b/www/idp/index.php index bd9ca70..89bd251 100644 --- a/www/idp/index.php +++ b/www/idp/index.php @@ -94,7 +94,7 @@ break; case "/api/accounts/change-password": case "/api/accounts/change-password/": - Account::respondToChangePassword(); + Account::respondToAccountChangePassword(); break; case "/api/accounts/delete": case "/api/accounts/delete/": From a779436dd601641f8d9e30a59e948ab4024f3707 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 11 Jul 2025 14:39:43 +0200 Subject: [PATCH 05/27] get user from session, prevent double start session --- lib/Api/Solid.php | 4 ++++ lib/Session.php | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/Api/Solid.php b/lib/Api/Solid.php index 0870631..f7e0647 100644 --- a/lib/Api/Solid.php +++ b/lib/Api/Solid.php @@ -4,6 +4,7 @@ use Pdsinterop\PhpSolid\Server; use Pdsinterop\PhpSolid\ClientRegistration; use Pdsinterop\PhpSolid\User; + use Pdsinterop\PhpSolid\Session; class Solid { public static function respondToJwks() { @@ -19,6 +20,8 @@ public static function respondToWellKnownOpenIdConfiguration() { } public static function respondToAuthorize() { + $user = User::getUser(Session::getLoggedInUser()); + $clientId = $_GET['client_id']; $getVars = $_GET; @@ -146,6 +149,7 @@ public static function respondToRegister() { } public static function respondToSharing() { + $user = User::getUser(Session::getLoggedInUser()); $clientId = $_POST['client_id']; $userId = $user['userId']; if ($_POST['consent'] === 'true') { diff --git a/lib/Session.php b/lib/Session.php index 4711287..13aeb90 100644 --- a/lib/Session.php +++ b/lib/Session.php @@ -4,14 +4,18 @@ class Session { private $cookieLifetime = 24*60*60; public static function start($username) { - session_start([ - 'cookie_lifetime' => 24*60*60 // 1 day - ]); + if (session_status() === PHP_SESSION_NONE) { + session_start([ + 'cookie_lifetime' => 24*60*60 // 1 day + ]); + } $_SESSION['username'] = $username; } public static function getLoggedInUser() { - session_start(); + if (session_status() === PHP_SESSION_NONE) { + session_start(); + } if (!isset($_SESSION['username'])) { return false; } From 02eb72bd6deebf9d84fc1d53bffc7991cbc2f181 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 11 Jul 2025 14:41:48 +0200 Subject: [PATCH 06/27] typofix --- www/idp/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/idp/index.php b/www/idp/index.php index 89bd251..1d5b087 100644 --- a/www/idp/index.php +++ b/www/idp/index.php @@ -10,9 +10,9 @@ use Pdsinterop\PhpSolid\Api\Account; use Pdsinterop\PhpSolid\Api\Solid; - use Pdsinterip\PhpSolid\User; - use Pdsinterip\PhpSolid\IpAttempts; - use Pdsinterip\PhpSolid\JtiStore; + use Pdsinterop\PhpSolid\User; + use Pdsinterop\PhpSolid\IpAttempts; + use Pdsinterop\PhpSolid\JtiStore; $request = explode("?", $_SERVER['REQUEST_URI'], 2)[0]; $method = $_SERVER['REQUEST_METHOD']; From 942809923b43ec412577b3aefc883117503cc45d Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 11 Jul 2025 14:44:10 +0200 Subject: [PATCH 07/27] rename Api/Solid to Api/SolidIdp --- lib/Api/{Solid.php => SolidIdp.php} | 2 +- www/idp/index.php | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) rename lib/Api/{Solid.php => SolidIdp.php} (99%) diff --git a/lib/Api/Solid.php b/lib/Api/SolidIdp.php similarity index 99% rename from lib/Api/Solid.php rename to lib/Api/SolidIdp.php index f7e0647..0d7363a 100644 --- a/lib/Api/Solid.php +++ b/lib/Api/SolidIdp.php @@ -6,7 +6,7 @@ use Pdsinterop\PhpSolid\User; use Pdsinterop\PhpSolid\Session; - class Solid { + class SolidIdp { public static function respondToJwks() { $authServer = Server::getAuthServer(); $response = $authServer->respondToJwksMetadataRequest(); diff --git a/www/idp/index.php b/www/idp/index.php index 1d5b087..02645af 100644 --- a/www/idp/index.php +++ b/www/idp/index.php @@ -8,7 +8,7 @@ use Pdsinterop\PhpSolid\Middleware; use Pdsinterop\PhpSolid\Api\Account; - use Pdsinterop\PhpSolid\Api\Solid; + use Pdsinterop\PhpSolid\Api\SolidIdp; use Pdsinterop\PhpSolid\User; use Pdsinterop\PhpSolid\IpAttempts; @@ -24,15 +24,15 @@ switch ($request) { case "/jwks": case "/jwks/": - Solid::respondToJwks(); + SolidIdp::respondToJwks(); break; case "/.well-known/openid-configuration": - Solid::respondToWellKnownOpenIdConfiguration(); + SolidIdp::respondToWellKnownOpenIdConfiguration(); break; case "/authorize": case "/authorize/": Account::requireLoggedInUser(); - Solid::respondToAuthorize(); + SolidIdp::respondToAuthorize(); break; case "/dashboard": case "/dashboard/": @@ -110,15 +110,15 @@ break; case "/register": case "/register/": - Solid::respondToRegister(); + SolidIdp::respondToRegister(); break; case "/api/sharing": case "/api/sharing/": - Solid::respondToSharing(); + SolidIdp::respondToSharing(); break; case "/token": case "/token/": - Solid::respondToToken(); + SolidIdp::respondToToken(); break; default: header($_SERVER['SERVER_PROTOCOL'] . " 404 Not found"); From 6402e295084b4d0bcf4a1e139c72bbbaa9e76b76 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 11 Jul 2025 15:01:11 +0200 Subject: [PATCH 08/27] cleanup user profile route file --- lib/Api/SolidUserProfile.php | 51 ++++++++++++++++++++++++++++++++++++ www/user/profile.php | 45 ++----------------------------- 2 files changed, 53 insertions(+), 43 deletions(-) create mode 100644 lib/Api/SolidUserProfile.php diff --git a/lib/Api/SolidUserProfile.php b/lib/Api/SolidUserProfile.php new file mode 100644 index 0000000..7303c14 --- /dev/null +++ b/lib/Api/SolidUserProfile.php @@ -0,0 +1,51 @@ +. +@prefix acl: . +@prefix foaf: . +@prefix ldp: . +@prefix schema: . +@prefix solid: . +@prefix space: . +@prefix vcard: . +@prefix pro: <./>. +@prefix inbox: <{$user['storage']}inbox/>. + +<> a foaf:PersonalProfileDocument; foaf:maker :me; foaf:primaryTopic :me. + +:me + a schema:Person, foaf:Person; + ldp:inbox inbox:; + space:preferencesFile <{$user['storage']}settings/prefs.ttl>; + space:storage <{$user['storage']}>; + solid:account <{$user['storage']}>; + solid:oidcIssuer <{$user['issuer']}>; + solid:privateTypeIndex <{$user['storage']}settings/privateTypeIndex.ttl>; + solid:publicTypeIndex <{$user['storage']}settings/publicTypeIndex.ttl>. +EOF; + header('Content-Type: text/turtle'); + echo $profile; + } + } + \ No newline at end of file diff --git a/www/user/profile.php b/www/user/profile.php index c18e534..11a26a4 100644 --- a/www/user/profile.php +++ b/www/user/profile.php @@ -6,8 +6,7 @@ require_once(__DIR__ . "/../../vendor/autoload.php"); use Pdsinterop\PhpSolid\Middleware; - use Pdsinterop\PhpSolid\Server; - use Pdsinterop\PhpSolid\User; + use Pdsinterop\PhpSolid\Api\SolidUserProfile; $request = explode("?", $_SERVER['REQUEST_URI'], 2)[0]; $method = $_SERVER['REQUEST_METHOD']; @@ -18,47 +17,7 @@ case "GET": switch ($request) { case "/": - $serverName = $_SERVER['SERVER_NAME']; - [$idPart, $rest] = explode(".", $serverName, 2); - $userId = preg_replace("/^id-/", "", $idPart); - - $user = User::getUserById($userId); - if (!isset($user['storage']) || !$user['storage']) { - $user['storage'] = "https://storage-" . $userId . "." . BASEDOMAIN . "/"; - } - if (is_array($user['storage'])) { // empty array is already handled - $user['storage'] = array_values($user['storage'])[0]; // FIXME: Handle multiple storage pods - } - if (!isset($user['issuer'])) { - $user['issuer'] = BASEURL; - } - - $profile = <<<"EOF" -@prefix : <#>. -@prefix acl: . -@prefix foaf: . -@prefix ldp: . -@prefix schema: . -@prefix solid: . -@prefix space: . -@prefix vcard: . -@prefix pro: <./>. -@prefix inbox: <{$user['storage']}inbox/>. - -<> a foaf:PersonalProfileDocument; foaf:maker :me; foaf:primaryTopic :me. - -:me - a schema:Person, foaf:Person; - ldp:inbox inbox:; - space:preferencesFile <{$user['storage']}settings/prefs.ttl>; - space:storage <{$user['storage']}>; - solid:account <{$user['storage']}>; - solid:oidcIssuer <{$user['issuer']}>; - solid:privateTypeIndex <{$user['storage']}settings/privateTypeIndex.ttl>; - solid:publicTypeIndex <{$user['storage']}settings/publicTypeIndex.ttl>. -EOF; - header('Content-Type: text/turtle'); - echo $profile; + SolidUserProfile::respondToProfile(); break; } break; From c413924cd44e60b5653953833a0186167ed8acab Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 11 Jul 2025 15:01:25 +0200 Subject: [PATCH 09/27] cleanup user storage route file --- lib/Api/SolidStorage.php | 74 ++++++++++++++++++++++++++++++++++++++++ www/user/storage.php | 71 +++----------------------------------- 2 files changed, 78 insertions(+), 67 deletions(-) create mode 100644 lib/Api/SolidStorage.php diff --git a/lib/Api/SolidStorage.php b/lib/Api/SolidStorage.php new file mode 100644 index 0000000..bb0c99f --- /dev/null +++ b/lib/Api/SolidStorage.php @@ -0,0 +1,74 @@ +fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); + $response = new Response(); + + StorageServer::initializeStorage(); + $filesystem = StorageServer::getFileSystem(); + + $resourceServer = new ResourceServer($filesystem, $response, null); + $solidNotifications = new SolidNotifications(); + $resourceServer->setNotifications($solidNotifications); + + $wac = new WAC($filesystem); + + $baseUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['SERVER_NAME']; + + $resourceServer->setBaseUrl($baseUrl); + $wac->setBaseUrl($baseUrl); + + $webId = StorageServer::getWebId($rawRequest); + + if (!isset($webId)) { + $response = $resourceServer->getResponse() + ->withStatus(409, "Invalid token"); + StorageServer::respond($response); + exit(); + } + + $origin = $rawRequest->getHeaderLine("Origin"); + + // FIXME: Read allowed clients from the profile instead; + $owner = StorageServer::getOwner(); + + $allowedClients = $owner['allowedClients'] ?? []; + $allowedOrigins = []; + foreach ($allowedClients as $clientId) { + $clientRegistration = ClientRegistration::getRegistration($clientId); + if (isset($clientRegistration['client_name'])) { + $allowedOrigins[] = $clientRegistration['client_name']; + } + if (isset($clientRegistration['origin'])) { + $allowedOrigins[] = $clientRegistration['origin']; + } + } + if ($origin =="") { + $allowedOrigins[] = "app://unset"; // FIXME: this should not be here. + $origin = "app://unset"; + } + + if (!$wac->isAllowed($rawRequest, $webId, $origin, $allowedOrigins)) { + $response = new Response(); + $response = $response->withStatus(403, "Access denied!"); + StorageServer::respond($response); + exit(); + } + + $response = $resourceServer->respondToRequest($rawRequest); + $response = $wac->addWACHeaders($rawRequest, $response, $webId); + StorageServer::respond($response); + } + } + \ No newline at end of file diff --git a/www/user/storage.php b/www/user/storage.php index b8f5eb8..97aa8b3 100644 --- a/www/user/storage.php +++ b/www/user/storage.php @@ -6,15 +6,8 @@ require_once(__DIR__ . "/../../vendor/autoload.php"); use Pdsinterop\PhpSolid\Middleware; - use Pdsinterop\PhpSolid\StorageServer; - use Pdsinterop\PhpSolid\ClientRegistration; - use Pdsinterop\PhpSolid\SolidNotifications; - use Pdsinterop\Solid\Auth\WAC; - use Pdsinterop\Solid\Resources\Server as ResourceServer; - use Laminas\Diactoros\ServerRequestFactory; - use Laminas\Diactoros\Response; + use Pdsinterop\PhpSolid\Api\SolidStorage; - $request = explode("?", $_SERVER['REQUEST_URI'], 2)[0]; $method = $_SERVER['REQUEST_METHOD']; Middleware::cors(); @@ -25,63 +18,7 @@ echo "OK"; return; break; + default: + SolidStorage::respondToStorage(); + break; } - - $requestFactory = new ServerRequestFactory(); - $rawRequest = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); - $response = new Response(); - - StorageServer::initializeStorage(); - $filesystem = StorageServer::getFileSystem(); - - $resourceServer = new ResourceServer($filesystem, $response, null); - $solidNotifications = new SolidNotifications(); - $resourceServer->setNotifications($solidNotifications); - - $wac = new WAC($filesystem); - - $baseUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['SERVER_NAME']; - - $resourceServer->setBaseUrl($baseUrl); - $wac->setBaseUrl($baseUrl); - - $webId = StorageServer::getWebId($rawRequest); - - if (!isset($webId)) { - $response = $resourceServer->getResponse() - ->withStatus(409, "Invalid token"); - StorageServer::respond($response); - exit(); - } - - $origin = $rawRequest->getHeaderLine("Origin"); - - // FIXME: Read allowed clients from the profile instead; - $owner = StorageServer::getOwner(); - - $allowedClients = $owner['allowedClients'] ?? []; - $allowedOrigins = []; - foreach ($allowedClients as $clientId) { - $clientRegistration = ClientRegistration::getRegistration($clientId); - if (isset($clientRegistration['client_name'])) { - $allowedOrigins[] = $clientRegistration['client_name']; - } - if (isset($clientRegistration['origin'])) { - $allowedOrigins[] = $clientRegistration['origin']; - } - } - if ($origin =="") { - $allowedOrigins[] = "app://unset"; // FIXME: this should not be here. - $origin = "app://unset"; - } - - if (!$wac->isAllowed($rawRequest, $webId, $origin, $allowedOrigins)) { - $response = new Response(); - $response = $response->withStatus(403, "Access denied!"); - StorageServer::respond($response); - exit(); - } - - $response = $resourceServer->respondToRequest($rawRequest); - $response = $wac->addWACHeaders($rawRequest, $response, $webId); - StorageServer::respond($response); From a73bf25ba864e24ccf94ae61c6410e981631df99 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 11 Jul 2025 16:24:59 +0200 Subject: [PATCH 10/27] rename Api to Routes --- lib/{Api => Routes}/Account.php | 2 +- lib/{Api => Routes}/SolidIdp.php | 2 +- lib/{Api => Routes}/SolidStorage.php | 2 +- lib/{Api => Routes}/SolidUserProfile.php | 2 +- www/idp/index.php | 4 ++-- www/user/profile.php | 2 +- www/user/storage.php | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) rename lib/{Api => Routes}/Account.php (99%) rename lib/{Api => Routes}/SolidIdp.php (99%) rename lib/{Api => Routes}/SolidStorage.php (98%) rename lib/{Api => Routes}/SolidUserProfile.php (97%) diff --git a/lib/Api/Account.php b/lib/Routes/Account.php similarity index 99% rename from lib/Api/Account.php rename to lib/Routes/Account.php index 22b0360..cfbdd65 100644 --- a/lib/Api/Account.php +++ b/lib/Routes/Account.php @@ -1,5 +1,5 @@ Date: Mon, 14 Jul 2025 14:01:55 +0200 Subject: [PATCH 11/27] add throw on error --- lib/Routes/Account.php | 6 +++--- lib/Routes/SolidIdp.php | 2 +- lib/Server.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Routes/Account.php b/lib/Routes/Account.php index cfbdd65..5d05b06 100644 --- a/lib/Routes/Account.php +++ b/lib/Routes/Account.php @@ -50,7 +50,7 @@ public static function respondToAccountVerify() { $responseData = "OK"; header("HTTP/1.1 201 Created"); header("Content-type: application/json"); - echo json_encode($responseData, JSON_PRETTY_PRINT); + echo json_encode($responseData, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); } public static function respondToAccountNew() { @@ -95,7 +95,7 @@ public static function respondToAccountNew() { header("HTTP/1.1 201 Created"); header("Content-type: application/json"); Session::start($_POST['email']); - echo json_encode($responseData, JSON_PRETTY_PRINT); + echo json_encode($responseData, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); } public static function respondToAccountResetPassword() { @@ -221,7 +221,7 @@ public static function respondToRegister() { ); header("HTTP/1.1 201 Created"); header("Content-type: application/json"); - echo json_encode($responseData, JSON_PRETTY_PRINT); + echo json_encode($responseData, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); } public static function respondToSharing() { diff --git a/lib/Routes/SolidIdp.php b/lib/Routes/SolidIdp.php index 375a778..bf7333f 100644 --- a/lib/Routes/SolidIdp.php +++ b/lib/Routes/SolidIdp.php @@ -145,7 +145,7 @@ public static function respondToRegister() { ); header("HTTP/1.1 201 Created"); header("Content-type: application/json"); - echo json_encode($responseData, JSON_PRETTY_PRINT); + echo json_encode($responseData, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); } public static function respondToSharing() { diff --git a/lib/Server.php b/lib/Server.php index 39317ec..9913ece 100644 --- a/lib/Server.php +++ b/lib/Server.php @@ -161,6 +161,6 @@ public static function respond($response) { header($header . ":" . $value); } } - echo json_encode($body, JSON_PRETTY_PRINT); + echo json_encode($body, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); } } From 6ac73cc2d332f9ab04dafc47d1c76c5505b110c5 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:09:57 +0200 Subject: [PATCH 12/27] get user from logged in session --- lib/Routes/Account.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Routes/Account.php b/lib/Routes/Account.php index 5d05b06..9807338 100644 --- a/lib/Routes/Account.php +++ b/lib/Routes/Account.php @@ -225,6 +225,7 @@ public static function respondToRegister() { } public static function respondToSharing() { + $user = User::getUser(Session::getLoggedInUser()); $clientId = $_POST['client_id']; $userId = $user['userId']; if ($_POST['consent'] === 'true') { From aee5981b2b0f5dcba8aed4eb5e00017c552ce6f6 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:11:19 +0200 Subject: [PATCH 13/27] use ?? --- lib/Routes/Account.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Routes/Account.php b/lib/Routes/Account.php index 9807338..cb0def0 100644 --- a/lib/Routes/Account.php +++ b/lib/Routes/Account.php @@ -243,8 +243,8 @@ public static function respondToToken() { $request = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); $requestBody = $request->getParsedBody(); - $grantType = isset($requestBody['grant_type']) ? $requestBody['grant_type'] : null; - $clientId = isset($requestBody['client_id']) ? $requestBody['client_id'] : null; + $grantType = $requestBody['grant_type'] ?? null; + $clientId = $requestBody['client_id'] ?? null; switch ($grantType) { case "authorization_code": $code = $requestBody['code']; From ab524f9d6a6d4f4adb5e6b6a0bfae56b08397027 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:14:16 +0200 Subject: [PATCH 14/27] whitespace fixes --- lib/Mailer.php | 20 ++-- lib/PasswordValidator.php | 192 +++++++++++++++++++------------------- lib/Routes/Account.php | 12 +-- lib/Routes/SolidIdp.php | 8 +- lib/SolidPubSub.php | 50 +++++----- lib/StorageServer.php | 4 +- 6 files changed, 143 insertions(+), 143 deletions(-) diff --git a/lib/Mailer.php b/lib/Mailer.php index b38e032..3268f13 100644 --- a/lib/Mailer.php +++ b/lib/Mailer.php @@ -14,13 +14,13 @@ public static function getMailer() { // Settings $mailer->IsSMTP(); $mailer->CharSet = 'UTF-8'; - $mailer->Host = MAILER['host']; - $mailer->SMTPDebug = 0; - $mailer->Port = MAILER['port']; + $mailer->Host = MAILER['host']; + $mailer->SMTPDebug = 0; + $mailer->Port = MAILER['port']; if (isset(MAILER['user'])) { - $mailer->SMTPAuth = true; - $mailer->Username = MAILER['user']; - $mailer->Password = MAILER['password']; + $mailer->SMTPAuth = true; + $mailer->Username = MAILER['user']; + $mailer->Password = MAILER['password']; } $mailer->isHTML(true); $mailer->setFrom(MAILER['from']); @@ -50,7 +50,7 @@ public static function sendAccountCreated($data) { $mailer->addAddress($mailTo); $mailer->Subject = $mailSubject; - $mailer->Body = $mailHtmlBody; + $mailer->Body = $mailHtmlBody; $mailer->AltBody = $mailPlainBody; $mailer->send(); @@ -79,7 +79,7 @@ public static function sendVerify($data) { $mailer->addAddress($mailTo); $mailer->Subject = $mailSubject; - $mailer->Body = $mailHtmlBody; + $mailer->Body = $mailHtmlBody; $mailer->AltBody = $mailPlainBody; $mailer->send(); @@ -107,7 +107,7 @@ public static function sendResetPassword($data) { $mailer->addAddress($mailTo); $mailer->Subject = $mailSubject; - $mailer->Body = $mailHtmlBody; + $mailer->Body = $mailHtmlBody; $mailer->AltBody = $mailPlainBody; $mailer->send(); @@ -135,7 +135,7 @@ public static function sendDeleteAccount($data) { $mailer->addAddress($mailTo); $mailer->Subject = $mailSubject; - $mailer->Body = $mailHtmlBody; + $mailer->Body = $mailHtmlBody; $mailer->AltBody = $mailPlainBody; $mailer->send(); diff --git a/lib/PasswordValidator.php b/lib/PasswordValidator.php index f5d98ec..38704f4 100644 --- a/lib/PasswordValidator.php +++ b/lib/PasswordValidator.php @@ -1,114 +1,114 @@ ?@[\]^_{|}~'; - private static string $lowercaseCharacters = 'abcdefghijklmnopqrstuvwxyz'; - private static string $uppercaseCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - private static string $numbers = '0123456789'; + private static string $specialCharacters = ' !"#$%&\'()*+,-./:;<=>?@[\]^_{|}~'; + private static string $lowercaseCharacters = 'abcdefghijklmnopqrstuvwxyz'; + private static string $uppercaseCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + private static string $numbers = '0123456789'; - /** - * The maximum number of times the same character can appear in the password - * @var int - */ - private static int $maxOccurrences = 2; + /** + * The maximum number of times the same character can appear in the password + * @var int + */ + private static int $maxOccurrences = 2; - /** - * Get the base amount of characters from the characters used in the password. - * This is the number of possible characters to pick from in the used character sets - * i.e. 26 for only lower case passwords - * @param $password - * @return int - */ - public static function getBase(string $password): int - { - $characters = str_split($password); - $base = 0; - $hasSpecial = false; - $hasLower = false; - $hasUpper = false; - $hasDigits = false; + /** + * Get the base amount of characters from the characters used in the password. + * This is the number of possible characters to pick from in the used character sets + * i.e. 26 for only lower case passwords + * @param $password + * @return int + */ + public static function getBase(string $password): int + { + $characters = str_split($password); + $base = 0; + $hasSpecial = false; + $hasLower = false; + $hasUpper = false; + $hasDigits = false; - foreach ($characters as $character) { - if (!$hasLower && strpos(self::$lowercaseCharacters, $character) !== false) { - $hasLower = true; - $base += strlen(self::$lowercaseCharacters); - } - if (!$hasUpper && strpos(self::$uppercaseCharacters, $character) !== false) { - $hasUpper = true; - $base += strlen(self::$uppercaseCharacters); - } - if (!$hasSpecial && strpos(self::$specialCharacters, $character) !== false) { - $hasSpecial = true; - $base += strlen(self::$specialCharacters); - } - if (!$hasDigits && strpos(self::$numbers, $character) !== false) { - $hasDigits = true; - $base += strlen(self::$numbers); - } + foreach ($characters as $character) { + if (!$hasLower && strpos(self::$lowercaseCharacters, $character) !== false) { + $hasLower = true; + $base += strlen(self::$lowercaseCharacters); + } + if (!$hasUpper && strpos(self::$uppercaseCharacters, $character) !== false) { + $hasUpper = true; + $base += strlen(self::$uppercaseCharacters); + } + if (!$hasSpecial && strpos(self::$specialCharacters, $character) !== false) { + $hasSpecial = true; + $base += strlen(self::$specialCharacters); + } + if (!$hasDigits && strpos(self::$numbers, $character) !== false) { + $hasDigits = true; + $base += strlen(self::$numbers); + } - if ( - strpos(self::$lowercaseCharacters, $character) === false - && strpos(self::$uppercaseCharacters, $character) === false - && strpos(self::$specialCharacters, $character) === false - && strpos(self::$numbers, $character) === false - ) { - $base++; - } - } + if ( + strpos(self::$lowercaseCharacters, $character) === false + && strpos(self::$uppercaseCharacters, $character) === false + && strpos(self::$specialCharacters, $character) === false + && strpos(self::$numbers, $character) === false + ) { + $base++; + } + } - return $base; - } + return $base; + } - /** - * get the calculated entropy of the password based on the rules for excluding duplicate characters - * If a password is in the banned list, entropy will be 0. - * @see bannedPassords() - * @param string $password - * @param array $bannedPasswords a custom list of passwords to disallow - * @return float - */ - public static function getEntropy(string $password, array $bannedPasswords = []): float - { - if (in_array(strtolower($password), $bannedPasswords)) { - // these are so weak, we just want to outright ban them. Entropy will be 0 for anything in this list. - return 0; - } - $base = self::getBase($password); - $length = self::getLength($password); + /** + * get the calculated entropy of the password based on the rules for excluding duplicate characters + * If a password is in the banned list, entropy will be 0. + * @see bannedPassords() + * @param string $password + * @param array $bannedPasswords a custom list of passwords to disallow + * @return float + */ + public static function getEntropy(string $password, array $bannedPasswords = []): float + { + if (in_array(strtolower($password), $bannedPasswords)) { + // these are so weak, we just want to outright ban them. Entropy will be 0 for anything in this list. + return 0; + } + $base = self::getBase($password); + $length = self::getLength($password); - $decimalPlaces = 2; - return number_format(log($base ** $length), $decimalPlaces); - } + $decimalPlaces = 2; + return number_format(log($base ** $length), $decimalPlaces); + } - /** - * Check the length of the password based on known rules - * Characters will only be counted a maximum of 2 times e.g. aaa has length 2 - * @param $password - * @return int - */ - public static function getLength(string $password): int - { - $usedCharacters = []; - $characters = str_split($password); - $length = 0; + /** + * Check the length of the password based on known rules + * Characters will only be counted a maximum of 2 times e.g. aaa has length 2 + * @param $password + * @return int + */ + public static function getLength(string $password): int + { + $usedCharacters = []; + $characters = str_split($password); + $length = 0; - foreach ($characters as $character) - { - if (array_key_exists($character, $usedCharacters) && $usedCharacters[$character] < self::$maxOccurrences) { - $length++; - $usedCharacters[$character]++; - } - if (!array_key_exists($character, $usedCharacters)) { - $usedCharacters[$character] = 1; - $length++; - } - } + foreach ($characters as $character) + { + if (array_key_exists($character, $usedCharacters) && $usedCharacters[$character] < self::$maxOccurrences) { + $length++; + $usedCharacters[$character]++; + } + if (!array_key_exists($character, $usedCharacters)) { + $usedCharacters[$character] = 1; + $length++; + } + } - return $length; - } + return $length; + } } diff --git a/lib/Routes/Account.php b/lib/Routes/Account.php index cb0def0..965f60d 100644 --- a/lib/Routes/Account.php +++ b/lib/Routes/Account.php @@ -1,12 +1,12 @@ pubsub = $pubsubUrl; - } + class SolidPubSub implements SolidNotificationsInterface + { + private $pubsub; + public function __construct($pubsubUrl) { + $this->pubsub = $pubsubUrl; + } - public function send($path, $type) { - $pubsub = str_replace(["https://", "http://"], "wss://", $this->pubsub); + public function send($path, $type) { + $pubsub = str_replace(["https://", "http://"], "wss://", $this->pubsub); - $client = new Client($pubsub); - $client->setContext(['ssl' => [ - 'verify_peer' => false, // if false, accept SSL handshake without client certificate - 'verify_peer_name' => false, - 'allow_self_signed' => true, - ]]); - $client->addHeader("Sec-WebSocket-Protocol", "solid-0.1"); - try { - $client->text("pub $path\n"); - } catch (\Throwable $exception) { - throw new \Exception('Could not write to pubsub server', 502, $exception); - } - } - } + $client = new Client($pubsub); + $client->setContext(['ssl' => [ + 'verify_peer' => false, // if false, accept SSL handshake without client certificate + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ]]); + $client->addHeader("Sec-WebSocket-Protocol", "solid-0.1"); + try { + $client->text("pub $path\n"); + } catch (\Throwable $exception) { + throw new \Exception('Could not write to pubsub server', 502, $exception); + } + } + } diff --git a/lib/StorageServer.php b/lib/StorageServer.php index baafaa1..72cf645 100644 --- a/lib/StorageServer.php +++ b/lib/StorageServer.php @@ -10,8 +10,8 @@ public static function getFileSystem() { // The internal adapter $adapter = new \League\Flysystem\Adapter\Local( - // Determine root directory - STORAGEBASE . "$storageId/" + // Determine root directory + STORAGEBASE . "$storageId/" ); $graph = new \EasyRdf\Graph(); From d7e0f9656a15de116340b48da608b2f468f52f9c Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:15:59 +0200 Subject: [PATCH 15/27] use in_array --- lib/Routes/SolidIdp.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/Routes/SolidIdp.php b/lib/Routes/SolidIdp.php index 1ca99d6..52ac87b 100644 --- a/lib/Routes/SolidIdp.php +++ b/lib/Routes/SolidIdp.php @@ -32,10 +32,8 @@ public static function respondToAuthorize() { $getVars['response_type'] = "token"; $requestedResponseTypes = explode(" ", ($_GET['response_type'] ?? '')); - foreach ($requestedResponseTypes as $responseType) { - if ($responseType == "code") { - $getVars['response_type'] = "code"; - } + if (in_array("code", $requestedResponseTypes)) { + $getVars['response_type'] = "code"; } $keys = Server::getKeys(); From 69afc3675b69df91e9bfab69732f9d417d7e786c Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:18:50 +0200 Subject: [PATCH 16/27] add fixme --- lib/Routes/SolidIdp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Routes/SolidIdp.php b/lib/Routes/SolidIdp.php index 52ac87b..f56a28f 100644 --- a/lib/Routes/SolidIdp.php +++ b/lib/Routes/SolidIdp.php @@ -110,7 +110,7 @@ public static function respondToRegister() { header("HTTP/1.1 400 Bad request"); return; } - $parsedOrigin = parse_url($clientData['redirect_uris'][0]); + $parsedOrigin = parse_url($clientData['redirect_uris'][0]); // FIXME: Should we have multiple origins? $origin = $parsedOrigin['scheme'] . '://' . $parsedOrigin['host']; if (isset($parsedOrigin['port'])) { $origin .= ":" . $parsedOrigin['port']; From 354bf811da432fad43fe55f24d3b348570b66fd7 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:19:37 +0200 Subject: [PATCH 17/27] use ?? --- lib/Routes/SolidIdp.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Routes/SolidIdp.php b/lib/Routes/SolidIdp.php index f56a28f..55bc219 100644 --- a/lib/Routes/SolidIdp.php +++ b/lib/Routes/SolidIdp.php @@ -165,8 +165,8 @@ public static function respondToToken() { $request = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); $requestBody = $request->getParsedBody(); - $grantType = isset($requestBody['grant_type']) ? $requestBody['grant_type'] : null; - $clientId = isset($requestBody['client_id']) ? $requestBody['client_id'] : null; + $grantType = $requestBody['grant_type'] ?? null; + $clientId = $requestBody['client_id'] ?? null; switch ($grantType) { case "authorization_code": $code = $requestBody['code']; From fcf2c1708026622803a5d83b820bb91e6fb2e043 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:21:28 +0200 Subject: [PATCH 18/27] more explicit comparison --- lib/Routes/SolidStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Routes/SolidStorage.php b/lib/Routes/SolidStorage.php index 6d09b82..18cf22a 100644 --- a/lib/Routes/SolidStorage.php +++ b/lib/Routes/SolidStorage.php @@ -54,7 +54,7 @@ public static function respondToStorage() { $allowedOrigins[] = $clientRegistration['origin']; } } - if ($origin =="") { + if (!isset($origin) || ($origin === "")) { $allowedOrigins[] = "app://unset"; // FIXME: this should not be here. $origin = "app://unset"; } From 60d37e2a7855a64a8d600fe3a621449b5a719d5f Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:23:03 +0200 Subject: [PATCH 19/27] remove unused cookielifetime --- lib/Session.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Session.php b/lib/Session.php index 13aeb90..7a707e3 100644 --- a/lib/Session.php +++ b/lib/Session.php @@ -2,7 +2,6 @@ namespace Pdsinterop\PhpSolid; class Session { - private $cookieLifetime = 24*60*60; public static function start($username) { if (session_status() === PHP_SESSION_NONE) { session_start([ From a68e46836b5d6dc6206bd7b370b70d7f8a04d702 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:25:17 +0200 Subject: [PATCH 20/27] remove unused var, directly create it --- lib/Routes/SolidStorage.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Routes/SolidStorage.php b/lib/Routes/SolidStorage.php index 18cf22a..382e295 100644 --- a/lib/Routes/SolidStorage.php +++ b/lib/Routes/SolidStorage.php @@ -13,12 +13,11 @@ class SolidStorage { public static function respondToStorage() { $requestFactory = new ServerRequestFactory(); $rawRequest = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); - $response = new Response(); StorageServer::initializeStorage(); $filesystem = StorageServer::getFileSystem(); - $resourceServer = new ResourceServer($filesystem, $response, null); + $resourceServer = new ResourceServer($filesystem, new Response(), null); $solidNotifications = new SolidNotifications(); $resourceServer->setNotifications($solidNotifications); From a745b74ca40e7ca8240c1a55a86586b4e50b5c66 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:34:42 +0200 Subject: [PATCH 21/27] add fixme --- lib/Routes/SolidStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Routes/SolidStorage.php b/lib/Routes/SolidStorage.php index 382e295..fa8d83d 100644 --- a/lib/Routes/SolidStorage.php +++ b/lib/Routes/SolidStorage.php @@ -23,7 +23,7 @@ public static function respondToStorage() { $wac = new WAC($filesystem); - $baseUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['SERVER_NAME']; + $baseUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['SERVER_NAME']; // FIXME: get this from the rawRequest instead? $resourceServer->setBaseUrl($baseUrl); $wac->setBaseUrl($baseUrl); From 037a0bc2a61eb00704ba69f93b5b287ec0090330 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:41:54 +0200 Subject: [PATCH 22/27] centralize the use of $_SERVER to easier refactor later --- lib/Routes/SolidStorage.php | 4 ++-- lib/Routes/SolidUserProfile.php | 3 ++- lib/StorageServer.php | 9 +++------ lib/Util.php | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/lib/Routes/SolidStorage.php b/lib/Routes/SolidStorage.php index fa8d83d..8a4952c 100644 --- a/lib/Routes/SolidStorage.php +++ b/lib/Routes/SolidStorage.php @@ -4,6 +4,7 @@ use Pdsinterop\PhpSolid\StorageServer; use Pdsinterop\PhpSolid\ClientRegistration; use Pdsinterop\PhpSolid\SolidNotifications; + use Pdsinterop\PhpSolid\Util use Pdsinterop\Solid\Auth\WAC; use Pdsinterop\Solid\Resources\Server as ResourceServer; use Laminas\Diactoros\ServerRequestFactory; @@ -23,8 +24,7 @@ public static function respondToStorage() { $wac = new WAC($filesystem); - $baseUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['SERVER_NAME']; // FIXME: get this from the rawRequest instead? - + $baseUrl = Util::getServerBaseUrl(); $resourceServer->setBaseUrl($baseUrl); $wac->setBaseUrl($baseUrl); diff --git a/lib/Routes/SolidUserProfile.php b/lib/Routes/SolidUserProfile.php index 727eb82..78ae9ac 100644 --- a/lib/Routes/SolidUserProfile.php +++ b/lib/Routes/SolidUserProfile.php @@ -2,10 +2,11 @@ namespace Pdsinterop\PhpSolid\Routes; use Pdsinterop\PhpSolid\User; + use Pdsinterop\PhpSolid\Util; class SolidUserProfile { public static function respondToProfile() { - $serverName = $_SERVER['SERVER_NAME']; + $serverName = Util::getServerName(); [$idPart, $rest] = explode(".", $serverName, 2); $userId = preg_replace("/^id-/", "", $idPart); diff --git a/lib/StorageServer.php b/lib/StorageServer.php index 72cf645..12ac7d0 100644 --- a/lib/StorageServer.php +++ b/lib/StorageServer.php @@ -3,6 +3,7 @@ use Pdsinterop\PhpSolid\Server; use Pdsinterop\PhpSolid\User; + use Pdsinterop\PhpSolid\Util; class StorageServer extends Server { public static function getFileSystem() { @@ -17,11 +18,7 @@ public static function getFileSystem() { $graph = new \EasyRdf\Graph(); // Create Formats objects $formats = new \Pdsinterop\Rdf\Formats(); - - $scheme = $_SERVER['REQUEST_SCHEME']; - $domain = $_SERVER['SERVER_NAME']; - $path = $_SERVER['REQUEST_URI']; - $serverUri = "{$scheme}://{$domain}{$path}"; // FIXME: doublecheck that this is the correct url; + $serverUri = Util::getServerUri(); // Create the RDF Adapter $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf($adapter, $graph, $formats, $serverUri); @@ -62,7 +59,7 @@ public static function getWebId($rawRequest) { } private static function getStorageId() { - $serverName = $_SERVER['SERVER_NAME']; + $serverName = Util::getServerName(); $idParts = explode(".", $serverName, 2); $storageId = preg_replace("/^storage-/", "", $idParts[0]); return $storageId; diff --git a/lib/Util.php b/lib/Util.php index 2b66f49..642e687 100644 --- a/lib/Util.php +++ b/lib/Util.php @@ -9,4 +9,19 @@ public static function base64_url_encode($text) { public static function base64_url_decode($text) { return base64_decode(str_replace(['-', '_', ''], ['+', '/', '='], $text)); } + public static function getServerName() { + // FIXME: Depending on the setup, SERVER_NAME might not be the domain of the server. + return $_SERVER['SERVER_NAME']; + } + public static function getServerUri() { + $scheme = $_SERVER['REQUEST_SCHEME']; + $domain = $_SERVER['SERVER_NAME']; + $path = $_SERVER['REQUEST_URI']; + return "{$scheme}://{$domain}{$path}"; + } + public static function getServerBaseUrl() { + $scheme = $_SERVER['REQUEST_SCHEME']; + $domain = $_SERVER['SERVER_NAME']; + return "{$scheme}://{$domain}"; + } } From 799607b7374358fc53388b773f44674cdb5f4016 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 14 Jul 2025 14:45:09 +0200 Subject: [PATCH 23/27] typofix --- lib/Routes/SolidStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Routes/SolidStorage.php b/lib/Routes/SolidStorage.php index 8a4952c..5b90424 100644 --- a/lib/Routes/SolidStorage.php +++ b/lib/Routes/SolidStorage.php @@ -4,7 +4,7 @@ use Pdsinterop\PhpSolid\StorageServer; use Pdsinterop\PhpSolid\ClientRegistration; use Pdsinterop\PhpSolid\SolidNotifications; - use Pdsinterop\PhpSolid\Util + use Pdsinterop\PhpSolid\Util; use Pdsinterop\Solid\Auth\WAC; use Pdsinterop\Solid\Resources\Server as ResourceServer; use Laminas\Diactoros\ServerRequestFactory; From 096986ac6127838cf5f283446bfcf6639ca01bed Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Thu, 17 Jul 2025 09:57:53 +0200 Subject: [PATCH 24/27] remove unused functions, they are in SolidIdp --- lib/Routes/Account.php | 104 ----------------------------------------- 1 file changed, 104 deletions(-) diff --git a/lib/Routes/Account.php b/lib/Routes/Account.php index 965f60d..87ed8c8 100644 --- a/lib/Routes/Account.php +++ b/lib/Routes/Account.php @@ -180,108 +180,4 @@ public static function respondToLogin() { header("Location: /login/"); } } - - public static function respondToRegister() { - $postData = file_get_contents("php://input"); - $clientData = json_decode($postData, true); - if (!isset($clientData)) { - header("HTTP/1.1 400 Bad request"); - return; - } - $parsedOrigin = parse_url($clientData['redirect_uris'][0]); - $origin = $parsedOrigin['scheme'] . '://' . $parsedOrigin['host']; - if (isset($parsedOrigin['port'])) { - $origin .= ":" . $parsedOrigin['port']; - } - - - $generatedClientId = md5(random_bytes(32)); - $generatedClientSecret = md5(random_bytes(32)); - - $clientData['client_id_issued_at'] = time(); - $clientData['client_id'] = $generatedClientId; - $clientData['client_secret'] = $generatedClientSecret; - $clientData['origin'] = $origin; - ClientRegistration::saveClientRegistration($clientData); - - $client = ClientRegistration::getRegistration($generatedClientId); - - $responseData = array( - 'redirect_uris' => $client['redirect_uris'], - 'client_id' => $client['client_id'], - 'client_secret' => $client['client_secret'], - 'response_types' => array('code'), - 'grant_types' => array('authorization_code', 'refresh_token'), - 'application_type' => $client['application_type'] ?? 'web', - 'client_name' => $client['client_name'] ?? $client['client_id'], - 'id_token_signed_response_alg' => 'RS256', - 'token_endpoint_auth_method' => 'client_secret_basic', - 'client_id_issued_at' => $client['client_id_issued_at'], - 'client_secret_expires_at' => 0 - ); - header("HTTP/1.1 201 Created"); - header("Content-type: application/json"); - echo json_encode($responseData, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); - } - - public static function respondToSharing() { - $user = User::getUser(Session::getLoggedInUser()); - $clientId = $_POST['client_id']; - $userId = $user['userId']; - if ($_POST['consent'] === 'true') { - User::allowClientForUser($clientId, $userId); - } - $returnUrl = urldecode($_POST['returnUrl']); - header("Location: $returnUrl"); - } - - public static function respondToToken() { - $authServer = Server::getAuthServer(); - $tokenGenerator = Server::getTokenGenerator(); - - $requestFactory = new \Laminas\Diactoros\ServerRequestFactory(); - $request = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); - $requestBody = $request->getParsedBody(); - - $grantType = $requestBody['grant_type'] ?? null; - $clientId = $requestBody['client_id'] ?? null; - switch ($grantType) { - case "authorization_code": - $code = $requestBody['code']; - $codeInfo = $tokenGenerator->getCodeInfo($code); - $userId = $codeInfo['user_id']; - if (!$clientId) { - $clientId = $codeInfo['client_id']; - } - break; - case "refresh_token": - $refreshToken = $requestBody['refresh_token']; - $tokenInfo = $tokenGenerator->getCodeInfo($refreshToken); // FIXME: getCodeInfo should be named 'decrypt' or 'getInfo'? - $userId = $tokenInfo['user_id']; - if (!$clientId) { - $clientId = $tokenInfo['client_id']; - } - break; - default: - $userId = false; - break; - } - - $httpDpop = $request->getServerParams()['HTTP_DPOP']; - - $response = $authServer->respondToAccessTokenRequest($request); - - if (isset($userId)) { - $response = $tokenGenerator->addIdTokenToResponse( - $response, - $clientId, - $userId, - ($_SESSION['nonce'] ?? ''), - Server::getKeys()['privateKey'], - $httpDpop - ); - } - - Server::respond($response); - } } From 6c3e22c6e7f978f04c7a789ef6f3d1e5f36bffd8 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Thu, 17 Jul 2025 10:01:11 +0200 Subject: [PATCH 25/27] remove usage of md5 --- lib/User.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/User.php b/lib/User.php index 2b108d4..2affb0b 100644 --- a/lib/User.php +++ b/lib/User.php @@ -13,7 +13,7 @@ private static function generateTokenCode() { } private static function generateTokenHex() { - return md5(random_bytes(32)); + return bin2hex(random_bytes(16)); } private static function generateExpiresTimestamp($lifetime) { @@ -89,9 +89,9 @@ public static function createUser($newUser) { if (!self::validatePasswordStrength($newUser['password'])) { return false; } - $generatedUserId = md5(random_bytes(32)); + $generatedUserId = bin2hex(random_bytes(16)); while (self::userIdExists($generatedUserId)) { - $generatedUserId = md5(random_bytes(32)); + $generatedUserId = bin2hex(random_bytes(16)); } $query = Db::$pdo->prepare( 'INSERT INTO users VALUES (:userId, :email, :passwordHash, :data)' From fe2dd7cd091e600eb2dd48b3c9040fc395cf93a8 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Thu, 17 Jul 2025 10:01:27 +0200 Subject: [PATCH 26/27] remove usage of md5 --- lib/Routes/SolidIdp.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Routes/SolidIdp.php b/lib/Routes/SolidIdp.php index 55bc219..4d15982 100644 --- a/lib/Routes/SolidIdp.php +++ b/lib/Routes/SolidIdp.php @@ -117,8 +117,8 @@ public static function respondToRegister() { } - $generatedClientId = md5(random_bytes(32)); - $generatedClientSecret = md5(random_bytes(32)); + $generatedClientId = bin2hex(random_bytes(16)); // 32 chars for the client Id + $generatedClientSecret = bin2hex(random_bytes(32)); // and 64 chars for the client secret $clientData['client_id_issued_at'] = time(); $clientData['client_id'] = $generatedClientId; From 8a6762efebadc125e04918d2bfe5efa1d47ac61b Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Thu, 17 Jul 2025 10:07:44 +0200 Subject: [PATCH 27/27] return result in case something needs to use it --- lib/Mailer.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Mailer.php b/lib/Mailer.php index 3268f13..7d50853 100644 --- a/lib/Mailer.php +++ b/lib/Mailer.php @@ -53,7 +53,7 @@ public static function sendAccountCreated($data) { $mailer->Body = $mailHtmlBody; $mailer->AltBody = $mailPlainBody; - $mailer->send(); + return $mailer->send(); } public static function sendVerify($data) { @@ -82,7 +82,7 @@ public static function sendVerify($data) { $mailer->Body = $mailHtmlBody; $mailer->AltBody = $mailPlainBody; - $mailer->send(); + return $mailer->send(); } public static function sendResetPassword($data) { @@ -110,7 +110,7 @@ public static function sendResetPassword($data) { $mailer->Body = $mailHtmlBody; $mailer->AltBody = $mailPlainBody; - $mailer->send(); + return $mailer->send(); } public static function sendDeleteAccount($data) { @@ -138,6 +138,6 @@ public static function sendDeleteAccount($data) { $mailer->Body = $mailHtmlBody; $mailer->AltBody = $mailPlainBody; - $mailer->send(); + return $mailer->send(); } }