summaryrefslogtreecommitdiff
path: root/src/web
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2024-05-30 13:05:46 -0400
committerFreya Murphy <freya@freyacat.org>2024-05-30 13:05:46 -0400
commit39bcb09a367251bed7cfb445f546252547058e66 (patch)
treea1bb8e2c137e16202836ea6df8d7004b5e48e8a6 /src/web
parentam dumb (diff)
downloadldap_forwardauth-39bcb09a367251bed7cfb445f546252547058e66.tar.gz
ldap_forwardauth-39bcb09a367251bed7cfb445f546252547058e66.tar.bz2
ldap_forwardauth-39bcb09a367251bed7cfb445f546252547058e66.zip
many changes
Diffstat (limited to 'src/web')
-rw-r--r--src/web/helpers/auth.php140
-rw-r--r--src/web/helpers/ldap.php133
-rw-r--r--src/web/helpers/schema.php236
-rw-r--r--src/web/index.php51
-rw-r--r--src/web/router.php154
5 files changed, 570 insertions, 144 deletions
diff --git a/src/web/helpers/auth.php b/src/web/helpers/auth.php
index 9228706..187f556 100644
--- a/src/web/helpers/auth.php
+++ b/src/web/helpers/auth.php
@@ -1,85 +1,83 @@
<?php /* Copyright (c) 2024 Freya Murphy */
-$keys = array();
+class AuthHelper {
-function get_cookie() {
- $cookie_name = 'X-LDAP-Auth-Key';
- if(isset($_COOKIE[$cookie_name])) {
- return $_COOKIE[$cookie_name];
- } else {
- return FALSE;
- }
-}
-
-function store_cookie($key) {
- $cookie_name = 'X-LDAP-Auth-Key';
- $cookie_options = array (
- 'expires' => time() + 60*60*24*30,
- 'path' => '/',
- 'domain' => getenv("COOKIE_DOMAIN"),
- 'secure' => true,
- 'httponly' => true,
- 'samesite' => 'None'
- );
- setcookie(
- $cookie_name,
- $key,
- $cookie_options
- );
-}
+ private $session_lifetime_seconds;
-function load_key($key) {
- $file = "/tmp/$key";
- if (!file_exists($file))
- return FALSE;
- $content = explode("\n", file_get_contents($file));
- return array(
- 'user' => $content[0],
- 'time' => $content[1]
- );
-}
+ function __construct() {
+ $this->session_lifetime_seconds = 60 * 60 * 24 * 3;
+ }
-function store_key($key, $user) {
- $file = "/tmp/$key";
- $now = (string)time();
- $content = "$user\n{$now}";
- file_put_contents($file, $content, LOCK_EX);
-}
+ /**
+ * Generate a random token
+ * @param int $length
+ */
+ private function gen_token(int $length): string {
+ $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ $random = '';
-function get_random($n)
-{
- $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
- $randomString = '';
+ for ($i = 0; $i < $length; $i++) {
+ $index = rand(0, strlen($characters) - 1);
+ $random .= $characters[$index];
+ }
- for ($i = 0; $i < $n; $i++) {
- $index = rand(0, strlen($characters) - 1);
- $randomString .= $characters[$index];
- }
+ return $random;
+ }
- return $randomString;
-}
+ /**
+ * Saves a user into the session specified by their auth key
+ * @param Session $session - the session user data
+ */
+ public function save_session(Session $session): void {
+ $path = "/tmp/{$session->token}";
+ $data = json_encode($session->to_array());
+ file_put_contents($path, $data, LOCK_EX);
+ }
-function key_auth() {
- $key = get_cookie();
- if ($key === FALSE) {
- return FALSE;
+ /**
+ * Loads the auth session associated with a specific key
+ * @param string $token - the session $key
+ */
+ private function load_session(string $token): ?Session {
+ try {
+ $path = "/tmp/$token";
+ if (!file_exists($path)) {
+ return NULL;
+ }
+ $content = file_get_contents($path);
+ $json = json_decode($content, TRUE);
+ $session = new Session();
+ if ($session->from_array($json))
+ return NULL;
+ return $session;
+ } catch (Exception $e) {
+ return NULL;
+ }
}
- $data = load_key($key);
- if ($data === FALSE) {
- return FALSE;
+
+ /**
+ * Creates a new session for a user
+ */
+ public function create_session(User $user): Session {
+ $session = new Session();
+ $session->token = $this->gen_token(128);
+ $session->created = time();
+ $session->user = $user;
+ $session->reset_expiry();
+ $this->save_session($session);
+ return $session;
}
- $user = $data['user'];
- $time = $data['time'];
- $now = time();
- if ($time > $now || $now - $time > 60 * 60 * 24) {
- return FALSE;
+
+ /**
+ * Gets the current authed session
+ */
+ public function get_session(): ?Session {
+ $cookie_name = getenv("COOKIE_NAME");
+ if(!isset($_COOKIE[$cookie_name])) {
+ return NULL;
+ }
+ $token = $_COOKIE[$cookie_name];
+ return $this->load_session($token);
}
- store_key($key, $user);
- return $user;
-}
-function key_new($user) {
- $key = get_random(128);
- store_key($key, $user);
- store_cookie($key);
}
diff --git a/src/web/helpers/ldap.php b/src/web/helpers/ldap.php
index f3697cc..46bbe69 100644
--- a/src/web/helpers/ldap.php
+++ b/src/web/helpers/ldap.php
@@ -1,41 +1,120 @@
<?php /* Copyright (c) 2024 Freya Murphy */
-function ldap_auth($auth_username, $auth_password) {
- $url = getenv("LDAP_URL");
- $bind = getenv("LDAP_BIND_DN");
- $password = getenv("LDAP_BIND_PASSWORD");
- $bound = getenv("LDAP_BASE_DN");
- $filter = getenv("LDAP_FILTER");
- $uid = getenv("LDAP_UID");
+class LDAPHelper {
- $conn = @ldap_connect($url);
- if (!$conn) {
- return NULL;
+ private ?\LDAP\Connection $conn;
+ private array $env;
+ private array $matchers;
+
+ private ?string $bound;
+
+ function __construct() {
+ $this->env = array(
+ # ldap host
+ 'url' => getenv("LDAP_URL"),
+ # ldap credentials
+ 'bind' => getenv("LDAP_BIND_DN"),
+ 'password' => getenv("LDAP_BIND_PASSWORD"),
+ # ldap search
+ 'base' => getenv("LDAP_BASE_DN"),
+ 'filter' => getenv("LDAP_FILTER"),
+ 'uid' => getenv("LDAP_UID"),
+ );
+
+ $this->matchers = array(
+ 'username' => getenv("LDAP_USERNAME_MATCHER"),
+ 'email' => getenv("LDAP_EMAIL_MATCHER"),
+ 'first_name' => getenv("LDAP_FIRST_NAME_MATCHER"),
+ 'last_name' => getenv("LDAP_LAST_NAME_MATCHER"),
+ );
+
+ $this->bound = NULL;
+ $this->conn = NULL;
+ }
+
+ private function connect(): int {
+ if (($this->conn = @ldap_connect($this->env['url'])) == FALSE) {
+ $this->conn = NULL;
+ return 1;
+ }
+ @ldap_set_option($this->conn, LDAP_OPT_PROTOCOL_VERSION, 3);
+ return 0;
}
- ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3);
- $bind_conn = @ldap_bind($conn, $bind, $password);
- if (!$bind_conn) {
- return NULL;
+ private function rebind(): int {
+ if ($this->bound != $this->env['bind']) {
+ return $this->bind(
+ $this->env['bind'],
+ $this->env['password']);
+ }
+ return 0;
}
- $search = @ldap_search($conn, $bound, $filter);
+ public function bind(
+ string $dn,
+ #[\SensitiveParameter] string $password
+ ): int {
+ if ($this->conn == NULL && $this->connect()) {
+ return 1;
+ }
+ if (@ldap_bind($this->conn, $dn, $password) == FALSE) {
+ return 1;
+ }
+ $this->bound = $dn;
+ return 0;
+ }
- $info = @ldap_get_entries($conn, $search);
- $user = NULL;
- for ($i=0; $i<$info['count']; $i++) {
- $user = $info[$i];
- if (!array_key_exists($uid, $user))
- continue;
- if ($user[$uid][0] == $auth_username)
- break;
+ /**
+ * @param array<int,mixed> $user
+ */
+ private function find_entry(array $user, string $field): mixed {
+ if (!isset($user[$field]))
+ return NULL;
+ $data = $user[$field];
+ if (is_array($data))
+ $data = $data[0];
+ return $data;
}
- if ($user == NULL) {
- return FALSE;
+ public function search(
+ string ...$usernames
+ ): ?array {
+ if ($this->rebind())
+ return NULL;
+
+ $search = @ldap_search(
+ $this->conn,
+ $this->env['base'],
+ $this->env['filter']
+ );
+ if ($search == FALSE)
+ return NULL;
+
+ $info = @ldap_get_entries($this->conn, $search);
+
+ $users = array();
+
+ for ($i=0; $i<$info['count']; $i++) {
+ $user_arr = $info[$i];
+ $user_data = array (
+ 'dn' => $user_arr['dn'],
+ 'username' => $this->find_entry($user_arr, $this->matchers['username']),
+ 'email' => $this->find_entry($user_arr, $this->matchers['email']),
+ 'first_name' => $this->find_entry($user_arr, $this->matchers['first_name']),
+ 'last_name' => $this->find_entry($user_arr, $this->matchers['last_name'])
+ );
+ $user = new User();
+ if ($user->from_array($user_data)) {
+ continue;
+ }
+ if (count($usernames) && !in_array($user->username, $usernames)) {
+ continue;
+ }
+ $users[] = $user;
+ }
+
+ return $users;
}
- $succ = @ldap_bind($conn, $user['dn'], $auth_password);
- return !!$succ;
}
diff --git a/src/web/helpers/schema.php b/src/web/helpers/schema.php
new file mode 100644
index 0000000..6afa43f
--- /dev/null
+++ b/src/web/helpers/schema.php
@@ -0,0 +1,236 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+
+class User {
+
+ public ?string $dn;
+ public ?string $username;
+ public ?string $email;
+ public ?string $first_name;
+ public ?string $last_name;
+
+ function __construct() {}
+
+ /**
+ * Validates all required fields are set
+ */
+ private function validate(): int {
+ return (
+ $this->dn == NULL ||
+ $this->username == NULL ||
+ $this->email == NULL
+ ) ? 1 : 0;
+ }
+
+ /**
+ * Loads Data from the array to self
+ * @param array $data - the data to load
+ * @return int 0 on success, 1 on error
+ */
+ public function from_array(array $data): int {
+ $this->dn = NULL;
+ $this->username = NULL;
+ $this->email = NULL;
+ $this->first_name = NULL;
+ $this->last_name = NULL;
+
+ foreach ($data as $key => $value) {
+ if ($value == NULL)
+ continue;
+ $type = gettype($value);
+ switch ($key) {
+ case 'dn': {
+ if ($type != 'string')
+ return 1;
+ $this->dn = $value;
+ } break;
+ case 'username': {
+ if ($type != 'string')
+ return 1;
+ $this->username = $value;
+ } break;
+ case 'email': {
+ if ($type != 'string')
+ return 1;
+ $this->email = $value;
+ } break;
+ case 'first_name': {
+ if ($type != 'string')
+ return 1;
+ $this->first_name = $value;
+ } break;
+ case 'last_name': {
+ if ($type != 'string')
+ return 1;
+ $this->last_name = $value;
+ } break;
+ }
+ }
+
+ return $this->validate();
+ }
+
+ /**
+ * Converts the user into an array
+ * @return ?array<string,string>
+ */
+ public function to_array(): ?array {
+ if ($this->validate())
+ return NULL;
+ $data = array(
+ 'dn' => $this->dn,
+ 'username' => $this->username,
+ 'email' => $this->email
+ );
+ if ($this->first_name)
+ $data['first_name'] = $this->first_name;
+ if ($this->last_name)
+ $data['last_name'] = $this->last_name;
+ return $data;
+ }
+
+ /**
+ * Writes the HTTP headers
+ */
+ public function write_headers(): int {
+ if ($this->validate())
+ return 1;
+
+ $header_username = getenv("HTTP_USERNAME_HEADER");
+ $header_email = getenv("HTTP_EMAIL_HEADER");
+ $header_first = getenv("HTTP_FIRST_NAME_HEADER");
+ $header_last = getenv("HTTP_LAST_NAME_HEADER");
+
+ header("{$header_username}: {$this->username}");
+ header("{$header_email}: {$this->email}");
+ if ($this->first_name)
+ header("{$header_first}: {$this->first_name}");
+ if ($this->last_name)
+ header("{$header_last}: {$this->last_name}");
+
+ return 0;
+ }
+
+}
+
+class Session {
+
+ public ?User $user;
+ public ?int $created;
+ public ?int $expires;
+ public ?string $token;
+
+ private int $session_lifetime_seconds;
+
+ function __construct() {
+ $this->session_lifetime_seconds = 60 * 60 * 24 * 3;
+ }
+
+ /**
+ * Validates all required fields are set
+ */
+ private function validate(): int {
+ if (
+ $this->user == NULL ||
+ $this->created == NULL ||
+ $this->expires == NULL ||
+ $this->token == NULL
+ ) {
+ return 1;
+ }
+ if ($this->expires < time())
+ return 1;
+ return 0;
+ }
+
+ /**
+ * Loads Data from the array to self
+ * @param array $data - the data to load
+ * @return int 0 on success, 1 on error
+ */
+ public function from_array(array $data): int {
+ $this->user = NULL;
+ $this->created = NULL;
+ $this->expires = NULL;
+ $this->token = NULL;
+
+ foreach ($data as $key => $value) {
+ if ($value == NULL)
+ continue;
+ $type = gettype($value);
+ switch ($key) {
+ case 'user': {
+ $this->user = new User();
+ if ($this->user->from_array($value))
+ return 1;
+ } break;
+ case 'created': {
+ if ($type != 'integer')
+ return 1;
+ $this->created = $value;
+ } break;
+ case 'expires': {
+ if ($type != 'integer')
+ return 1;
+ $this->expires = $value;
+ } break;
+ case 'token': {
+ if ($type != 'string')
+ return 1;
+ $this->token = $value;
+ } break;
+ }
+ }
+
+ return $this->validate();
+ }
+
+ /**
+ * Renew the expiry clock
+ */
+ public function reset_expiry(): void {
+ $this->expires = time() + $this->session_lifetime_seconds;
+ }
+
+ /**
+ * Converts the session into an array
+ * @return ?array<string,mixed>
+ */
+ public function to_array(): ?array {
+ if ($this->validate())
+ return NULL;
+ return array(
+ 'user' => $this->user->to_array(),
+ 'created' => $this->created,
+ 'expires' => $this->expires,
+ 'token' => $this->token
+ );
+ }
+
+ /**
+ * Writes the HTTP headers
+ */
+ public function write_headers(): int {
+ if ($this->validate())
+ return 1;
+ if ($this->user->write_headers())
+ return 1;
+
+ $cookie_name = getenv("COOKIE_NAME");
+ $cookie_options = array (
+ 'expires' => $this->expires,
+ 'path' => '/',
+ 'domain' => getenv("COOKIE_DOMAIN"),
+ 'secure' => true,
+ 'httponly' => true,
+ 'samesite' => 'Lax'
+ );
+ setcookie(
+ $cookie_name,
+ $this->token,
+ $cookie_options
+ );
+
+ return 0;
+ }
+
+}
diff --git a/src/web/index.php b/src/web/index.php
index 8ae7a95..d4271c9 100644
--- a/src/web/index.php
+++ b/src/web/index.php
@@ -6,52 +6,11 @@ $webroot = dirname(__FILE__);
$publicroot = realpath(dirname(__FILE__) . '/../public');
// load stuff
+require($webroot . '/helpers/schema.php');
require($webroot . '/helpers/ldap.php');
require($webroot . '/helpers/auth.php');
+require($webroot . '/router.php');
-// start session
-function page($file, $data = array()) {
- extract($data);
- $webroot = $GLOBALS['webroot'];
- require($webroot . '/views/header.php');
- require($webroot . "/views/$file.php");
- require($webroot . '/views/footer.php');
-}
-
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- parse_str(file_get_contents('php://input'), $post);
- $res = ldap_auth($post['username'], $post['password']);
- $msg = '';
- $title = '';
- if ($res) {
- $msg = 'Authenticated. You can now go back to your content';
- $title = 'Success';
- key_new($post['username']);
- } else {
- $msg = 'Invalid Credentials';
- $title = 'Error';
- }
- page('message', array(
- 'title' => $title,
- 'msg' => $msg
- ));
-} else {
- if (($user = key_auth())) {
- http_response_code(200);
- header("X-Webauth-User: $user");
- die();
- }
-
- $host = $_SERVER['HTTP_HOST'];
- $env = getenv("HTTP_HOST");
- if ($_SERVER['REQUEST_URI'] !== '/login') {
- // we are being forwarded authed
- // redirect
- http_response_code(303);
- header("Location: http://$env/login");
- } else {
- page('login', array(
- 'title' => 'Login'
- ));
- }
-}
+// do the
+$router = new Router();
+$router->handle();
diff --git a/src/web/router.php b/src/web/router.php
new file mode 100644
index 0000000..91deaa2
--- /dev/null
+++ b/src/web/router.php
@@ -0,0 +1,154 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+
+class Router {
+
+ private $ldap;
+ private $auth;
+
+ private $domain;
+
+ function __construct() {
+ $this->ldap = new LDAPHelper();
+ $this->auth = new AuthHelper();
+
+ $this->domain = getenv("HTTP_HOST");
+ }
+
+ /**
+ * Displays a page to the user
+ * @param string $file
+ * @param array<string,mixed> $data
+ */
+ private function send_page(
+ string $file,
+ array $data = array()
+ ): void {
+ extract($data);
+ $webroot = $GLOBALS['webroot'];
+ require($webroot . '/views/header.php');
+ require($webroot . "/views/$file.php");
+ require($webroot . '/views/footer.php');
+ }
+
+ /**
+ * Displays a message to the user (message page)
+ * @param string $title
+ * @param string $msg
+ * @param int $code
+ */
+ private function send_message(
+ string $title,
+ string $msg
+ ): void {
+ $this->send_page('message', array(
+ 'title' => $title,
+ 'msg' => $msg
+ ));
+ }
+
+ /**
+ * Gets the HTTP request information
+ */
+ private function get_req(): array {
+ return array(
+ 'path' => $_SERVER['REQUEST_URI'],
+ 'method' => $_SERVER['REQUEST_METHOD'],
+ );
+ }
+
+ /**
+ * @param array<string> $fields
+ */
+ private function get_post_info(
+ string ...$fields
+ ): ?array {
+ $values = array();
+
+ try {
+ $temp = NULL;
+ parse_str(file_get_contents('php://input'), $temp);
+ foreach ($temp as $key => $value) {
+ $_POST[$key] = $value;
+ }
+ } catch (Exception $_e) {}
+
+ foreach ($fields as $key) {
+ if (!isset($_POST[$key]))
+ return NULL;
+ $values[$key] = $_POST[$key];
+ }
+
+ return $values;
+ }
+
+ private function handle_login(): void {
+ $info = $this->get_post_info('username', 'password');
+ if ($info == NULL) {
+ http_response_code(400);
+ $this->send_message('Bad Requet', 'Credentials were not supplied');
+ return;
+ }
+
+ $user = $this->ldap->search($info['username']);
+ if ($user == NULL || !count($user)) {
+ http_response_code(400);
+ $this->send_message('Bad Requst', 'User does not exist');
+ return;
+ }
+
+ $user = $user[0];
+
+ if ($this->ldap->bind(
+ $user->dn,
+ $info['password']
+ )) {
+ http_response_code(400);
+ $this->send_message('Bad Requst', 'Invalid Credentials');
+ return;
+ }
+
+ $session = $this->auth->create_session($user);
+
+ http_response_code(200);
+ $session->write_headers();
+ $this->send_message('Success', 'Authenticated. You can now go back to your content');
+ }
+
+ /**
+ * Handles the HTTP request
+ * @param array<string,string> $req
+ */
+ private function handle_req(array $req): void {
+ if ($req['method'] == 'POST') {
+ $this->handle_login();
+ return;
+ }
+ $session = $this->auth->get_session();
+ if ($session == NULL) {
+ // user is NOT authenticated
+ if ($req['path'] == '/login') {
+ // user is requesting login page
+ http_response_code(200);
+ $this->send_page('login', array(
+ 'title' => 'Login'
+ ));
+ } else {
+ // user is trying to forward auth
+ // redirect them to login
+ http_response_code(303);
+ header("Location: http://{$this->domain}/login");
+ }
+ } else {
+ // user is authenticated
+ $session->reset_expiry();
+ $session->write_headers();
+ $this->auth->save_session($session);
+ }
+ }
+
+ public function handle(): void {
+ $req = $this->get_req();
+ $this->handle_req($req);
+ }
+
+}