From b422b6ded608807c7d3bb8b965b3797ca513610b Mon Sep 17 00:00:00 2001 From: winter Sparkles Date: Sat, 8 Aug 2026 23:01:51 +0100 Subject: implement a large chunk of the auth system itself :D --- Psso/AuthFlow.php | 9 ++++++ Psso/AuthInterface/Password.php | 9 ++++++ Psso/AuthInterface/Totp.php | 9 ++++++ Psso/AuthProvider.php | 10 ++++++ Psso/AuthProvider/ConfigFile.php | 16 ++++++++++ Psso/AuthProvider/Dummy.php | 20 ++++++++++++ Psso/Challenge.php | 59 +++++++++++++++++++++++++++++++++++ Psso/Challenge/Password.php | 32 +++++++++++++++++++ Psso/Challenge/Totp.php | 27 ++++++++++++++++ Psso/Challenge/Username.php | 16 ++++++++++ Psso/ChallengeResult.php | 21 +++++++++++++ Psso/Context.php | 13 ++++++++ Psso/Input.php | 42 +++++++++++++++++++++++++ Psso/Router.php | 66 ++++++++++++++++++++++++++++++++++++++++ Psso/XMLResponse.php | 44 +++++++++++++++++++++++++++ 15 files changed, 393 insertions(+) create mode 100644 Psso/AuthFlow.php create mode 100644 Psso/AuthInterface/Password.php create mode 100644 Psso/AuthInterface/Totp.php create mode 100644 Psso/AuthProvider.php create mode 100644 Psso/AuthProvider/ConfigFile.php create mode 100644 Psso/AuthProvider/Dummy.php create mode 100644 Psso/Challenge.php create mode 100644 Psso/Challenge/Password.php create mode 100644 Psso/Challenge/Totp.php create mode 100644 Psso/Challenge/Username.php create mode 100644 Psso/ChallengeResult.php create mode 100644 Psso/Context.php create mode 100644 Psso/Input.php create mode 100644 Psso/Router.php create mode 100644 Psso/XMLResponse.php (limited to 'Psso') diff --git a/Psso/AuthFlow.php b/Psso/AuthFlow.php new file mode 100644 index 0000000..c2a68ac --- /dev/null +++ b/Psso/AuthFlow.php @@ -0,0 +1,9 @@ +results); + return eval(file_get_contents(__DIR__ . '/../auth-flow.conf.php')); + } +} diff --git a/Psso/AuthInterface/Password.php b/Psso/AuthInterface/Password.php new file mode 100644 index 0000000..d20d759 --- /dev/null +++ b/Psso/AuthInterface/Password.php @@ -0,0 +1,9 @@ +config = $config; + } +} diff --git a/Psso/AuthProvider/ConfigFile.php b/Psso/AuthProvider/ConfigFile.php new file mode 100644 index 0000000..0d855d0 --- /dev/null +++ b/Psso/AuthProvider/ConfigFile.php @@ -0,0 +1,16 @@ +config['users']["$username.password-hash"] ?? null; + if (!isset($hash)) { + return false; + } + return password_verify($password, $hash); + } +} diff --git a/Psso/AuthProvider/Dummy.php b/Psso/AuthProvider/Dummy.php new file mode 100644 index 0000000..15d44ee --- /dev/null +++ b/Psso/AuthProvider/Dummy.php @@ -0,0 +1,20 @@ +context = $context; + $this->serial = base64_encode(random_bytes(9)); + } + + public static function create(Context $context): ?static { + return new static($context); + } + + public abstract function getInputs(): array; + + public abstract function validate( + AuthProvider $provider, array $inputData + ): ChallengeResult; + + public function addAsHtml(\SimpleXMLElement $parent): void { + $form = $parent->addChild('form'); + $form->addAttribute('method', 'post'); + foreach ($this->getInputs() as $input) { + $input->addAsHtml($form); + $this->inputs[$input->serial] = $input; + } + $serial = $form->addChild('input'); + $serial->addAttribute('type', 'hidden'); + $serial->addAttribute('name', 'challenge'); + $serial->addAttribute('value', $this->serial); + $submit = $form->addChild('button', L('challenge.continue')); + } + + public function findInput(string $serial): ?Input { + return $this->inputs[$serial] ?? null; + } + + protected static function requireInterface( + AuthProvider $provider, string $interface + ): void { + if (!($provider instanceof $interface)) { + throw new \InvalidArgumentException( + "Provider must implement $interface" + ); + }; + } + + protected static function requireKnownUser(Context $context): void { + if (!isset($context->user)) { + throw new \RuntimeException( + 'Unknown user, but known user required for ' . static::class + ); + } + } +} diff --git a/Psso/Challenge/Password.php b/Psso/Challenge/Password.php new file mode 100644 index 0000000..c20f5e2 --- /dev/null +++ b/Psso/Challenge/Password.php @@ -0,0 +1,32 @@ +context->user)) { + return [ + new Input('password', Input::SECRET, 'challenge.input.password') + ]; + } + return [ + new Input('username', Input::TEXT, 'challenge.input.username'), + new Input('password', Input::SECRET, 'challenge.input.password'), + ]; + } + + public function validate( + AuthProvider $provider, array $inputData + ): ChallengeResult { + self::requireInterface($provider, AuthInterface\Password::class); + $successful = $provider->validatePassword( + $inputData['username'] ?? $this->context->user, + $inputData['password'] + ); + return new ChallengeResult( + self::class, $successful, $inputData['username'] ?? null, + $successful ? null : 'challenge.message.wrong-password' + ); + } +} diff --git a/Psso/Challenge/Totp.php b/Psso/Challenge/Totp.php new file mode 100644 index 0000000..4fd47f4 --- /dev/null +++ b/Psso/Challenge/Totp.php @@ -0,0 +1,27 @@ +validateTotp( + $this->context->user, $inputData['otp'] + ); + return new ChallengeResult( + self::class, $success, null, + $success ? null : 'challenge.message.wrong-otp' + ); + } +} diff --git a/Psso/Challenge/Username.php b/Psso/Challenge/Username.php new file mode 100644 index 0000000..43de085 --- /dev/null +++ b/Psso/Challenge/Username.php @@ -0,0 +1,16 @@ +type = $type; + $this->successful = $successful; + $this->user = $user; + $this->message = $message; + } +} diff --git a/Psso/Context.php b/Psso/Context.php new file mode 100644 index 0000000..4a2cd82 --- /dev/null +++ b/Psso/Context.php @@ -0,0 +1,13 @@ +results[] = $result; + } +} diff --git a/Psso/Input.php b/Psso/Input.php new file mode 100644 index 0000000..019d79f --- /dev/null +++ b/Psso/Input.php @@ -0,0 +1,42 @@ +id = $id; + $this->type = $type; + $this->label = $label; + $this->required = $required; + $this->serial = base64_encode(random_bytes(9)); + } + + private function getUniqueName(): string { + return $this->id . '__' . $this->serial; + } + + public function addAsHtml(\SimpleXMLElement $parent): void { + $label = $parent->addChild('label', L($this->label)); + $label->addAttribute('for', 'input-' . $this->getUniqueName()); + $input = $parent->addChild('input'); + $input->addAttribute('type', $this->type); + $input->addAttribute('name', $this->getUniqueName()); + $input->addAttribute('id', 'input-' . $this->getUniqueName()); + if ($this->required) $input->addAttribute('required', ''); + } +} diff --git a/Psso/Router.php b/Psso/Router.php new file mode 100644 index 0000000..db1f851 --- /dev/null +++ b/Psso/Router.php @@ -0,0 +1,66 @@ +routesDir = $routesDir; + $this->config = $config; + } + + function registerStatusHandler(int $status, callable $handler) { + $this->statusHandlers[$status] = $handler; + } + + function setExceptionHandler(callable $handler) { + $this->exceptionHandler = \Closure::fromCallable($handler); + } + + private function dealWithError(int $status, string $path) { + http_response_code($status); + if (isset($this->statusHandlers[$status])) { + $this->statusHandlers[$status]($path); + } else { + echo '

' . $status . ' for ' . htmlspecialchars($path) . '

'; + } + } + + private function dealWithException(\Throwable $e) { + http_response_code(500); + if (isset($this->exceptionHandler)) { + ($this->exceptionHandler)($e); + } else { + echo '

Internal Server Error

'; + } + } + + function dispatch(string $path) { + if (str_contains($path, '..')) { + $this->dealWithError(404, $path); + return; + } + + if (file_exists($this->routesDir . $path . '.php')) { + require_once $this->routesDir . $path . '.php'; + } elseif (file_exists($this->routesDir . $path . 'index.php')) { + require_once $this->routesDir . $path . 'index.php'; + } else { + $this->dealWithError(404, $path); + return; + } + + if (function_exists($_SERVER['REQUEST_METHOD'])) { + try { + $_SERVER['REQUEST_METHOD']($this->config); + } catch (\Exception|\Error $e) { + $this->dealWithException($e); + } + } else { + $this->dealWithError(405, $path); + } + } +} diff --git a/Psso/XMLResponse.php b/Psso/XMLResponse.php new file mode 100644 index 0000000..1b17660 --- /dev/null +++ b/Psso/XMLResponse.php @@ -0,0 +1,44 @@ +doc = new \SimpleXMLElement("<$rootElement/>"); + foreach (self::$runFirst as $fn) { + $fn($this->doc); + } + } + + function sendRaw() { + header('Content-Type: application/xml'); + echo $this->doc->asXML(); + } + + function send() { + if (isset($_GET['RawXML'])) { + $this->sendRaw(); + return; + } + + //header('Content-Type: application/xhtml+xml'); + $processor = new \XSLTProcessor; + foreach (self::$stylesheets as $sheet) { + $processor->importStylesheet($sheet); + } + $root = \Dom\import_simplexml($this->doc); + echo $processor->transformToXml($root); + } +} -- cgit v1.3