Skip to content

Commit

Permalink
hashing service; continued work on auth service
Browse files Browse the repository at this point in the history
  • Loading branch information
donwilson committed Dec 14, 2023
1 parent b918dba commit 085e6bf
Show file tree
Hide file tree
Showing 32 changed files with 900 additions and 54 deletions.
3 changes: 3 additions & 0 deletions src/Magnetar/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ public function registerCoreContainerAliases(): void {
'files' => [
\Magnetar\Filesystem\Filesystem::class,
],
'hashing' => [
\Magnetar\Hashing\HashingManager::class,
],
'logger' => [
\Magnetar\Log\Logger::class,
],
Expand Down
103 changes: 95 additions & 8 deletions src/Magnetar/Auth/AuthManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
use Magnetar\Application;
use Magnetar\Model\Model;
use Magnetar\Auth\Exceptions\AuthorizationException;
use Magnetar\Http\Request;
use Magnetar\Utilities\Cryptography\Encryption;
use Magnetar\Utilities\Str;

/**
* Authentication manager
Expand Down Expand Up @@ -41,7 +44,7 @@ public function __construct(
* @return void
*/
protected function setDefaultUserModel(): void {
$this->user_model = $this->app->config('auth.model.class') ?? null;
$this->user_model = $this->app['config']['auth.model.class'] ?? null;
}

/**
Expand All @@ -50,7 +53,7 @@ protected function setDefaultUserModel(): void {
*
* @throws \Magnetar\Auth\Exceptions\AuthorizationException
*/
protected function getNewModel(): Model {
protected function newUserModel(): Model {
if(null === $this->user_model) {
throw new AuthorizationException('Model class for authentication is not specified');
}
Expand All @@ -60,16 +63,23 @@ protected function getNewModel(): Model {

/**
* Attempt to authenticate a user. The $credentials array should specify the columns to validate against and their values
* @param mixed $credentials The object to authenticate with. Can be a Request object or an assoc array
* @param array|null $credentials The object to authenticate with. Can be a Request object or an assoc array
* @param bool $remember Whether to remember the user. If true, a cookie will be set
* @return bool
*/
public function attempt(mixed $credentials, bool $remember=false): bool {
public function attempt(array|null $credentials=null, bool $remember=false): bool {
if(null === $credentials) {
$credentials = $this->app->request();
}

// @TODO
if($credentials instanceof Request) {
// @TODO
// use cookie to remember user
$cookies = $credentials->cookies();

die(var_dump($cookies));
} else if(is_array($credentials)) {
// @TODO

}

return false;
Expand All @@ -80,7 +90,12 @@ public function attempt(mixed $credentials, bool $remember=false): bool {
* @return bool
*/
public function check(): bool {
// @TODO
if(null !== $this->user) {
return true;
}



return false;
}

Expand All @@ -94,7 +109,10 @@ public function user(): User {
return new User();
}


/**
* Get the ID of the currently authenticated user. Returns 0 if no user is authenticated
* @return int
*/
public function id(): int {
// @TODO

Expand All @@ -109,5 +127,74 @@ public function logout(): void {
// @TODO
}

/**
* Remember the user by looking up the 'remember me' cookie
* @return bool
*/
public function remember(): bool {
if(null !== $this->user) {
return true;
}

// get cookie
if(null === ($raw_cookie = $this->getRememberCookie())) {
return false;
}

// decode and decrypt cookie
$cookie = (new Encryption(
$this->app['config']['app.key'],
null,//$this->app['config']['app.digest'],
$this->app['config']['app.cipher']
))::decrypt($raw_cookie);

// validate cookie
if(!isset($cookie['id']) || !isset($cookie['token'])) {
$this->invalidateRememberCookie();

return false;
}

// validate token
if(!hash_equals($cookie['token'], hash_hmac('sha256', $cookie['id'], $this->app['config']['app.key']))) {
$this->invalidateRememberCookie();

return false;
}

// get user
$this->user = $this->newUserModel()->findOrNull(
$cookie
);

return (null !== $this->user);
}

/**
* Invalidate the existing 'remember me' cookie
* @return void
*/
protected function invalidateRememberCookie(): void {
// @TODO check if works properly
$this->app['cookie']->remove($this->rememberCookieName());
}

/**
* Get the value of the 'remember me' cookie
* @return array|null
*/
protected function getRememberCookie(): array|null {
return $this->app['request']->cookie(
$this->rememberCookieName(),
null
);
}

/**
* Get the name of the cookie used to remember the user
* @return string
*/
protected function rememberCookieName(): string {
return Str::snake_case($this->app['config']['app.name'] ?? 'magnetar') .'_auth'. ((null !== $this->user_model)?'_'. substr(md5($this->user_model), 0, 10):'');
}
}
1 change: 1 addition & 0 deletions src/Magnetar/Auth/AuthServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function register(): void {
public function provides(): array {
return [
AuthManager::class,
'auth',
];
}
}
12 changes: 12 additions & 0 deletions src/Magnetar/Auth/Exceptions/InvalidSessionDetailsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);

namespace Magnetar\Auth\Exceptions;

class InvalidSessionDetailsException extends Exception {
/**
* The exception message
* @var string
*/
protected string $message = 'Invalid session details';
}
7 changes: 3 additions & 4 deletions src/Magnetar/Auth/Middleware/AuthenticateMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,11 @@ public function handle(Request $request, Closure $next): Response {
* @throws \Magnetar\Auth\Exceptions\AuthorizationException
*/
protected function authenticate(Request $request): void {
// @TODO: Implement authentication
if(Auth::attempt($request)) {
if(app('auth')->remember()) {
return;
}

$this->unauthorized();
$this->unauthorized($request);
}

/**
Expand All @@ -50,7 +49,7 @@ protected function authenticate(Request $request): void {
*
* @throws \Magnetar\Auth\Exceptions\AuthorizationException
*/
protected function unauthorizedResponse(Request $request): void {
protected function unauthorized(Request $request): void {
throw (new AuthorizationException())->respondWith($this->redirect($request));
}

Expand Down
135 changes: 134 additions & 1 deletion src/Magnetar/Auth/SessionDetails.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

namespace Magnetar\Auth;

use Magnetar\Utilities\Cryptography\Scramble;
use Magnetar\Auth\Exceptions\InvalidSessionDetailsException;
use Magnetar\Utilities\Cryptography\Encryption;

class SessionDetails {
/**
* SessionDetails constructor
Expand All @@ -19,6 +23,12 @@ public function __construct(
* @var string
*/
protected string $password,

/**
* The user's session token
* @var string
*/
protected string $token
) {
// @TODO use model identifier key, not ID
// @TODO use session token?
Expand All @@ -42,8 +52,14 @@ public static function make(
* @var string
*/
string $password,

/**
* The user's session token
* @var string
*/
string $token
): static {
return new static($id, $password);
return new static($id, $password, $token);
}

/**
Expand All @@ -62,4 +78,121 @@ public function getPassword(): string {
return $this->password;
}

/**
* Get the user's remember token
* @return string
*/
public function getToken(): string {
return $this->token;
}

/**
* Determine if this object contains a valid set of session details.
* Only checks for the presence of required properties, not their validity
* @return bool
*/
public function isValid(): bool {
if(!$this->id || empty($this->id)) {
return false;
}

if(!$this->password || ('' === $this->password)) {
return false;
}

if(!$this->token || ('' === $this->token)) {
return false;
}

return true;
}

/**
* Encrypt this object for storage in the session
* @return string
*/
public function encryptForClient(): string {
// @TODO needs closer inspection

// confirm this object is valid
if(!$this->isValid()) {
throw new InvalidSessionDetailsException('Invalid session details');
}

// generate stored message
$message = implode('|', [
$this->getId(),
$this->getPassword(),
$this->getToken()
]);

// message validation
$mac = hash_hmac('sha256', $message, app()->config('app.key'));

$encrypted_message = (new Encryption(
app()->config('app.key'),
null,
app()->config('app.cipher')
))->encrypt([
'mac' => $mac,
'message' => $message,
]);

if(false === $encrypted_message) {
throw new InvalidSessionDetailsException('Unable to encrypt session details');
}

return Scramble::encode(
$encrypted_message
);
}

/**
* Decrypt a string from the session into a SessionDetails object
* @param string $encrypted The encrypted string from the session
* @return static
*/
public static function decryptFromClient(string $encrypted): static {
// @TODO needs closer inspection

$soft_decrypted = Scramble::decode($encrypted);

if(false === $soft_decrypted) {
throw new InvalidSessionDetailsException('Unable to initially decrypt session details');
}

$decrypted = (new Encryption(
app()->config('app.key'),
null,
app()->config('app.cipher')
))->decrypt($soft_decrypted);

if(false === $decrypted) {
throw new InvalidSessionDetailsException('Unable to decrypt session details');
}

// validation
if(!isset($decrypted['mac']) || !isset($decrypted['message'])) {
throw new InvalidSessionDetailsException('Invalid encrypted session details');
}

// mac validation
$mac = hash_hmac('sha256', $decrypted['message'], app()->config('app.key'));

if(!hash_equals($mac, $decrypted['mac'])) {
throw new InvalidSessionDetailsException('Encrypted session details failed validation');
}

$parts = explode('|', $decrypted['message']);

if(count($parts) < 3) {
throw new InvalidSessionDetailsException('Invalid session details message');
}

return new static(
(int) $parts[0],
(string) $parts[1],
(string) $parts[2]
);
}
}
15 changes: 15 additions & 0 deletions src/Magnetar/Hashing/Argon2IdHasher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);

namespace Magnetar\Hashing;

use Magnetar\Hashing\ArgonHasher;

class Argon2IdHasher extends ArgonHasher {
/**
* {@inheritDoc}
*/
protected function algorithm(): string {
return PASSWORD_ARGON2ID;
}
}
Loading

0 comments on commit 085e6bf

Please sign in to comment.