New Feature OpenID Connect (#7355)

* Refactor authentication to allow for OpenID Connect
The authentication class has been refactored to separate the `validate` function for the method `create_user_session`. This is needed for the OpenID Connect module to authenticate a user without going through the process of all authentication plugins. Only the function `check_user_cidr` has been rewritten to use a single loop instead of a double-loop.
Due to the fact that create_user_function is public, the $result associative array is checked to ensure the required fields are present and the UUID fields are valid UUIDs. Further checking could be done against the database to ensure the UUIDs exist but this has been suppressed at this time because database queries would be required.

* Database class modified to allow for login banners for OpenID Connect
The database class is responsible for creating the view for login. The OpenID application required banners to be present so that any OpenID Connect authentication mechanism could be used. Each banner displayed has the properties of: name, image, alt, and url. The name is the class name of the authentication plugin. This will match the action in the URL. The image is the image banner used for the login. The alt is the alternate text used for screen readers. Each authenticator is responsible for providing each of the field values.

* Added PHPDocs for the check_cidr function

* Add backwards compatibility for PHP versions below 8

* Remove get_banner_alt

* Update check_cidr to allow cidr array

* Update authentication.php

* Update functions.php

* Update authentication.php
This commit is contained in:
frytimo
2025-04-17 16:48:16 -03:00
committed by GitHub
parent 25ae0180ce
commit e7393cc7c0
4 changed files with 303 additions and 225 deletions

View File

@@ -47,14 +47,18 @@ class authentication {
*/
public function __construct() {
$this->database = database::new();
$this->user_uuid = null;
}
/**
* validate uses authentication plugins to check if a user is authorized to login
* @return array [plugin] => last plugin used to authenticate the user [authorized] => true or false
* @return array|false [plugin] => last plugin used to authenticate the user [authorized] => true or false
*/
public function validate() {
//set default return array as null
$result = null;
//get the domain_name and domain_uuid
if (!isset($this->domain_name) || !isset($this->domain_uuid)) {
$this->get_domain();
@@ -79,7 +83,7 @@ class authentication {
}
//check if contacts app exists
$contacts_exists = file_exists($_SERVER["DOCUMENT_ROOT"].PROJECT_PATH.'/core/contacts/') ? true : false;
$contacts_exists = file_exists(dirname(__DIR__, 4) . '/core/contacts/');
//use the authentication plugins
foreach ($_SESSION['authentication']['methods'] as $name) {
@@ -121,10 +125,10 @@ class authentication {
$result['user_uuid'] = $array["user_uuid"];
$result['contact_uuid'] = $array["contact_uuid"];
if ($contacts_exists) {
$result["contact_organization"] = $array["contact_organization"];
$result["contact_name_given"] = $array["contact_name_given"];
$result["contact_name_family"] = $array["contact_name_family"];
$result["contact_image"] = $array["contact_image"];
$result["contact_organization"] = $array["contact_organization"] ?? '';
$result["contact_name_given"] = $array["contact_name_given"] ?? '';
$result["contact_name_family"] = $array["contact_name_family"] ?? '';
$result["contact_image"] = $array["contact_image"] ?? '';
}
$result['domain_uuid'] = $array["domain_uuid"];
$result['authorized'] = $array["authorized"];
@@ -156,7 +160,7 @@ class authentication {
$_SESSION['authentication']['plugin'][$name]['username'] = $_SESSION['username'];
$_SESSION['authentication']['plugin'][$name]['user_uuid'] = $_SESSION['user_uuid'];
$_SESSION['authentication']['plugin'][$name]['user_email'] = $_SESSION['user_email'];
$_SESSION['authentication']['plugin'][$name]['authorized'] = 0;
$_SESSION['authentication']['plugin'][$name]['authorized'] = false;
}
}
}
@@ -178,222 +182,22 @@ class authentication {
}
}
//result array
// $result["plugin"] = "database";
// $result["domain_name"] = $_SESSION['domain_name'];
// if (!isset($_SESSION['username'])) {
// $result["username"] = $_SESSION['username'];
// }
// if (!isset($_SESSION['user_uuid'])) {
// $result["user_uuid"] = $_SESSION['user_uuid'];
// }
// $result["domain_uuid"] = $_SESSION['domain_uuid'];
// if (!isset($_SESSION['contact_uuid'])) {
// $result["contact_uuid"] = $_SESSION['contact_uuid'];
// }
// $result["authorized"] = $authorized;
//user is authorized - get user settings, check user cidr
if ($authorized) {
//get the cidr restrictions from global, domain, and user default settings
$user_settings = new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]);
$cidr_list = $user_settings->get('domain', 'cidr', []);
if (check_cidr($cidr_list, $_SERVER['REMOTE_ADDR'])) {
//user passed the cidr check
self::create_user_session($result, $user_settings);
} else {
//user failed the cidr check - no longer authorized
$authorized = false;
}
}
//get the user settings
$sql = "select * from v_user_settings ";
$sql .= "where domain_uuid = :domain_uuid ";
$sql .= "and user_uuid = :user_uuid ";
$sql .= "and user_setting_enabled = 'true' ";
$parameters['domain_uuid'] = $result["domain_uuid"];
$parameters['user_uuid'] = $result["user_uuid"];
$user_settings = $this->database->select($sql, $parameters, 'all');
unset($sql, $parameters);
//build the user cidr array
if (is_array($user_settings) && @sizeof($user_settings) != 0) {
foreach ($user_settings as $row) {
if ($row['user_setting_category'] == "domain" && $row['user_setting_subcategory'] == "cidr" && $row['user_setting_name'] == "array") {
$cidr_array[] = $row['user_setting_value'];
}
}
}
//check to see if user address is in the cidr array
if (isset($cidr_array) && !defined('STDIN')) {
$found = false;
foreach($cidr_array as $cidr) {
if (check_cidr($cidr, $_SERVER['REMOTE_ADDR'])) {
$found = true;
break;
}
}
if (!$found) {
//log the failed attempt
$plugin_classname = substr($class_name, 7);
user_logs::add($_SESSION['authentication']['plugin'][$plugin_classname]);
//destroy session
session_unset();
session_destroy();
//send http 403
header('HTTP/1.0 403 Forbidden', true, 403);
//exit the code
exit();
}
}
//set the session variables
$_SESSION["domain_uuid"] = $result["domain_uuid"];
$_SESSION["domain_name"] = $result["domain_name"];
$_SESSION["user_uuid"] = $result["user_uuid"];
$_SESSION["context"] = $result['domain_name'];
//build the session server array to validate the session
global $conf;
if (!isset($conf['session.validate'])) { $conf['session.validate'][] = 'HTTP_USER_AGENT'; }
foreach($conf['session.validate'] as $name) {
$server_array[$name] = $_SERVER[$name];
}
//save the user hash to be used in validate the session
$_SESSION["user_hash"] = hash('sha256', implode($server_array));
//user session array
$_SESSION["user"]["domain_uuid"] = $result["domain_uuid"];
$_SESSION["user"]["domain_name"] = $result["domain_name"];
$_SESSION["user"]["user_uuid"] = $result["user_uuid"];
$_SESSION["user"]["username"] = $result["username"];
$_SESSION["user"]["contact_uuid"] = $result["contact_uuid"];
if ($contacts_exists) {
$_SESSION["user"]["contact_organization"] = $result["contact_organization"] ?? null;
$_SESSION["user"]["contact_name"] = trim(($result["contact_name_given"] ?? '').' '.($result["contact_name_family"] ?? ''));
$_SESSION["user"]["contact_name_given"] = $result["contact_name_given"] ?? null;
$_SESSION["user"]["contact_name_family"] = $result["contact_name_family"] ?? null;
$_SESSION["user"]["contact_image"] = !empty($result["contact_image"]) && is_uuid($result["contact_image"]) ? $result["contact_image"] : null;
}
//empty the permissions
if (isset($_SESSION['permissions'])) {
unset($_SESSION['permissions']);
}
//get the groups assigned to the user
$group = new groups($this->database, $result["domain_uuid"], $result["user_uuid"]);
$group->session();
//get the permissions assigned to the user through the assigned groups
$permission = new permissions($this->database, $result["domain_uuid"], $result["user_uuid"]);
$permission->session();
//get the domains
if (file_exists($_SERVER["PROJECT_ROOT"]."/app/domains/app_config.php") && !is_cli()){
require_once "app/domains/resources/domains.php";
}
//get the user settings
if (is_array($user_settings) && @sizeof($user_settings) != 0) {
foreach ($user_settings as $row) {
$name = $row['user_setting_name'];
$category = $row['user_setting_category'];
$subcategory = $row['user_setting_subcategory'];
if (!empty($row['user_setting_value'])) {
if (empty($subcategory)) {
//$$category[$name] = $row['domain_setting_value'];
if ($name == "array") {
$_SESSION[$category][] = $row['user_setting_value'];
}
else {
$_SESSION[$category][$name] = $row['user_setting_value'];
}
}
else {
//$$category[$subcategory][$name] = $row['domain_setting_value'];
if ($name == "array") {
$_SESSION[$category][$subcategory][] = $row['user_setting_value'];
}
else {
$_SESSION[$category][$subcategory][$name] = $row['user_setting_value'];
}
}
}
}
}
unset($user_settings);
//get the extensions that are assigned to this user
if (file_exists($_SERVER["PROJECT_ROOT"]."/app/extensions/app_config.php")) {
if (isset($_SESSION["user"]) && is_uuid($_SESSION["user_uuid"]) && is_uuid($_SESSION["domain_uuid"]) && !isset($_SESSION['user']['extension'])) {
//get the user extension list
$_SESSION['user']['extension'] = null;
$sql = "select ";
$sql .= "e.extension_uuid, ";
$sql .= "e.extension, ";
$sql .= "e.number_alias, ";
$sql .= "e.user_context, ";
$sql .= "e.outbound_caller_id_name, ";
$sql .= "e.outbound_caller_id_number, ";
$sql .= "e.description ";
$sql .= "from ";
$sql .= "v_extension_users as u, ";
$sql .= "v_extensions as e ";
$sql .= "where ";
$sql .= "e.domain_uuid = :domain_uuid ";
$sql .= "and e.extension_uuid = u.extension_uuid ";
$sql .= "and u.user_uuid = :user_uuid ";
$sql .= "and e.enabled = 'true' ";
$sql .= "order by ";
$sql .= "e.extension asc ";
$parameters['domain_uuid'] = $_SESSION['domain_uuid'];
$parameters['user_uuid'] = $_SESSION['user_uuid'];
$result = $this->database->select($sql, $parameters, 'all');
if (is_array($result) && @sizeof($result) != 0) {
foreach($result as $x => $row) {
//set the destination
$destination = $row['extension'];
if (!empty($row['number_alias'])) {
$destination = $row['number_alias'];
}
//build the user array
$_SESSION['user']['extension'][$x]['user'] = $row['extension'];
$_SESSION['user']['extension'][$x]['number_alias'] = $row['number_alias'];
$_SESSION['user']['extension'][$x]['destination'] = $destination;
$_SESSION['user']['extension'][$x]['extension_uuid'] = $row['extension_uuid'];
$_SESSION['user']['extension'][$x]['outbound_caller_id_name'] = $row['outbound_caller_id_name'];
$_SESSION['user']['extension'][$x]['outbound_caller_id_number'] = $row['outbound_caller_id_number'];
$_SESSION['user']['extension'][$x]['user_context'] = $row['user_context'];
$_SESSION['user']['extension'][$x]['description'] = $row['description'];
//set the context
$_SESSION['user']['user_context'] = $row["user_context"];
$_SESSION['user_context'] = $row["user_context"];
}
}
unset($sql, $parameters, $result, $row);
}
}
//set the time zone
if (!isset($_SESSION["time_zone"]["user"])) { $_SESSION["time_zone"]["user"] = null; }
if (strlen($_SESSION["time_zone"]["user"] ?? '') === 0) {
//set the domain time zone as the default time zone
date_default_timezone_set($_SESSION['domain']['time_zone']['name']);
}
else {
//set the user defined time zone
date_default_timezone_set($_SESSION["time_zone"]["user"]);
}
//regenerate the session on login
//session_regenerate_id(true);
//set a session variable to indicate authorized is set to true
$_SESSION['authorized'] = true;
//add the username to the session - username session could be set so check_auth uses an authorized session variable instead
$_SESSION['username'] = $_SESSION['user']["username"];
} //authorized true
//set a session variable to indicate whether or not we are authorized
$_SESSION['authorized'] = $authorized;
//log the attempt
$plugin_classname = substr($class_name, 7);
@@ -403,6 +207,211 @@ class authentication {
return $result ?? false;
}
/**
* Creates a valid user session in the superglobal $_SESSION.
* <p>The $result must be a validated user with the appropriate variables set.<br>
* The associative array
* @global database $database
* @global string $conf
* @param array|bool $result Associative array containing: domain_uuid, domain_name, user_uuid, username. Contact keys can be empty, but should still be present. They include: contact_uuid, contact_name_given, contact_name_family, contact_image.
* @param array $user_settings User settings from the v_user_settings table or an empty array.
* @return void
*/
public static function create_user_session($result = [], $user_settings = []): void {
global $database;
//
// Validate data
//
if (empty($result)) {
return;
}
// Required keys
$required_keys = [
'domain_uuid' => true,
'domain_name' => true,
'user_uuid' => true,
'username' => true,
];
// Any missing required_fields are left in the $diff array.
// When all keys are present the $diff array will be empty.
$diff = array_diff_key($required_keys, $result);
// All required keys must be present in the $result associative array
if (!empty($diff)) {
return;
}
// Domain and User UUIDs must be valid UUIDs
if (!is_uuid($result['domain_uuid']) || !is_uuid($result['user_uuid'])) {
return;
}
// If Contact UUID has a value it must be a valid UUID
if (!empty($result['contact_uuid']) && !is_uuid($result['contact_uuid'])) {
return;
}
//
// All data validated continue to create session
//
// Set project root directory
$project_root = dirname(__DIR__, 4);
// Set the session variables
$_SESSION["domain_uuid"] = $result["domain_uuid"];
$_SESSION["domain_name"] = $result["domain_name"];
$_SESSION["user_uuid"] = $result["user_uuid"];
$_SESSION["context"] = $result['domain_name'];
// Build the session server array to validate the session
global $conf;
if (!isset($conf['session.validate'])) {
$conf['session.validate'][] = 'HTTP_USER_AGENT';
}
foreach ($conf['session.validate'] as $name) {
$server_array[$name] = $_SERVER[$name];
}
// Save the user hash to be used in check_auth
$_SESSION["user_hash"] = hash('sha256', implode($server_array));
// User session array
$_SESSION["user"]["domain_uuid"] = $result["domain_uuid"];
$_SESSION["user"]["domain_name"] = $result["domain_name"];
$_SESSION["user"]["user_uuid"] = $result["user_uuid"];
$_SESSION["user"]["username"] = $result["username"];
$_SESSION["user"]["contact_uuid"] = $result["contact_uuid"] ?? null; //contact_uuid is optional
// Check for contacts
if (file_exists($project_root . '/core/contacts/')) {
$_SESSION["user"]["contact_organization"] = $result["contact_organization"] ?? null;
$_SESSION["user"]["contact_name"] = trim(($result["contact_name_given"] ?? '') . ' ' . ($result["contact_name_family"] ?? ''));
$_SESSION["user"]["contact_name_given"] = $result["contact_name_given"] ?? null;
$_SESSION["user"]["contact_name_family"] = $result["contact_name_family"] ?? null;
$_SESSION["user"]["contact_image"] = !empty($result["contact_image"]) && is_uuid($result["contact_image"]) ? $result["contact_image"] : null;
}
//empty the permissions
if (isset($_SESSION['permissions'])) {
unset($_SESSION['permissions']);
}
//get the groups assigned to the user
$group = new groups($database, $result["domain_uuid"], $result["user_uuid"]);
$group->session();
//get the permissions assigned to the user through the assigned groups
$permission = new permissions($database, $result["domain_uuid"], $result["user_uuid"]);
$permission->session();
//get the domains
if (file_exists($project_root . '/app/domains/resources/domains.php') && !is_cli()) {
require_once $project_root . 'app/domains/resources/domains.php';
}
//store user settings in the session when available
if (is_array($user_settings)) {
foreach ($user_settings as $row) {
$name = $row['user_setting_name'];
$category = $row['user_setting_category'];
$subcategory = $row['user_setting_subcategory'];
if (!empty($row['user_setting_value'])) {
if (empty($subcategory)) {
//$$category[$name] = $row['domain_setting_value'];
if ($name == "array") {
$_SESSION[$category][] = $row['user_setting_value'];
} else {
$_SESSION[$category][$name] = $row['user_setting_value'];
}
} else {
//$$category[$subcategory][$name] = $row['domain_setting_value'];
if ($name == "array") {
$_SESSION[$category][$subcategory][] = $row['user_setting_value'];
} else {
$_SESSION[$category][$subcategory][$name] = $row['user_setting_value'];
}
}
}
}
}
//get the extensions that are assigned to this user
if (file_exists($project_root . '/app/extensions/app_config.php')) {
if (isset($_SESSION["user"]) && is_uuid($_SESSION["user_uuid"]) && is_uuid($_SESSION["domain_uuid"]) && !isset($_SESSION['user']['extension'])) {
$parameters = [];
//get the user extension list
$_SESSION['user']['extension'] = null;
$sql = "select ";
$sql .= "e.extension_uuid, ";
$sql .= "e.extension, ";
$sql .= "e.number_alias, ";
$sql .= "e.user_context, ";
$sql .= "e.outbound_caller_id_name, ";
$sql .= "e.outbound_caller_id_number, ";
$sql .= "e.description ";
$sql .= "from ";
$sql .= "v_extension_users as u, ";
$sql .= "v_extensions as e ";
$sql .= "where ";
$sql .= "e.domain_uuid = :domain_uuid ";
$sql .= "and e.extension_uuid = u.extension_uuid ";
$sql .= "and u.user_uuid = :user_uuid ";
$sql .= "and e.enabled = 'true' ";
$sql .= "order by ";
$sql .= "e.extension asc ";
$parameters['domain_uuid'] = $_SESSION['domain_uuid'];
$parameters['user_uuid'] = $_SESSION['user_uuid'];
$extensions = $database->select($sql, $parameters, 'all');
if (!empty($extensions)) {
foreach ($extensions as $x => $row) {
//set the destination
$destination = $row['extension'];
if (!empty($row['number_alias'])) {
$destination = $row['number_alias'];
}
//build the user array
$_SESSION['user']['extension'][$x]['user'] = $row['extension'];
$_SESSION['user']['extension'][$x]['number_alias'] = $row['number_alias'];
$_SESSION['user']['extension'][$x]['destination'] = $destination;
$_SESSION['user']['extension'][$x]['extension_uuid'] = $row['extension_uuid'];
$_SESSION['user']['extension'][$x]['outbound_caller_id_name'] = $row['outbound_caller_id_name'];
$_SESSION['user']['extension'][$x]['outbound_caller_id_number'] = $row['outbound_caller_id_number'];
$_SESSION['user']['extension'][$x]['user_context'] = $row['user_context'];
$_SESSION['user']['extension'][$x]['description'] = $row['description'];
//set the context
$_SESSION['user']['user_context'] = $row["user_context"];
$_SESSION['user_context'] = $row["user_context"];
}
}
}
}
//set the time zone
if (!isset($_SESSION["time_zone"]["user"])) {
$_SESSION["time_zone"]["user"] = null;
}
if (strlen($_SESSION["time_zone"]["user"] ?? '') === 0) {
//set the domain time zone as the default time zone
date_default_timezone_set($_SESSION['domain']['time_zone']['name']);
} else {
//set the user defined time zone
date_default_timezone_set($_SESSION["time_zone"]["user"]);
}
//regenerate the session on login
//session_regenerate_id(true);
//add the username to the session - username session could be set so check_auth uses an authorized session variable instead
$_SESSION['username'] = $_SESSION['user']["username"];
return;
}
/**
* get_domain used to get the domain name from the URL or username and then sets both domain_name and domain_uuid
*/

View File

@@ -122,6 +122,26 @@ class plugin_database {
$view->assign("login_password_description", $text['label-password_description']);
$view->assign("button_cancel", $text['button-cancel']);
$view->assign("button_forgot_password", $text['button-forgot_password']);
//assign openid values to the template
if ($settings->get('open_id', 'enabled', false)) {
$classes = $settings->get('open_id', 'methods', []);
$banners = [];
foreach ($classes as $open_id_class) {
if (class_exists($open_id_class)) {
$banners[] = [
'name' => $open_id_class,
'image' => $open_id_class::get_banner_image(),
'url' => '/app/open_id/open_id.php?action=' . $open_id_class,
];
}
}
if (count($banners) > 0) {
$view->assign('banners', $banners);
}
}
//assign user to the template
if (!empty($_SESSION['username'])) {
$view->assign("username", $_SESSION['username']);
}

View File

@@ -99,6 +99,18 @@
{if !empty($username)}
<a class='login_link' style='display: inline-block; margin-top: 15px;' href='{$project_path}/logout.php'>{$button_cancel}</a>
{/if}
{if isset($banners)}
<div style="display: flex; align-items: center; gap: 10px;">
<hr style="flex: 1; border: none; border-top: 1px solid #ccc;">
<span style="font-size: 0.8em; color: #666;">or</span>
<hr style="flex: 1; border: none; border-top: 1px solid #ccc;">
</div>
{foreach $banners as $banner}
<a class='{$banner.class}' style='display: inline-block; margin-top: 15px;' href='{$project_path}{$banner.url}'>
{$banner.image}
</a><br />
{/foreach}
{/if}
</div>
</form>
</div>

View File

@@ -27,6 +27,21 @@
Luis Daniel Lucio Quiroz <dlucio@okay.com.mx>
*/
if (!function_exists('str_contains')) {
/**
* Determine if a string contains a given substring
* <p>Performs a case-sensitive check indicating if <b>needle</b> is contained in <b>haystack</b>.</p>
* @param string $haystack The string to search in.
* @param string $needle The substring to search for in the <b>haystack</b>.
* @return bool Returns <i>true</i> if <b>needle</b> is in <b>haystack</b>, <i>false</i> otherwise
* @link https://www.php.net/manual/en/function.str-contains.php Official PHP documentation
* @see str_ends_with(), str_starts_with(), strpos(), stripos(), strrpos(), strripos(), strstr(), strpbrk(), substr(), preg_match()
*/
function str_contains(string $haystack, string $needle): bool {
return strpos($haystack, $needle) !== false;
}
}
if (!function_exists('str_starts_with')) {
/**
* Checks if a string starts with a given substring
@@ -116,13 +131,35 @@
if (!function_exists('check_cidr')) {
/**
* Checks if the $ip_address is within the range of the given $cidr
* @param string|array $cidr
* @param string $ip_address
* @return bool return true if the IP address is in CIDR or if it is empty
*/
function check_cidr($cidr, $ip_address) {
if (isset($cidr) && !empty($cidr)) {
list ($subnet, $mask) = explode('/', $cidr);
return ( ip2long($ip_address) & ~((1 << (32 - $mask)) - 1) ) == ip2long($subnet);
} else {
return false;
//no cidr restriction
if (empty($cidr)) {
return true;
}
//check to see if the user's remote address is in the cidr array
if (is_array($cidr) {
//cidr is an array
foreach ($cidr as $value) {
if (check_cidr($value, $ip_address)) {
return true;
}
}
} else {
//cidr is a string
list ($subnet, $mask) = explode('/', $cidr);
return (ip2long($ip_address) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet);
}
//value not found in cidr
return false;
}
}