34 Commits
Author SHA1 Message Date
mcndave 1e1d6e1ce3 Merge branch 'v2.x'
# Conflicts:
#	src/Lib/Authenticate.php
#	src/Middleware/HeaderAuthenticationMiddleware.php
#	src/Middleware/PostRequestAuthenticationMiddleware.php
2026-10-20 08:21:12 -04:00
Dave Mc Nicoll 6a24bcad76 - Added a new CookieAuthentication middleware 2026-10-20 11:57:30 +00:00
mcndave 8d648e88c8 - WIP on package manager 2026-07-16 14:13:22 -04:00
mcndave 61db8fcc95 Merge branch 'master' of https://git.mcnd.ca/mcndave/ulmus-user 2026-07-16 13:28:36 -04:00
mcndave 6ddf358f41 - Added Ulmus version dependencies 2026-07-16 13:28:17 -04:00
mcndave 33ff1b5b8c - User now has a reverse fullname parameter
- forgetMe() defined as void
2026-07-14 16:49:44 -04:00
mcndave 142509e9f6 Merge branch 'master' of https://git.mcnd.ca/mcndave/ulmus-user 2026-07-01 14:29:54 -04:00
mcndave 45546fbcc0 - WIP on securing PostRequestAuth and began adding some LAN auth also 2026-07-01 14:29:45 -04:00
Dave Mc Nicoll 2e7b46eae6 - Fixed some mistyped variables 2026-06-17 20:59:55 +00:00
Dave Mc Nicoll 0eaaf79ad8 - WIP on v2.x authentication 2026-02-25 20:10:09 +00:00
Dave Mc Nicoll 0271aec31b - WIP on V 2.x which added a OAuth2 server 2025-06-13 19:10:51 +00:00
Dave Mc Nicoll 6582378b6e - WIP on 2.x auth. method 2025-03-28 13:38:28 +00:00
Dave Mc Nicoll d149e0a741 - Moved Authenticate to DI file def 2025-03-28 13:37:38 +00:00
Dave Mc Nicoll 23079e6040 - Added payload expiration time validation 2024-12-09 21:58:42 +00:00
Dave Mc Nicoll d3bb2c248a - Added RememberMe middleware 2024-12-05 15:32:01 -05:00
Dave Mc Nicoll e63ea439d6 - Added JsonWebToken encoder 2024-05-31 19:00:04 +00:00
Dave Mc Nicoll c4e4db7a45 -WIP on Bearer token 2024-05-31 12:27:07 +00:00
mcndave 0657622eab - Fixed a bug where given entity would failed it's loading with an error 2023-12-12 18:57:03 -05:00
mcndave 70daae93f5 - Added a new invalid pass. exception 2023-12-04 12:36:56 -05:00
Dave Mc Nicoll 9570320a3b - Work done to support Basic / Bearer / Digest method 2023-11-13 14:23:24 -05:00
mcndave 0f2886b266 - WIP on authentication 2023-11-09 13:38:07 -05:00
mcndave 9ae3e2d6f9 - WIP on user auth 2023-11-08 06:53:13 -05:00
mcndave 7769b1a1ca - WIP on authentication portal 2023-11-03 19:44:46 -04:00
mcndave 1559ceb248 Merge branch 'master' of https://git.mcnd.ca/mcndave/ulmus-user 2023-11-03 12:46:28 +00:00
mcndave c7b04d7e46 - Defined creation/updated dates as readonly 2023-11-03 12:46:14 +00:00
mcndave c6fc5d020b Merge branch 'master' of https://git.mcnd.ca/mcndave/ulmus-user 2023-11-03 08:27:06 -04:00
mcndave 944c524cfb - WIP on header-based authentication 2023-11-03 08:26:48 -04:00
mcndave fbf9bc871c - Fixed Composer.sjon 2023-10-30 15:28:48 +00:00
Dave Mc Nicoll ad802fc35d Merge branch 'attributes' 2023-02-08 16:38:48 +00:00
mcndave 4664a8779a Merge branch 'master' of https://git.mcnd.ca/mcndave/ulmus-user 2023-02-01 18:04:32 +00:00
mcndave cef536caa5 - Fixed a bug within the merge 2022-11-24 20:01:49 +00:00
mcndave 10ad77d607 Merge branch 'master' of https://git.mcnd.ca/mcndave/ulmus-user 2022-10-12 18:18:49 +00:00
mcndave 015f0c70b1 - hashPassword can now be used as a standalone method 2022-10-12 18:18:36 +00:00
Dave Mc Nicoll 267fe5cadd Fixed some loading problems with loaded user and ajusted the Email allowed length (to 255) 2022-10-10 14:27:55 +00:00
39 changed files with 1188 additions and 63 deletions

No files matched your search

+8 -2
View File
@@ -6,12 +6,18 @@
"authors": [
{
"name": "Dave Mc Nicoll",
"email": "mcndave@gmail.com"
"email": "info@mcnd.ca"
}
],
"require": {
"mcnd/notes": "master-dev"
"mcnd/ulmus": "^1.0 || ^2.0"
},
"repositories": [
{
"type": "composer",
"url": "https://git.mcnd.ca/api/packages/mcndave/composer"
}
],
"autoload": {
"psr-4": {
"Ulmus\\User\\": "src/"
@@ -0,0 +1,13 @@
<?php
namespace Ulmus\User\Authorize;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Entity\UserInterface;
interface AuthorizeMethodInterface
{
public function connect(ServerRequestInterface $request) : ServerRequestInterface;
public function catchRequest(ServerRequestInterface $request) : bool;
}
@@ -0,0 +1,15 @@
<?php
namespace Ulmus\User\Authorize\Bearer\Algorithms;
use Ulmus\User\Authorize\Bearer\JsonWebTokenException;
class HmacSha
{
public static function encode(string $encodedHeader, string $encodedPayload, string $secretKey) : string
{
}
}
@@ -0,0 +1,68 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
enum JsonWebTokenAlgorithmEnum
{
case HS256;
case HS384;
case HS512;
case RS256;
case RS384;
case RS512;
case ES256;
case ES384;
case ES512;
case PS256;
case PS384;
case PS512;
public static function list() : array
{
return array_map(fn(JsonWebTokenAlgorithmEnum $case) => $case->name, static::cases());
}
public static function fromString(string $name) : self
{
foreach (static::cases() as $item) {
if ($item->name === $name) {
return $item;
}
}
}
public static function exists(string $key) : bool
{
return in_array($key, static::list());
}
public function assessOperability() : void
{
$method = $this->phpAlgoMethods();
if (empty($method) || ! in_array($method[0], $method[2])) {
throw new JsonWebTokenException(sprintf("Choosen algorithm do not seems to be supported by your PHP version '%s'", $this->name));
}
}
public function phpAlgoMethods() : false|array
{
return match($this) {
self::HS256 => [ 'sha256', 'hash_hmac', hash_hmac_algos() ],
self::HS384 => [ 'sha384', 'hash_hmac', hash_hmac_algos() ],
self::HS512 => [ 'sha512', 'hash_hmac', hash_hmac_algos() ],
# Support for other algorithms not implemented yet...
} ?? false;
}
}
@@ -0,0 +1,84 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
class JsonWebTokenDecoder
{
protected array $header;
protected array $payload;
public function __construct(
public string $encoded,
public string $secretKey,
) {}
protected function parse() : bool
{
try {
list($encodedHeader, $encodedPayload, $signature) = explode('.', $this->encoded);
foreach([ 'header' => $encodedHeader, 'payload' => $encodedPayload ] as $key => $value) {
$decoded = static::base64url_decode($value) ;
if ( $decoded !== false ){
$jsonArray = json_decode($decoded, true);
if ( is_array($jsonArray) ) {
if ($key === 'header') {
JsonWebTokenValidate::validateHeaderType($jsonArray);
JsonWebTokenValidate::validateHeaderAlgorithm($jsonArray);
}
elseif ($key === 'payload') {
JsonWebTokenValidate::validatePayloadExpiration($jsonArray);
}
$this->$key = $jsonArray;
}
else {
throw new JsonWebTokenDecodingError(sprintf("Invalid JSON returned while decoding section %s ; an array must be provided", $key));
}
}
else {
throw new JsonWebTokenDecodingError(sprintf("An error occured while decoding a base64 string from section %s", $key));
}
}
JsonWebTokenValidate::validateSignature($this->header['alg'], $this->secretKey, $encodedHeader, $encodedPayload, $signature);
}
catch(\Throwable $t) {
throw new JsonWebTokenDecodingError($t->getMessage(), $t->getCode(), $t);
}
return true;
}
public static function base64url_decode($data) : string|false
{
return base64_decode(strtr($data, '-_', '+/'));
}
public function decode() : bool
{
return $this->parse();
}
/**
* Basic validation of JWT encoded string
* @return bool
*/
public function isJWT() : bool
{
try {
return strlen($this->encoded) > 23 && count(explode('.', $this->encoded)) === 3;
}
catch(\Throwable $t) {
return false;
}
}
public function getPayload() : array
{
return $this->payload;
}
}
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
class JsonWebTokenDecodingError extends JsonWebTokenException
{
}
@@ -0,0 +1,51 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
class JsonWebTokenEncoder
{
protected string $token;
protected array $header = [
"typ" => "JWT",
];
public function __construct(
public array $payload,
public string $secretKey,
protected JsonWebTokenAlgorithmEnum $algorithm = JsonWebTokenAlgorithmEnum::HS256,
) {
$this->header['alg'] = $this->algorithm->name;
}
public static function base64url_encode($data) : string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
public function encode() : string
{
$jsonHeader = json_encode($this->header);
$jsonPayload = json_encode($this->payload);
$encodedHeader = static::base64url_encode($jsonHeader);
$encodedPayload = static::base64url_encode($jsonPayload);
list($algo, $method, ) = $this->algorithm->phpAlgoMethods();
switch($method) {
case 'hash_hmac':
$signature = hash_hmac($algo, sprintf("%s.%s", $encodedHeader, $encodedPayload), $this->secretKey, true);
break;
}
$this->token = sprintf("%s.%s.%s", $encodedHeader, $encodedPayload, static::base64url_encode($signature));
return $this->getToken();
}
public function getToken() : string
{
return $this->token;
}
}
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
class JsonWebTokenException extends \Exception
{
}
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
class JsonWebTokenInvalidSignatureError extends JsonWebTokenException
{
}
@@ -0,0 +1,18 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
enum JsonWebTokenTypeEnum
{
case JWT;
public static function list() : array
{
return array_map(fn(JsonWebTokenTypeEnum $case) => $case->name, static::cases());
}
public static function exists(string $key) : bool
{
return in_array($key, static::list());
}
}
@@ -0,0 +1,65 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
abstract class JsonWebTokenValidate
{
public static function validateHeaderAlgorithm(array $header) : void
{
if (! array_key_exists('alg', $header)) {
throw new JsonWebTokenDecodingError("Your header data is missing a valid algorithm (alg).");
}
if ( ! JsonWebTokenAlgorithmEnum::exists($header['alg']) ) {
throw new JsonWebTokenDecodingError(
sprintf("Given algorithm '%s' is not supported. Please try with one of the above : %s", $header['alg'], implode(', ', JsonWebTokenAlgorithmEnum::list()))
);
}
}
public static function validateHeaderType(array $header) : void
{
if ( array_key_exists('typ', $header) && ! JsonWebTokenTypeEnum::exists($header['typ']) ) {
throw new JsonWebTokenDecodingError(
sprintf("Given type '%s' is not supported. Please try with one of the above : %s", $header['typ'], implode(', ', JsonWebTokenTypeEnum::list()))
);
}
}
public static function validatePayloadExpiration(array $payload) : void
{
if ( array_key_exists('exp', $payload) && ( $payload['exp'] < time() ) ) {
throw new JsonWebTokenDecodingError(
sprintf("Given token is expired (%s)", ( new \DateTime())->setTimestamp($payload['exp'])->format(\DateTime::ISO8601) )
);
}
}
public static function validateSignature(string $alg, string $secret, string $encodedHeader, string $encodedPayload, string $encodedSignature) : void
{
$algorithm = JsonWebTokenAlgorithmEnum::fromString($alg);
static::validateAlgorithm($algorithm);
$decodedSignature = JsonWebTokenDecoder::base64url_decode($encodedSignature);
list($algo, $method, ) = $algorithm->phpAlgoMethods();
switch($method) {
case 'hash_hmac':
$compare = hash_hmac($algo, sprintf("%s.%s", $encodedHeader, $encodedPayload), $secret, true);
break;
}
if ( ($compare ?? null) !== $decodedSignature) {
throw new JsonWebTokenDecodingError(
sprintf("Given signature (%s) do not match computed signature", $encodedSignature)
);
}
}
public static function validateAlgorithm(JsonWebTokenAlgorithmEnum $algorithm) : void
{
$algorithm->assessOperability();
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace Ulmus\User\Authorize;
use DI\Attribute\Inject;
use Psr\Http\Message\ServerRequestInterface;
use Storage\Cookie;
use Ulmus\User\Lib\Authenticate;
use Ulmus\User\Lib\HeaderAuthenticationTypeEnum;
class CookieAuthentication implements AuthorizeMethodInterface
{
public function __construct(
protected Authenticate $authorize,
protected Cookie $cookie,
) { }
public function connect(ServerRequestInterface $request): ServerRequestInterface
{
$methodObj = new Header\BearerMethod($this->cookie->get('jwt'));
if (isset($methodObj)) {
$request = $methodObj->execute($request);
}
return $request;
}
public function catchRequest(ServerRequestInterface $request) : bool
{
return $this->cookie->has('jwt');
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace Ulmus\User\Authorize\Header;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Lib\AuthenticationMethodEnum;
class BasicMethod implements MethodInterface
{
public function __construct(
protected string|array $arguments
) {}
public function execute(ServerRequestInterface $request) : ServerRequestInterface
{
if ( false === $decoded = base64_decode($this->arguments) ) {
throw new \RuntimeException("Base64 decoding of given username:password failed");
}
list($username, $password) = explode(':', $decoded) + [ null, null ];
if ( empty($username) ) {
throw new \RuntimeException("A username must be provided");
}
elseif ( empty($password) ) {
throw new \RuntimeException("A password must be provided");
}
return $request->withAttribute('authentication_middleware:method', AuthenticationMethodEnum::UsernamePassword)
->withAttribute('authentication_middleware:username', $username)
->withAttribute('authentication_middleware:password', $password);
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace Ulmus\User\Authorize\Header;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Authorize\Bearer\JsonWebTokenDecoder;
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Lib\Authenticate;
use Ulmus\User\Lib\AuthenticationMethodEnum;
class BearerMethod implements MethodInterface
{
protected JsonWebTokenDecoder $jwt;
public function __construct(
protected string $token
) {}
public function execute(ServerRequestInterface $request) : ServerRequestInterface
{
switch($this->autodetectTokenType()) {
case BearerTokenTypeEnum::JsonWebToken:
$this->jwt->decode();
$payload = $this->jwt->getPayload();
if ( $payload['sub'] ?? false) {
$request = $request
->withAttribute('authentication_middleware:method', AuthenticationMethodEnum::ForceLogin)
->withAttribute('authentication_middleware:jwt', $payload)
->withAttribute('authentication_middleware:user_id', $payload['sub']);
}
else {
throw new \InvalidArgumentException("Given JsonWebToken is missing a 'sub' key (which match with a user's id)");
}
break;
case BearerTokenTypeEnum::UniqueKey:
# @TODO
break;
default:
}
return $request;
}
public function autodetectTokenType() : BearerTokenTypeEnum
{
try {
$this->jwt = new JsonWebTokenDecoder($this->token, hash('sha512', getenv('LEAN_RANDOM')));
if ( $this->jwt->isJWT() ) {
return BearerTokenTypeEnum::JsonWebToken;
}
}
catch(\Throwable $ex) {/* Not a JWT */}
return BearerTokenTypeEnum::UniqueKey;
}
}
@@ -0,0 +1,10 @@
<?php
namespace Ulmus\User\Authorize\Header;
enum BearerTokenTypeEnum : string
{
case JsonWebToken = "JWT";
case UniqueKey = "UniqueKey";
}
+111
View File
@@ -0,0 +1,111 @@
<?php
namespace Ulmus\User\Authorize\Header;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Entity\DigestAuthUserInterface;
use Ulmus\User\Lib\Authenticate;
class DigestMethod implements MethodInterface
{
public function __construct(
# protected Authenticate $authorize,
# protected DigestAuthUserInterface $user,
protected string|array $arguments
) {}
public function execute(ServerRequestInterface $request) : ServerRequestInterface
{
$arguments = $this->parseDigestArguments($this->arguments);
if (empty($arguments['username'])) {
throw new \InvalidArgumentException("A 'username' key is required to authenticate using Digest");
}
$isSess = stripos($arguments['algorithm'] ?? "", "-SESS") !== false;
$hashMethod = $this->getDigestAlgorithmHash($arguments);
$arguments['nc'] = str_pad($arguments['nc'] ?? "1", 8, '0', STR_PAD_LEFT);
$ha1 = "@TODO !@"; # $this->authorize->user->getSecretHash();
if ($isSess) {
$ha1 = hash($hashMethod, implode(':', [
$ha1, $arguments['nonce'] , $arguments['cnonce']
]));
}
switch($arguments['qop'] ?? 'auth') {
case 'auth-int':
$ha2 = hash($hashMethod, implode(':', [
strtoupper($request->getMethod()), $arguments['uri'], hash($hashMethod, $body ?? "")
]));
break;
case 'auth':
default:
$ha2 = hash($hashMethod, implode(':', [
strtoupper($request->getMethod()), $arguments['uri']
]));
break;
}
if (isset($arguments['qop'])) {
$response = hash($hashMethod, implode(':', [
$ha1, $arguments['nonce'], $arguments['nc'], $arguments['cnonce'], $arguments['qop'], $ha2
]));
}
else {
$response = hash($hashMethod, implode(':', [
$ha1, $arguments['nonce'], $ha2
]));
}
if ($response === $arguments['response']) {
$request = $request->withAttribute('authentication_middleware:user_id', $arguments['username']);
# @TODO ! Add Type here to match login with Username Only
}
return $request;
}
protected function parseDigestArguments(string|array $arguments) : array
{
if (is_string($arguments)) {
$keys = [ 'nonce', 'nc', 'cnonce', 'qop', 'username', 'uri', 'realm', 'response', 'opaque', 'algorithm' ];
# From https://www.php.net/manual/en/features.http-auth.php
preg_match_all('@(' . implode('|', $keys) . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $arguments, $matches, PREG_SET_ORDER);
$arguments = [];
foreach ($matches as $match) {
$arguments[$match[1]] = $match[3] ?: $match[4];
}
}
return $arguments;
}
protected function getDigestAlgorithmHash(array $arguments) : string
{
switch(strtoupper($arguments['algorithm'] ?? "")) {
case "SHA-512-256":
case "SHA-512-256-SESS":
return "sha512/256";
case "SHA-256":
case "SHA-256-SESS":
return "sha256";
default:
case 'MD5-SESS':
case 'MD5':
return "md5";
}
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Ulmus\User\Authorize\Header;
use Psr\Http\Message\ServerRequestInterface;
interface MethodInterface
{
public function execute(ServerRequestInterface $request) : ServerRequestInterface;
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace Ulmus\User\Authorize;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Common\AuthorizeContentTypeEnum;
use Ulmus\User\Entity\{ DigestAuthUserInterface, UserInterface };
use Ulmus\User\Lib\Authenticate;
use Ulmus\User\Lib\HeaderAuthenticationTypeEnum;
class HeaderAuthentication implements AuthorizeMethodInterface
{
public function __construct(
protected Authenticate $authorize,
) { }
public function connect(ServerRequestInterface $request): ServerRequestInterface
{
$type = HeaderAuthenticationTypeEnum::fromRequest($request);
if ( null !== $type ) {
list(, $value) = explode(' ', $request->getHeaderLine('Authorization'), 2) + [ null, null ];
switch($type) {
case HeaderAuthenticationTypeEnum::Basic:
$methodObj = new Header\BasicMethod($value);
break;
case HeaderAuthenticationTypeEnum::Digest:
#if (! $user instanceof DigestAuthUserInterface) {
# throw new \RuntimeException("Your user entity must provide a valid hash of `user:realm:password` ");
#}
$methodObj = new Header\DigestMethod($value);
break;
case HeaderAuthenticationTypeEnum::Bearer:
$methodObj = new Header\BearerMethod($value);
break;
case HeaderAuthenticationTypeEnum::Token:
// @TODO !
break;
}
}
if (isset($methodObj)) {
$request = $methodObj->execute($request);
}
return $request;
}
public function catchRequest(ServerRequestInterface $request) : bool
{
# Previously only catched from JSON content-type ; now accepts every connections
/*foreach(array_merge($request->getHeader('Accept'), $request->getHeader('Content-Type')) as $accept) {
foreach(explode(',', $accept) as $contentType) {
if ( AuthorizeContentTypeEnum::tryFrom(strtolower($contentType)) ) {
return true;
}
}
}*/
return $request->hasHeader('authorization');
}
}
@@ -0,0 +1,55 @@
<?php
namespace Ulmus\User\Authorize;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Lib\Authenticate;
class MacAddressAuthentication implements AuthorizeMethodInterface
{
public function __construct(
public Authenticate $authenticate,
protected string $fieldUser = "mac",
) {}
public function connect(ServerRequestInterface $request, UserInterface $user): bool
{
$server = $request->getServerParams();
# var_dump($server, $this->getArpMacAddress($server['REMOTE_ADDR']));die();
return false;
return $this->authenticate->authenticate([ $this->fieldUser => $post[$this->postFieldUser] ], $post[$this->postFieldPassword]);
}
public function catchRequest(ServerRequestInterface $request): bool
{
return true;
$post = $request->getParsedBody();
return strtoupper( $request->getMethod() ) === "POST" && isset($post[$this->postFieldUser], $post[$this->postFieldPassword]);
}
protected function getArpMacAddress(string $ip) : ?string
{
#run the external command, break output into lines
$arp = shell_exec(sprintf("arp -a %s", escapeshellarg($ip)));
$lines = explode("\n", $arp);
#look for the output line describing our IP address
foreach($lines as $line)
{
$cols = preg_split('/\s+/', trim($line));
if ($cols[0] === $ip)
{
return $cols[0];
}
}
return false;
}
}
@@ -0,0 +1,33 @@
<?php
namespace Ulmus\User\Authorize;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Lib\Authenticate;
use Ulmus\User\Lib\AuthenticationMethodEnum;
class PostRequestAuthentication implements AuthorizeMethodInterface
{
public function __construct(
protected string $fieldUser = "email",
protected string $postFieldUser = "email",
protected string $postFieldPassword = "password",
) {}
public function connect(ServerRequestInterface $request) : ServerRequestInterface
{
$post = $request->getParsedBody();
return $request->withAttribute('authentication_middleware:method', AuthenticationMethodEnum::UsernamePassword)
->withAttribute('authentication_middleware:username', [ $this->fieldUser => $post[$this->postFieldUser] ])
->withAttribute('authentication_middleware:password', $post[$this->postFieldPassword]);
}
public function catchRequest(ServerRequestInterface $request): bool
{
$post = $request->getParsedBody();
return strtoupper( $request->getMethod() ) === "POST" && isset($post[$this->postFieldUser], $post[$this->postFieldPassword]);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Ulmus\User\Common;
enum AuthorizeContentTypeEnum: string
{
case ApplicationJson = "application/json";
case ApplicationLDJson = "application/+json";
case ApplicationVndApiJson = "application/vnd.api+json";
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\User\Entity;
interface BasicAuthUserInterface extends UserInterface
{
public function usernameField() : string;
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Ulmus\User\Entity;
interface DigestAuthUserInterface extends UserInterface
{
# Represent a hash (md5, sha256, ...) of username:realm:password
public function getSecretHash() : string;
}
+22 -12
View File
@@ -2,11 +2,12 @@
namespace Ulmus\User\Entity;
use Ulmus\Entity\EntityInterface;
use Ulmus\Entity\Field\Datetime;
use Ulmus\Attribute\Property\Field;
class User {
abstract class User implements UserInterface {
#[Field\Id(readonly: true)]
public int $id;
@@ -47,40 +48,49 @@ class User {
#[Field]
public string $password;
#[Field\UpdatedAt(readonly: true, name: "updated_at")]
public ? Datetime $updatedAt;
#[Field\CreatedAt(readonly: true, name: "created_at")]
#[Field\CreatedAt(name: "created_at", readonly: true)]
public Datetime $createdAt;
#[Field\UpdatedAt(name: "updated_at", readonly: true)]
public ? Datetime $updatedAt;
public bool $logged = false;
public function __toString() : string
{
return $this->fullname();
return $this->fullName();
}
public function setPassword($password) : self
public function setPassword($password) : static
{
$this->password = $password;
return $this->hashPassword();
}
public function hashPassword() : self
public function hashPassword(? string $password = null) : static
{
$this->password = password_hash($this->password, PASSWORD_DEFAULT);
$this->password = password_hash($password ?: $this->password, PASSWORD_DEFAULT);
return $this;
}
public function verifyPassword(string $password) : bool
{
return password_verify($password, $this->password );
return password_verify($password, $this->password ?? null);
}
public function fullname() : string
public function fullname(bool $reverse = false) : string
{
return trim( ( $this->firstName ?? "" ) . " " . ( $this->lastName ?? "" ) );
if (! $this->isLoaded() ) {
return "???";
}
return trim($reverse ? "{$this->lastName}, {$this->firstName}" : "{$this->firstName} {$this->lastName}");
}
public function loggedIn(?bool $set = null): bool
{
return $set !== null ? $this->logged = $set : $this->logged;
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Ulmus\User\Entity;
use Ulmus\Entity\EntityInterface;
interface UserInterface extends EntityInterface
{
public function __toString() : string;
public function loggedIn(?bool $set = null) : bool;
public function verifyPassword(string $password) : bool;
public function hashPassword(? string $password = null) : static;
public function setPassword($password) : static;
}
@@ -0,0 +1,5 @@
<?php
namespace Ulmus\User\Exception;
class InvalidPasswordException extends \Exception {}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace Ulmus\User\Exception;
class InvalidUserException extends \Exception {}
+84 -49
View File
@@ -5,70 +5,60 @@ namespace Ulmus\User\Lib;
use Storage\{Session, Cookie};
use \Closure;
use Ulmus\User\Entity\User;
use Ulmus\Exception;
use Ulmus\User\Entity\UserInterface;
class Authenticate {
protected ? Session $session;
protected ? Cookie $cookie;
protected bool $logged = false;
protected Closure $authenticationEvent;
public ? User $user = null;
public string $connection_fields = 'email';
public UserInterface $user;
public function __construct(
? Session $session = null,
? Cookie $cookie = null,
? Closure $authenticationEvent = null
UserInterface $user,
protected ? Session $session = null,
protected ? Cookie $cookie = null,
? Closure $authenticationEvent = null,
) {
$this->session = $session;
$this->cookie = $cookie;
$this->authenticationEvent = $authenticationEvent ?: function(bool $authenticated, string $message, ? User $user, array $data = []) : ? bool {return null;} ;
$this->authenticationEvent = $authenticationEvent ?: fn(bool $authenticated, string $message, ? UserInterface $user, array $data = []) : ? bool => null;
$this->user = $user;
}
public function rememberMe(\Ulmus\Repository $repository) : ? User
public function rememberMe() : ? UserInterface
{
$logUser = function(int $id) use ($repository) {
if ( null === ( $user = $repository->loadFromPk($id) ) ) {
throw new \Exception("User not found.");
}
$user->logged = true;
return $user;
};
if ( $this->session && $this->session->has("user.id") ) {
return $logUser($this->session->get("user.id"));
return $this->logUser($this->session->get("user.id"));
}
if ( $this->cookie && $this->cookie->has("user.id") ) {
return $logUser($this->cookie->get("user.id"));
return $this->logUser($this->cookie->get("user.id"));
}
return null;
}
public function forgetMe() {
public function forgetMe() : void
{
$this->cookie->delete("user.id");
$this->session->destroy();
}
public function authenticate(\Ulmus\Repository $repository, array $userLogin, string $password) : User
public function authenticate(string|array $userLogin, string $password) : bool
{
$repository = $this->user::repository();
if (is_string($userLogin)) {
$userLogin = [ $this->user->usernameField() => $userLogin ];
}
foreach($userLogin as $field => $value) {
$repository->or($field, $value);
}
try {
$this->user = $repository->loadOne() ?: $repository->instanciateEntity();
if (null !== $entity = $repository->loadOne()) {
$this->user->fromArray($entity->toArray());
}
}
catch(Exception\EmptyDatasetException $e) {
if ( ! call_user_func_array($this->authenticationEvent, [ false, 'userNotFound', $this->user ]) ) {
@@ -84,15 +74,7 @@ class Authenticate {
$response = call_user_func_array($this->authenticationEvent, [ false, 'verifyPassword', $this->user, [ 'password' => $password ] ]);
if ( $response !== null ? $response : $this->user->verifyPassword($password) ) {
$this->user->logged = true;
if ( $this->session ) {
$this->session->set("user.id", $this->user->id);
}
if ( $this->cookie ) {
$this->cookie->set("user.id", $this->user->id);
}
$this->login();
call_user_func_array($this->authenticationEvent, [ true, 'success', $this->user ]);
}
@@ -107,14 +89,31 @@ class Authenticate {
call_user_func_array($this->authenticationEvent, [ false, 'authenticationFailed', $this->user, [ 'user_login' => $userLogin, 'password' => $password ] ]);
}
return $this->user;
return $this->user->logged;
}
public function login() : void
{
if ( !$this->user->isLoaded() ) {
return;
}
$this->user->logged = true;
if ( $this->session ) {
$this->session->set("user.id", $this->user->id);
}
if ( $this->cookie && ! $this->cookie->has('user.id')) {
$this->cookie->set("user.id", $this->user->id, samesite: "none");
}
}
/**
* Force user disconnection and handle memory trashing
*/
public function logout() : self
{
$this->user->logged = false;
if ( $this->session ) {
$this->session->delete('user.id');
}
@@ -123,8 +122,44 @@ class Authenticate {
$this->cookie->delete('user.id');
}
$this->user->logged = false;
return $this;
}
}
public function loadUser(?int $id) : ? UserInterface
{
try {
if ($id === null || null === ($entity = $this->user::repository()->loadFromPk($id))) {
return null;
}
}
catch(\Exception $ex) {
return null;
}
$this->user->fromArray($entity);
return $this->user;
}
public function logUser(null|int|object $id) : ? UserInterface
{
if (! is_object($id)) {
try {
if ($id === null || null === ($entity = $this->user::repository()->loadFromPk($id))) {
return null;
}
} catch (\Exception $ex) {
return null;
}
}
else {
$entity = $id;
}
$this->user->fromArray($entity);
$this->user->loggedIn(true);
return $this->user;
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Ulmus\User\Lib;
enum AuthenticationMethodEnum
{
case ForceLogin;
case UsernamePassword;
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace Ulmus\User\Lib;
use Ulmus\User\Entity\UserInterface;
class Authorize extends Authenticate
{
public function rememberMe() : ? UserInterface
{
return null;
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Ulmus\User\Lib;
use Psr\Http\Message\ServerRequestInterface;
enum HeaderAuthenticationTypeEnum : string
{
case Basic = "basic";
case Digest = "digest";
case Bearer = "bearer";
case Token = "token";
public static function fromRequest(ServerRequestInterface $request) : ? static
{
if ($request->hasHeader('authorization')) {
list($method, ) = explode(' ', $request->getHeaderLine('authorization'), 2);
return static::tryFrom(strtolower($method));
}
return null;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Ulmus\User\Middleware;
use Psr\Http\{
Message\ResponseInterface,
Message\ServerRequestInterface,
Server\MiddlewareInterface,
Server\RequestHandlerInterface
};
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Authorize\PostRequestAuthentication;
use Ulmus\User\Exception\InvalidUserException;
use Ulmus\User\Lib\Authenticate;
use Ulmus\User\Lib\AuthenticationMethodEnum;
class AuthenticationMiddleware implements MiddlewareInterface
{
public function __construct(
protected Authenticate $authenticator,
protected \Closure $loginFailedResponse,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$this->launchAuthentication($request);
}
catch(InvalidUserException $e) {
return call_user_func($this->loginFailedResponse, [ 'api.error_message' => $e->getMessage() ]);
}
return $handler->handle($request);
}
protected function launchAuthentication(ServerRequestInterface $request) : void
{
if (null !== $id = $request->getAttribute('authentication_middleware:user_id')) {
$this->authenticator->loadUser($id);
if ( ! $this->authenticator->user->isLoaded() ) {
throw new InvalidUserException("Given user id do not match with an existing/active user");
}
}
switch($request->getAttribute('authentication_middleware:method')) {
case AuthenticationMethodEnum::ForceLogin:
$this->authenticator->login();
break;
case AuthenticationMethodEnum::UsernamePassword:
$this->authenticator->authenticate($request->getAttribute('authentication_middleware:username'), $request->getAttribute('authentication_middleware:password'));
break;
}
}
}
@@ -0,0 +1,30 @@
<?php
namespace Ulmus\User\Middleware;
use DI\Attribute\Inject;
use Psr\Http\{
Message\ResponseInterface,
Message\ServerRequestInterface,
Server\MiddlewareInterface,
Server\RequestHandlerInterface
};
use Ulmus\User\Authorize\CookieAuthentication;
class CookieAuthenticationMiddleware implements MiddlewareInterface
{
public function __construct(
#[Inject]
protected CookieAuthentication $authenticator,
) { }
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ( $this->authenticator->catchRequest($request) ) {
$request = $this->authenticator->connect($request);
}
return $handler->handle($request);
}
}
@@ -0,0 +1,29 @@
<?php
namespace Ulmus\User\Middleware;
use Psr\Http\{
Message\ResponseInterface,
Message\ServerRequestInterface,
Server\MiddlewareInterface,
Server\RequestHandlerInterface
};
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Authorize\HeaderAuthentication;
use Ulmus\User\Lib\Authenticate;
class HeaderAuthenticationMiddleware implements MiddlewareInterface
{
public function __construct(
protected HeaderAuthentication $authenticator,
) { }
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ( $this->authenticator->catchRequest($request) ) {
$request = $this->authenticator->connect($request);
}
return $handler->handle($request);
}
}
@@ -0,0 +1,40 @@
<?php
namespace Ulmus\User\Middleware;
use Psr\Http\{
Message\ResponseInterface,
Message\ServerRequestInterface,
Server\MiddlewareInterface,
Server\RequestHandlerInterface
};
use Ulmus\User\Authorize\MacAddressAuthentication;
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Authorize\PostRequestAuthentication;
use Ulmus\User\Lib\Authenticate;
class LocalMacAddressMiddleware implements MiddlewareInterface
{
protected MacAddressAuthentication $authenticator;
public function __construct(
protected UserInterface $entity,
protected \Closure $loginFailedResponse,
? MacAddressAuthentication $authenticator = null,
) {
$this->authenticator = $authenticator ?: new MacAddressAuthentication(new Authenticate($entity));
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->authenticator->authenticate->rememberMe();
if ( $this->authenticator->catchRequest($request) ) {
if ( ! $this->authenticator->connect($request, $this->entity) ) {
return call_user_func($this->loginFailedResponse, "Login failed");
}
}
return $handler->handle($request);
}
}
@@ -0,0 +1,30 @@
<?php
namespace Ulmus\User\Middleware;
use Psr\Http\{
Message\ResponseInterface,
Message\ServerRequestInterface,
Server\MiddlewareInterface,
Server\RequestHandlerInterface
};
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Authorize\PostRequestAuthentication;
use Ulmus\User\Lib\Authenticate;
class PostRequestAuthenticationMiddleware implements MiddlewareInterface
{
public function __construct(
protected PostRequestAuthentication $authenticator,
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ( $this->authenticator->catchRequest($request) ) {
$request = $this->authenticator->connect($request);
}
return $handler->handle($request);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Ulmus\User\Middleware;
use Psr\Http\{
Message\ResponseInterface,
Message\ServerRequestInterface,
Server\MiddlewareInterface,
Server\RequestHandlerInterface
};
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Authorize\PostRequestAuthentication;
use Ulmus\User\Lib\Authenticate;
class RememberMeMiddleware implements MiddlewareInterface
{
public function __construct(
protected UserInterface $entity,
protected Authenticate $authenticator,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->authenticator->rememberMe();
return $handler->handle($request);
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace Ulmus\User\Role;
interface RoleAnonymousInterface {}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace Ulmus\User\Role;
interface RoleIdentifiedInterface {}