diff --git a/.env.example b/.env.example
index 7eebf5d..5da0ea6 100644
--- a/.env.example
+++ b/.env.example
@@ -21,3 +21,4 @@ RECAPTCHA_SITEKEY=your_recaptcha_sitekey
RECAPTCHA_SECRET=your_recaptcha_secret
JWT_RSA_PRIVATE_KEY=jwt-rsa256-private.pem
JWT_RSA_PUBLIC_KEY=jwt-rsa256-public.pem
+JWT_KEY_KID=1
diff --git a/database/migrations/structure/202411226_2042_oauth_update.sql b/database/migrations/structure/202411226_2042_oauth_update.sql
new file mode 100644
index 0000000..9ce2891
--- /dev/null
+++ b/database/migrations/structure/202411226_2042_oauth_update.sql
@@ -0,0 +1,26 @@
+CREATE TABLE `oauth_sessions` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `client_id` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
+ `scope` varchar(255) NOT NULL DEFAULT '',
+ `nonce` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
+ `user_id` int(10) unsigned DEFAULT NULL,
+ `code` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
+ `code_challenge` varchar(128) NULL,
+ `code_challenge_method` enum('plain', 'S256') NULL,
+ `token_claimed` tinyint(1) NOT NULL DEFAULT 0,
+ `created` timestamp NOT NULL DEFAULT current_timestamp(),
+ `expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `code` (`code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+ALTER TABLE `oauth_tokens`
+ADD `session_id` int(10) unsigned NULL,
+ADD CONSTRAINT `oauth_tokens_session_id` FOREIGN KEY (`session_id`) REFERENCES `oauth_sessions` (`id`),
+DROP INDEX `code`,
+DROP INDEX `access_token`,
+DROP `scope`,
+DROP `nonce`,
+DROP `user_id`,
+DROP `code`,
+DROP `access_token`;
diff --git a/src/Cli/MaintainDatabaseCommand.php b/src/Cli/MaintainDatabaseCommand.php
index c17b876..4701534 100644
--- a/src/Cli/MaintainDatabaseCommand.php
+++ b/src/Cli/MaintainDatabaseCommand.php
@@ -5,6 +5,8 @@ use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet;
use RVR\Repository\UserPasswordResetterRepository;
+use RVR\Repository\OAuthTokenRepository;
+use RVR\Repository\OAuthSessionRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -13,11 +15,17 @@ class MaintainDatabaseCommand extends Command
{
private UserPasswordResetterRepository $userPasswordResetterRepository;
+ private OAuthTokenRepository $oauthTokenRepository;
+
+ private OAuthSessionRepository $oauthSessionRepository;
+
public function __construct()
{
parent::__construct();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
+ $this->oauthTokenRepository = new OAuthTokenRepository();
+ $this->oauthSessionRepository = new OAuthSessionRepository();
}
public function configure(): void
@@ -31,6 +39,8 @@ class MaintainDatabaseCommand extends Command
try {
$this->deleteExpiredPasswordResetters();
$this->deleteExpiredSessions();
+ $this->deleteExpiredOauthTokens();
+ $this->deleteExpiredOauthSessions();
} catch (\Exception $e) {
$output->writeln('Maintenance failed!');
$output->writeln('');
@@ -69,4 +79,21 @@ class MaintainDatabaseCommand extends Command
$modify->delete();
}
}
+
+ private function deleteExpiredOauthTokens(): void
+ {
+ foreach ($this->oauthTokenRepository->getAllExpired() as $oauthToken) {
+ \Container::$persistentDataManager->deleteFromDb($oauthToken);
+ }
+ }
+
+ private function deleteExpiredOauthSessions(): void
+ {
+ foreach ($this->oauthSessionRepository->getAllExpired() as $oauthSession) {
+ if ($this->oauthTokenRepository->countAllBySession($oauthSession) > 0) {
+ continue;
+ }
+ \Container::$persistentDataManager->deleteFromDb($oauthSession);
+ }
+ }
}
diff --git a/src/Controller/OAuthAuthController.php b/src/Controller/OAuthAuthController.php
deleted file mode 100644
index 7c5d3e6..0000000
--- a/src/Controller/OAuthAuthController.php
+++ /dev/null
@@ -1,79 +0,0 @@
-oAuthClientRepository = new OAuthClientRepository();
- }
-
- public function isAuthenticationRequired(): bool
- {
- return true;
- }
-
- public function auth()
- {
- $redirectUri = \Container::$request->query('redirect_uri');
- $clientId = \Container::$request->query('client_id');
- $scope = \Container::$request->query('scope') ? \Container::$request->query('scope'): '';
- $state = \Container::$request->query('state');
- $nonce = \Container::$request->query('nonce') ? \Container::$request->query('nonce'): '';
-
- if (!$clientId || !$redirectUri || !$state) {
- return new HtmlContent('oauth/oauth_error', ['error' => 'An invalid request was made. Please start authentication again.']);
- }
-
- $client = $this->oAuthClientRepository->getByClientId($clientId);
- if ($client === null) {
- return new HtmlContent('oauth/oauth_error', ['error' => 'Client is not authorized.']);
- }
-
- $redirectUriParsed = parse_url($redirectUri);
- $redirectUriBase = $redirectUriParsed['scheme'] . '://' . $redirectUriParsed['host'] . $redirectUriParsed['path'];
- $redirectUriQuery = [];
- if (isset($redirectUriParsed['query'])) {
- parse_str($redirectUriParsed['query'], $redirectUriQuery);
- }
-
- if (!in_array($redirectUriBase, $client->getRedirectUrisArray())) {
- return new HtmlContent('oauth/oauth_error', ['error' => 'Redirect URI \'' . $redirectUriBase .'\' is not allowed for this client.']);
- }
-
- /**
- * @var ?User $user
- */
- $user = \Container::$request->user();
- $code = bin2hex(random_bytes(16));
- $accessToken = bin2hex(random_bytes(16));
-
- $token = new OAuthToken();
- $token->setNonce($nonce);
- $token->setScope($scope);
- $token->setUser($user);
- $token->setCode($code);
- $token->setAccessToken($accessToken);
- $token->setCreatedDate(new DateTime());
- $token->setExpiresDate(new DateTime('+5 minutes'));
- \Container::$persistentDataManager->saveToDb($token);
-
- $redirectUriQuery = array_merge($redirectUriQuery, [
- 'state' => $state,
- 'code' => $code
- ]);
- $finalRedirectUri = $redirectUriBase . '?' . http_build_query($redirectUriQuery);
-
- return new Redirect($finalRedirectUri, IRedirect::TEMPORARY);
- }
-}
diff --git a/src/Controller/OAuthController.php b/src/Controller/OAuthController.php
index 48b318e..66aaa4b 100644
--- a/src/Controller/OAuthController.php
+++ b/src/Controller/OAuthController.php
@@ -2,9 +2,16 @@
use DateTime;
use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+use Firebase\JWT\SignatureInvalidException;
+use Firebase\JWT\BeforeValidException;
+use Firebase\JWT\ExpiredException;
+use RVR\Repository\OAuthSessionRepository;
use RVR\Repository\OAuthTokenRepository;
use RVR\Repository\UserRepository;
use RVR\PersistentData\Model\User;
+use RVR\PersistentData\Model\OAuthSession;
+use RVR\PersistentData\Model\OAuthToken;
use RVR\Repository\OAuthClientRepository;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent;
@@ -13,6 +20,8 @@ class OAuthController
{
private OAuthClientRepository $oAuthClientRepository;
+ private OAuthSessionRepository $oAuthSessionRepository;
+
private OAuthTokenRepository $oAuthTokenRepository;
private UserRepository $userRepository;
@@ -20,60 +29,181 @@ class OAuthController
public function __construct()
{
$this->oAuthClientRepository = new OAuthClientRepository();
+ $this->oAuthSessionRepository = new OAuthSessionRepository();
$this->oAuthTokenRepository = new OAuthTokenRepository();
$this->userRepository = new UserRepository();
}
- public function getToken(): ?IContent
+ public function generateToken(): ?IContent
{
- $clientId = \Container::$request->post('client_id');
- $clientSecret = \Container::$request->post('client_secret');
+ $credentials = $this->getClientCredentials();
$code = \Container::$request->post('code');
+ $redirectUri = \Container::$request->post('redirect_uri');
- if (!$clientId || !$clientSecret || !$code) {
+ if (!$credentials['clientId'] || !$code || !$redirectUri) {
return new JsonContent([
'error' => 'An invalid request was made.'
]);
}
- $client = $this->oAuthClientRepository->getByClientId($clientId);
- if ($client === null || $client->getClientSecret() !== $clientSecret) {
+ $client = $this->oAuthClientRepository->getByClientId($credentials['clientId']);
+ if ($client === null) {
return new JsonContent([
- 'error' => 'Client is not authorized.'
+ 'error' => 'Client is not found.'
]);
}
- $token = $this->oAuthTokenRepository->getByCode($code);
- if ($token === null || $token->getExpiresDate() < new DateTime()) {
+ $redirectUriBase = explode('?', $redirectUri)[0];
+ if (!in_array($redirectUriBase, $client->getRedirectUrisArray())) {
+ return new JsonContent([
+ 'error' => 'Redirect URI \'' . $redirectUriBase .'\' is not allowed for this client.'
+ ]);
+ }
+
+ $session = $this->oAuthSessionRepository->getByCode($code);
+ if ($session === null || $session->getTokenClaimed() || $session->getExpiresDate() < new DateTime()) {
return new JsonContent([
'error' => 'The provided code is invalid.'
]);
}
- $payload = array_merge([
+ $codeChallenge = $session->getCodeChallenge();
+ if ($codeChallenge === null && $client->getClientSecret() !== $credentials['clientSecret']) {
+ return new JsonContent([
+ 'error' => 'Client is not authorized.'
+ ]);
+ }
+
+ if ($session->getClientId() !== $credentials['clientId']) {
+ return new JsonContent([
+ 'error' => 'This code cannot be used by this client!'
+ ]);
+ }
+
+ if ($codeChallenge !== null) {
+ $codeVerifier = \Container::$request->post('code_verifier') ?: '';
+ if ($session->getCodeChallengeMethod() === 'S256') {
+ $hash = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
+ } else {
+ $hash = $codeVerifier;
+ }
+ if ($codeChallenge !== $hash) {
+ return new JsonContent([
+ 'error' => 'Code challenge failed!'
+ ]);
+ }
+ }
+
+ $session->setTokenClaimed(true);
+ \Container::$persistentDataManager->saveToDb($session);
+
+ $token = new OAuthToken();
+ $token->setSession($session);
+ $token->setCreatedDate(new DateTime());
+ $token->setExpiresDate(new DateTime('+1 hours'));
+ \Container::$persistentDataManager->saveToDb($token);
+
+ $commonPayload = [
'iss' => $_ENV['APP_URL'],
- 'iat' => (int)$token->getCreatedDate()->getTimestamp(),
- 'nbf' => (int)$token->getCreatedDate()->getTimestamp(),
- 'exp' => (int)$token->getExpiresDate()->getTimestamp(),
- 'aud' => $clientId,
- 'nonce' => $token->getNonce()
- ], $this->getUserInfoInternal(
- $this->userRepository->getById($token->getUserId()),
- $token->getScopeArray())
+ 'iat' => $token->getCreatedDate()->getTimestamp(),
+ 'nbf' => $session->getCreatedDate()->getTimestamp(),
+ 'exp' => $token->getExpiresDate()->getTimestamp(),
+ 'aud' => $session->getClientId(),
+ 'nonce' => $session->getNonce()
+ ];
+ $idTokenPayload = array_merge($commonPayload, $this->getUserInfoInternal(
+ $this->userRepository->getById($session->getUserId()),
+ $session->getScopeArray())
);
+ $accessTokenPayload = array_merge($commonPayload, [
+ 'jti' => $token->getId(),
+ ]);
$privateKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PRIVATE_KEY']);
- $jwt = JWT::encode($payload, $privateKey, 'RS256');
+ $idToken = JWT::encode($idTokenPayload, $privateKey, 'RS256', $_ENV['JWT_KEY_KID']);
+ $accessToken = JWT::encode($accessTokenPayload, $privateKey, 'RS256', $_ENV['JWT_KEY_KID']);
return new JsonContent([
- 'access_token' => $token->getAccessToken(),
+ 'access_token' => $accessToken,
'expires_in' => $token->getExpiresDate()->getTimestamp() - (new DateTime())->getTimestamp(),
- 'scope' => $token->getScope(),
- 'id_token' => $jwt,
+ 'scope' => $session->getScope(),
+ 'id_token' => $idToken,
'token_type' => 'Bearer'
]);
}
+ public function introspectToken(): ?IContent
+ {
+ $credentials = $this->getClientCredentials();
+ $accessToken = \Container::$request->post('token');
+
+ if (!$credentials['clientId'] || !$credentials['clientSecret'] || !$accessToken) {
+ return new JsonContent([
+ 'error' => 'An invalid request was made.'
+ ]);
+ }
+
+ $client = $this->oAuthClientRepository->getByClientId($credentials['clientId']);
+ if ($client === null || $client->getClientSecret() !== $credentials['clientSecret']) {
+ return new JsonContent([
+ 'error' => 'Client is not authorized.'
+ ]);
+ }
+
+ $tokenValidated = $this->validateTokenAndSession($accessToken, $token, $session);
+ if (!$tokenValidated) {
+ return new JsonContent([
+ 'active' => false
+ ]);
+ }
+
+ if ($session->getClientId() !== $credentials['clientId']) {
+ return new JsonContent([
+ 'active' => false
+ ]);
+ }
+
+ return new JsonContent([
+ 'active' => true,
+ 'scope' => $session->getScope(),
+ 'client_id' => $session->getClientId(),
+ 'exp' => $token->getExpiresDate()->getTimestamp(),
+ ]);
+ }
+
+ public function revokeToken(): ?IContent
+ {
+ $credentials = $this->getClientCredentials();
+ $accessToken = \Container::$request->post('token');
+
+ if (!$credentials['clientId'] || !$credentials['clientSecret'] || !$accessToken) {
+ return new JsonContent([
+ 'error' => 'An invalid request was made.'
+ ]);
+ }
+
+ $client = $this->oAuthClientRepository->getByClientId($credentials['clientId']);
+ if ($client === null || $client->getClientSecret() !== $credentials['clientSecret']) {
+ return new JsonContent([
+ 'error' => 'Client is not authorized.'
+ ]);
+ }
+
+ $tokenValidated = $this->validateTokenAndSession($accessToken, $token, $session);
+ if (!$tokenValidated) {
+ return new JsonContent([]);
+ }
+
+ $session = $this->oAuthSessionRepository->getById($token->getSessionId());
+ if ($session->getClientId() !== $credentials['clientId']) {
+ return new JsonContent([]);
+ }
+
+ \Container::$persistentDataManager->deleteFromDb($token);
+
+ return new JsonContent([]);
+ }
+
public function getUserInfo() : IContent
{
$authorization = \Container::$request->header('Authorization');
@@ -84,9 +214,8 @@ class OAuthController
}
$accessToken = substr($authorization, strlen('Bearer '));
- $token = $this->oAuthTokenRepository->getByAccessToken($accessToken);
-
- if ($token === null || $token->getExpiresDate() < new DateTime()) {
+ $tokenValidated = $this->validateTokenAndSession($accessToken, $token, $session);
+ if (!$tokenValidated) {
return new JsonContent([
'error' => 'The provided access token is invalid.'
]);
@@ -94,8 +223,8 @@ class OAuthController
return new JsonContent(
$this->getUserInfoInternal(
- $this->userRepository->getById($token->getUserId()),
- $token->getScopeArray()
+ $this->userRepository->getById($session->getUserId()),
+ $session->getScopeArray()
)
);
}
@@ -106,7 +235,10 @@ class OAuthController
'issuer' => $_ENV['APP_URL'],
'authorization_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.auth')->generateLink(),
'token_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token')->generateLink(),
+ 'introspection_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token.introspect')->generateLink(),
+ 'revocation_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token.revoke')->generateLink(),
'userinfo_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.userinfo')->generateLink(),
+ 'end_session_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('logout')->generateLink(),
'jwks_uri' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.certs')->generateLink(),
'response_types_supported' =>
[
@@ -128,6 +260,7 @@ class OAuthController
],
'token_endpoint_auth_methods_supported' =>
[
+ 'client_secret_basic',
'client_secret_post',
],
'claims_supported' =>
@@ -167,13 +300,66 @@ class OAuthController
'kty' => 'RSA',
'alg' => 'RS256',
'use' => 'sig',
- 'kid' => '1',
+ 'kid' => $_ENV['JWT_KEY_KID'],
'n' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['n'])),
'e' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['e'])),
]
]]);
}
+ private function getClientCredentials(): array
+ {
+ $authorization = \Container::$request->header('Authorization');
+ if ($authorization !== null) {
+ $basicAuthEncoded = substr($authorization, strlen('Basic '));
+ $basicAuth = explode(':', base64_decode($basicAuthEncoded));
+ if (count($basicAuth) === 2) {
+ $clientId = rawurldecode($basicAuth[0]);
+ $clientSecret = rawurldecode($basicAuth[1]);
+ } else {
+ $clientId = null;
+ $clientSecret = null;
+ }
+ } else {
+ $clientId = \Container::$request->post('client_id');
+ $clientSecret = \Container::$request->post('client_secret');
+ }
+ return ['clientId' => $clientId, 'clientSecret' => $clientSecret];
+ }
+
+ private function validateTokenAndSession(
+ string $accessToken,
+ ?OAuthToken &$token,
+ ?OAuthSession &$session): bool
+ {
+ $publicKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PUBLIC_KEY']);
+ try {
+ $payload = JWT::decode($accessToken, new Key($publicKey, 'RS256'));
+ $token = $this->oAuthTokenRepository->getById($payload->jti);
+ } catch (SignatureInvalidException | BeforeValidException | ExpiredException) {
+ $token = null;
+ } catch (\UnexpectedValueException $e) {
+ error_log($e->getMessage() . ' Token was: ' . $accessToken);
+ $token = null;
+ }
+
+ if ($token === null || $token->getExpiresDate() < new DateTime()) {
+ return false;
+ }
+
+ $session = $this->oAuthSessionRepository->getById($token->getSessionId());
+ if ($session === null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param User $user
+ * @param string[] $scope
+ * @return array
+ */
private function getUserInfoInternal(User $user, array $scope): array
{
$userInfo = [];
diff --git a/src/Controller/OAuthSessionController.php b/src/Controller/OAuthSessionController.php
new file mode 100644
index 0000000..6644ca5
--- /dev/null
+++ b/src/Controller/OAuthSessionController.php
@@ -0,0 +1,90 @@
+oAuthClientRepository = new OAuthClientRepository();
+ }
+
+ public function isAuthenticationRequired(): bool
+ {
+ return true;
+ }
+
+ public function auth(): HtmlContent|Redirect
+ {
+ $redirectUri = \Container::$request->query('redirect_uri');
+ $clientId = \Container::$request->query('client_id');
+ $scope = \Container::$request->query('scope') ? \Container::$request->query('scope'): '';
+ $state = \Container::$request->query('state');
+ $nonce = \Container::$request->query('nonce') ? \Container::$request->query('nonce'): '';
+ $codeChallenge = \Container::$request->query('code_challenge') ?: null;
+ $codeChallengeMethod = \Container::$request->query('code_challenge_method') ?: null;
+
+ if (!$clientId || !$redirectUri || !$state || (!$codeChallenge && $codeChallengeMethod)) {
+ return new HtmlContent('oauth/oauth_error', ['error' => 'An invalid request was made. Please start authentication again.']);
+ }
+
+ if ($codeChallenge && (strlen($codeChallenge) < 43 || strlen($codeChallenge) > 128)) {
+ return new HtmlContent('oauth/oauth_error', ['error' => 'Code challenge should be one between 43 and 128 characters long.']);
+ }
+ $possibleCodeChallengeMethods = ['plain', 'S256'];
+ if ($codeChallenge && !in_array($codeChallengeMethod, $possibleCodeChallengeMethods)) {
+ return new HtmlContent('oauth/oauth_error', ['error' => 'Code challenge method should be one of the following: ' . implode(',', $possibleCodeChallengeMethods)]);
+ }
+
+ $client = $this->oAuthClientRepository->getByClientId($clientId);
+ if ($client === null) {
+ return new HtmlContent('oauth/oauth_error', ['error' => 'Client is not found.']);
+ }
+
+ $redirectUriQueryParsed = [];
+ if (str_contains('?', $redirectUri)) {
+ [$redirectUriBase, $redirectUriQuery] = explode('?', $redirectUri, 2);
+ parse_str($redirectUriQuery, $redirectUriQueryParsed);
+ } else {
+ $redirectUriBase = $redirectUri;
+ }
+ if (!in_array($redirectUriBase, $client->getRedirectUrisArray())) {
+ return new HtmlContent('oauth/oauth_error', ['error' => 'Redirect URI \'' . $redirectUriBase .'\' is not allowed for this client.']);
+ }
+
+ /**
+ * @var ?User $user
+ */
+ $user = \Container::$request->user();
+ $code = bin2hex(random_bytes(16));
+
+ $session = new OAuthSession();
+ $session->setClientId($clientId);
+ $session->setNonce($nonce);
+ $session->setScope($scope);
+ $session->setCodeChallenge($codeChallenge);
+ $session->setCodeChallengeMethod($codeChallengeMethod);
+ $session->setUser($user);
+ $session->setCode($code);
+ $session->setCreatedDate(new DateTime());
+ $session->setExpiresDate(new DateTime('+5 minutes'));
+ \Container::$persistentDataManager->saveToDb($session);
+
+ $redirectUriQueryParsed = array_merge($redirectUriQueryParsed, [
+ 'state' => $state,
+ 'code' => $code
+ ]);
+ $finalRedirectUri = $redirectUriBase . '?' . http_build_query($redirectUriQueryParsed);
+
+ return new Redirect($finalRedirectUri, IRedirect::TEMPORARY);
+ }
+}
diff --git a/src/PersistentData/Model/OAuthSession.php b/src/PersistentData/Model/OAuthSession.php
new file mode 100644
index 0000000..d0f1579
--- /dev/null
+++ b/src/PersistentData/Model/OAuthSession.php
@@ -0,0 +1,182 @@
+ User::class];
+
+ private static array $possibleScopeValues = ['openid', 'email', 'profile', 'union_profile'];
+
+ private static array $possibleCodeChallengeMethodValues = ['plain', 'S256'];
+
+ private string $clientId = '';
+
+ private array $scope = [];
+
+ private string $nonce = '';
+
+ private ?string $codeChallenge = null;
+
+ private ?string $codeChallengeMethod = null;
+
+ private ?User $user = null;
+
+ private ?int $userId = null;
+
+ private string $code = '';
+
+ private DateTime $created;
+
+ private DateTime $expires;
+
+ private bool $tokenClaimed = false;
+
+ public function setScopeArray(array $scope): void
+ {
+ $this->scope = array_intersect($scope, self::$possibleScopeValues);
+ }
+
+ public function setClientId(string $clientId): void
+ {
+ $this->clientId = $clientId;
+ }
+
+ public function setScope(string $scope): void
+ {
+ $this->setScopeArray(explode(' ', $scope));
+ }
+
+ public function setNonce(string $nonce): void
+ {
+ $this->nonce = $nonce;
+ }
+
+ public function setCodeChallenge(?string $codeChallenge): void
+ {
+ $this->codeChallenge = $codeChallenge;
+ }
+
+ public function setCodeChallengeMethod(?string $codeChallengeMethod): void
+ {
+ if ($codeChallengeMethod !== null && !in_array($codeChallengeMethod, self::$possibleCodeChallengeMethodValues)) {
+ throw new \UnexpectedValueException($codeChallengeMethod . ' is not possible for challengeMethod!');
+ }
+ $this->codeChallengeMethod = $codeChallengeMethod;
+ }
+
+ public function setUser(User $user): void
+ {
+ $this->user = $user;
+ }
+
+ public function setUserId(int $userId): void
+ {
+ $this->userId = $userId;
+ }
+
+ public function setCode(string $code): void
+ {
+ $this->code = $code;
+ }
+
+ public function setCreatedDate(DateTime $created): void
+ {
+ $this->created = $created;
+ }
+
+ public function setExpiresDate(DateTime $expires): void
+ {
+ $this->expires = $expires;
+ }
+
+ public function setCreated(string $created): void
+ {
+ $this->created = new DateTime($created);
+ }
+
+ public function setExpires(string $expires): void
+ {
+ $this->expires = new DateTime($expires);
+ }
+
+ public function setTokenClaimed(bool $tokenClaimed): void
+ {
+ $this->tokenClaimed = $tokenClaimed;
+ }
+
+ public function getClientId(): string
+ {
+ return $this->clientId;
+ }
+
+ public function getScope(): string
+ {
+ return implode(' ', $this->scope);
+ }
+
+ public function getScopeArray(): array
+ {
+ return $this->scope;
+ }
+
+ public function getNonce(): string
+ {
+ return $this->nonce;
+ }
+
+ public function getCodeChallenge(): ?string
+ {
+ return $this->codeChallenge;
+ }
+
+ public function getCodeChallengeMethod(): ?string
+ {
+ return $this->codeChallengeMethod;
+ }
+
+ public function getUser(): ?User
+ {
+ return $this->user;
+ }
+
+ public function getUserId(): ?int
+ {
+ return $this->userId;
+ }
+
+ public function getCode(): string
+ {
+ return $this->code;
+ }
+
+ public function getCreatedDate(): DateTime
+ {
+ return $this->created;
+ }
+
+ public function getCreated(): string
+ {
+ return $this->created->format('Y-m-d H:i:s');
+ }
+
+ public function getExpiresDate(): DateTime
+ {
+ return $this->expires;
+ }
+
+ public function getExpires(): string
+ {
+ return $this->expires->format('Y-m-d H:i:s');
+ }
+
+ public function getTokenClaimed(): bool
+ {
+ return $this->tokenClaimed;
+ }
+}
diff --git a/src/PersistentData/Model/OAuthToken.php b/src/PersistentData/Model/OAuthToken.php
index e82242c..ffe6180 100644
--- a/src/PersistentData/Model/OAuthToken.php
+++ b/src/PersistentData/Model/OAuthToken.php
@@ -7,61 +7,26 @@ class OAuthToken extends Model
{
protected static string $table = 'oauth_tokens';
- protected static array $fields = ['scope', 'nonce', 'user_id', 'code', 'access_token', 'created', 'expires'];
+ protected static array $fields = ['session_id', 'created', 'expires'];
- protected static array $relations = ['user' => User::class];
+ protected static array $relations = ['session' => OAuthSession::class];
- private static array $possibleScopeValues = ['openid', 'email', 'profile'];
+ private ?OAuthSession $session = null;
- private array $scope = [];
-
- private string $nonce = '';
-
- private ?User $user = null;
-
- private ?int $userId = null;
-
- private string $code = '';
-
- private string $accessToken = '';
+ private ?int $sessionId = null;
private DateTime $created;
private DateTime $expires;
- public function setScopeArray(array $scope): void
+ public function setSession(OAuthSession $session): void
{
- $this->scope = array_intersect($scope, self::$possibleScopeValues);
+ $this->session = $session;
}
- public function setScope(string $scope): void
+ public function setSessionId(int $sessionId): void
{
- $this->setScopeArray(explode(' ', $scope));
- }
-
- public function setNonce(string $nonce): void
- {
- $this->nonce = $nonce;
- }
-
- public function setUser(User $user): void
- {
- $this->user = $user;
- }
-
- public function setUserId(int $userId): void
- {
- $this->userId = $userId;
- }
-
- public function setCode(string $code): void
- {
- $this->code = $code;
- }
-
- public function setAccessToken(string $accessToken): void
- {
- $this->accessToken = $accessToken;
+ $this->sessionId = $sessionId;
}
public function setCreatedDate(DateTime $created): void
@@ -84,39 +49,14 @@ class OAuthToken extends Model
$this->expires = new DateTime($expires);
}
- public function getScope(): string
+ public function getSession(): ?OAuthSession
{
- return implode(' ', $this->scope);
+ return $this->session;
}
- public function getScopeArray(): array
+ public function getSessionId(): ?int
{
- return $this->scope;
- }
-
- public function getNonce(): string
- {
- return $this->nonce;
- }
-
- public function getUser(): ?User
- {
- return $this->user;
- }
-
- public function getUserId(): ?int
- {
- return $this->userId;
- }
-
- public function getCode(): string
- {
- return $this->code;
- }
-
- public function getAccessToken(): string
- {
- return $this->accessToken;
+ return $this->sessionId;
}
public function getCreatedDate(): DateTime
diff --git a/src/Repository/OAuthSessionRepository.php b/src/Repository/OAuthSessionRepository.php
new file mode 100644
index 0000000..2cc4061
--- /dev/null
+++ b/src/Repository/OAuthSessionRepository.php
@@ -0,0 +1,30 @@
+selectFromDbById($id, OAuthSession::class);
+ }
+
+ public function getByCode(string $code): ?OAuthSession
+ {
+ $select = new Select(\Container::$dbConnection);
+ $select->where('code', '=', $code);
+
+ return \Container::$persistentDataManager->selectFromDb($select, OAuthSession::class);
+ }
+
+ public function getAllExpired(): Generator
+ {
+ $select = new Select(\Container::$dbConnection);
+ $select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
+
+ yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthSession::class);
+ }
+}
diff --git a/src/Repository/OAuthTokenRepository.php b/src/Repository/OAuthTokenRepository.php
index 4aaa05b..d85ee4b 100644
--- a/src/Repository/OAuthTokenRepository.php
+++ b/src/Repository/OAuthTokenRepository.php
@@ -4,6 +4,7 @@ use DateTime;
use Generator;
use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\OAuthToken;
+use RVR\PersistentData\Model\OAuthSession;
class OAuthTokenRepository
{
@@ -12,20 +13,16 @@ class OAuthTokenRepository
return \Container::$persistentDataManager->selectFromDbById($id, OAuthToken::class);
}
- public function getByCode(string $code): ?OAuthToken
+ public function getAllBySession(OAuthSession $session, bool $useRelations = false, array $withRelations = []): Generator
{
- $select = new Select(\Container::$dbConnection);
- $select->where('code', '=', $code);
+ $select = $this->selectAllBySession($session);
- return \Container::$persistentDataManager->selectFromDb($select, OAuthToken::class);
+ yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthToken::class, $useRelations, $withRelations);
}
- public function getByAccessToken(string $accessToken): ?OAuthToken
+ public function countAllBySession(OAuthSession $session): int
{
- $select = new Select(\Container::$dbConnection);
- $select->where('access_token', '=', $accessToken);
-
- return \Container::$persistentDataManager->selectFromDb($select, OAuthToken::class);
+ return $this->selectAllBySession($session)->count();
}
public function getAllExpired(): Generator
@@ -35,4 +32,11 @@ class OAuthTokenRepository
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthToken::class);
}
+
+ private function selectAllBySession(OAuthSession $session): Select
+ {
+ $select = new Select(\Container::$dbConnection, OAuthToken::getTable());
+ $select->where('session_id', '=', $session->getId());
+ return $select;
+ }
}
diff --git a/web.php b/web.php
index c56baf8..df0f0b3 100644
--- a/web.php
+++ b/web.php
@@ -7,7 +7,7 @@ use SokoWeb\Request\Request;
use SokoWeb\Request\Session;
use RVR\Controller\HomeController;
use RVR\Controller\LoginController;
-use RVR\Controller\OAuthAuthController;
+use RVR\Controller\OAuthSessionController;
use RVR\Controller\OAuthController;
use RVR\Controller\UserController;
use RVR\Controller\UserSearchController;
@@ -37,8 +37,10 @@ Container::$routeCollection->group('login', function (RouteCollection $routeColl
$routeCollection->get('login.google-action', 'google/code', [LoginController::class, 'loginWithGoogle']);
});
Container::$routeCollection->group('oauth', function (RouteCollection $routeCollection) {
- $routeCollection->get('oauth.auth', 'auth', [OAuthAuthController::class, 'auth']);
- $routeCollection->post('oauth.token', 'token', [OAuthController::class, 'getToken']);
+ $routeCollection->get('oauth.auth', 'auth', [OAuthSessionController::class, 'auth']);
+ $routeCollection->post('oauth.token', 'token', [OAuthController::class, 'generateToken']);
+ $routeCollection->post('oauth.token.introspect', 'token/introspect', [OAuthController::class, 'introspectToken']);
+ $routeCollection->post('oauth.token.revoke', 'token/revoke', [OAuthController::class, 'revokeToken']);
$routeCollection->get('oauth.userinfo', 'userinfo', [OAuthController::class, 'getUserInfo']);
$routeCollection->get('oauth.config', '.well-known/openid-configuration', [OAuthController::class, 'getConfig']);
$routeCollection->get('oauth.certs', 'certs', [OAuthController::class, 'getCerts']);