- WIP on authentication

This commit is contained in:
Dave M. 2023-11-09 13:38:07 -05:00
parent 9ae3e2d6f9
commit 0f2886b266
6 changed files with 189 additions and 30 deletions

View File

@ -0,0 +1,35 @@
<?php
namespace Ulmus\User\Authorize\Header;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Lib\Authorize;
class BasicMethod
{
public function __construct(
protected UserInterface $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([ 'email' => $userName, 'username' => $userName ], $password);
return $this->user->loggedIn();
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Ulmus\User\Authorize\Header;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Entity\UserInterface;
class BearerMethod
{
public function __construct(
protected UserInterface $user,
protected string $token
) {}
public function execute(ServerRequestInterface $request) : bool
{
return false;
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace Ulmus\User\Authorize\Header;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Entity\DigestAuthUserInterface;
class DigestMethod
{
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";
}
}
}

View File

@ -4,20 +4,28 @@ namespace Ulmus\User\Authorize;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\User\Common\AuthorizeContentTypeEnum;
use Ulmus\User\Entity\UserInterface;
use Ulmus\User\Lib\Authorize;
use Ulmus\User\Entity\{ DigestAuthUserInterface, UserInterface };
class HeaderAuthentication implements AuthorizeMethodInterface
{
public function connect(ServerRequestInterface $request, UserInterface $user): bool
{
if ( null !== ( $auth = $request->getHeaderLine('Authorization') ) ) {
if (null !== ( $auth = $request->getHeaderLine('Authorization') )) {
list($method, $value) = explode(' ', $auth, 2) + [ null, null ];
list($method, $userPass) = explode(' ', $auth, 2) + [ null, null ];
switch(strtolower(strtolower($method))) {
switch(strtolower($method)) {
case "basic":
return $this->basicMethod($user, $userPass);
return (new Header\BasicMethod($user, $value))->execute($request);
case "digest":
if (! $user instanceof DigestAuthUserInterface) {
throw new \RuntimeException("Your user entity must provide a valid hash of `user:realm:password` ");
}
return (new Header\DigestMethod($user, $value))->execute($request);
case "bearer":
return (new Header\BearerMethod($user, $value))->execute($request);
default:
throw new \InvalidArgumentException("An authentication method must be provided");
@ -27,25 +35,6 @@ class HeaderAuthentication implements AuthorizeMethodInterface
return false;
}
protected function basicMethod(UserInterface $user, string $userPassword) : bool
{
if ( false === $decoded = base64_decode($userPassword) ) {
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($user) )->authenticate([ 'email' => $userName, 'username' => $userName ], $password);
return $user->isLoaded();
}
public function catchRequest(ServerRequestInterface $request) : bool
{
@ -59,5 +48,4 @@ class HeaderAuthentication implements AuthorizeMethodInterface
return false;
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Ulmus\User\Entity;
interface DigestAuthUserInterface
{
# Represent a hash (md5, sha256, ...) of username:realm:password
public function getSecretHash() : string;
}

View File

@ -25,11 +25,16 @@ class HeaderAuthenticationMiddleware implements MiddlewareInterface
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);
}