Compare commits
21 Commits
attributes
...
master
Author | SHA1 | Date |
---|---|---|
Dave Mc Nicoll | 23079e6040 | |
Dave Mc Nicoll | d3bb2c248a | |
Dave Mc Nicoll | e63ea439d6 | |
Dave Mc Nicoll | c4e4db7a45 | |
Dave M. | 0657622eab | |
Dave M. | 70daae93f5 | |
Dave Mc Nicoll | 9570320a3b | |
Dave M. | 0f2886b266 | |
Dave M. | 9ae3e2d6f9 | |
Dave M. | 7769b1a1ca | |
Dave M. | 1559ceb248 | |
Dave M. | c7b04d7e46 | |
Dave M. | c6fc5d020b | |
Dave M. | 944c524cfb | |
Dave M. | fbf9bc871c | |
Dave Mc Nicoll | ad802fc35d | |
Dave M. | 4664a8779a | |
Dave M. | cef536caa5 | |
Dave M. | 10ad77d607 | |
Dave M. | 015f0c70b1 | |
Dave Mc Nicoll | 267fe5cadd |
|
@ -6,12 +6,18 @@
|
|||
"authors": [
|
||||
{
|
||||
"name": "Dave Mc Nicoll",
|
||||
"email": "mcndave@gmail.com"
|
||||
"email": "info@mcnd.ca"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"mcnd/notes": "master-dev"
|
||||
"mcnd/ulmus": "dev-master"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://git.mcnd.ca/mcNdave/ulmus.git"
|
||||
}
|
||||
],
|
||||
"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, UserInterface $user) : bool;
|
||||
|
||||
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 $this->parse();
|
||||
}
|
||||
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 (%s)", $encodedSignature, JsonWebTokenEncoder::base64url_encode($compare))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function validateAlgorithm(JsonWebTokenAlgorithmEnum $algorithm) : void
|
||||
{
|
||||
$algorithm->assessOperability();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Header;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Ulmus\User\Entity\BasicAuthUserInterface;
|
||||
use Ulmus\User\Entity\UserInterface;
|
||||
use Ulmus\User\Lib\Authorize;
|
||||
|
||||
class BasicMethod implements MethodInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected BasicAuthUserInterface $user,
|
||||
protected string|array $arguments
|
||||
) {}
|
||||
|
||||
public function execute(ServerRequestInterface $request) : bool
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
( new Authorize($this->user) )->authenticate([ $this->user->usernameField() => $userName ], $password);
|
||||
|
||||
return $this->user->loggedIn();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Header;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Ulmus\User\Authorize\Bearer\JsonWebToken;
|
||||
use Ulmus\User\Authorize\Bearer\JsonWebTokenDecoder;
|
||||
use Ulmus\User\Entity\UserInterface;
|
||||
use Ulmus\User\Lib\Authorize;
|
||||
|
||||
class BearerMethod implements MethodInterface
|
||||
{
|
||||
protected JsonWebTokenDecoder $jwt;
|
||||
|
||||
public function __construct(
|
||||
protected UserInterface $user,
|
||||
protected string $token
|
||||
) {}
|
||||
|
||||
public function execute(ServerRequestInterface $request) : bool
|
||||
{
|
||||
switch($this->autodetectTokenType()) {
|
||||
case BearerTokenTypeEnum::JsonWebToken:
|
||||
$authorize = new Authorize($this->user);
|
||||
|
||||
$payload = $this->jwt->getPayload();
|
||||
|
||||
if ($payload['sub'] ?? false) {
|
||||
if ( ! $authorize->logUser((int) $payload['sub']) ) {
|
||||
throw new \Exception("Given user id do not match with an existing/active user");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new \InvalidArgumentException("Given JsonWebToken is missing a 'sub' key (which concords to user id)");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case BearerTokenTypeEnum::UniqueKey:
|
||||
# @TODO
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return $this->user->loggedIn();
|
||||
}
|
||||
|
||||
public function autodetectTokenType() : BearerTokenTypeEnum
|
||||
{
|
||||
$this->jwt = new JsonWebTokenDecoder($this->token, getenv('LEAN_RANDOM'));
|
||||
|
||||
if ( $this->jwt->isJWT() ) {
|
||||
return BearerTokenTypeEnum::JsonWebToken;
|
||||
}
|
||||
|
||||
return BearerTokenTypeEnum::UniqueKey;
|
||||
}
|
||||
|
||||
protected function userFromJWT() : UserInterface
|
||||
{
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Header;
|
||||
|
||||
enum BearerTokenTypeEnum : string
|
||||
{
|
||||
case JsonWebToken = "JWT";
|
||||
|
||||
case UniqueKey = "UniqueKey";
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Header;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Ulmus\User\Entity\DigestAuthUserInterface;
|
||||
|
||||
class DigestMethod implements MethodInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected DigestAuthUserInterface $user,
|
||||
protected string|array $arguments
|
||||
) {}
|
||||
|
||||
public function execute(ServerRequestInterface $request) : bool
|
||||
{
|
||||
$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 = $this->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
|
||||
]));
|
||||
}
|
||||
|
||||
return $response === $arguments['response'];
|
||||
}
|
||||
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Header;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
interface MethodInterface
|
||||
{
|
||||
public function execute(ServerRequestInterface $request) : bool;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Ulmus\User\Common\AuthorizeContentTypeEnum;
|
||||
use Ulmus\User\Entity\{ DigestAuthUserInterface, UserInterface };
|
||||
|
||||
class HeaderAuthentication implements AuthorizeMethodInterface
|
||||
{
|
||||
public function connect(ServerRequestInterface $request, UserInterface $user): bool
|
||||
{
|
||||
if (null !== ( $auth = $request->getHeaderLine('Authorization') )) {
|
||||
list($method, $value) = explode(' ', $auth, 2) + [ null, null ];
|
||||
|
||||
switch(strtolower($method)) {
|
||||
case "basic":
|
||||
$methodObj = new Header\BasicMethod($user, $value);
|
||||
break;
|
||||
|
||||
case "digest":
|
||||
if (! $user instanceof DigestAuthUserInterface) {
|
||||
throw new \RuntimeException("Your user entity must provide a valid hash of `user:realm:password` ");
|
||||
}
|
||||
|
||||
$methodObj = new Header\DigestMethod($user, $value);
|
||||
break;
|
||||
|
||||
case "bearer":
|
||||
$methodObj = new Header\BearerMethod($user, $value);
|
||||
break;
|
||||
|
||||
case "token":
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException("An authentication method must be provided");
|
||||
}
|
||||
}
|
||||
|
||||
return isset($methodObj) && $methodObj->execute($request);
|
||||
}
|
||||
|
||||
|
||||
public function catchRequest(ServerRequestInterface $request) : bool
|
||||
{
|
||||
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 false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Ulmus\User\Entity\UserInterface;
|
||||
use Ulmus\User\Lib\Authenticate;
|
||||
|
||||
class PostRequestAuthentication implements AuthorizeMethodInterface
|
||||
{
|
||||
public function __construct(
|
||||
public Authenticate $authenticate,
|
||||
protected string $fieldUser = "email",
|
||||
protected string $postFieldUser = "email",
|
||||
protected string $postFieldPassword = "password",
|
||||
) {}
|
||||
|
||||
public function connect(ServerRequestInterface $request, UserInterface $user): bool
|
||||
{
|
||||
$post = $request->getParsedBody();
|
||||
|
||||
return $this->authenticate->authenticate([ $this->fieldUser => $post[$this->postFieldUser] ], $post[$this->postFieldPassword]);
|
||||
}
|
||||
|
||||
public function catchRequest(ServerRequestInterface $request): bool
|
||||
{
|
||||
$post = $request->getParsedBody();
|
||||
|
||||
return strtoupper( $request->getMethod() ) === "POST" && isset($post[$this->postFieldUser], $post[$this->postFieldPassword]);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Entity;
|
||||
|
||||
interface BasicAuthUserInterface extends UserInterface
|
||||
{
|
||||
public function usernameField() : string;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,45 @@ class User {
|
|||
#[Field]
|
||||
public string $password;
|
||||
|
||||
#[Field\UpdatedAt(readonly: true, name: "updated_at")]
|
||||
#[Field\UpdatedAt(name: "updated_at", readonly: true)]
|
||||
public ? Datetime $updatedAt;
|
||||
|
||||
#[Field\CreatedAt(readonly: true, name: "created_at")]
|
||||
#[Field\CreatedAt(name: "created_at", readonly: true)]
|
||||
public Datetime $createdAt;
|
||||
|
||||
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() : string
|
||||
{
|
||||
return trim( ( $this->firstName ?? "" ) . " " . ( $this->lastName ?? "" ) );
|
||||
}
|
||||
|
||||
public function loggedIn(?bool $set = null): bool
|
||||
{
|
||||
return $set !== null ? $this->logged = $set : $this->logged;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,52 +5,33 @@ 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;
|
||||
|
@ -61,14 +42,18 @@ class Authenticate {
|
|||
$this->session->destroy();
|
||||
}
|
||||
|
||||
public function authenticate(\Ulmus\Repository $repository, array $userLogin, string $password) : User
|
||||
public function authenticate(array $userLogin, string $password) : bool
|
||||
{
|
||||
$repository = $this->user::repository();
|
||||
|
||||
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 +69,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 +84,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->set("user.id", $this->user->id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +117,24 @@ class Authenticate {
|
|||
$this->cookie->delete('user.id');
|
||||
}
|
||||
|
||||
$this->user->logged = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
public function logUser(?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);
|
||||
|
||||
$this->user->loggedIn(true);
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Lib;
|
||||
|
||||
use Ulmus\User\Entity\UserInterface;
|
||||
|
||||
class Authorize extends Authenticate
|
||||
{
|
||||
public function rememberMe() : ? UserInterface
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?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;
|
||||
|
||||
class HeaderAuthenticationMiddleware implements MiddlewareInterface
|
||||
{
|
||||
protected HeaderAuthentication $authenticator;
|
||||
|
||||
public function __construct(
|
||||
protected UserInterface $entity,
|
||||
protected \Closure $loginFailedResponse,
|
||||
HeaderAuthentication $authenticator = null,
|
||||
) {
|
||||
$this->authenticator = $authenticator ?: new HeaderAuthentication();
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
try {
|
||||
if ( $this->authenticator->catchRequest($request) ) {
|
||||
if ( ! $this->authenticator->connect($request, $this->entity) ) {
|
||||
return call_user_func($this->loginFailedResponse, [ 'api.process' => "Auth failed" ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(\Exception $e) {
|
||||
return call_user_func($this->loginFailedResponse, [ 'api.error_message' => $e->getMessage() ]);
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?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
|
||||
{
|
||||
protected PostRequestAuthentication $authenticator;
|
||||
|
||||
public function __construct(
|
||||
protected UserInterface $entity,
|
||||
protected \Closure $loginFailedResponse,
|
||||
PostRequestAuthentication $authenticator = null,
|
||||
) {
|
||||
$this->authenticator = $authenticator ?: new PostRequestAuthentication(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,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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Role;
|
||||
|
||||
interface RoleAnonymousInterface {}
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Role;
|
||||
|
||||
interface RoleIdentifiedInterface {}
|
Loading…
Reference in New Issue