DSP Project first push, date: 29/01/2026
This commit is contained in:
257
classes/OAuth.php
Normal file
257
classes/OAuth.php
Normal file
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lightweight OAuth 2.0 data/access service for DSP -> JupyterHub integration.
|
||||
*/
|
||||
class OAuthService
|
||||
{
|
||||
private const AUTH_CODE_TTL = 600; // 10 minutes
|
||||
private const ACCESS_TOKEN_TTL = 3600; // 1 hour
|
||||
private const REFRESH_TOKEN_TTL = 2592000; // 30 days
|
||||
|
||||
private PDO $pdo;
|
||||
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function getClient(string $clientId): ?array
|
||||
{
|
||||
$sql = "SELECT client_id, client_name, client_secret_hash, redirect_uris, allowed_scopes, is_confidential
|
||||
FROM dsp_oauth_clients
|
||||
WHERE client_id = :client_id AND is_revoked = 0";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':client_id' => $clientId]);
|
||||
$client = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $client ?: null;
|
||||
}
|
||||
|
||||
public function verifyClientSecret(array $client, string $candidate): bool
|
||||
{
|
||||
if (empty($client['client_secret_hash'])) {
|
||||
return $candidate === '';
|
||||
}
|
||||
|
||||
return password_verify($candidate, $client['client_secret_hash']);
|
||||
}
|
||||
|
||||
public function isRedirectUriAllowed(array $client, string $redirectUri): bool
|
||||
{
|
||||
$allowed = array_filter(array_map('trim', preg_split('/[\s,]+/', (string) ($client['redirect_uris'] ?? ''))));
|
||||
if (empty($allowed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($allowed as $prefix) {
|
||||
if (stripos($redirectUri, $prefix) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isScopeAllowed(array $client, ?string $requestedScope): bool
|
||||
{
|
||||
$requestedScope = trim((string) $requestedScope);
|
||||
if ($requestedScope === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$allowedScopes = array_filter(array_map('trim', explode(' ', (string) ($client['allowed_scopes'] ?? ''))));
|
||||
|
||||
if (empty($allowedScopes)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (explode(' ', $requestedScope) as $scope) {
|
||||
if (!in_array($scope, $allowedScopes, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function issueAuthorizationCode(string $clientId, int $personId, string $redirectUri, ?string $scope = null): array
|
||||
{
|
||||
$code = $this->generateToken(32);
|
||||
$codeHash = $this->hashToken($code);
|
||||
$expiresAt = time() + self::AUTH_CODE_TTL;
|
||||
|
||||
$sql = "INSERT INTO dsp_oauth_auth_codes
|
||||
(code_hash, client_id, person_id, scope, redirect_uri, expires_at, created_at)
|
||||
VALUES (:code_hash, :client_id, :person_id, :scope, :redirect_uri, FROM_UNIXTIME(:expires_at), NOW())";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
':code_hash' => $codeHash,
|
||||
':client_id' => $clientId,
|
||||
':person_id' => $personId,
|
||||
':scope' => $scope,
|
||||
':redirect_uri' => $redirectUri,
|
||||
':expires_at' => $expiresAt,
|
||||
]);
|
||||
|
||||
return [
|
||||
'code' => $code,
|
||||
'expires_at' => $expiresAt,
|
||||
];
|
||||
}
|
||||
|
||||
public function consumeAuthorizationCode(string $code, string $clientId): ?array
|
||||
{
|
||||
$codeHash = $this->hashToken($code);
|
||||
|
||||
$sql = "SELECT code_hash, client_id, person_id, scope, redirect_uri, UNIX_TIMESTAMP(expires_at) AS expires_at
|
||||
FROM dsp_oauth_auth_codes
|
||||
WHERE code_hash = :code_hash";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':code_hash' => $codeHash]);
|
||||
$record = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Delete regardless of outcome
|
||||
$deleteStmt = $this->pdo->prepare("DELETE FROM dsp_oauth_auth_codes WHERE code_hash = :code_hash");
|
||||
$deleteStmt->execute([':code_hash' => $codeHash]);
|
||||
|
||||
if ((int) $record['expires_at'] < time()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($record['client_id'] !== $clientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
public function issueTokens(string $clientId, int $personId, ?string $scope = null, bool $includeRefresh = true): array
|
||||
{
|
||||
$accessToken = $this->generateToken(43);
|
||||
$accessHash = $this->hashToken($accessToken);
|
||||
$accessExpiresAt = time() + self::ACCESS_TOKEN_TTL;
|
||||
|
||||
$refreshToken = null;
|
||||
$refreshHash = null;
|
||||
$refreshExpiresAt = null;
|
||||
|
||||
if ($includeRefresh) {
|
||||
$refreshToken = $this->generateToken(43);
|
||||
$refreshHash = $this->hashToken($refreshToken);
|
||||
$refreshExpiresAt = time() + self::REFRESH_TOKEN_TTL;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO dsp_oauth_access_tokens
|
||||
(token_hash, client_id, person_id, scope, expires_at, refresh_token_hash, refresh_expires_at, created_at)
|
||||
VALUES (:token_hash, :client_id, :person_id, :scope, FROM_UNIXTIME(:expires_at),
|
||||
:refresh_hash, " . ($refreshExpiresAt ? "FROM_UNIXTIME(:refresh_expires_at)" : "NULL") . ", NOW())";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
':token_hash' => $accessHash,
|
||||
':client_id' => $clientId,
|
||||
':person_id' => $personId,
|
||||
':scope' => $scope,
|
||||
':expires_at' => $accessExpiresAt,
|
||||
':refresh_hash' => $refreshHash,
|
||||
':refresh_expires_at' => $refreshExpiresAt,
|
||||
]);
|
||||
|
||||
return [
|
||||
'access_token' => $accessToken,
|
||||
'access_expires_at' => $accessExpiresAt,
|
||||
'refresh_token' => $refreshToken,
|
||||
'refresh_expires_at' => $refreshExpiresAt,
|
||||
'token_type' => 'Bearer',
|
||||
'scope' => $scope,
|
||||
];
|
||||
}
|
||||
|
||||
public function exchangeRefreshToken(string $clientId, string $refreshToken): ?array
|
||||
{
|
||||
$refreshHash = $this->hashToken($refreshToken);
|
||||
|
||||
$sql = "SELECT token_hash, client_id, person_id, scope, UNIX_TIMESTAMP(refresh_expires_at) AS refresh_expires_at
|
||||
FROM dsp_oauth_access_tokens
|
||||
WHERE refresh_token_hash = :refresh_hash AND is_revoked = 0";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':refresh_hash' => $refreshHash]);
|
||||
$record = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($record['client_id'] !== $clientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!empty($record['refresh_expires_at']) && (int) $record['refresh_expires_at'] < time()) {
|
||||
$this->revokeTokenByHash($record['token_hash']);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Revoke old access token
|
||||
$this->revokeTokenByHash($record['token_hash']);
|
||||
|
||||
// Issue new pair
|
||||
return $this->issueTokens($clientId, (int) $record['person_id'], $record['scope'], true);
|
||||
}
|
||||
|
||||
public function getAccessToken(string $token): ?array
|
||||
{
|
||||
$hash = $this->hashToken($token);
|
||||
$sql = "SELECT token_hash, client_id, person_id, scope,
|
||||
UNIX_TIMESTAMP(expires_at) AS expires_at,
|
||||
refresh_token_hash,
|
||||
UNIX_TIMESTAMP(refresh_expires_at) AS refresh_expires_at
|
||||
FROM dsp_oauth_access_tokens
|
||||
WHERE token_hash = :hash AND is_revoked = 0";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':hash' => $hash]);
|
||||
$record = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((int) $record['expires_at'] < time()) {
|
||||
$this->revokeTokenByHash($record['token_hash']);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
public function revokeTokenByHash(string $hash): void
|
||||
{
|
||||
$sql = "UPDATE dsp_oauth_access_tokens SET is_revoked = 1, revoked_at = NOW()
|
||||
WHERE token_hash = :hash";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':hash' => $hash]);
|
||||
}
|
||||
|
||||
public function recordTokenUsage(string $hash): void
|
||||
{
|
||||
$sql = "UPDATE dsp_oauth_access_tokens SET last_used_at = NOW() WHERE token_hash = :hash";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':hash' => $hash]);
|
||||
}
|
||||
|
||||
private function generateToken(int $length): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode(random_bytes($length)), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
private function hashToken(string $token): string
|
||||
{
|
||||
return hash('sha256', $token);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user