12 Commits
13 changed files with 268 additions and 32 deletions

No files matched your search

+8 -2
View File
@@ -6,12 +6,18 @@
"authors": [ "authors": [
{ {
"name": "Dave Mc Nicoll", "name": "Dave Mc Nicoll",
"email": "mcndave@gmail.com" "email": "info@mcnd.ca"
} }
], ],
"require": { "require": {
"mcnd/notes": "master-dev" "mcnd/ulmus": "dev-master"
}, },
"repositories": [
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcNdave/ulmus.git"
}
],
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Ulmus\\User\\": "src/" "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) : UserInterface|false;
public function catchRequest(ServerRequestInterface $request) : bool;
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Ulmus\User\Authorize;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Common\AuthorizeContentTypeEnum;
use Ulmus\User\Entity\User;
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Lib\Authenticate;
class HeaderAuthentication implements AuthorizeMethodInterface
{
public function connect(ServerRequestInterface $request, UserInterface $user): UserInterface|false
{
if ( null !== ( $auth = $request->getHeaderLine('Authorization') ) ) {
list($method, $userPass) = explode(' ', $auth, 2) + [ null, null ];
switch(strtolower(strtolower($method))) {
case "basic":
return $this->basicMethod($userPass);
default:
throw new \InvalidArgumentException("An authentication method must be provided");
}
}
return false;
}
protected function basicMethod(string $userPassword) : UserInterface|false
{
if ( false === $decoded = base64_decode($userPassword) ) {
throw new \RuntimeException("Base64 decoding of given username:password failed");
}
list($user, $password) = explode(':', $decoded) + [ null, null ];
if ( empty($user) ) {
throw new \RuntimeException("A username must be provided");
}
elseif ( empty($password) ) {
throw new \RuntimeException("A password must be provided");
}
$authenticate = new Authenticate();
return false;
}
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(
protected Authenticate $authenticate,
protected string $fieldUser = "username",
protected string $postFieldUser = "username",
protected string $postFieldPassword = "password",
) {}
public function connect(ServerRequestInterface $request, UserInterface $user): UserInterface|false
{
$post = $request->getParsedBody();
return $this->authenticate->authenticate($user::repository(), [ $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]);
}
}
+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";
}
+15 -9
View File
@@ -2,11 +2,12 @@
namespace Ulmus\User\Entity; namespace Ulmus\User\Entity;
use Ulmus\Entity\EntityInterface;
use Ulmus\Entity\Field\Datetime; use Ulmus\Entity\Field\Datetime;
use Ulmus\Attribute\Property\Field; use Ulmus\Attribute\Property\Field;
class User { abstract class User implements UserInterface {
#[Field\Id(readonly: true)] #[Field\Id(readonly: true)]
public int $id; public int $id;
@@ -47,40 +48,45 @@ class User {
#[Field] #[Field]
public string $password; public string $password;
#[Field\UpdatedAt(readonly: true, name: "updated_at")] #[Field\UpdatedAt(name: "updated_at", readonly: true)]
public ? Datetime $updatedAt; public ? Datetime $updatedAt;
#[Field\CreatedAt(readonly: true, name: "created_at")] #[Field\CreatedAt(name: "created_at", readonly: true)]
public Datetime $createdAt; public Datetime $createdAt;
public bool $logged = false; public bool $logged = false;
public function __toString() : string public function __toString() : string
{ {
return $this->fullname(); return $this->fullName();
} }
public function setPassword($password) : self public function setPassword($password) : static
{ {
$this->password = $password; $this->password = $password;
return $this->hashPassword(); 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; return $this;
} }
public function verifyPassword(string $password) : bool public function verifyPassword(string $password) : bool
{ {
return password_verify($password, $this->password ); return password_verify($password, $this->password);
} }
public function fullname() : string public function fullName() : string
{ {
return trim( ( $this->firstName ?? "" ) . " " . ( $this->lastName ?? "" ) ); return trim( ( $this->firstName ?? "" ) . " " . ( $this->lastName ?? "" ) );
} }
public function loggedIn(): bool
{
return $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;
public function verifyPassword(string $password) : bool;
public function hashPassword(? string $password = null) : static;
public function setPassword($password) : static;
}
+11 -21
View File
@@ -10,38 +10,29 @@ use Ulmus\User\Entity\User;
use Ulmus\Exception; use Ulmus\Exception;
class Authenticate { class Authenticate {
protected ? Session $session;
protected ? Cookie $cookie;
protected bool $logged = false; protected bool $logged = false;
protected Closure $authenticationEvent; protected Closure $authenticationEvent;
public ? User $user = null; public ? User $user = null;
public string $connection_fields = 'email';
public function __construct( public function __construct(
? Session $session = null, protected ? Session $session = null,
? Cookie $cookie = null, protected ? Cookie $cookie = null,
? Closure $authenticationEvent = null ? Closure $authenticationEvent = null
) { ) {
$this->session = $session; $this->authenticationEvent = $authenticationEvent ?: fn(bool $authenticated, string $message, ? User $user, array $data = []) : ? bool => null;
$this->cookie = $cookie;
$this->authenticationEvent = $authenticationEvent ?: function(bool $authenticated, string $message, ? User $user, array $data = []) : ? bool {return null;} ;
} }
public function rememberMe(\Ulmus\Repository $repository) : ? User public function rememberMe(\Ulmus\Repository $repository) : ? User
{ {
$logUser = function(int $id) use ($repository) { $logUser = function(? int $id) use ($repository) {
if ( null === ( $user = $repository->loadFromPk($id) ) ) { if ( $id === null || null === ( $user = $repository->loadFromPk($id) ) ) {
throw new \Exception("User not found."); throw new \InvalidArgumentException(sprintf("User having id '%s' was not found.", $id));
} }
$user->logged = true; $user->logged = true;
return $user; return $user;
}; };
@@ -110,9 +101,6 @@ class Authenticate {
return $this->user; return $this->user;
} }
/**
* Force user disconnection and handle memory trashing
*/
public function logout() : self public function logout() : self
{ {
if ( $this->session ) { if ( $this->session ) {
@@ -123,8 +111,10 @@ class Authenticate {
$this->cookie->delete('user.id'); $this->cookie->delete('user.id');
} }
$this->user->logged = false; if ( isset($this->user) ) {
$this->user->logged = false;
}
return $this; return $this;
} }
} }
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace Ulmus\User\Lib;
use Ulmus\User\Entity\User;
class Authorize extends Authenticate
{
public function rememberMe(\Ulmus\Repository $repository) : ? User
{
return null;
}
public function authorize(\Ulmus\Repository $repository, array $userLogin, string $password) : User
{
}
}
@@ -0,0 +1,36 @@
<?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
{
if ( $this->authenticator->catchRequest($request) ) {
if ( ! $this->authenticator->connect($request, $this->entity) ) {
return call_user_func($this->loginFailedResponse, [ 'api.process' => "Auth failed" ]);
}
}
return $handler->handle($request);
}
}
@@ -0,0 +1,37 @@
<?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());
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ( $this->authenticator->catchRequest($request) ) {
if ( ! $this->authenticator->connect($request, $this->entity) ) {
return call_user_func($this->loginFailedResponse, "Login failed");
}
}
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 {}