diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 16bf7a0..0000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-# These are supported funding model platforms
-
-open_collective: leaf
-github: leafsphp
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..ddd79f9
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,34 @@
+name: Lint code
+
+on: [push]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: true
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.head_ref }}
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.2'
+ extensions: json, zip, dom, curl, libxml, mbstring
+ tools: composer:v2
+ coverage: none
+
+ - name: Install PHP dependencies
+ run: composer update --no-interaction --no-progress
+
+ - name: Run Linter
+ run: composer run lint
+
+ - name: Commit changes
+ uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ commit_message: 'chore: fix styling'
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index 6ee6474..0000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-name: Run Tests
-
-on: ['push', 'pull_request']
-
-env:
- MYSQL_DATABASE: leaf
- DB_USER: root
- DB_PASSWORD: root
-
-jobs:
- ci:
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ubuntu-latest]
- php: ['7.4', '8.0', '8.1', '8.2', '8.3']
-
- name: PHP ${{ matrix.php }} - ${{ matrix.os }}
-
- steps:
- - name: Checkout
- uses: actions/checkout@v2
-
- - name: Initialize MySQL
- run: sudo systemctl start mysql.service
-
- - name: Initialize first database
- run: |
- mysql -e 'CREATE DATABASE ${{ env.MYSQL_DATABASE }};' \
- -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }}
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- tools: composer:v2
- coverage: xdebug
-
- - name: Install PHP dependencies
- run: composer update --no-interaction --no-progress
-
- - name: All Tests
- run: composer run-script test
diff --git a/.gitignore b/.gitignore
index 55e18db..536447e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,9 +5,8 @@ composer.lock
package-lock.json
vendor/
test/
+old/
*.tests.php
-workflows
-!.github/workflows
# OS Generated
.DS_Store*
@@ -18,3 +17,7 @@ Thumbs.db
# phpstorm
.idea/*
+
+# Alchemy
+.alchemy
+.phpunit.result.cache
diff --git a/alchemy.config.php b/alchemy.config.php
deleted file mode 100644
index 7224127..0000000
--- a/alchemy.config.php
+++ /dev/null
@@ -1,26 +0,0 @@
- 'pest',
-
- // php unit options
- 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
- 'xsi:noNamespaceSchemaLocation' => './vendor/phpunit/phpunit/phpunit.xsd',
- 'bootstrap' => 'vendor/autoload.php',
- 'colors' => true,
-
- // you can have multiple testsuites
- 'testsuites' => [
- 'directory' => './tests'
- ],
-
- // coverage options
- 'coverage' => [
- 'processUncoveredFiles' => true,
- 'include' => [
- './app' => '.php',
- './src' => '.php'
- ]
- ]
-];
diff --git a/alchemy.yml b/alchemy.yml
new file mode 100644
index 0000000..1177beb
--- /dev/null
+++ b/alchemy.yml
@@ -0,0 +1,35 @@
+app:
+ - src
+
+tests:
+ engine: pest
+ parallel: true
+ paths:
+ - tests
+ files:
+ - '*.test.php'
+
+lint:
+ preset: PSR12
+ rules:
+ no_unused_imports: true
+ not_operator_with_successor_space: false
+ single_quote: true
+
+actions:
+ run:
+ - lint
+ - tests
+ os:
+ - ubuntu-latest
+ php:
+ extensions: json, zip, dom, curl, libxml, mbstring, PDO_PGSQL
+ versions:
+ - '8.3'
+ - '8.2'
+ - '8.1'
+ - '8.0'
+ - '7.4'
+ events:
+ - push
+ - pull_request
diff --git a/composer.json b/composer.json
index 661f552..e5ce7b8 100644
--- a/composer.json
+++ b/composer.json
@@ -26,18 +26,19 @@
"Leaf\\": "src"
},
"files": [
- "src/functions.php"
- ]
+ "src/functions.php"
+ ]
},
"minimum-stability": "stable",
- "prefer-stable": true,
+ "prefer-stable": true,
"require": {
"leafs/date": "*",
"leafs/password": "*",
"leafs/session": "*",
"leafs/db": "*",
"leafs/form": "*",
- "leafs/http": "*"
+ "leafs/http": "*",
+ "firebase/php-jwt": "^6.10"
},
"config": {
"allow-plugins": {
@@ -45,10 +46,14 @@
}
},
"require-dev": {
- "leafs/alchemy": "^1.0",
- "pestphp/pest": "^1.0 | ^2.0"
+ "pestphp/pest": "^1.0 | ^2.0",
+ "friendsofphp/php-cs-fixer": "^3.64",
+ "leafs/alchemy": "^2.0"
},
"scripts": {
- "test": "vendor/bin/pest --colors=always --coverage"
+ "test": "./vendor/bin/alchemy setup --test",
+ "alchemy": "./vendor/bin/alchemy setup",
+ "lint": "./vendor/bin/alchemy setup --lint",
+ "actions": "./vendor/bin/alchemy setup --actions"
}
}
diff --git a/phpunit.xml b/phpunit.xml
deleted file mode 100644
index 8f4b58c..0000000
--- a/phpunit.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
- ./tests
-
-
-
-
- ./app
- ./src
-
-
-
diff --git a/src/Auth.php b/src/Auth.php
index b28d247..9d43740 100644
--- a/src/Auth.php
+++ b/src/Auth.php
@@ -2,582 +2,586 @@
namespace Leaf;
-use Leaf\Auth\Core;
-use Leaf\Helpers\Authentication;
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+use Leaf\Auth\Config;
+use Leaf\Auth\User;
use Leaf\Helpers\Password;
+use Leaf\Http\Session;
/**
* Leaf Simple Auth
* -------------------------
- * Simple, straightforward authentication.
+ * Simple, lightweight authentication.
*
* @author Michael Darko
* @since 1.5.0
- * @version 2.0.0
+ * @version 3.0.0
*/
-class Auth extends Core
+class Auth
{
/**
- * Simple user login
- *
- * @param array $credentials User credentials
- *
- * @return array|false false or all user info + tokens + session data
+ * The currently authenticated user
+ * @var User
*/
- public static function login(array $credentials)
- {
- static::leafDbConnect();
+ protected $user;
- $table = static::$settings['DB_TABLE'];
+ /**
+ * Internal instance of Leaf DB
+ * @var Db
+ */
+ protected $db;
- if (static::config('USE_SESSION')) {
- static::useSession();
- }
+ /**
+ * All errors caught
+ * @var array
+ */
+ protected $errorsArray = [];
- $passKey = static::$settings['PASSWORD_KEY'];
- $password = $credentials[$passKey] ?? null;
+ /**
+ * Connect leaf auth to the database
+ * @param array $dbConfig Configuration for leaf db connection
+ * @return $this
+ */
+ public function connect($dbConfig = [])
+ {
+ $this->db = new Db();
+ $this->db->connect($dbConfig);
- if (isset($credentials[$passKey])) {
- unset($credentials[$passKey]);
- } else {
- static::$settings['AUTH_NO_PASS'] = true;
- }
+ return $this;
+ }
- $user = static::$db->select($table)->where($credentials)->fetchAssoc();
+ /**
+ * Connect to database using environment variables
+ *
+ * @param array $pdoOptions Options for PDO connection
+ * @return $this
+ */
+ public function autoConnect(array $pdoOptions = [])
+ {
+ $this->db = new Db();
+ $this->db->autoConnect($pdoOptions);
- if (!$user) {
- static::$errors['auth'] = static::$settings['LOGIN_PARAMS_ERROR'];
- return false;
- }
+ return $this;
+ }
- if (static::$settings['AUTH_NO_PASS'] === false) {
- $passwordIsValid = false;
+ /**
+ * Pass in db connection instance directly
+ *
+ * @param \PDO $connection A connection instance of your db
+ * @return $this;
+ */
+ public function dbConnection(\PDO $connection)
+ {
+ $this->db = new Db();
+ $this->db->connection($connection);
- if (static::$settings['PASSWORD_VERIFY'] !== false && isset($user[$passKey])) {
- if (is_callable(static::$settings['PASSWORD_VERIFY'])) {
- $passwordIsValid = call_user_func(static::$settings['PASSWORD_VERIFY'], $password, $user[$passKey]);
- } else if (static::$settings['PASSWORD_VERIFY'] === Password::MD5) {
- $passwordIsValid = md5($password) === $user[$passKey];
- } else {
- $passwordIsValid = Password::verify($password, $user[$passKey]);
- }
- }
+ return $this;
+ }
- if (!$passwordIsValid) {
- static::$errors['password'] = static::$settings['LOGIN_PASSWORD_ERROR'];
- return false;
- }
+ /**
+ * Get/Set Leaf Auth config
+ *
+ * @param string|array $config The auth config key or array of config
+ * @param mixed $value The value if $config is a string
+ */
+ public function config($config, $value = null)
+ {
+ if (is_string($config) && $value === null) {
+ return Config::get($config);
}
- $token = Authentication::generateSimpleToken(
- $user[static::$settings['ID_KEY']],
- static::config('TOKEN_SECRET'),
- static::config('TOKEN_LIFETIME')
+ Config::set(
+ is_string($config)
+ ? [$config => $value]
+ : $config
);
+ }
- if (isset($user[static::$settings['ID_KEY']])) {
- $userId = $user[static::$settings['ID_KEY']];
+ /**
+ * Sign a user in
+ * ---
+ * Verify user credentials and sign them in with token or session
+ *
+ * @param array $credentials User credentials
+ * @return bool
+ */
+ public function login(array $credentials): bool
+ {
+ $this->checkDbConnection();
- if (static::$settings['HIDE_ID']) {
- unset($user[static::$settings['ID_KEY']]);
- }
- }
+ $table = Config::get('db.table');
+ $passwordKey = Config::get('password.key');
- if (static::$settings['HIDE_PASSWORD'] && (isset($user[$passKey]) || !$user[$passKey])) {
- unset($user[$passKey]);
- }
+ $userPassword = $credentials[$passwordKey] ?? null;
- if (!$token) {
- static::$errors = array_merge(static::$errors, Authentication::errors());
- return false;
+ if ($userPassword) {
+ unset($credentials[$passwordKey]);
}
- if (static::config('USE_SESSION')) {
- if (isset($userId)) {
- $user[static::$settings['ID_KEY']] = $userId;
+ try {
+ $user = $this->db->select($table)->where($credentials)->first();
+
+ if (!$user) {
+ $this->errorsArray['auth'] = Config::get('messages.loginParamsError');
+ return false;
}
+ } catch (\Throwable $th) {
+ throw new \Exception($th->getMessage());
+ }
- self::setUserToSession($user, $token);
+ if ($passwordKey !== false) {
+ $passwordIsValid = (Config::get('password.verify') !== false && isset($user[$passwordKey]))
+ ? ((is_callable(Config::get('password.verify')))
+ ? call_user_func(Config::get('password.verify'), $userPassword, $user[$passwordKey])
+ : Password::verify($userPassword, $user[$passwordKey]))
+ : false;
- if (static::config('SESSION_REDIRECT_ON_LOGIN')) {
- exit(header('location: ' . static::config('GUARD_HOME')));
+ if (!$passwordIsValid) {
+ $this->errorsArray['password'] = Config::get('messages.loginPasswordError');
+ return false;
}
}
- $response['user'] = $user;
- $response['token'] = $token;
+ $this->user = new User($user);
- return $response;
+ return true;
}
/**
- * Simple user registration
- *
- * @param array $credentials Information for new user
- * @param array $uniques Parameters which should be unique
+ * Register a new user
+ * ---
+ * Save a new user to the database
*
- * @return array|false false or all user info + tokens + session data
+ * @param array $userData User data
+ * @return bool
*/
- public static function register(array $credentials, array $uniques = [])
+ public function register(array $userData): bool
{
- static::leafDbConnect();
+ $this->checkDbConnection();
- $table = static::$settings['DB_TABLE'];
- $passKey = static::$settings['PASSWORD_KEY'];
+ $table = Config::get('db.table');
+ $passwordKey = Config::get('password.key');
+ $passwordEncode = Config::get('password.encode');
- if (!isset($credentials[$passKey])) {
- static::$settings['AUTH_NO_PASS'] = true;
+ if ($passwordEncode !== false && $passwordKey !== false) {
+ $userData[$passwordKey] = (is_callable($passwordEncode))
+ ? call_user_func($passwordEncode, $userData[$passwordKey])
+ : Password::hash($userData[$passwordKey]);
}
- if (static::$settings['AUTH_NO_PASS'] === false) {
- if (static::$settings['PASSWORD_ENCODE'] !== false) {
- if (is_callable(static::$settings['PASSWORD_ENCODE'])) {
- $credentials[$passKey] = call_user_func(static::$settings['PASSWORD_ENCODE'], $credentials[$passKey]);
- } else if (static::$settings['PASSWORD_ENCODE'] === 'md5') {
- $credentials[$passKey] = md5($credentials[$passKey]);
- } else {
- $credentials[$passKey] = Password::hash($credentials[$passKey]);
- }
- }
+ if (Config::get('timestamps')) {
+ $now = (new Date())->tick()->format(Config::get('timestamps.format'));
+ $userData['created_at'] = $now;
+ $userData['updated_at'] = $now;
}
- if (static::$settings['USE_TIMESTAMPS']) {
- $now = (new \Leaf\Date())->tick()->format(static::$settings['TIMESTAMP_FORMAT']);
- $credentials['created_at'] = $now;
- $credentials['updated_at'] = $now;
- }
-
- if (static::$settings['USE_UUID'] !== false) {
- $credentials[static::$settings['ID_KEY']] = static::$settings['USE_UUID'];
+ if (isset($credentials[Config::get('id.key')])) {
+ $userData[Config::get('id.key')] = is_callable($userData[Config::get('id.key')])
+ ? call_user_func($userData[Config::get('id.key')])
+ : $userData[Config::get('id.key')];
}
try {
- $query = static::$db->insert($table)->params($credentials)->unique($uniques)->execute();
- } catch (\Throwable $th) {
- trigger_error($th->getMessage());
- }
+ $query = $this->db->insert($table)->params($userData)->unique(Config::get('unique'))->execute();
- if (!$query) {
- static::$errors = array_merge(static::$errors, static::$db->errors());
- return false;
+ if (!$query) {
+ $this->errorsArray = array_merge($this->errorsArray, $this->db->errors());
+ return false;
+ }
+ } catch (\Throwable $th) {
+ throw new \Exception($th->getMessage());
}
- $user = static::$db->select($table)->where($credentials)->fetchAssoc();
+ $user = $this->db->select($table)->where($userData)->first();
if (!$user) {
- static::$errors = array_merge(static::$errors, static::$db->errors());
- return false;
- }
-
- $token = Authentication::generateSimpleToken(
- $user[static::$settings['ID_KEY']],
- static::config('TOKEN_SECRET'),
- static::config('TOKEN_LIFETIME')
- );
-
- if (isset($user[static::$settings['ID_KEY']])) {
- $userId = $user[static::$settings['ID_KEY']];
- }
-
- if (static::$settings['HIDE_ID']) {
- unset($user[static::$settings['ID_KEY']]);
- }
-
- if (static::$settings['HIDE_PASSWORD'] && (isset($user[$passKey]) || !$user[$passKey])) {
- unset($user[$passKey]);
- }
-
- if (!$token) {
- static::$errors = array_merge(static::$errors, Authentication::errors());
+ $this->errorsArray = array_merge($this->errorsArray, $this->db->errors());
return false;
}
- if (static::config('USE_SESSION')) {
- if (static::config('SESSION_ON_REGISTER')) {
- if (isset($userId)) {
- $user[static::$settings['ID_KEY']] = $userId;
- }
-
- self::setUserToSession($user, $token);
-
- exit(header('location: ' . static::config('GUARD_HOME')));
- } else {
- if (static::config('SESSION_REDIRECT_ON_REGISTER')) {
- exit(header('location: ' . static::config('GUARD_LOGIN')));
- }
- }
- }
+ $this->user = new User($user);
- $response['user'] = $user;
- $response['token'] = $token;
-
- return $response;
+ return true;
}
/**
- * Simple user update
- *
- * @param array $credentials New information for user
- * @param array $uniques Parameters which should be unique
+ * Update user data
+ * ---
+ * Update user data in the database
*
- * @return array|false all user info + tokens + session data
+ * @param array $userData User data
+ * @return bool
*/
- public static function update(array $credentials, array $uniques = [])
+ public function update(array $userData): bool
{
- static::leafDbConnect();
+ $this->checkDbConnection();
- $table = static::$settings['DB_TABLE'];
+ $user = $this->user();
- if (static::config('USE_SESSION')) {
- static::useSession();
- }
-
- $passKey = static::$settings['PASSWORD_KEY'];
- $loggedInUser = static::user();
-
- if (!$loggedInUser) {
- static::$errors['auth'] = 'Not authenticated';
+ if (!$user) {
return false;
}
- $where = isset($loggedInUser[static::$settings['ID_KEY']]) ? [static::$settings['ID_KEY'] => $loggedInUser[static::$settings['ID_KEY']]] : $loggedInUser;
+ $idKey = Config::get('id.key');
+ $table = Config::get('db.table');
- if (!isset($credentials[$passKey])) {
- static::$settings['AUTH_NO_PASS'] = true;
+ if (Config::get('timestamps')) {
+ $userData['updated_at'] = (new Date())->tick()->format(Config::get('timestamps.format'));
}
- if (
- static::$settings['AUTH_NO_PASS'] === false &&
- static::$settings['PASSWORD_ENCODE'] !== false
- ) {
- if (is_callable(static::$settings['PASSWORD_ENCODE'])) {
- $credentials[$passKey] = call_user_func(static::$settings['PASSWORD_ENCODE'], $credentials[$passKey]);
- } else if (static::$settings['PASSWORD_ENCODE'] === 'md5') {
- $credentials[$passKey] = md5($credentials[$passKey]);
- } else {
- $credentials[$passKey] = Password::hash($credentials[$passKey]);
- }
- }
-
- if (static::$settings['USE_TIMESTAMPS']) {
- $credentials['updated_at'] = (new \Leaf\Date())->tick()->format(static::$settings['TIMESTAMP_FORMAT']);
- }
-
- if (count($uniques) > 0) {
- foreach ($uniques as $unique) {
- if (!isset($credentials[$unique])) {
- trigger_error("$unique not found in credentials.");
+ if (count(Config::get('unique')) > 0) {
+ foreach (Config::get('unique') as $unique) {
+ if (!isset($userData[$unique])) {
+ continue;
}
- $data = static::$db->select($table)->where($unique, $credentials[$unique])->fetchAssoc();
-
- $wKeys = array_keys($where);
- $wValues = array_values($where);
+ $data = $this->db->select($table, Config::get('id.key'))->where($unique, $userData[$unique])->first();
- if (isset($data[$wKeys[0]]) && $data[$wKeys[0]] != $wValues[0]) {
- static::$errors[$unique] = "$unique already exists";
+ if ($data && $data[Config::get('id.key')] !== $this->id()) {
+ $this->errorsArray[$unique] = "$unique already exists";
}
}
- if (count(static::$errors) > 0) {
+ if (count($this->errorsArray) > 0) {
return false;
}
}
try {
- $query = static::$db->update($table)->params($credentials)->where($where)->execute();
+ $query = $this->db->update($table)->params($userData)->where($idKey, $this->user->{$idKey})->execute();
+
+ if (!$query) {
+ $this->errorsArray = array_merge($this->errorsArray, $this->db->errors());
+ return false;
+ }
} catch (\Throwable $th) {
- trigger_error($th->getMessage());
+ throw new \Exception($th->getMessage());
}
- if (!$query) {
- static::$errors = array_merge(static::$errors, static::$db->errors());
- return false;
+ if (Config::get('session')) {
+ session_regenerate_id();
}
- if (isset($credentials['updated_at'])) {
- unset($credentials['updated_at']);
+ foreach ($userData as $key => $value) {
+ $this->user->{$key} = $value;
}
- $user = static::$db->select($table)->where($credentials)->fetchAssoc();
+ return true;
+ }
- if (!$user) {
- static::$errors = array_merge(static::$errors, static::$db->errors());
- return false;
- }
+ /**
+ * Update user password
+ * ---
+ * Update user password in the database
+ *
+ * @param string $oldPassword Old password
+ * @param string $newPassword New password
+ * @return bool
+ */
+ public function updatePassword(string $oldPassword, string $newPassword): bool
+ {
+ $this->checkDbConnection();
- $token = Authentication::generateSimpleToken(
- $user[static::$settings['ID_KEY']],
- static::config('TOKEN_SECRET'),
- static::config('TOKEN_LIFETIME')
- );
+ $user = $this->user();
- if (isset($user[static::$settings['ID_KEY']])) {
- $userId = $user[static::$settings['ID_KEY']];
+ if (!$user) {
+ return false;
}
- if (static::$settings['HIDE_ID'] && isset($user[static::$settings['ID_KEY']])) {
- unset($user[static::$settings['ID_KEY']]);
- }
+ $passwordKey = Config::get('password.key');
- if (static::$settings['HIDE_PASSWORD'] && (isset($user[$passKey]) || !$user[$passKey])) {
- unset($user[$passKey]);
- }
+ if (Config::get('password.verify') !== false && isset($user->{$passwordKey})) {
+ $passwordIsValid = (is_callable(Config::get('password.verify')))
+ ? call_user_func(Config::get('password.verify'), $oldPassword, $user->{$passwordKey})
+ : Password::verify($oldPassword, $user->{$passwordKey});
- if (!$token) {
- static::$errors = array_merge(static::$errors, Authentication::errors());
- return false;
+ if (!$passwordIsValid) {
+ $this->errorsArray['password'] = Config::get('messages.loginPasswordError');
+ return false;
+ }
}
- if (static::config('USE_SESSION')) {
- if (isset($userId)) {
- $user[static::$settings['ID_KEY']] = $userId;
- }
+ $newPassword = (Config::get('password.encode') !== false)
+ ? ((is_callable(Config::get('password.encode')))
+ ? call_user_func(Config::get('password.encode'), $newPassword)
+ : Password::hash($newPassword))
+ : $newPassword;
- static::$session->set('AUTH_USER', $user);
- static::$session->set('HAS_SESSION', true);
+ try {
+ $query = $this->db->update(Config::get('db.table'))
+ ->params([$passwordKey => $newPassword])
+ ->where(Config::get('id.key'), $this->id())
+ ->execute();
- if (static::config('SAVE_SESSION_JWT')) {
- static::$session->set('AUTH_TOKEN', $token);
+ if (!$query) {
+ $this->errorsArray = array_merge($this->errorsArray, $this->db->errors());
+ return false;
}
-
- return $user;
+ } catch (\Throwable $th) {
+ throw new \Exception($th->getMessage());
}
- $response['user'] = $user;
- $response['token'] = $token;
-
- return $response;
- }
+ $this->user->{$passwordKey} = $newPassword;
- /**
- * Manually start an auth session
- */
- public static function useSession()
- {
- static::config('USE_SESSION', true);
- static::$session = Auth\Session::init(static::config('SESSION_COOKIE_PARAMS'));
+ return true;
}
/**
- * Throw a 'use session' warning
+ * Sign a user out
+ * ---
+ * Sign out the currently authenticated user
+ *
+ * @param string|array|callable|null $redirectUrl Redirect to this url after logout
+ * @return bool
*/
- protected static function sessionCheck()
+ public function logout($action = null): bool
{
- if (!static::config('USE_SESSION')) {
- trigger_error('Turn on USE_SESSION to use this feature.');
- }
-
- if (!static::$session) {
- static::useSession();
+ if (Config::get('session')) {
+ Session::unset('auth');
}
- }
- /**
- * A simple auth guard: 'guest' pages can't be viewed when logged in,
- * 'auth' pages can't be viewed without authentication
- *
- * @param string $type The type of guard/guard options
- */
- public static function guard(string $type)
- {
- static::sessionCheck();
+ $this->user = null;
- if ($type === 'guest' && static::status()) {
- exit(header('location: ' . static::config('GUARD_HOME'), true, 302));
+ if (is_callable($action)) {
+ $action($this);
+ return true;
}
- if ($type === 'auth' && !static::status()) {
- exit(header('location: ' . static::config('GUARD_LOGIN'), true, 302));
+ if ($action) {
+ response()->redirect($action);
+ exit;
}
+
+ return true;
}
/**
- * Check session status
+ * Get the id of the currently authenticated user
+ * @return string|int
*/
- public static function status()
+ public function id()
{
- static::sessionCheck();
- static::expireSession();
-
- return static::$session->get('AUTH_USER') ?? false;
+ return Config::get('session')
+ ? $this->getFromSession('auth.id')
+ : ($this->user ? $this->user->id() : ($this->parseToken()['user.id'] ?? null));
}
/**
- * Return the user id encoded in token or session
+ * Get the currently authenticated user
+ * @return User|null
*/
- public static function id()
+ public function user()
{
- static::leafDbConnect();
+ if (Config::get('session')) {
+ $userId = $this->getFromSession('auth.id');
- if (static::config('USE_SESSION')) {
- if (static::expireSession()) {
+ if (!$userId) {
return null;
}
-
- return static::$session->get('AUTH_USER')[static::$settings['ID_KEY']] ?? null;
}
- $payload = static::validateToken(static::config('TOKEN_SECRET'));
-
- return $payload->user_id ?? null;
- }
-
- /**
- * Get the current user data from token
- *
- * @param array $hidden Fields to hide from user array
- */
- public static function user(array $hidden = [])
- {
- $table = static::$settings['DB_TABLE'];
+ if ($this->user) {
+ return $this->user;
+ }
- if (!static::id()) {
- if (static::config('USE_SESSION')) {
- return static::$session->get('AUTH_USER');
- }
+ $userId = $this->id();
+ if (!$userId) {
return null;
}
- $user = static::$db->select($table)->where(static::$settings['ID_KEY'], static::id())->fetchAssoc();
+ $idKey = Config::get('id.key');
+ $table = Config::get('db.table');
- if (count($hidden) > 0) {
- foreach ($hidden as $item) {
- if (isset($user[$item]) || !$user[$item]) {
- unset($user[$item]);
- }
+ try {
+ $user = $this->db->select($table)->where($idKey, $userId)->first();
+
+ if (!$user) {
+ $this->errorsArray = $this->db->errors();
+ return null;
}
+ } catch (\Throwable $th) {
+ throw new \Exception($th->getMessage());
}
- return $user;
+ return $this->user = new User(
+ $user
+ );
}
/**
- * End a session
- *
- * @param string $location A route to redirect to after logout
+ * Get data generated on user login
+ * @return object|null
*/
- public static function logout(?string $location = null)
+ public function data()
{
- static::sessionCheck();
+ $user = $this->user();
- static::$session->destroy();
-
- if (is_string($location)) {
- \Leaf\Http\Headers::status(302);
- $route = static::config($location) ?? $location;
-
- exit(header("location: $route"));
+ if (!$user) {
+ return null;
}
+
+ return $user->getAuthInfo();
}
/**
- * @return bool
+ * Get generated access tokens
+ * @return array|null
*/
- private static function expireSession(): bool
+ public function tokens()
{
- self::sessionCheck();
+ $user = $this->user();
+
+ if (!$user) {
+ return null;
+ }
- $sessionTtl = static::$session->get('SESSION_TTL');
+ return $user->tokens();
+ }
- if (!$sessionTtl) {
- return false;
+ /**
+ * Register auth middleware for your Leaf apps
+ * @param string $middleware The middleware to register
+ * @param callable $callback The callback to run if middleware fails
+ */
+ public function middleware(string $middleware, callable $callback)
+ {
+ if (!class_exists(\Leaf\App::class)) {
+ throw new \Exception('This feature is only available for Leaf apps');
}
- $isSessionExpired = time() > $sessionTtl;
+ if ($middleware === 'auth.required') {
+ return app()->registerMiddleware('auth.required', function () use ($callback) {
+ if (!$this->user()) {
+ $callback();
+ }
+ });
+ }
- if ($isSessionExpired) {
- static::$session->unset('AUTH_USER');
- static::$session->unset('HAS_SESSION');
- static::$session->unset('AUTH_TOKEN');
- static::$session->unset('SESSION_STARTED_AT');
- static::$session->unset('SESSION_LAST_ACTIVITY');
- static::$session->unset('SESSION_TTL');
+ if ($middleware === 'auth.guest') {
+ return app()->registerMiddleware('auth.guest', function () use ($callback) {
+ if ($this->user()) {
+ $callback();
+ }
+ });
}
- return $isSessionExpired;
+ app()->registerMiddleware($middleware, $callback);
}
/**
- * Session last active
+ * Parse the current user's token
*/
- public static function lastActive()
+ public function parseToken()
{
- static::sessionCheck();
+ $bearerToken = $this->getTokenFromRequest();
+
+ if ($bearerToken === null) {
+ return null;
+ }
- return time() - static::$session->get('SESSION_LAST_ACTIVITY');
+ return (array) JWT::decode(
+ $bearerToken,
+ new Key(Config::get('token.secret'), 'HS256')
+ );
}
/**
- * Refresh session
+ * Return the current db instance
*
- * @param bool $clearData Remove existing session data
+ * @return Db
*/
- public static function refresh(bool $clearData = true)
+ public function db()
{
- static::sessionCheck();
-
- $success = static::$session->regenerate($clearData);
-
- static::$session->set('SESSION_STARTED_AT', time());
- static::$session->set('SESSION_LAST_ACTIVITY', time());
- static::setSessionTtl();
-
- return $success;
+ return $this->db;
}
- /**
- * Check how long a session has been going on
- */
- public static function length()
+ protected function checkDbConnection(): void
{
- static::sessionCheck();
+ if (!$this->db && function_exists('db')) {
+ if (db()->connection() instanceof \PDO || db()->autoConnect()) {
+ $this->db = db();
+ }
+ }
- return time() - static::$session->get('SESSION_STARTED_AT');
+ if (!$this->db) {
+ throw new \Exception('You need to connect to your database first');
+ }
}
- /**
- * @param array $user
- * @param string $token
- *
- * @return void
- */
- private static function setUserToSession(array $user, string $token): void
+ protected function getFromSession($value)
{
- session_regenerate_id();
+ if ($this->checkAndExpireSession()) {
+ return null;
+ }
- static::$session->set('AUTH_USER', $user);
- static::$session->set('HAS_SESSION', true);
- static::setSessionTtl();
+ return Session::get($value);
+ }
- if (static::config('SAVE_SESSION_JWT')) {
- static::$session->set('AUTH_TOKEN', $token);
+ protected function sessionCheck()
+ {
+ if (!Config::get('session')) {
+ throw new \Exception('Turn on sessions to use this feature.');
}
}
- /**
- * @return void
- */
- private static function setSessionTtl(): void
+ protected function checkAndExpireSession(): bool
{
- $sessionLifetime = static::config('SESSION_LIFETIME');
+ $sessionTtl = Session::get('auth.ttl');
- if ($sessionLifetime === 0) {
- return;
+ if (!$sessionTtl) {
+ return false;
}
- if (is_int($sessionLifetime)) {
- static::$session->set('SESSION_TTL', time() + $sessionLifetime);
- return;
+ $isSessionExpired = time() > $sessionTtl;
+
+ if ($isSessionExpired) {
+ Session::unset('auth');
}
- $sessionLifetimeInTime = strtotime($sessionLifetime);
+ return $isSessionExpired;
+ }
- if (!$sessionLifetimeInTime) {
- throw new \Exception('Provided string could not be converted to time');
+ protected function getTokenFromRequest()
+ {
+ $headers = null;
+
+ if (isset($_SERVER['Authorization'])) {
+ $headers = trim($_SERVER['Authorization']);
+ } elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) {
+ $headers = trim($_SERVER['HTTP_AUTHORIZATION']);
+ } elseif (function_exists('apache_request_headers')) {
+ $requestHeaders = apache_request_headers();
+ // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
+ $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
+
+ if (isset($requestHeaders['Authorization'])) {
+ $headers = trim($requestHeaders['Authorization']);
+ }
+ }
+
+ if (!empty($headers)) {
+ if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
+ return $matches[1];
+ }
}
- static::$session->set('SESSION_TTL', $sessionLifetimeInTime);
+ $this->errorsArray['token'] = 'Access token not found';
+
+ return null;
+ }
+
+ protected function getTokenFromSession()
+ {
+ return Session::get('auth.token');
+ }
+
+ /**
+ * Return all errors caught
+ */
+ public function errors(): array
+ {
+ return $this->errorsArray;
}
}
diff --git a/src/Auth/Config.php b/src/Auth/Config.php
new file mode 100644
index 0000000..dfa9e04
--- /dev/null
+++ b/src/Auth/Config.php
@@ -0,0 +1,71 @@
+ 'id',
+ 'db.table' => 'users',
+
+ 'timestamps' => true,
+ 'timestamps.format' => 'YYYY-MM-DD HH:mm:ss',
+
+ 'password.encode' => null,
+ 'password.verify' => null,
+ 'password.key' => 'password',
+
+ 'unique' => ['email', 'username'],
+ 'hidden' => ['field.id', 'field.password'],
+
+ 'session' => false,
+ 'session.lifetime' => 60 * 60 * 24,
+ 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'],
+
+ 'token.lifetime' => null,
+ 'token.secret' => '@_leaf$0Secret!',
+
+ 'messages.loginParamsError' => 'Incorrect credentials!',
+ 'messages.loginPasswordError' => 'Password is incorrect!',
+ ];
+
+ /**
+ * Set Leaf Auth config
+ */
+ public static function set($config): void
+ {
+ static::$config = array_merge(static::$config, $config);
+ }
+
+ /**
+ * Overwrite Leaf Auth config
+ */
+ public static function overwrite($config): void
+ {
+ static::$config = $config;
+ }
+
+ /**
+ * Get Leaf Auth config
+ */
+ public static function get($key = null)
+ {
+ if ($key) {
+ return static::$config[$key] ?? null;
+ }
+
+ return static::$config;
+ }
+}
diff --git a/src/Auth/Core.php b/src/Auth/Core.php
deleted file mode 100644
index 28f587b..0000000
--- a/src/Auth/Core.php
+++ /dev/null
@@ -1,201 +0,0 @@
- 'users',
- 'AUTH_NO_PASS' => false,
- 'USE_TIMESTAMPS' => true,
- 'TIMESTAMP_FORMAT' => 'c',
- 'PASSWORD_ENCODE' => null,
- 'PASSWORD_VERIFY' => null,
- 'PASSWORD_KEY' => 'password',
- 'HIDE_ID' => true,
- 'ID_KEY' => 'id',
- 'USE_UUID' => false,
- 'HIDE_PASSWORD' => true,
- 'LOGIN_PARAMS_ERROR' => 'Incorrect credentials!',
- 'LOGIN_PASSWORD_ERROR' => 'Password is incorrect!',
- 'USE_SESSION' => false,
- 'SESSION_ON_REGISTER' => false,
- 'GUARD_LOGIN' => '/auth/login',
- 'GUARD_REGISTER' => '/auth/register',
- 'GUARD_HOME' => '/home',
- 'GUARD_LOGOUT' => '/auth/logout',
- 'SAVE_SESSION_JWT' => false,
- 'TOKEN_LIFETIME' => null,
- 'TOKEN_SECRET' => '@_leaf$0Secret!',
- 'SESSION_REDIRECT_ON_LOGIN' => true,
- 'SESSION_LIFETIME' => self::TIMESTAMP_OF_ONE_DAY,
- 'SESSION_COOKIE_PARAMS' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'],
- ];
-
- /**
- * @var \Leaf\Db
- */
- protected static $db;
-
- /**
- * @var \Leaf\Http\Session
- */
- protected static $session;
-
- /**
- * Connect leaf auth to the database
- *
- * @param string|array $host Host Name or full config
- * @param string $dbname Database name
- * @param string $user Database username
- * @param string $password Database password
- * @param string $dbtype Type of database: mysql, postgres, sqlite, ...
- * @param array $pdoOptions Options for PDO connection
- */
- public static function connect(
- $host,
- string $dbname,
- string $user,
- string $password,
- string $dbtype,
- array $pdoOptions = []
- ) {
- $db = new \Leaf\Db();
- $db->connect($host, $dbname, $user, $password, $dbtype, $pdoOptions);
- static::$db = $db;
- }
-
- /**
- * Connect to database using environment variables
- *
- * @param array $pdoOptions Options for PDO connection
- */
- public static function autoConnect(array $pdoOptions = [])
- {
- $db = new \Leaf\Db();
- $db->autoConnect($pdoOptions);
- static::$db = $db;
- }
-
- /**
- * Pass in db connetion instance directly
- *
- * @param \PDO $connection A connection instance of your db
- */
- public static function dbConnection(\PDO $connection)
- {
- $db = new \Leaf\Db();
- $db->connection($connection);
- static::$db = $db;
- }
-
- /**
- * Auto connect to leaf db
- */
- protected static function leafDbConnect()
- {
- if (!static::$db && function_exists('db')) {
- if (db()->connection() instanceof \PDO || db()->autoConnect()) {
- static::$db = db();
- }
- }
-
- if (!static::$db) {
- trigger_error('You need to connect to your database first');
- }
- }
-
- /**
- * Set auth config
- *
- * @param string|array $config The auth config key or array of config
- * @param mixed $value The value if $config is a string
- */
- public static function config($config, $value = null)
- {
- if (is_array($config)) {
- foreach ($config as $key => $configValue) {
- static::config($key, $configValue);
- }
- } else {
- if ($value === null) {
- return static::$settings[$config] ?? null;
- }
-
- static::$settings[$config] = $value;
- }
- }
-
- /**
- * Validate Json Web Token
- *
- * @param string $token The token validate
- * @param string $secretKey The secret key used to encode token
- */
- public static function validateUserToken(string $token, ?string $secretKey = null)
- {
- $payload = Authentication::validate($token, $secretKey ?? static::config("TOKEN_SECRET"));
- if ($payload) return $payload;
-
- static::$errors = array_merge(static::$errors, Authentication::errors());
-
- return null;
- }
-
- /**
- * Validate Bearer Token
- *
- * @param string $secretKey The secret key used to encode token
- */
- public static function validateToken(?string $secretKey = null)
- {
- $payload = Authentication::validateToken($secretKey ?? static::config("TOKEN_SECRET"));
- if ($payload) return $payload;
-
- static::$errors = array_merge(static::$errors, Authentication::errors());
-
- return null;
- }
-
- /**
- * Get Bearer token
- */
- public static function getBearerToken()
- {
- $token = Authentication::getBearerToken();
- if ($token) return $token;
-
- static::$errors = array_merge(static::$errors, Authentication::errors());
-
- return null;
- }
-
- /**
- * Get all authentication errors as associative array
- */
- public static function errors(): array
- {
- return static::$errors;
- }
-}
diff --git a/src/Auth/Session.php b/src/Auth/Session.php
deleted file mode 100644
index e8da0d7..0000000
--- a/src/Auth/Session.php
+++ /dev/null
@@ -1,39 +0,0 @@
-get("SESSION_STARTED_AT")) {
- static::$session->set("SESSION_STARTED_AT", time());
- }
-
- static::$session->set("SESSION_LAST_ACTIVITY", time());
-
- return static::$session;
- }
-}
diff --git a/src/Auth/User.php b/src/Auth/User.php
new file mode 100644
index 0000000..785420e
--- /dev/null
+++ b/src/Auth/User.php
@@ -0,0 +1,224 @@
+data = $data;
+
+ $sessionLifetime = Config::get('token.lifetime');
+
+ if (Config::get('session')) {
+ $sessionLifetime = Config::get('session.lifetime');
+
+ if (session_status() !== PHP_SESSION_ACTIVE) {
+ session_set_cookie_params(Config::get('session.cookie'));
+ session_start();
+ }
+
+ session_regenerate_id();
+
+ if (!Session::has('auth.startedAt')) {
+ Session::set('auth.startedAt', time());
+ }
+
+ Session::set('auth.lastActivity', time());
+ Session::set('auth.id', $this->id());
+ Session::set('auth.user', $this->get());
+
+ if ($sessionLifetime !== 0 && $sessionLifetime !== null) {
+ if (!is_int($sessionLifetime)) {
+ $sessionLifetime = strtotime($sessionLifetime);
+
+ if (!$sessionLifetime) {
+ throw new \Exception('Invalid session lifetime');
+ }
+ } else {
+ $sessionLifetime = time() + $sessionLifetime;
+ }
+
+ Session::set('auth.ttl', $sessionLifetime);
+ }
+ }
+
+ $this->tokens['access'] = $this->generateToken($sessionLifetime);
+ $this->tokens['refresh'] = $this->generateToken($sessionLifetime + 259200);
+ }
+
+ /**
+ * Return the id of current user
+ * @return string|int
+ */
+ public function id()
+ {
+ return $this->data['id'] ?? null;
+ }
+
+ /**
+ * Get auth information to be sent to the client
+ * @return object
+ */
+ public function getAuthInfo(): object
+ {
+ $dataToReturn = (object) [
+ 'user' => $this->get(),
+ 'accessToken' => $this->tokens['access'],
+ 'refreshToken' => $this->tokens['refresh'],
+ ];
+
+ if (count($this->roles ?? [])) {
+ $dataToReturn->roles = $this->roles;
+ }
+
+ if (count($this->permissions ?? [])) {
+ $dataToReturn->permissions = $this->permissions;
+ }
+
+ return $dataToReturn;
+ }
+
+ /**
+ * Return generated tokens
+ * @return array
+ */
+ public function tokens(): array
+ {
+ return $this->tokens;
+ }
+
+ /**
+ * Generate a new JWT for the user
+ * @return string
+ */
+ public function generateToken($tokenLifetime): string
+ {
+ $userIdKey = Config::get('id.key');
+ $secretPhrase = Config::get('token.secret');
+
+ // no fallback because we need the user id
+ $userId = $this->data[$userIdKey];
+
+ $payload = [
+ 'user.id' => $userId,
+ 'iat' => time(),
+ 'exp' => $tokenLifetime,
+ 'iss' => $_SERVER['HTTP_HOST'] ?? 'localhost',
+ ];
+
+ $token = JWT::encode($payload, $secretPhrase, 'HS256');
+
+ return $token;
+ }
+
+ public function get()
+ {
+ $userData = $this->data;
+
+ $idKey = Config::get('id.key');
+ $hidden = Config::get('hidden');
+ $passwordKey = Config::get('password.key');
+
+ if (count($hidden) > 0) {
+ foreach ($hidden as $item) {
+ if (isset($userData[$item])) {
+ unset($userData[$item]);
+ }
+
+ if ($item === 'field.id' && isset($userData[$idKey])) {
+ unset($userData[$idKey]);
+ }
+
+ if ($item === 'field.password' && isset($userData[$passwordKey])) {
+ unset($userData[$passwordKey]);
+ }
+ }
+ }
+
+ return $userData;
+ }
+
+ public function __toString()
+ {
+ return json_encode($this->get());
+ }
+
+ public function __get($name)
+ {
+ // using data instead of get() here because
+ // we want people to be able to user()->get hidden fields
+ // since it's expected to be used within the app
+ return $this->data[$name] ?? null;
+ }
+
+ public function __set($name, $value)
+ {
+ $this->data[$name] = $value;
+ }
+
+ public function __isset($name)
+ {
+ return isset($this->data[$name]);
+ }
+
+ public function __unset($name)
+ {
+ unset($this->data[$name]);
+ }
+
+ /**
+ * Get a "user to many" table relation
+ *
+ *
+ * auth()->user()->orders()->all();
+ * auth()->user()->transactions()->where('amount', '>', 100)->get();
+ * auth()->user()->notes()->where('title', 'like', '%important%')->get();
+ * auth()->user()->posts()->where('published', true)->all();
+ *
+ *
+ * @param mixed $method The table to relate to
+ * @param mixed $args
+ * @throws \Exception
+ * @return Db
+ */
+ public function __call($method, $args)
+ {
+ if (!class_exists('Leaf\App')) {
+ throw new \Exception('Relations are only available in Leaf apps.');
+ }
+
+ return auth()
+ ->db()
+ ->select($method)
+ ->where('user_id', $this->id());
+ }
+}
diff --git a/src/Auth/UsesRoles.php b/src/Auth/UsesRoles.php
new file mode 100644
index 0000000..3650afa
--- /dev/null
+++ b/src/Auth/UsesRoles.php
@@ -0,0 +1,125 @@
+permissions = array_merge(
+ $this->permissions,
+ is_array($permission) ? $permission : [$permission]
+ );
+ }
+
+ /**
+ * Assign new role to user
+ * @param string|array $role The role to assign
+ */
+ public function assignRoles($role): void
+ {
+ // will need to verify roles here
+
+ $this->roles = array_merge(
+ $this->roles,
+ is_array($role) ? $role : [$role]
+ );
+
+ // persist via storage contract
+
+ foreach ($this->roles as $role) {
+ $this->grantPermissions($this->getRolePermissions($role));
+ }
+
+ // persist via storage contract
+ }
+
+ /**
+ * Check if user has a permission
+ * @param string|array $permission The permission(s) to check
+ * @return bool
+ */
+ public function can($permission): bool
+ {
+ if (is_array($permission)) {
+ return count(array_intersect($permission, $this->permissions)) === count($permission);
+ }
+
+ return in_array($permission, $this->permissions);
+ }
+
+ /**
+ * Check if user has a role
+ * @param string|array $role The role(s) to check
+ * @return bool
+ */
+ public function is($role): bool
+ {
+ if (is_array($role)) {
+ return count(array_intersect($role, $this->roles)) === count($role);
+ }
+
+ return in_array($role, $this->roles);
+ }
+
+ /**
+ * Revoke a permission from a user
+ * @param string|array $permission The permission(s) to revoke
+ */
+ public function revokePermissions($permission): void
+ {
+ // persist via storage contract
+ $this->permissions = array_diff(
+ $this->permissions,
+ is_array($permission) ? $permission : [$permission]
+ );
+ }
+
+ /**
+ * Remove a role from a user
+ * @param string|array $role The role(s) to revoke
+ */
+ public function removeRoles($role): void
+ {
+ // persist via storage contract
+ $this->roles = array_diff(
+ $this->roles,
+ is_array($role) ? $role : [$role]
+ );
+ }
+
+ /**
+ * Get the permissions for a role
+ * @param string $role
+ * @return array
+ */
+ protected function getRolePermissions($role): array
+ {
+ // get permissions from storage contract
+ return [];
+ }
+}
diff --git a/src/Helpers/Authentication.php b/src/Helpers/Authentication.php
deleted file mode 100755
index 5df9384..0000000
--- a/src/Helpers/Authentication.php
+++ /dev/null
@@ -1,134 +0,0 @@
-
- * @since v1.2.0
- */
-class Authentication
-{
- /**
- * Any errors caught
- */
- protected static $errorsArray = [];
-
- /**
- * Quickly generate a JWT encoding a user id
- *
- * @param string $userId The user id to encode
- * @param string $secretPhrase The user id to encode
- * @param int $expiresAt Token lifetime
- *
- * @return string The generated token
- */
- public static function generateSimpleToken(string $userId, string $secretPhrase, int $expiresAt = null): string
- {
- $payload = [
- 'iat' => time(),
- 'iss' => 'localhost',
- 'exp' => time() + ($expiresAt ?? (60 * 60 * 24)),
- 'user_id' => $userId
- ];
-
- return self::generateToken($payload, $secretPhrase);
- }
-
- /**
- * Create a JWT with your own payload
- *
- * @param array $payload The JWT payload
- * @param string $secretPhrase The user id to encode
- *
- * @return string The generated token
- */
- public static function generateToken(array $payload, string $secretPhrase): string
- {
- return JWT::encode($payload, $secretPhrase);
- }
-
- /**
- * Get Authorization Headers
- */
- public static function getAuthorizationHeader()
- {
- $headers = null;
-
- if (isset($_SERVER['Authorization'])) {
- $headers = trim($_SERVER["Authorization"]);
- } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
- $headers = trim($_SERVER["HTTP_AUTHORIZATION"]);
- } else if (function_exists('apache_request_headers')) {
- $requestHeaders = apache_request_headers();
- // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
- $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
-
- if (isset($requestHeaders['Authorization'])) {
- $headers = trim($requestHeaders['Authorization']);
- }
- }
-
- return $headers;
- }
-
- /**
- * get access token from header
- */
- public static function getBearerToken()
- {
- $headers = self::getAuthorizationHeader();
-
- if (!empty($headers)) {
- if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
- return $matches[1];
- }
-
- self::$errorsArray["token"] = "Access token not found";
- return null;
- }
-
- self::$errorsArray["token"] = "Access token not found";
- return null;
- }
-
- /**
- * Validate and decode access token in header
- */
- public static function validateToken($secretPhrase)
- {
- $bearerToken = self::getBearerToken();
- if ($bearerToken === null) return null;
-
- return self::validate($bearerToken, $secretPhrase);
- }
-
- /**
- * Validate access token
- *
- * @param string $token Access token to validate and decode
- */
- public static function validate($token, $secretPhrase)
- {
- try {
- $payload = JWT::decode($token, $secretPhrase, ['HS256']);
- if ($payload !== null) return $payload;
- } catch (\DomainException $exception) {
- self::$errorsArray["token"] = "Malformed token";
- }
-
- self::$errorsArray = array_merge(self::$errorsArray, JWT::errors());
- return null;
- }
-
- /**
- * Get all authentication errors as associative array
- */
- public static function errors()
- {
- return self::$errorsArray;
- }
-}
diff --git a/src/Helpers/JWT.php b/src/Helpers/JWT.php
deleted file mode 100755
index 8fe1da5..0000000
--- a/src/Helpers/JWT.php
+++ /dev/null
@@ -1,371 +0,0 @@
-
- * @author Anant Narayanan
- * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
- * @link https://github.com/firebase/php-jwt
- */
-class JWT
-{
- /**
- * Errors caught
- */
- protected static $errorsArray = [];
- /**
- * When checking nbf, iat or expiration times,
- * we want to provide some extra leeway time to
- * account for clock skew.
- */
- public static $leeway = 0;
- /**
- * Allow the current timestamp to be specified.
- * Useful for fixing a value within unit testing.
- *
- * Will default to PHP time() value if null.
- */
- public static $timestamp = null;
- public static $supported_algs = array(
- 'HS256' => array('hash_hmac', 'SHA256'),
- 'HS512' => array('hash_hmac', 'SHA512'),
- 'HS384' => array('hash_hmac', 'SHA384'),
- 'RS256' => array('openssl', 'SHA256'),
- 'RS384' => array('openssl', 'SHA384'),
- 'RS512' => array('openssl', 'SHA512'),
- );
- /**
- * Decodes a JWT string into a PHP object.
- *
- * @param string $jwt The JWT
- * @param string|array $key The key, or map of keys.
- * If the algorithm used is asymmetric, this is the public key
- * @param array $allowed_algs List of supported verification algorithms
- * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
- *
- * @return object The JWT's payload as a PHP object
- *
- *
- * @uses jsonDecode
- * @uses urlsafeB64Decode
- */
- public static function decode($jwt, $key, array $allowed_algs = array())
- {
- $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp;
- if (empty($key)) {
- return static::saveErr("Key may not be empty");
- }
- $tks = explode('.', $jwt);
- if (count($tks) != 3) {
- return static::saveErr("Wrong number of segments");
- }
- list($headb64, $bodyb64, $cryptob64) = $tks;
- if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
- return static::saveErr("Invalid header encoding");
- }
- if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
- return static::saveErr("Invalid claims encoding");
- }
- if (false === ($sig = static::urlsafeB64Decode($cryptob64))) {
- return static::saveErr("Invalid signature encoding");
- }
- if (empty($header->alg)) {
- return static::saveErr("Empty algorithm");
- }
- if (empty(static::$supported_algs[$header->alg])) {
- return static::saveErr("Algorithm not supported");
- }
- if (!in_array($header->alg, $allowed_algs)) {
- return static::saveErr("Algorithm not allowed");
- }
- if (is_array($key) || $key instanceof \ArrayAccess) {
- if (isset($header->kid)) {
- if (!isset($key[$header->kid])) {
- return static::saveErr("'kid' invalid, unable to lookup correct key");
- }
- $key = $key[$header->kid];
- } else {
- return static::saveErr("'kid' empty, unable to lookup correct key");
- }
- }
- // Check the signature
- if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
- return static::saveErr("Signature verification failed");
- }
- // Check if the nbf if it is defined. This is the time that the
- // token can actually be used. If it's not yet that time, abort.
- if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
- return static::saveErr(
- "Cannot handle token prior to " . date(\DateTime::ISO8601, $payload->nbf)
- );
- }
- // Check that this token has been created before "now". This prevents
- // using tokens that have been created for later use (and haven't
- // correctly used the nbf claim).
- if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
- return static::saveErr(
- "Cannot handle token prior to " . date(\DateTime::ISO8601, $payload->iat)
- );
- }
- // Check if this token has expired.
- if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
- return static::saveErr("Expired token");
- }
- return $payload;
- }
-
- /**
- * Converts and signs a PHP object or array into a JWT string.
- *
- * @param object|array $payload PHP object or array
- * @param string $key The secret key.
- * If the algorithm used is asymmetric, this is the private key
- * @param string $alg The signing algorithm.
- * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
- * @param mixed $keyId
- * @param array $head An array with header elements to attach
- *
- * @return string A signed JWT
- *
- * @uses jsonEncode
- * @uses urlsafeB64Encode
- */
- public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)
- {
- $header = array('typ' => 'JWT', 'alg' => $alg);
- if ($keyId !== null) {
- $header['kid'] = $keyId;
- }
- if (isset($head) && is_array($head)) {
- $header = array_merge($head, $header);
- }
- $segments = array();
- $segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
- $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
- $signing_input = implode('.', $segments);
- $signature = static::sign($signing_input, $key, $alg);
- $segments[] = static::urlsafeB64Encode($signature);
- return implode('.', $segments);
- }
-
- /**
- * Sign a string with a given key and algorithm.
- *
- * @param string $msg The message to sign
- * @param string|resource $key The secret key
- * @param string $alg The signing algorithm.
- * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
- *
- * @return string An encrypted message
- *
- * @throws \DomainException Unsupported algorithm was specified
- */
- public static function sign($msg, $key, $alg = 'HS256')
- {
- if (empty(static::$supported_algs[$alg])) {
- throw new \DomainException('Algorithm not supported');
- }
- list($function, $algorithm) = static::$supported_algs[$alg];
- switch ($function) {
- case 'hash_hmac':
- return hash_hmac($algorithm, $msg, $key, true);
- case 'openssl':
- $signature = '';
- $success = openssl_sign($msg, $signature, $key, $algorithm);
- if (!$success) {
- throw new \DomainException("OpenSSL unable to sign data");
- } else {
- return $signature;
- }
- }
- }
-
- /**
- * Verify a signature with the message, key and method. Not all methods
- * are symmetric, so we must have a separate verify and sign method.
- *
- * @param string $msg The original message (header and body)
- * @param string $signature The original signature
- * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key
- * @param string $alg The algorithm
- *
- * @return bool
- *
- * @throws \DomainException Invalid Algorithm or OpenSSL failure
- */
- private static function verify($msg, $signature, $key, $alg)
- {
- if (empty(static::$supported_algs[$alg])) {
- throw new \DomainException('Algorithm not supported');
- }
- list($function, $algorithm) = static::$supported_algs[$alg];
- switch ($function) {
- case 'openssl':
- $success = openssl_verify($msg, $signature, $key, $algorithm);
- if ($success === 1) {
- return true;
- } elseif ($success === 0) {
- return false;
- }
- // returns 1 on success, 0 on failure, -1 on error.
- throw new \DomainException(
- 'OpenSSL error: ' . openssl_error_string()
- );
- case 'hash_hmac':
- default:
- $hash = hash_hmac($algorithm, $msg, $key, true);
- if (function_exists('hash_equals')) {
- return hash_equals($signature, $hash);
- }
- $len = min(static::safeStrlen($signature), static::safeStrlen($hash));
- $status = 0;
- for ($i = 0; $i < $len; $i++) {
- $status |= (ord($signature[$i]) ^ ord($hash[$i]));
- }
- $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash));
- return ($status === 0);
- }
- }
-
- /**
- * Decode a JSON string into a PHP object.
- *
- * @param string $input JSON string
- *
- * @return object Object representation of JSON string
- *
- * @throws \DomainException Provided string was invalid JSON
- */
- public static function jsonDecode($input)
- {
- if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
- /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you
- * to specify that large ints (like Steam Transaction IDs) should be treated as
- * strings, rather than the PHP default behaviour of converting them to floats.
- */
- $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
- } else {
- /** Not all servers will support that, however, so for older versions we must
- * manually detect large ints in the JSON string and quote them (thus converting
- *them to strings) before decoding, hence the preg_replace() call.
- */
- $max_int_length = strlen((string) PHP_INT_MAX) - 1;
- $json_without_bigints = preg_replace('/:\s*(-?\d{' . $max_int_length . ',})/', ': "$1"', $input);
- $obj = json_decode($json_without_bigints);
- }
- if (function_exists('json_last_error') && $errno = json_last_error()) {
- static::handleJsonError($errno);
- } elseif ($obj === null && $input !== 'null') {
- throw new \DomainException('Null result with non-null input');
- }
- return $obj;
- }
-
- /**
- * Encode a PHP object into a JSON string.
- *
- * @param object|array $input A PHP object or array
- *
- * @return string JSON representation of the PHP object or array
- *
- * @throws \DomainException Provided object could not be encoded to valid JSON
- */
- public static function jsonEncode($input)
- {
- $json = json_encode($input);
- if (function_exists('json_last_error') && $errno = json_last_error()) {
- static::handleJsonError($errno);
- } elseif ($json === 'null' && $input !== null) {
- throw new \DomainException('Null result with non-null input');
- }
- return $json;
- }
-
- /**
- * Decode a string with URL-safe Base64.
- *
- * @param string $input A Base64 encoded string
- *
- * @return string A decoded string
- */
- public static function urlsafeB64Decode($input)
- {
- $remainder = strlen($input) % 4;
- if ($remainder) {
- $padlen = 4 - $remainder;
- $input .= str_repeat('=', $padlen);
- }
- return base64_decode(strtr($input, '-_', '+/'));
- }
-
- /**
- * Encode a string with URL-safe Base64.
- *
- * @param string $input The string you want encoded
- *
- * @return string The base64 encode of what you passed in
- */
- public static function urlsafeB64Encode($input)
- {
- return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
- }
-
- /**
- * Helper method to create a JSON error.
- *
- * @param int $errno An error number from json_last_error()
- *
- * @return void
- */
- private static function handleJsonError($errno)
- {
- $messages = array(
- JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
- JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
- JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
- JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
- JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
- );
- throw new \DomainException(
- isset($messages[$errno])
- ? $messages[$errno]
- : 'Unknown JSON error: ' . $errno
- );
- }
-
- /**
- * Get the number of bytes in cryptographic strings.
- *
- * @param string
- *
- * @return int
- */
- private static function safeStrlen($str)
- {
- if (function_exists('mb_strlen')) {
- return mb_strlen($str, '8bit');
- }
- return strlen($str);
- }
-
- protected static function saveErr($err, $key = "token")
- {
- self::$errorsArray[$key] = $err;
- return null;
- }
-
- /**
- * Return all errors found
- */
- public static function errors()
- {
- return static::$errorsArray;
- }
-}
diff --git a/src/functions.php b/src/functions.php
index 555fcbf..a88c508 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -3,7 +3,7 @@
if (!function_exists('auth') && class_exists('Leaf\App')) {
/**
* Return the leaf auth object
- *
+ *
* @return Leaf\Auth
*/
function auth()
@@ -17,35 +17,3 @@ function auth()
return \Leaf\Config::get('auth');
}
}
-
-if (!function_exists('guard') && function_exists('auth')) {
- /**
- * Run an auth guard
- *
- * @param string $guard The auth guard to run
- */
- function guard(string $guard)
- {
- return auth()->guard($guard);
- }
-}
-
-if (!function_exists('hasAuth') && function_exists('auth')) {
- /**
- * Find out if there's an active sesion
- */
- function hasAuth(): bool
- {
- return !!sessionUser();
- }
-}
-
-if (!function_exists('sessionUser') && function_exists('auth')) {
- /**
- * Get the currently logged in user
- */
- function sessionUser()
- {
- return \Leaf\Http\Session::get('AUTH_USER');
- }
-}
diff --git a/tests/AuthSessionTest.php b/tests/AuthSessionTest.php
deleted file mode 100644
index 54224ef..0000000
--- a/tests/AuthSessionTest.php
+++ /dev/null
@@ -1,165 +0,0 @@
- 'login-user', 'password' => 'login-pass']);
-
- $session = new \Leaf\Http\Session(false);
- $user = $session->get('AUTH_USER');
-
- expect($user['username'])->toBe('login-user');
-});
-
-test('login should set session ttl', function () {
- $auth = new \Leaf\Auth();
- $auth::config(getAuthConfig());
-
- $timeBeforeLogin = time();
- $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
-
- $session = new \Leaf\Http\Session(false);
- $sessionTtl = $session->get('SESSION_TTL');
-
- expect($sessionTtl > $timeBeforeLogin)->toBeTrue();
-});
-
-test('login should set regenerate session id', function () {
- $auth = new \Leaf\Auth();
- $auth::config(getAuthConfig());
-
- $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
-
- $originalSessionId = session_id();
-
- $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
-
- expect(session_id())->not()->toBe($originalSessionId);
-});
-
-test('login should set secure session cookie params', function () {
- $auth = new \Leaf\Auth();
- $auth::config(getAuthConfig());
-
- $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
-
- $cookieParams = session_get_cookie_params();
-
- expect($cookieParams['secure'])->toBeTrue();
- expect($cookieParams['httponly'])->toBeTrue();
- expect($cookieParams['samesite'])->toBe('lax');
-});
-
-test('register should set session ttl on login', function () {
- $auth = new \Leaf\Auth();
- $auth::config(getAuthConfig());
-
- $timeBeforeLogin = time();
- $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
-
- $session = new \Leaf\Http\Session(false);
- $sessionTtl = $session->get('SESSION_TTL');
-
- expect($sessionTtl > $timeBeforeLogin)->toBeTrue();
-});
-
-test('Session should expire when fetching user, and then login is possible again', function () {
- $auth = new \Leaf\Auth();
- $auth::config(getAuthConfig(['SESSION_LIFETIME' => 2]));
-
- $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
-
- $user = $auth::user();
- expect($user)->not()->toBeNull();
- expect($user['username'])->toBe('login-user');
-
- sleep(1);
- expect($auth::user())->not()->toBeNull();
-
- sleep(2);
- expect($auth::user())->toBeNull();
-
- $userAfterReLogin = $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
- expect($userAfterReLogin)->not()->toBeNull();
- expect($userAfterReLogin['user']['username'])->toBe('login-user');
-});
-
-test('Session should not expire when fetching user if session lifetime is 0', function () {
- $auth = new \Leaf\Auth();
- $auth::config(getAuthConfig(['SESSION_LIFETIME' => 0]));
-
- $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
-
- $user = $auth::user();
- expect($user)->not()->toBeNull();
- expect($user['username'])->toBe('login-user');
-
- sleep(2);
- expect($auth::user())->not()->toBeNull();
-});
-
-test('Session should expire when fetching user id', function () {
- $auth = new \Leaf\Auth();
- $auth::config(getAuthConfig(['SESSION_LIFETIME' => 2]));
-
- $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
-
- expect($auth::id())->not()->toBeNull();
-
- sleep(1);
- expect($auth::id())->not()->toBeNull();
-
- sleep(2);
- expect($auth::id())->toBeNull();
-});
-
-test('Session should expire when fetching status', function () {
- $auth = new \Leaf\Auth();
- $auth::config(getAuthConfig(['SESSION_LIFETIME' => 2]));
- $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
-
- expect($auth::status())->not()->toBeNull();
-
- sleep(1);
- expect($auth::status())->not()->toBeNull();
-
- sleep(2);
- expect($auth::status())->toBeFalse();
-});
-
-test('Session lifetime should set correct session ttl when string is configured instead of timestamp', function () {
- $auth = new \Leaf\Auth();
- $auth::config(getAuthConfig(['SESSION_LIFETIME' => '1 day']));
- $auth::login(['username' => 'login-user', 'password' => 'login-pass']);
-
- expect($auth::status())->not()->toBeNull();
-
- $timestampOneDay = 60 * 60 * 24;
- $session = new \Leaf\Http\Session(false);
- $sessionTtl = $session->get('SESSION_TTL');
-
- expect($sessionTtl)->toBe(time() + $timestampOneDay);
-});
-
-test('Login should throw error when lifetime string is invalid', function () {
- $auth = new \Leaf\Auth();
- $auth::config(getAuthConfig(['SESSION_LIFETIME' => 'invalid string']));
-
- expect(fn() => $auth::login(['username' => 'login-user', 'password' => 'login-pass']))
- ->toThrow(Exception::class, 'Provided string could not be converted to time');
-});
diff --git a/tests/AuthTest.php b/tests/AuthTest.php
deleted file mode 100644
index a16a3d9..0000000
--- a/tests/AuthTest.php
+++ /dev/null
@@ -1,17 +0,0 @@
- false]));
- $response = $auth::register(['username' => 'test-user', 'password' => 'test-password']);
-
- expect($response['user']['username'])->toBe('test-user');
-});
diff --git a/tests/Pest.php b/tests/Pest.php
index dac93b2..06ecfcd 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -1,119 +1,65 @@
'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+]]]);
-// uses(Tests\TestCase::class)->in('Feature');
-
-/*
-|--------------------------------------------------------------------------
-| Expectations
-|--------------------------------------------------------------------------
-|
-| When you're writing tests, you often need to check that values meet certain conditions. The
-| "expect()" function gives you access to a set of "expectations" methods that you can use
-| to assert different things. Of course, you may extend the Expectation API at any time.
-|
-*/
-
-expect()->extend('toBeOne', function () {
- return $this->toBe(1);
-});
-
-/*
-|--------------------------------------------------------------------------
-| Functions
-|--------------------------------------------------------------------------
-|
-| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
-| project that you don't want to repeat in every file. Here you can also expose helpers as
-| global functions to help you to reduce the number of lines of code in your test files.
-|
-*/
+function getDatabaseConnection(): array
+{
+ return [
+ 'dbtype' => 'pgsql',
+ 'port' => '5432',
+ 'host' => 'ep-autumn-block-a28alwsy.eu-central-1.aws.neon.tech',
+ 'username' => 'sandbox_owner',
+ 'password' => 'WH1qpBIf7LYc',
+ 'dbname' => 'sandbox',
+ ];
+}
-function createUsersTable()
+function dbInstance(): \Leaf\Db
{
$db = new \Leaf\Db();
- $db->connect(...getConnectionConfig());
+ $db->connect(getDatabaseConnection());
- $db->createTableIfNotExists(
- 'users',
- [
- 'id' => 'int NOT NULL AUTO_INCREMENT',
- 'username' => 'varchar(255)',
- 'password' => 'varchar(255)',
- 'created_at' => 'datetime',
- 'updated_at' => 'datetime',
- 'PRIMARY KEY' => '(id)',
- ]
- )->execute();
+ return $db;
}
-function haveRegisteredUser(string $username, string $password): array
+function authInstance(): \Leaf\Auth
{
- \Leaf\Auth\Core::connect(...getConnectionConfig('mysql'));
-
$auth = new \Leaf\Auth();
- $auth::config(getAuthConfig(['USE_SESSION' => false]));
+ $auth->dbConnection(dbInstance()->connection());
- return $auth::register(['username' => $username, 'password' => $password]);
+ return $auth;
}
-function deleteUser(string $username)
+function deleteUser(string $username, $table = 'users')
{
$db = new \Leaf\Db();
- $db->connect(...getConnectionConfig());
+ $db->connect(getDatabaseConnection());
- $db->delete('users')->where('username', '=', $username)->execute();
+ $db->delete($table)->where('username', $username)->execute();
}
-function getConnectionConfig(?string $dbType = null): array
+function createTableForUsers($table = 'users'): void
{
- $config = ['localhost', 'leaf', 'root', 'root'];
-
- if ($dbType) {
- $config[] = $dbType;
+ $db = dbInstance();
+
+ try {
+ $db
+ ->query("CREATE TABLE IF NOT EXISTS $table (
+ id SERIAL PRIMARY KEY,
+ username VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ permissions JSONB,
+ roles JSONB,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )")
+ ->execute();
+ } catch (\Throwable $th) {
+ throw new \Exception('Failed to create table for users: ' . $th->getMessage());
}
-
- return $config;
-}
-
-function getAuthConfig(array $settingsReplacement = []): array
-{
- $settings = [
- 'DB_TABLE' => 'users',
- 'AUTH_NO_PASS' => false,
- 'USE_TIMESTAMPS' => false,
- 'TIMESTAMP_FORMAT' => 'c',
- 'PASSWORD_ENCODE' => null,
- 'PASSWORD_VERIFY' => null,
- 'PASSWORD_KEY' => 'password',
- 'HIDE_ID' => true,
- 'ID_KEY' => 'id',
- 'USE_UUID' => false,
- 'HIDE_PASSWORD' => true,
- 'LOGIN_PARAMS_ERROR' => 'Incorrect credentials!',
- 'LOGIN_PASSWORD_ERROR' => 'Password is incorrect!',
- 'USE_SESSION' => true,
- 'SESSION_ON_REGISTER' => false,
- 'GUARD_LOGIN' => '/auth/login',
- 'GUARD_REGISTER' => '/auth/register',
- 'GUARD_HOME' => '/home',
- 'GUARD_LOGOUT' => '/auth/logout',
- 'SAVE_SESSION_JWT' => false,
- 'TOKEN_LIFETIME' => null,
- 'TOKEN_SECRET' => '@_leaf$0Secret!',
- 'SESSION_REDIRECT_ON_LOGIN' => false,
- 'SESSION_LIFETIME' => 60 * 60 * 24,
- ];
-
- return array_replace($settings, $settingsReplacement);
}
diff --git a/tests/db.test.php b/tests/db.test.php
new file mode 100644
index 0000000..e69de29
diff --git a/tests/login.test.php b/tests/login.test.php
new file mode 100644
index 0000000..954826a
--- /dev/null
+++ b/tests/login.test.php
@@ -0,0 +1,107 @@
+insert('users')
+ ->params([
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => password_hash('password', PASSWORD_BCRYPT)
+ ])
+ ->execute();
+ } catch (\Throwable $th) {
+ throw $th;
+ }
+});
+
+afterAll(function () {
+ dbInstance()->delete('users')->execute();
+});
+
+test('user can login', function () {
+ $auth = authInstance();
+
+ $testUser = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->login($testUser);
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($testUser['username']);
+});
+
+test('login generates tokens on success', function () {
+ $auth = authInstance();
+
+ $testUser = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->login($testUser);
+
+ expect($success)->toBeTrue();
+ expect($auth->data())->not()->toBeNull();
+ expect($auth->data()->accessToken)->toBeString();
+ expect($auth->data()->refreshToken)->toBeString();
+});
+
+test('login fails with incorrect password', function () {
+ $auth = authInstance();
+
+ $userData = [
+ 'username' => 'test-user',
+ 'password' => 'wrong-password'
+ ];
+
+ $success = $auth->login($userData);
+
+ $loginPasswordError = $auth->config('messages.loginPasswordError');
+
+ expect($success)->toBeFalse();
+ expect($auth->user())->toBeNull();
+ expect($auth->errors()['password'] ?? null)->toBe($loginPasswordError);
+});
+
+test('login should fail if user does not exist', function () {
+ $auth = authInstance();
+
+ $userData = [
+ 'username' => 'non-existent-user',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->login($userData);
+
+ $loginParamsError = $auth->config('messages.loginParamsError');
+
+ expect($success)->toBeFalse();
+ expect($auth->user())->toBeNull();
+ expect($auth->errors()['auth'] ?? null)->toBe($loginParamsError);
+});
+
+test('login should work without password is password.key is false', function () {
+ $auth = authInstance();
+
+ $auth->config('password.key', false);
+
+ $userData = [
+ 'username' => 'test-user'
+ ];
+
+ $success = $auth->login($userData);
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($userData['username']);
+
+ $auth->config('password.key', 'password');
+});
diff --git a/tests/register.test.php b/tests/register.test.php
new file mode 100644
index 0000000..630335f
--- /dev/null
+++ b/tests/register.test.php
@@ -0,0 +1,101 @@
+delete('users')->execute();
+});
+
+afterEach(function () {
+ dbInstance()->delete('users')->execute();
+});
+
+test('user can register an account', function () {
+ $auth = authInstance();
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->register($userData);
+
+ if (!$success) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($userData['username']);
+});
+
+test('user can login after registering', function () {
+ $auth = authInstance();
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $registerSuccess = $auth->register($userData);
+
+ expect($registerSuccess)->toBeTrue();
+
+ $loginSuccess = $auth->login($userData);
+
+ expect($loginSuccess)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($userData['username']);
+});
+
+test('user can only sign up once', function () {
+ $auth = authInstance();
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $auth->config([
+ 'unique' => ['email', 'username']
+ ]);
+
+ $registerSuccess = $auth->register($userData);
+
+ expect($registerSuccess)->toBeTrue();
+
+ $registerAgain = $auth->register($userData);
+
+ expect($registerAgain)->toBeFalse();
+
+ expect($auth->errors())->toBe([
+ 'email' => 'email already exists',
+ 'username' => 'username already exists',
+ ]);
+});
+
+test('register passwords are encrypted', function () {
+ $auth = authInstance();
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $auth->config([
+ 'hidden' => []
+ ]);
+
+ $registerSuccess = $auth->register($userData);
+
+ expect($registerSuccess)->toBeTrue();
+ expect($auth->user()->password)->not()->toBe($userData['password']);
+ expect(password_verify($userData['password'], $auth->user()->password))->toBeTrue();
+});
diff --git a/tests/session.test.php b/tests/session.test.php
new file mode 100644
index 0000000..55a3c2c
--- /dev/null
+++ b/tests/session.test.php
@@ -0,0 +1,205 @@
+delete('users')->execute();
+});
+
+afterEach(function () {
+ if (session_status() === PHP_SESSION_ACTIVE) {
+ $_SESSION = [];
+ session_destroy();
+ }
+});
+
+afterAll(function () {
+ dbInstance()->delete('users')->execute();
+});
+
+test('register should create a new session when session => true', function () {
+ $auth = authInstance();
+ $auth->config(['session' => true]);
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->register($userData);
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($userData['username']);
+
+ expect(session_status())->toBe(PHP_SESSION_ACTIVE);
+ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']);
+});
+
+test('register should not create a new session when session => false', function () {
+ $auth = authInstance();
+ $auth->config(['session' => false]);
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user2',
+ 'email' => 'test-user2@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->register($userData);
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+
+ expect(session_status())->toBe(PHP_SESSION_NONE);
+ expect($_SESSION['auth']['user']['username'] ?? null)->toBeNull();
+});
+
+test('login should create session when session => true', function () {
+ $auth = authInstance();
+ $auth->config(['session' => true]);
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->login($userData);
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($userData['username']);
+
+ expect(session_status())->toBe(PHP_SESSION_ACTIVE);
+ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']);
+});
+
+test('session should create auth.ttl when session.lifetime is not 0', function () {
+ $auth = authInstance();
+ $auth->config(['session' => true, 'session.lifetime' => 2]);
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $timeBeforeLogin = time();
+
+ $success = $auth->login($userData);
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($userData['username']);
+
+ expect(session_status())->toBe(PHP_SESSION_ACTIVE);
+ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']);
+
+ expect($_SESSION['auth']['ttl'])->toBeGreaterThan($timeBeforeLogin);
+});
+
+test('session should not create auth.ttl when session.lifetime is 0', function () {
+ $auth = authInstance();
+ $auth->config(['session' => true, 'session.lifetime' => 0]);
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->login($userData);
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($userData['username']);
+
+ expect(session_status())->toBe(PHP_SESSION_ACTIVE);
+ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']);
+
+ expect($_SESSION['auth']['ttl'] ?? null)->toBeNull();
+});
+
+test('session should expire after session.lifetime', function () {
+ $auth = authInstance();
+ $auth->config(['session' => true, 'session.lifetime' => 2]);
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->login($userData);
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($userData['username']);
+
+ expect(session_status())->toBe(PHP_SESSION_ACTIVE);
+ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']);
+
+ sleep(3);
+
+ expect($auth->id())->toBeNull();
+ expect($auth->user())->toBeNull();
+});
+
+test('login should regenerate session id when session => true and session is already active', function () {
+ $auth = authInstance();
+ $auth->config(['session' => true]);
+ $auth->config(['db.table' => 'users']);
+
+ session_start();
+
+ $sessionId = session_id();
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->login($userData);
+
+ $newSessionId = session_id();
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($userData['username']);
+
+ expect(session_status())->toBe(PHP_SESSION_ACTIVE);
+ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']);
+
+ expect($newSessionId)->not()->toBe($sessionId);
+});
+
+test('logout should remove auth info from session when session => true', function () {
+ $auth = authInstance();
+ $auth->config(['session' => true]);
+ $auth->config(['db.table' => 'users']);
+
+ $userData = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->login($userData);
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']);
+
+ $auth->logout();
+
+ expect($auth->user())->toBeNull();
+ expect($_SESSION['auth']['user']['username'] ?? null)->toBeNull();
+});
diff --git a/tests/table.test.php b/tests/table.test.php
new file mode 100644
index 0000000..ff235cd
--- /dev/null
+++ b/tests/table.test.php
@@ -0,0 +1,93 @@
+delete('myusers')->execute();
+});
+
+test('register should save user in user defined table', function () {
+ $auth = authInstance();
+ $auth->config(['session' => false, 'db.table' => 'myusers']);
+
+ $success = $auth->register([
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ]);
+
+ if (!$success) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ expect($auth->user()->username)->toBe('test-user');
+});
+
+test('login should work with user defined table', function () {
+ $auth = authInstance();
+ $auth->config(['session' => false, 'db.table' => 'myusers']);
+
+ $success = $auth->login([
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ]);
+
+ if (!$success) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ expect($auth->user()->username)->toBe('test-user');
+});
+
+test('update should work with user defined table', function () {
+ $auth = authInstance();
+ $auth->config(['session' => true, 'db.table' => 'myusers', 'session.lifetime' => '1 day']);
+
+ $success = $auth->login([
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ]);
+
+ if (!$success) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ $response = $auth->update([
+ 'username' => 'test-user55',
+ 'email' => 'test-user55@example.com',
+ ]);
+
+ if (!$response) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ expect($response['user']['username'])->toBe('test-user55');
+ expect($response['user']['email'])->toBe('test-user55@example.com');
+})->skip();
+
+test('user table can use uuid as id', function () {
+ createUsersTable('uuid_users', true);
+
+ $auth = authInstance();
+ $auth->config(['session' => false, 'db.table' => 'uuid_users']);
+
+ $response = $auth->register([
+ 'id' => '123e4567-e89b-12d3-a456-426614174000',
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password',
+ ]);
+
+ if (!$response) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ expect($response['user']['username'])->toBe('test-user');
+})->skip();
diff --git a/tests/update.test.php b/tests/update.test.php
new file mode 100644
index 0000000..76b2286
--- /dev/null
+++ b/tests/update.test.php
@@ -0,0 +1,135 @@
+config(['db.table' => 'users']);
+
+ $auth->register([
+ 'username' => 'test-user-1',
+ 'email' => 'test-user-1@example.com',
+ 'password' => 'password',
+ ]);
+
+ $auth->register([
+ 'username' => 'test-user-2',
+ 'email' => 'test-user-2@example.com',
+ 'password' => 'password',
+ ]);
+
+ sleep(1);
+});
+
+afterAll(function () {
+ dbInstance()->delete('users')->execute();
+});
+
+test('update should update user data', function () {
+ $auth = authInstance();
+ $auth->config(['db.table' => 'users']);
+
+ $success = $auth->login([
+ 'username' => 'test-user-1',
+ 'password' => 'password'
+ ]);
+
+ if (!$success) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ $updateData = [
+ 'username' => 'test-user-3',
+ ];
+
+ $updateSuccess = $auth->update($updateData);
+
+ if (!$updateSuccess) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ expect($auth->user()->username)->toBe($updateData['username']);
+});
+
+test('update should fail if user already exists', function () {
+ $auth = authInstance();
+ $auth->config(['db.table' => 'users']);
+
+ $success = $auth->login([
+ 'username' => 'test-user-3',
+ 'password' => 'password'
+ ]);
+
+ if (!$success) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ $updateData = [
+ 'username' => 'test-user-2',
+ ];
+
+ $updateSuccess = $auth->update($updateData);
+
+ expect($updateSuccess)->toBeFalse();
+ expect($auth->errors()['username'])->toBe('username already exists');
+});
+
+test('updatePassword should update user password', function () {
+ $auth = authInstance();
+ $auth->config(['unique' => ['username']]);
+ $auth->config(['db.table' => 'users']);
+
+ $success = $auth->login([
+ 'username' => 'test-user-2',
+ 'password' => 'password'
+ ]);
+
+ if (!$success) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ $oldPassword = 'password';
+ $newPassword = 'new-password';
+
+ $updateSuccess = $auth->updatePassword($oldPassword, $newPassword);
+
+ if (!$updateSuccess) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ $loginSuccess = $auth->login([
+ 'username' => 'test-user-2',
+ 'password' => 'new-password'
+ ]);
+
+ expect($loginSuccess)->toBeTrue();
+ expect($auth->user()->{$auth->config('password.key')})->not()->toBe($oldPassword);
+});
+
+test('update should regenerate session id if session => true', function () {
+ $auth = authInstance();
+ $auth->config(['session' => true]);
+ $auth->config(['db.table' => 'users']);
+
+ $success = $auth->login([
+ 'username' => 'test-user-2',
+ 'password' => 'new-password'
+ ]);
+
+ if (!$success) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ $updateData = [
+ 'username' => 'test-user-5',
+ ];
+
+ $initialSessionId = session_id();
+
+ $updateSuccess = $auth->update($updateData);
+
+ if (!$updateSuccess) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ expect($auth->user()->username)->toBe($updateData['username']);
+ expect($initialSessionId)->not()->toBe(session_id());
+});
diff --git a/tests/user.test.php b/tests/user.test.php
new file mode 100644
index 0000000..4086518
--- /dev/null
+++ b/tests/user.test.php
@@ -0,0 +1,71 @@
+delete('users')->execute();
+
+ try {
+ dbInstance()
+ ->insert('users')
+ ->params([
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => password_hash('password', PASSWORD_BCRYPT)
+ ])
+ ->execute();
+ } catch (\Throwable $th) {
+ throw $th;
+ }
+});
+
+afterAll(function () {
+ dbInstance()->delete('users')->execute();
+});
+
+test('auth user is instance of Leaf\Auth\User', function () {
+ $auth = authInstance();
+ $auth->config(['db.table' => 'users']);
+
+ $testUser = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->login($testUser);
+
+ if (!$success) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($testUser['username']);
+});
+
+test('logout can use logout callback to run custom action', function () {
+ $auth = authInstance();
+ $auth->config(['db.table' => 'users']);
+
+ $testUser = [
+ 'username' => 'test-user',
+ 'email' => 'test-user@example.com',
+ 'password' => 'password'
+ ];
+
+ $success = $auth->login($testUser);
+
+ if (!$success) {
+ $this->fail(json_encode($auth->errors()));
+ }
+
+ expect($success)->toBeTrue();
+ expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class);
+ expect($auth->user()->username)->toBe($testUser['username']);
+
+ $auth->logout(function ($auth) use ($testUser) {
+ expect($auth)->toBeInstanceOf(\Leaf\Auth::class);
+ });
+
+ expect($auth->user())->toBeNull();
+});