diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..9b881f2
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+open_collective: leaf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f56cefe
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+test
+Experimental
+vendor
+composer.lock
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2646e58
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+
+
+
+
+
Leaf Auth Module
+
+
+
+# Leaf PHP
+
+[![Latest Stable Version](https://poser.pugx.org/leafs/auth/v/stable)](https://packagist.org/packages/leafs/auth)
+[![Total Downloads](https://poser.pugx.org/leafs/auth/downloads)](https://packagist.org/packages/leafs/auth)
+[![License](https://poser.pugx.org/leafs/auth/license)](https://packagist.org/packages/leafs/auth)
+
+Leaf's auth helper packaged as a serve-yourself module.
+
+## Installation
+
+You can easily install Leaf using [Composer](https://getcomposer.org/).
+
+```bash
+composer require leafs/auth
+```
+
+## View Leaf's docs [here](https://leafphp.netlify.app/#/)
+
+Built with ❤ by [**Mychi Darko**](https://mychi.netlify.app)
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..37baaf4
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,37 @@
+{
+ "name": "leafs/auth",
+ "description": "Leaf PHP auth helper",
+ "keywords": [
+ "authentication",
+ "simple auth",
+ "login",
+ "register",
+ "leaf",
+ "php",
+ "framework"
+ ],
+ "homepage": "https://leafphp.netlify.app/#/",
+ "type": "library",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Michael Darko",
+ "email": "mickdd22@gmail.com",
+ "homepage": "https://mychi.netlify.app",
+ "role": "Developer"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "Leaf\\": "src"
+ }
+ },
+ "minimum-stability": "stable",
+ "require": {
+ "leafs/password": "^1.0",
+ "leafs/session": "^1.0",
+ "leafs/db": "^1.0",
+ "leafs/form": "^1.0",
+ "leafs/http": "^1.0"
+ }
+}
diff --git a/src/Auth.php b/src/Auth.php
new file mode 100755
index 0000000..3411b26
--- /dev/null
+++ b/src/Auth.php
@@ -0,0 +1,683 @@
+ true,
+ "PASSWORD_ENCODE" => null,
+ "PASSWORD_VERIFY" => null,
+ "PASSWORD_KEY" => "password",
+ "HIDE_ID" => true,
+ "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",
+ "SAVE_SESSION_JWT" => false,
+ ];
+
+ /**
+ * @var \Leaf\Db
+ */
+ public static $db;
+
+ /**
+ * @var \Leaf\Form
+ */
+ public static $form;
+
+ public function __construct($useSession = false)
+ {
+ static::$form = new Form;
+ static::$db = new Db;
+
+ if ($useSession) {
+ static::$useSession();
+ }
+ }
+
+ /**
+ * Create a db connection
+ *
+ * @param string $host The db host name
+ * @param string $host The db user
+ * @param string $host The db password
+ * @param string $host The db name
+ */
+ public static function connect(string $host, string $user, string $password, string $dbname): void
+ {
+ static::$form = new Form;
+ static::$db = new Db;
+
+ static::$db->connect($host, $user, $password, $dbname);
+ }
+
+ /**
+ * Create a database connection from env variables
+ */
+ public static function autoConnect(): void
+ {
+ static::connect(
+ getenv("DB_HOST"),
+ getenv("DB_USERNAME"),
+ getenv("DB_PASSWORD"),
+ getenv("DB_DATABASE")
+ );
+ }
+
+ /**
+ * Get or set the default token lifetime value
+ *
+ * @param int $lifeTime The new lifetime value for token
+ *
+ * @return int|string|void
+ */
+ public static function tokenLifetime($lifeTime = null)
+ {
+ if (!$lifeTime) return static::$lifeTime;
+
+ static::$lifeTime = $lifeTime;
+ }
+
+ /**
+ * Set token secret key for auth
+ *
+ * @param string $secretKey
+ */
+ public static function setSecretKey(string $secretKey)
+ {
+ static::$secretKey = $secretKey;
+ }
+
+ /**
+ * Get auth secret key
+ */
+ public static function getSecretKey()
+ {
+ return static::$secretKey;
+ }
+
+ /**
+ * Set auth config
+ */
+ public static function config($config, $value = null)
+ {
+ if (is_array($config)) {
+ foreach ($config as $key => $configValue) {
+ static::config($key, $configValue);
+ }
+ } else {
+ if (!$value) return static::$settings[$config] ?? null;
+ static::$settings[$config] = $value;
+ }
+ }
+
+ /**
+ * Exception for experimental features
+ */
+ protected static function experimental($method)
+ {
+ if (!static::config("USE_SESSION")) {
+ trigger_error("Auth::$method is experimental. Turn on USE_SESSION to use this feature.");
+ }
+ }
+
+ /**
+ * Manually start an auth session
+ */
+ public static function useSession()
+ {
+ static::$session = new \Leaf\Http\Session(false);
+ static::config("USE_SESSION", true);
+
+ session_start();
+
+ if (!static::$session->get("SESSION_STARTED_AT")) {
+ static::$session->set("SESSION_STARTED_AT", time());
+ }
+
+ static::$session->set("SESSION_LAST_ACTIVITY", time());
+ }
+
+ /**
+ * Session Length
+ */
+ public static function sessionLength()
+ {
+ static::experimental("sessionLength");
+
+ return time() - static::$session->get("SESSION_STARTED_AT");
+ }
+
+ /**
+ * Session last active
+ */
+ public static function sessionActive()
+ {
+ static::experimental("sessionActive");
+
+ return time() - static::$session->get("SESSION_LAST_ACTIVITY");
+ }
+
+ /**
+ * Refresh session
+ */
+ public static function refresh($clearData = true)
+ {
+ static::experimental("refresh");
+
+ $success = static::$session->regenerate($clearData);
+
+ static::$session->set("SESSION_STARTED_AT", time());
+ static::$session->set("SESSION_LAST_ACTIVITY", time());
+ static::$session->set("AUTH_SESISON", true);
+
+ return $success;
+ }
+
+ /**
+ * Define/Return session middleware
+ *
+ * **This method only works with session auth**
+ */
+ public static function middleware(string $name, callable $handler = null)
+ {
+ static::experimental("middleware");
+
+ if (!$handler) return static::$middleware[$name];
+
+ static::$middleware[$name] = $handler;
+ }
+
+ /**
+ * Check session status
+ */
+ public static function session()
+ {
+ static::experimental("session");
+
+ return static::$session->get("AUTH_USER") ?? false;
+ }
+
+ /**
+ * End a session
+ */
+ public static function endSession($location = null)
+ {
+ static::experimental("endSession");
+
+ static::$session->destroy();
+
+ if ($location) {
+ $route = static::config($location) ?? $location;
+ (new Http\Response)->redirect($route);
+ }
+ }
+
+ /**
+ * A simple auth guard: 'guest' pages can't be viewed when logged in,
+ * 'auth' pages can't be viewed without authentication
+ *
+ * @param array|string $type The type of guard/guard options
+ */
+ public static function guard($type)
+ {
+ static::experimental("guard");
+
+ if (is_array($type)) {
+ if (isset($type["hasAuth"])) {
+ $type = $type["hasAuth"] ? 'auth' : 'guest';
+ }
+ }
+
+ if ($type === 'guest' && static::session()) {
+ exit(header("location: " . static::config("GUARD_HOME"), true, 302));
+ }
+
+ if ($type === 'auth' && !static::session()) {
+ exit(header("location: " . static::config("GUARD_LOGIN"), true, 302));
+ }
+ }
+
+ /**
+ * Save some data to auth session
+ */
+ protected static function saveToSession($key, $data)
+ {
+ static::experimental("saveToSession");
+
+ static::$session->set($key, $data);
+ }
+
+ /**
+ * Simple user login
+ *
+ * @param string table: Table to look for users
+ * @param array $credentials User credentials
+ * @param array $validate Validation for parameters
+ *
+ * @return array user: all user info + tokens + session data
+ */
+ public static function login(string $table, array $credentials, array $validate = [])
+ {
+ $passKey = static::$settings["PASSWORD_KEY"];
+ $password = $credentials[$passKey];
+
+ if (isset($credentials[$passKey])) {
+ unset($credentials[$passKey]);
+ }
+
+ $user = static::$db->select($table)->where($credentials)->validate($validate)->fetchAssoc();
+ if (!$user) {
+ static::$errorsArray["auth"] = static::$settings["LOGIN_PARAMS_ERROR"];
+ return null;
+ }
+
+ $passwordIsValid = true;
+
+ 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]);
+ }
+ }
+
+ if (!$passwordIsValid) {
+ static::$errorsArray["password"] = static::$settings["LOGIN_PASSWORD_ERROR"];
+ return null;
+ }
+
+ $token = Authentication::generateSimpleToken($user["id"], static::$secretKey, static::$lifeTime);
+
+ if (isset($user["id"])) {
+ $userId = $user["id"];
+ }
+
+ if (static::$settings["HIDE_ID"]) {
+ unset($user["id"]);
+ }
+
+ if (static::$settings["HIDE_PASSWORD"] && (isset($user[$passKey]) || !$user[$passKey])) {
+ unset($user[$passKey]);
+ }
+
+ if (!$token) {
+ static::$errorsArray = array_merge(static::$errorsArray, Authentication::errors());
+ return null;
+ }
+
+ if (static::config("USE_SESSION")) {
+ if (isset($userId)) {
+ $user["id"] = $userId;
+ }
+
+ static::saveToSession("AUTH_USER", $user);
+ static::saveToSession("HAS_SESSION", true);
+
+ if (static::config("SAVE_SESSION_JWT")) {
+ static::saveToSession("AUTH_TOKEN", $token);
+ }
+
+ exit(header("location: " . static::config("GUARD_HOME")));
+ }
+
+ $response["user"] = $user;
+ $response["token"] = $token;
+
+ return $response;
+ }
+
+ /**
+ * Simple user registration
+ *
+ * @param string $table: Table to store user in
+ * @param array $credentials Information for new user
+ * @param array $uniques Parameters which should be unique
+ * @param array $validate Validation for parameters
+ *
+ * @return array user: all user info + tokens + session data
+ */
+ public static function register(string $table, array $credentials, array $uniques = [], array $validate = [])
+ {
+ $passKey = static::$settings["PASSWORD_KEY"];
+
+ if (static::$settings["PASSWORD_ENCODE"] !== false && isset($credentials[$passKey])) {
+ 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"]) {
+ $now = Date::now();
+ $credentials["created_at"] = $now;
+ $credentials["updated_at"] = $now;
+ }
+
+ try {
+ $query = static::$db->insert($table)->params($credentials)->unique($uniques)->validate($validate)->execute();
+ } catch (\Throwable $th) {
+ static::$errorsArray["dev"] = $th->getMessage();
+ return null;
+ }
+
+ if (!$query) {
+ static::$errorsArray = array_merge(static::$errorsArray, static::$db->errors());
+ return null;
+ }
+
+ $user = static::$db->select($table)->where($credentials)->validate($validate)->fetchAssoc();
+
+ if (!$user) {
+ static::$errorsArray = array_merge(static::$errorsArray, static::$db->errors());
+ return null;
+ }
+
+ $token = Authentication::generateSimpleToken($user["id"], static::$secretKey, static::$lifeTime);
+
+ if (isset($user["id"])) {
+ $userId = $user["id"];
+ }
+
+ if (static::$settings["HIDE_ID"]) {
+ unset($user["id"]);
+ }
+
+ if (static::$settings["HIDE_PASSWORD"] && (isset($user[$passKey]) || !$user[$passKey])) {
+ unset($user[$passKey]);
+ }
+
+ if (!$token) {
+ static::$errorsArray = array_merge(static::$errorsArray, Authentication::errors());
+ return null;
+ }
+
+ if (static::config("USE_SESSION")) {
+ if (static::config("SESSION_ON_REGISTER")) {
+ if (isset($userId)) {
+ $user["id"] = $userId;
+ }
+
+ static::saveToSession("AUTH_USER", $user);
+ static::saveToSession("HAS_SESSION", true);
+
+ if (static::config("SAVE_SESSION_JWT")) {
+ static::saveToSession("AUTH_TOKEN", $token);
+ }
+
+ exit(header("location: " . static::config("GUARD_HOME")));
+ } else {
+ exit(header("location: " . static::config("GUARD_LOGIN")));
+ }
+ }
+
+ $response["user"] = $user;
+ $response["token"] = $token;
+
+ return $response;
+ }
+
+ /**
+ * Simple user update
+ *
+ * @param string $table: Table to store user in
+ * @param array $credentials New information for user
+ * @param array $where Information to find user by
+ * @param array $uniques Parameters which should be unique
+ * @param array $validate Validation for parameters
+ *
+ * @return array user: all user info + tokens + session data
+ */
+ public static function update(string $table, array $credentials, array $where, array $uniques = [], array $validate = [])
+ {
+ $passKey = static::$settings["PASSWORD_KEY"];
+
+ if (static::$settings["PASSWORD_ENCODE"] !== false && isset($credentials[$passKey])) {
+ 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"] = Date::now();
+ }
+
+ if (count($uniques) > 0) {
+ foreach ($uniques as $unique) {
+ if (!isset($credentials[$unique])) {
+ (new Http\Response)->throwErr(["error" => "$unique not found in credentials."]);
+ }
+
+ $data = static::$db->select($table)->where($unique, $credentials[$unique])->fetchAssoc();
+
+ $wKeys = array_keys($where);
+ $wValues = array_values($where);
+
+ if (isset($data[$wKeys[0]]) && $data[$wKeys[0]] != $wValues[0]) {
+ static::$errorsArray[$unique] = "$unique already exists";
+ }
+ }
+
+ if (count(static::$errorsArray) > 0) return null;
+ }
+
+ try {
+ $query = static::$db->update($table)->params($credentials)->where($where)->validate($validate)->execute();
+ } catch (\Throwable $th) {
+ static::$errorsArray["dev"] = $th->getMessage();
+ return null;
+ }
+
+ if (!$query) {
+ static::$errorsArray = array_merge(static::$errorsArray, static::$db->errors());
+ return null;
+ }
+
+ if (isset($credentials["updated_at"])) {
+ unset($credentials["updated_at"]);
+ }
+
+ $user = static::$db->select($table)->where($credentials)->validate($validate)->fetchAssoc();
+ if (!$user) {
+ static::$errorsArray = array_merge(static::$errorsArray, static::$db->errors());
+ return null;
+ }
+
+ $token = Authentication::generateSimpleToken($user["id"], static::$secretKey, static::$lifeTime);
+
+ if (isset($user["id"])) {
+ $userId = $user["id"];
+ }
+
+ if (static::$settings["HIDE_ID"] && isset($user["id"])) {
+ unset($user["id"]);
+ }
+
+ if (static::$settings["HIDE_PASSWORD"] && (isset($user[$passKey]) || !$user[$passKey])) {
+ unset($user[$passKey]);
+ }
+
+ if (!$token) {
+ static::$errorsArray = array_merge(static::$errorsArray, Authentication::errors());
+ return null;
+ }
+
+ if (static::config("USE_SESSION")) {
+ if (isset($userId)) {
+ $user["id"] = $userId;
+ }
+
+ static::saveToSession("AUTH_USER", $user);
+ static::saveToSession("HAS_SESSION", true);
+
+ if (static::config("SAVE_SESSION_JWT")) {
+ static::saveToSession("AUTH_TOKEN", $token);
+ }
+
+ return $user;
+ }
+
+ $response["user"] = $user;
+ $response["token"] = $token;
+
+ return $response;
+ }
+
+ /**
+ * Validate Json Web Token
+ *
+ * @param string $token The token validate
+ * @param string $secretKey The secret key used to encode token
+ */
+ public static function validate($token, $secretKey = null)
+ {
+ $payload = Authentication::validate($token, $secretKey ?? static::$secretKey);
+ if ($payload) return $payload;
+
+ static::$errorsArray = array_merge(static::$errorsArray, Authentication::errors());
+
+ return null;
+ }
+
+ /**
+ * Validate Bearer Token
+ *
+ * @param string $secretKey The secret key used to encode token
+ */
+ public static function validateToken($secretKey = null)
+ {
+ $payload = Authentication::validateToken($secretKey ?? static::$secretKey);
+ if ($payload) return $payload;
+
+ static::$errorsArray = array_merge(static::$errorsArray, Authentication::errors());
+
+ return null;
+ }
+
+ /**
+ * Get Bearer token
+ */
+ public static function getBearerToken()
+ {
+ $token = Authentication::getBearerToken();
+ if ($token) return $token;
+
+ static::$errorsArray = array_merge(static::$errorsArray, Authentication::errors());
+
+ return null;
+ }
+
+ /**
+ * Get the current user data from token
+ *
+ * @param string $table The table to look for user
+ * @param array $hidden Fields to hide from user array
+ */
+ public static function user($table = "users", $hidden = [])
+ {
+ if (!static::id()) {
+ if (static::config("USE_SESSION")) {
+ return static::$session->get("AUTH_USER");
+ }
+
+ return null;
+ }
+
+ $user = static::$db->select($table)->where("id", static::id())->fetchAssoc();
+
+ if (count($hidden) > 0) {
+ foreach ($hidden as $item) {
+ if (isset($user[$item]) || !$user[$item]) {
+ unset($user[$item]);
+ }
+ }
+ }
+
+ return $user;
+ }
+
+ /**
+ * Return the user id encoded in token
+ */
+ public static function id()
+ {
+ if (static::config("USE_SESSION")) {
+ return static::$session->get("AUTH_USER")["id"] ?? null;
+ }
+
+ $payload = static::validateToken(static::getSecretKey());
+ if (!$payload) return null;
+ return $payload->user_id;
+ }
+
+ /**
+ * Return form field
+ */
+ public static function get($param)
+ {
+ return static::$form->get($param);
+ }
+
+ /**
+ * Get all authentication errors as associative array
+ */
+ public static function errors(): array
+ {
+ return static::$errorsArray;
+ }
+}
diff --git a/src/Helpers/Authentication.php b/src/Helpers/Authentication.php
new file mode 100755
index 0000000..91c5340
--- /dev/null
+++ b/src/Helpers/Authentication.php
@@ -0,0 +1,130 @@
+
+ * @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)
+ {
+ $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 string $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)
+ {
+ 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)
+ {
+ $payload = JWT::decode($token, $secretPhrase, ['HS256']);
+ if ($payload !== null) return $payload;
+
+ 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
new file mode 100755
index 0000000..8fe1da5
--- /dev/null
+++ b/src/Helpers/JWT.php
@@ -0,0 +1,371 @@
+
+ * @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;
+ }
+}