aboutsummaryrefslogtreecommitdiff
path: root/Psso
diff options
context:
space:
mode:
authorwinter Sparkles2026-08-09 02:26:33 +0100
committerwinter Sparkles2026-08-09 02:26:33 +0100
commit3354d45762bbe0db33e3d26d8997e6f6624f24d7 (patch)
tree4bef4bf59488f443ed54ff6e30002268836ac259 /Psso
parent4bb659e1f7bb5850db5695a536428a9b71e3ffe5 (diff)
implement sessions and also log in/out
session data is stored in sqlite, not using php sessions
Diffstat (limited to 'Psso')
-rw-r--r--Psso/AuthInterface/UserExists.php6
-rw-r--r--Psso/AuthProvider/ConfigFile.php9
-rw-r--r--Psso/Challenge/Username.php12
-rw-r--r--Psso/Context.php8
-rw-r--r--Psso/Identity.php12
-rw-r--r--Psso/Session.php101
6 files changed, 144 insertions, 4 deletions
diff --git a/Psso/AuthInterface/UserExists.php b/Psso/AuthInterface/UserExists.php
new file mode 100644
index 0000000..a75d5c6
--- /dev/null
+++ b/Psso/AuthInterface/UserExists.php
@@ -0,0 +1,6 @@
+<?php
+namespace Psso\AuthInterface;
+
+interface UserExists {
+ public function userExists(string $username): bool;
+}
diff --git a/Psso/AuthProvider/ConfigFile.php b/Psso/AuthProvider/ConfigFile.php
index 0d855d0..688c193 100644
--- a/Psso/AuthProvider/ConfigFile.php
+++ b/Psso/AuthProvider/ConfigFile.php
@@ -2,15 +2,20 @@
namespace Psso\AuthProvider;
use Psso\{AuthProvider, AuthInterface};
-class ConfigFile extends AuthProvider implements AuthInterface\Password {
+class ConfigFile extends AuthProvider
+implements AuthInterface\Password, AuthInterface\UserExists {
public function validatePassword(
string $username,
#[\SensitiveParameter] string $password
): bool {
- $hash = $this->config['users']["$username.password-hash"] ?? null;
+ $hash = $this->config['users'][$username]['password-hash'] ?? null;
if (!isset($hash)) {
return false;
}
return password_verify($password, $hash);
}
+
+ public function userExists(string $username): bool {
+ return isset($this->config['users'][$username]);
+ }
}
diff --git a/Psso/Challenge/Username.php b/Psso/Challenge/Username.php
index 43de085..c8fa762 100644
--- a/Psso/Challenge/Username.php
+++ b/Psso/Challenge/Username.php
@@ -1,6 +1,6 @@
<?php
namespace Psso\Challenge;
-use Psso\{Challenge, AuthProvider, ChallengeResult, Input};
+use Psso\{Challenge, AuthProvider, AuthInterface, ChallengeResult, Input};
/** Just asks for a username and sets it in the context. Always succeeds. */
class Username extends Challenge {
@@ -11,6 +11,14 @@ class Username extends Challenge {
public function validate(
AuthProvider $provider, array $inputData
): ChallengeResult {
- return new ChallengeResult(self::class, true, $inputData['user'], null);
+ $success = true;
+ $message = null;
+ if ($provider instanceof AuthInterface\UserExists) {
+ $success = $provider->userExists($inputData['user']);
+ if (!$success) $message = 'challenge.message.wrong-username';
+ }
+ return new ChallengeResult(
+ self::class, $success, $inputData['user'], $message
+ );
}
}
diff --git a/Psso/Context.php b/Psso/Context.php
index 4a2cd82..5af2102 100644
--- a/Psso/Context.php
+++ b/Psso/Context.php
@@ -10,4 +10,12 @@ class Context {
public function addResult(ChallengeResult $result): void {
$this->results[] = $result;
}
+
+ public function setTag(string $tag): void {
+ if (!$this->hasTag($tag)) $this->tags[] = $tag;
+ }
+
+ public function hasTag(string $tag): bool {
+ return in_array($tag, $this->tags);
+ }
}
diff --git a/Psso/Identity.php b/Psso/Identity.php
new file mode 100644
index 0000000..68272d4
--- /dev/null
+++ b/Psso/Identity.php
@@ -0,0 +1,12 @@
+<?php
+namespace Psso;
+
+class Identity {
+ public protected(set) string $user;
+ public protected(set) array $groups;
+
+ public function __construct(string $user, array $groups = []) {
+ $this->user = $user;
+ $this->groups = $groups;
+ }
+}
diff --git a/Psso/Session.php b/Psso/Session.php
new file mode 100644
index 0000000..35a6228
--- /dev/null
+++ b/Psso/Session.php
@@ -0,0 +1,101 @@
+<?php
+namespace Psso;
+
+class Session {
+ protected static ?self $instance = null;
+ protected static ?array $pdoSetup = null;
+ protected static ?array $config = null;
+
+ protected \PDO $db;
+ private ?string $token = null;
+
+ public static function setDsn(
+ array $config,
+ string $dsn,
+ ?string $username = null,
+ #[\SensitiveParameter] ?string $password = null
+ ): void {
+ static::$config = $config;
+ static::$pdoSetup = [$dsn, $username, $password];
+ }
+
+ protected function __construct() {
+ $args = static::$pdoSetup;
+ $args[] = [\PDO::ATTR_PERSISTENT => true];
+ $this->db = new \PDO(...$args);
+ $this->setup();
+ }
+
+ public static function get(): self {
+ if (!isset(self::$instance)) {
+ self::$instance = new self();
+ }
+ self::$instance->setCookie();
+ return self::$instance;
+ }
+
+ protected function setup(): void {
+ $this->db->exec(
+ <<<'END'
+ create table if not exists session (
+ token text primary key not null,
+ identity text not null default 'N;',
+ challenges text not null default 'N;'
+ );
+ END
+ );
+ }
+
+ protected function domain(): string {
+ return explode('.', $_SERVER['HTTP_HOST'], 2)[1];
+ }
+
+ protected function setCookie(): void {
+ if (isset($_COOKIE['PSSO_session'])) return;
+ header(
+ 'Set-Cookie: PSSO_session=' . $this->currentToken()
+ . '; Domain=' . $this->domain()
+ . '; HttpOnly'
+ . '; Max-Age=' . static::$config['auth']['lifetime']
+ . '; Path=/'
+ . '; SameSite=Lax'
+ . '; Secure'
+ );
+ }
+
+ protected function currentToken(): string {
+ return $this->token = $this->token ??
+ $_COOKIE['PSSO_session'] ??
+ base64_encode(random_bytes(129));
+ }
+
+ protected function setColumn(string $column, mixed $data): void {
+ $stmt = $this->db->prepare(
+ "insert into session(token, $column) values (?, ?) "
+ . "on conflict do update set $column=excluded.$column"
+ );
+ $stmt->execute([$this->currentToken(), serialize($data)]);
+ }
+
+ public function getColumn(string $column): mixed {
+ $stmt = $this->db->prepare("select $column from session where token=?");
+ $stmt->execute([$this->currentToken()]);
+ return unserialize($stmt->fetchColumn(0));
+ }
+
+ public function setIdentity(mixed $identity): void {
+ $this->setColumn('identity', $identity);
+ }
+
+ public function getIdentity(): mixed {
+ return $this->getColumn('identity');
+ }
+
+ public function setChallenges(mixed $challenges): void {
+ $this->setColumn('challenges', $challenges);
+ }
+
+ public function getChallenges(): mixed {
+ return $this->getColumn('challenges');
+ }
+}