- Improve user verification in `authentication.php` by adding a check for user existence and throwing meaningful exceptions for missing users. - Refactor `webauthn.php` to handle Base64URL decoding and COSE key normalization for consistent WebAuthn library compatibility. - Extend error logging with additional context for debugging (e.g., public key hex representation). - Add utility functions for Base64URL decoding and checking PEM/DER format. - Update `passkeysRoute.php` to normalize public keys and handle errors gracefully during WebAuthn workflows.
232 lines
7.7 KiB
PHP
232 lines
7.7 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
use classes\totp;
|
|
use Exception;
|
|
use interfaces\authentication_i;
|
|
use objects\plate_scanners_o;
|
|
use objects\tokens_o;
|
|
use objects\users_o;
|
|
use objects\subusers_o;
|
|
|
|
class authentication implements authentication_i
|
|
{
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function authenticate(int $customer_number, string $password): bool
|
|
{
|
|
// Get the customer from the database
|
|
$customer = (new users_o())->getUserByCustomerNumber($customer_number);
|
|
// Check if the customer exists
|
|
if (!$customer->exists()) {
|
|
return false;
|
|
}
|
|
// Check if the customer has a password
|
|
if (!$customer->hasPassword()) {
|
|
// Make sure the customer group is 0, to prevent higher privilege users from accessing the system through the default password.
|
|
if ((int)$customer->group_id->value() !== 0) {
|
|
return false;
|
|
}
|
|
// Does have a customer number, set the password to the last 4 digits of the customer number
|
|
if (!empty($customer->customer_number->value()) && strlen($customer->customer_number->value()) > 4) {
|
|
$customer->setPassword(substr($customer->customer_number->value(), -4));
|
|
}
|
|
}
|
|
// Check if the password is correct
|
|
if (!$this->match_passwords($password, $customer->getPassword())) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public function is_2fa_enabled(users_o|subusers_o $user): bool
|
|
{
|
|
return $user->isTwoFactorEnabled();
|
|
}
|
|
|
|
public function create_2fa_token(int $id, string $type): string
|
|
{
|
|
// Create a temporary 2FA token
|
|
$token = bin2hex(random_bytes(32));
|
|
(new tokens_o())->create($id, $token, $type);
|
|
return $token;
|
|
}
|
|
|
|
public function verify_2fa_code(users_o|subusers_o $user, string $code): bool
|
|
{
|
|
$secret = $user->getTwoFactorSecret();
|
|
if (!$secret) {
|
|
return false;
|
|
}
|
|
return (new totp())->verifyCode($secret, $code);
|
|
}
|
|
|
|
public function match_passwords($password, $hash): bool
|
|
{
|
|
// Compare the password with the hash
|
|
return password_verify($password, $hash);
|
|
}
|
|
|
|
public function create_token(int $customer_number): string
|
|
{
|
|
// Create a token
|
|
$token = bin2hex(random_bytes(32));
|
|
// Resolve the user by customer number and ensure it exists to avoid accessing an uninitialized typed property
|
|
$user = (new users_o())->getUserByCustomerNumber($customer_number);
|
|
if (!$user->exists()) {
|
|
throw new \Exception('User not found for customer number: ' . $customer_number);
|
|
}
|
|
$user_id = $user->id;
|
|
// Save the token in the database
|
|
(new tokens_o())->create($user_id, $token, 'AUTH_TOKEN');
|
|
return $token;
|
|
}
|
|
|
|
public function create_employee_token(int $employee_id): string
|
|
{
|
|
// Create a token
|
|
$token = bin2hex(random_bytes(32));
|
|
// Save the token in the database
|
|
(new tokens_o())->create($employee_id, $token, 'AUTH_TOKEN');
|
|
return $token;
|
|
}
|
|
|
|
public function validate_token(string $token): bool
|
|
{
|
|
// First: try validating as a classic user auth token
|
|
$dbToken = (new tokens_o())->getToken($token);
|
|
if ($dbToken && $dbToken->id) {
|
|
return true;
|
|
}
|
|
// Fallback: try validating as a subuser session token
|
|
$subuser = (new subusers_o())->getSubuserBySessionToken($token);
|
|
if ($subuser !== null) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function get_user(): users_o|false
|
|
{
|
|
/**
|
|
* Get the user from the token
|
|
*/
|
|
// Get the token from the headers
|
|
$headers = getallheaders();
|
|
if (!isset($headers['Authorization'])) {
|
|
return false;
|
|
}
|
|
$token = $headers['Authorization'];
|
|
// Strip the Bearer prefix
|
|
$token = str_replace('Bearer ', '', $token);
|
|
// Get the token from the database
|
|
$token = (new tokens_o())->getToken($token);
|
|
// Check if the token exists
|
|
if (!$token->id) {
|
|
return false;
|
|
}
|
|
if ($token->type->value() === "AUTH_TOKEN_SUBUSER") {
|
|
// Get the customer number from the headers
|
|
if (!isset($headers['X-Customer-Number'])) {
|
|
return false;
|
|
}
|
|
$customer_number = (int)$headers['X-Customer-Number'];
|
|
// Get the user by the customer number
|
|
return (new users_o())->getUserByCustomerNumber($customer_number);
|
|
}
|
|
// Get the user from the database
|
|
return (new users_o())->getUserById($token->user_id->value());
|
|
}
|
|
|
|
public function get_plate_scanner(): plate_scanners_o|false
|
|
{
|
|
// Get the token from the headers
|
|
$headers = getallheaders();
|
|
$tmp = json_decode(file_get_contents('php://input'), true);
|
|
if (!is_array($tmp)) {
|
|
$tmp = [];
|
|
}
|
|
if (!isset($headers['Authorization']) && !isset($_GET['token']) && !isset($_POST['token']) && !isset($tmp['token'])) {
|
|
return false;
|
|
}
|
|
$token = $_GET['token'] ?? $headers['Authorization'] ?? $tmp['token'] ?? $_POST['token'];
|
|
// Strip the Bearer prefix (If the token is from the headers)
|
|
if (isset($headers['Authorization'])) {
|
|
$token = str_replace('Bearer ', '', $token);
|
|
}
|
|
// Get the token from the database
|
|
$token = (new plate_scanners_o())->getPlateScannerByApiKey($token);
|
|
// Check if the token exists
|
|
if (!isset($token->id)) {
|
|
return false;
|
|
}
|
|
// Get the plate scanner from the database
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function get_subuser(): subusers_o|false
|
|
{
|
|
// Try to resolve a subuser from an incoming bearer token or explicit token parameter
|
|
$headers = getallheaders();
|
|
$tmp = json_decode(file_get_contents('php://input'), true);
|
|
if (!is_array($tmp)) {
|
|
$tmp = [];
|
|
}
|
|
if (!isset($headers['Authorization']) && !isset($_GET['token']) && !isset($_POST['token']) && !isset($tmp['token'])) {
|
|
return false;
|
|
}
|
|
$token = $_GET['token'] ?? $headers['Authorization'] ?? $tmp['token'] ?? $_POST['token'];
|
|
// Strip the Bearer prefix (If the token is from the headers)
|
|
if (isset($headers['Authorization'])) {
|
|
$token = str_replace('Bearer ', '', $token);
|
|
}
|
|
// Resolve subuser session from cache
|
|
$subuser = (new subusers_o())->getSubuserBySessionToken($token);
|
|
if ($subuser === null) {
|
|
return false;
|
|
}
|
|
return $subuser;
|
|
}
|
|
|
|
public function hash_password($password): string
|
|
{
|
|
// Hash the password
|
|
return password_hash($password, PASSWORD_DEFAULT);
|
|
}
|
|
|
|
public function authenticateEmployee(int $user_id, string $password): bool
|
|
{
|
|
// Get the employee from the database
|
|
$employee = (new users_o())->getUserById($user_id);
|
|
// Check if the employee exists
|
|
if (!$employee->exists()) {
|
|
return false;
|
|
}
|
|
// Check if the password is correct
|
|
if (!$this->match_passwords($password, $employee->getPassword())) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public function get_subuser_customer_number_target(): int|false
|
|
{
|
|
/**
|
|
* Decode the headers
|
|
*/
|
|
$headers = getallheaders();
|
|
if (!isset($headers['X-Customer-Number'])) {
|
|
return false;
|
|
}
|
|
return (int)$headers['X-Customer-Number'];
|
|
}
|
|
} |