218 lines
5.9 KiB
PHP
218 lines
5.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXF\Security\Authentication;
|
|
|
|
/**
|
|
* Authentication Session
|
|
*
|
|
* Represents an in-progress authentication flow
|
|
*/
|
|
class AuthenticationSession
|
|
{
|
|
// Session states
|
|
public const STATE_FRESH = 'fresh'; // Fresh session, no auth yet
|
|
public const STATE_IDENTIFIED = 'identified'; // User identity captured, awaiting auth method
|
|
public const STATE_AUTHENTICATING = 'authenticating'; // In the process of authenticating
|
|
public const STATE_COMPLETE = 'complete'; // All factors verified
|
|
|
|
// Default TTL: 5 minutes
|
|
public const DEFAULT_TTL = 300;
|
|
|
|
public function __construct(
|
|
public readonly string $id,
|
|
protected string $state,
|
|
public readonly string $tenantIdentifier = '',
|
|
public ?string $userIdentifier = null,
|
|
public ?string $userIdentity = null,
|
|
public int $methodsRequired = 1,
|
|
public array $methodsAvailable = [],
|
|
public array $methodsCompleted = [],
|
|
public array $metadata = [],
|
|
public readonly int $createdAt = 0,
|
|
public readonly int $expiresAt = 0,
|
|
) {}
|
|
|
|
/**
|
|
* Create a new authentication session
|
|
*/
|
|
public static function create(
|
|
string $tenantIdentifier,
|
|
string $state = self::STATE_FRESH,
|
|
int $ttl = self::DEFAULT_TTL
|
|
): self {
|
|
$now = time();
|
|
return new self(
|
|
id: self::generateId(),
|
|
state: $state,
|
|
createdAt: $now,
|
|
expiresAt: $now + $ttl,
|
|
tenantIdentifier: $tenantIdentifier,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate a unique session ID
|
|
*/
|
|
private static function generateId(): string
|
|
{
|
|
return 'auth_' . bin2hex(random_bytes(16));
|
|
}
|
|
|
|
/**
|
|
* Get current session state
|
|
*/
|
|
public function state(): string {
|
|
return $this->state;
|
|
}
|
|
|
|
/**
|
|
* Check if session has expired
|
|
*/
|
|
public function isExpired(): bool
|
|
{
|
|
return time() > $this->expiresAt;
|
|
}
|
|
|
|
/**
|
|
* Check if session is in initial state
|
|
*/
|
|
public function isFresh(): bool
|
|
{
|
|
return $this->state === self::STATE_FRESH;
|
|
}
|
|
|
|
/**
|
|
* Check if session has identity but awaiting authentication
|
|
*/
|
|
public function isIdentified(): bool
|
|
{
|
|
return $this->state === self::STATE_IDENTIFIED;
|
|
}
|
|
|
|
/**
|
|
* Check if session is in the process of authenticating
|
|
*/
|
|
public function isAuthenticating(): bool
|
|
{
|
|
return $this->state === self::STATE_AUTHENTICATING;
|
|
}
|
|
|
|
/**
|
|
* Check if session is complete
|
|
*/
|
|
public function isComplete(): bool
|
|
{
|
|
return $this->state === self::STATE_COMPLETE;
|
|
}
|
|
|
|
/**
|
|
* Set user identity (before authentication)
|
|
*/
|
|
public function setIdentity(string $value): void
|
|
{
|
|
$this->userIdentity = $value;
|
|
$this->state = self::STATE_IDENTIFIED;
|
|
}
|
|
|
|
public function setMethods(array $methods, int $require = 1): void
|
|
{
|
|
$this->methodsAvailable = $methods;
|
|
$this->methodsRequired = $require;
|
|
}
|
|
|
|
public function methodEligible(string $method): bool
|
|
{
|
|
return in_array($method, $this->methodsAvailable, true)
|
|
&& !in_array($method, $this->methodsCompleted, true);
|
|
}
|
|
/**
|
|
* Mark a method as completed
|
|
*/
|
|
public function methodCompleted(string $method): void
|
|
{
|
|
if (!in_array($method, $this->methodsCompleted, true)) {
|
|
$this->methodsCompleted[] = $method;
|
|
}
|
|
|
|
// If we have required factors and all are complete, mark session complete
|
|
if (count($this->methodsCompleted) >= $this->methodsRequired) {
|
|
$this->state = self::STATE_COMPLETE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get methods that still need to be completed
|
|
*/
|
|
public function methodsRemaining(): array
|
|
{
|
|
return array_values(array_diff($this->methodsAvailable, $this->methodsCompleted));
|
|
}
|
|
|
|
/**
|
|
* Promote session after successful primary auth (set user info)
|
|
*/
|
|
public function setUser(string $userIdentifier, string $userIdentity): void
|
|
{
|
|
$this->userIdentifier = $userIdentifier;
|
|
$this->userIdentity = $userIdentity;
|
|
}
|
|
|
|
/**
|
|
* Get metadata value
|
|
*/
|
|
public function getMeta(string $key, mixed $default = null): mixed
|
|
{
|
|
return $this->metadata[$key] ?? $default;
|
|
}
|
|
|
|
/**
|
|
* Set metadata value
|
|
*/
|
|
public function setMeta(string $key, mixed $value): void
|
|
{
|
|
$this->metadata[$key] = $value;
|
|
}
|
|
|
|
/**
|
|
* Serialize to array for storage
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
return [
|
|
'id' => $this->id,
|
|
'state' => $this->state,
|
|
'tenant_identifier' => $this->tenantIdentifier,
|
|
'user_identifier' => $this->userIdentifier,
|
|
'user_identity' => $this->userIdentity,
|
|
'methods_required' => $this->methodsRequired,
|
|
'methods_available' => $this->methodsAvailable,
|
|
'methods_completed' => $this->methodsCompleted,
|
|
'metadata' => $this->metadata,
|
|
'created_at' => $this->createdAt,
|
|
'expires_at' => $this->expiresAt,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Deserialize from array
|
|
*/
|
|
public static function fromArray(array $data): self
|
|
{
|
|
return new self(
|
|
id: $data['id'],
|
|
state: $data['state'],
|
|
tenantIdentifier: $data['tenant_identifier'],
|
|
userIdentifier: $data['user_identifier'] ?? null,
|
|
userIdentity: $data['user_identity'] ?? null,
|
|
methodsRequired: $data['methods_required'] ?? 1,
|
|
methodsAvailable: $data['methods_available'] ?? [],
|
|
methodsCompleted: $data['methods_completed'] ?? [],
|
|
metadata: $data['metadata'] ?? [],
|
|
createdAt: $data['created_at'],
|
|
expiresAt: $data['expires_at'],
|
|
);
|
|
}
|
|
}
|