mirror of
https://github.com/elyby/accounts.git
synced 2024-11-06 08:11:24 +05:30
Completely restored authorization_code grant for user side.
Reworked oauth_sessions table. Added extension to use MariaDB's JSON columns. Rewritten tests for authorization_code grant for client side. Deprecate some old shit. [skip ci]
This commit is contained in:
parent
8a1d7148d0
commit
45101d6453
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
namespace api\components\OAuth2;
|
||||
|
||||
use api\components\OAuth2\Keys\EmptyKey;
|
||||
use api\components\OAuth2\Repositories;
|
||||
use DateInterval;
|
||||
use League\OAuth2\Server\AuthorizationServer;
|
||||
use League\OAuth2\Server\Grant;
|
||||
|
@ -1,72 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\AuthCodeEntity;
|
||||
use common\components\Redis\Key;
|
||||
use common\components\Redis\Set;
|
||||
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
|
||||
use League\OAuth2\Server\Entity\ScopeEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\AuthCodeInterface;
|
||||
use yii\helpers\Json;
|
||||
|
||||
class AuthCodeStorage extends AbstractStorage implements AuthCodeInterface {
|
||||
|
||||
public $dataTable = 'oauth_auth_codes';
|
||||
|
||||
public function get($code) {
|
||||
$result = Json::decode((new Key($this->dataTable, $code))->getValue());
|
||||
if ($result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = new AuthCodeEntity($this->server);
|
||||
$entity->setId($result['id']);
|
||||
$entity->setExpireTime($result['expire_time']);
|
||||
$entity->setSessionId($result['session_id']);
|
||||
$entity->setRedirectUri($result['client_redirect_uri']);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function create($token, $expireTime, $sessionId, $redirectUri) {
|
||||
$payload = Json::encode([
|
||||
'id' => $token,
|
||||
'expire_time' => $expireTime,
|
||||
'session_id' => $sessionId,
|
||||
'client_redirect_uri' => $redirectUri,
|
||||
]);
|
||||
|
||||
$this->key($token)->setValue($payload)->expireAt($expireTime);
|
||||
}
|
||||
|
||||
public function getScopes(OriginalAuthCodeEntity $token) {
|
||||
$scopes = $this->scopes($token->getId());
|
||||
$scopesEntities = [];
|
||||
foreach ($scopes as $scope) {
|
||||
if ($this->server->getScopeStorage()->get($scope) !== null) {
|
||||
$scopesEntities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
|
||||
}
|
||||
}
|
||||
|
||||
return $scopesEntities;
|
||||
}
|
||||
|
||||
public function associateScope(OriginalAuthCodeEntity $token, ScopeEntity $scope) {
|
||||
$this->scopes($token->getId())->add($scope->getId())->expireAt($token->getExpireTime());
|
||||
}
|
||||
|
||||
public function delete(OriginalAuthCodeEntity $token) {
|
||||
$this->key($token->getId())->delete();
|
||||
$this->scopes($token->getId())->delete();
|
||||
}
|
||||
|
||||
private function key(string $token): Key {
|
||||
return new Key($this->dataTable, $token);
|
||||
}
|
||||
|
||||
private function scopes(string $token): Set {
|
||||
return new Set($this->dataTable, $token, 'scopes');
|
||||
}
|
||||
|
||||
}
|
@ -29,8 +29,6 @@ class ClientRepository implements ClientRepositoryInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: there is missing behavior of checking redirectUri. Is it now bundled into grant?
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1,97 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\AuthCodeEntity;
|
||||
use api\components\OAuth2\Entities\SessionEntity;
|
||||
use common\models\OauthSession;
|
||||
use ErrorException;
|
||||
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
|
||||
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
|
||||
use League\OAuth2\Server\Entity\ScopeEntity;
|
||||
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\SessionInterface;
|
||||
use yii\db\Exception;
|
||||
|
||||
class SessionStorage extends AbstractStorage implements SessionInterface {
|
||||
|
||||
/**
|
||||
* @param string $sessionId
|
||||
* @return SessionEntity|null
|
||||
*/
|
||||
public function getById($sessionId) {
|
||||
return $this->hydrate($this->getSessionModel($sessionId));
|
||||
}
|
||||
|
||||
public function getByAccessToken(OriginalAccessTokenEntity $accessToken) {
|
||||
throw new ErrorException('This method is not implemented and should not be used');
|
||||
}
|
||||
|
||||
public function getByAuthCode(OriginalAuthCodeEntity $authCode) {
|
||||
if (!$authCode instanceof AuthCodeEntity) {
|
||||
throw new ErrorException('This module assumes that $authCode typeof ' . AuthCodeEntity::class);
|
||||
}
|
||||
|
||||
return $this->getById($authCode->getSessionId());
|
||||
}
|
||||
|
||||
public function getScopes(OriginalSessionEntity $session) {
|
||||
$result = [];
|
||||
foreach ($this->getSessionModel($session->getId())->getScopes() as $scope) {
|
||||
if ($this->server->getScopeStorage()->get($scope) !== null) {
|
||||
$result[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function create($ownerType, $ownerId, $clientId, $clientRedirectUri = null) {
|
||||
$sessionId = OauthSession::find()
|
||||
->select('id')
|
||||
->andWhere([
|
||||
'client_id' => $clientId,
|
||||
'owner_type' => $ownerType,
|
||||
'owner_id' => (string)$ownerId, // Casts as a string to make the indexes work, because the varchar field
|
||||
])->scalar();
|
||||
|
||||
if ($sessionId === false) {
|
||||
$model = new OauthSession();
|
||||
$model->client_id = $clientId;
|
||||
$model->owner_type = $ownerType;
|
||||
$model->owner_id = $ownerId;
|
||||
$model->client_redirect_uri = $clientRedirectUri;
|
||||
|
||||
if (!$model->save()) {
|
||||
throw new Exception('Cannot save ' . OauthSession::class . ' model.');
|
||||
}
|
||||
|
||||
$sessionId = $model->id;
|
||||
}
|
||||
|
||||
return $sessionId;
|
||||
}
|
||||
|
||||
public function associateScope(OriginalSessionEntity $session, ScopeEntity $scope) {
|
||||
$this->getSessionModel($session->getId())->getScopes()->add($scope->getId());
|
||||
}
|
||||
|
||||
private function getSessionModel(string $sessionId): OauthSession {
|
||||
$session = OauthSession::findOne($sessionId);
|
||||
if ($session === null) {
|
||||
throw new ErrorException('Cannot find oauth session');
|
||||
}
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
private function hydrate(OauthSession $sessionModel) {
|
||||
$entity = new SessionEntity($this->server);
|
||||
$entity->setId($sessionModel->id);
|
||||
$entity->setClientId($sessionModel->client_id);
|
||||
$entity->setOwner($sessionModel->owner_type, $sessionModel->owner_id);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\RequestTypes;
|
||||
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\UserEntityInterface;
|
||||
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
|
||||
|
||||
class AuthorizationRequestProxy extends AuthorizationRequest {
|
||||
|
||||
/**
|
||||
* @var AuthorizationRequest
|
||||
*/
|
||||
private $authorizationRequest;
|
||||
|
||||
public function __construct(AuthorizationRequest $authorizationRequest) {
|
||||
$this->authorizationRequest = $authorizationRequest;
|
||||
}
|
||||
|
||||
public function getOriginalAuthorizationRequest(): AuthorizationRequest {
|
||||
return $this->authorizationRequest;
|
||||
}
|
||||
|
||||
public function getGrantTypeId(): string {
|
||||
return $this->authorizationRequest->getGrantTypeId();
|
||||
}
|
||||
|
||||
public function setGrantTypeId($grantTypeId): void {
|
||||
$this->authorizationRequest->setGrantTypeId($grantTypeId);
|
||||
}
|
||||
|
||||
public function getClient(): ClientEntityInterface {
|
||||
return $this->authorizationRequest->getClient();
|
||||
}
|
||||
|
||||
public function setClient(ClientEntityInterface $client): void {
|
||||
$this->authorizationRequest->setClient($client);
|
||||
}
|
||||
|
||||
public function getUser(): UserEntityInterface {
|
||||
return $this->authorizationRequest->getUser();
|
||||
}
|
||||
|
||||
public function setUser(UserEntityInterface $user): void {
|
||||
$this->authorizationRequest->setUser($user);
|
||||
}
|
||||
|
||||
public function getScopes(): array {
|
||||
return $this->authorizationRequest->getScopes();
|
||||
}
|
||||
|
||||
public function setScopes(array $scopes): void {
|
||||
$this->authorizationRequest->setScopes($scopes);
|
||||
}
|
||||
|
||||
public function isAuthorizationApproved(): bool {
|
||||
return $this->authorizationRequest->isAuthorizationApproved();
|
||||
}
|
||||
|
||||
public function setAuthorizationApproved($authorizationApproved): void {
|
||||
$this->authorizationRequest->setAuthorizationApproved($authorizationApproved);
|
||||
}
|
||||
|
||||
public function getRedirectUri(): ?string {
|
||||
return $this->authorizationRequest->getRedirectUri();
|
||||
}
|
||||
|
||||
public function setRedirectUri($redirectUri): void {
|
||||
$this->authorizationRequest->setRedirectUri($redirectUri);
|
||||
}
|
||||
|
||||
public function getState(): ?string {
|
||||
return $this->authorizationRequest->getState();
|
||||
}
|
||||
|
||||
public function setState($state): void {
|
||||
$this->authorizationRequest->setState($state);
|
||||
}
|
||||
|
||||
public function getCodeChallenge(): string {
|
||||
return $this->authorizationRequest->getCodeChallenge();
|
||||
}
|
||||
|
||||
public function setCodeChallenge($codeChallenge): void {
|
||||
$this->authorizationRequest->setCodeChallenge($codeChallenge);
|
||||
}
|
||||
|
||||
public function getCodeChallengeMethod(): string {
|
||||
return $this->authorizationRequest->getCodeChallengeMethod();
|
||||
}
|
||||
|
||||
public function setCodeChallengeMethod($codeChallengeMethod): void {
|
||||
$this->authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
|
||||
}
|
||||
|
||||
}
|
@ -7,6 +7,8 @@ namespace api\exceptions;
|
||||
* The exception can be used for cases where the outcome doesn't seem to be expected,
|
||||
* but can theoretically happen. The goal is to capture these areas and refine the logic
|
||||
* if such situations do occur.
|
||||
*
|
||||
* @deprecated use \Webmozart\Assert\Assert to ensure, that action has been successfully performed
|
||||
*/
|
||||
class ThisShouldNotHappenException extends Exception {
|
||||
|
||||
|
@ -7,12 +7,15 @@ use api\components\OAuth2\Entities\UserEntity;
|
||||
use api\rbac\Permissions as P;
|
||||
use common\models\Account;
|
||||
use common\models\OauthClient;
|
||||
use common\models\OauthSession;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use GuzzleHttp\Psr7\ServerRequest;
|
||||
use League\OAuth2\Server\AuthorizationServer;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
|
||||
class OauthProcess {
|
||||
@ -57,7 +60,7 @@ class OauthProcess {
|
||||
$clientModel = $this->findClient($client->getIdentifier());
|
||||
$response = $this->buildSuccessResponse($request, $clientModel, $authRequest->getScopes());
|
||||
} catch (OAuthServerException $e) {
|
||||
$response = $this->buildErrorResponse($e);
|
||||
$response = $this->buildCompleteErrorResponse($e);
|
||||
}
|
||||
|
||||
return $response;
|
||||
@ -90,29 +93,31 @@ class OauthProcess {
|
||||
$authRequest = $this->server->validateAuthorizationRequest($request);
|
||||
/** @var Account $account */
|
||||
$account = Yii::$app->user->identity->getAccount();
|
||||
/** @var OauthClient $clientModel */
|
||||
$clientModel = $this->findClient($authRequest->getClient()->getIdentifier());
|
||||
/** @var OauthClient $client */
|
||||
$client = $this->findClient($authRequest->getClient()->getIdentifier());
|
||||
|
||||
if (!$this->canAutoApprove($account, $clientModel, $authRequest)) {
|
||||
$approved = $this->canAutoApprove($account, $client, $authRequest);
|
||||
if (!$approved) {
|
||||
Yii::$app->statsd->inc('oauth.complete.approve_required');
|
||||
|
||||
$accept = ((array)$request->getParsedBody())['accept'] ?? null;
|
||||
if ($accept === null) {
|
||||
$acceptParam = ((array)$request->getParsedBody())['accept'] ?? null;
|
||||
if ($acceptParam === null) {
|
||||
throw $this->createAcceptRequiredException();
|
||||
}
|
||||
|
||||
if (!in_array($accept, [1, '1', true, 'true'], true)) {
|
||||
throw OAuthServerException::accessDenied(null, $authRequest->getRedirectUri());
|
||||
$approved = in_array($acceptParam, [1, '1', true, 'true'], true);
|
||||
if ($approved) {
|
||||
$this->storeOauthSession($account, $client, $authRequest);
|
||||
}
|
||||
}
|
||||
|
||||
$authRequest->setUser(new UserEntity($account->id));
|
||||
$authRequest->setAuthorizationApproved(true);
|
||||
$responseObj = $this->server->completeAuthorizationRequest($authRequest, new Response(200));
|
||||
$authRequest->setAuthorizationApproved($approved);
|
||||
$response = $this->server->completeAuthorizationRequest($authRequest, new Response(200));
|
||||
|
||||
$response = [
|
||||
$result = [
|
||||
'success' => true,
|
||||
'redirectUri' => $responseObj->getHeader('Location'), // TODO: ensure that this is correct type and behavior
|
||||
'redirectUri' => $response->getHeaderLine('Location'),
|
||||
];
|
||||
|
||||
Yii::$app->statsd->inc('oauth.complete.success');
|
||||
@ -121,10 +126,10 @@ class OauthProcess {
|
||||
Yii::$app->statsd->inc('oauth.complete.fail');
|
||||
}
|
||||
|
||||
$response = $this->buildErrorResponse($e);
|
||||
$result = $this->buildCompleteErrorResponse($e);
|
||||
}
|
||||
|
||||
return $response;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,10 +173,7 @@ class OauthProcess {
|
||||
Yii::$app->statsd->inc("oauth.issueToken_{$grantType}.fail");
|
||||
Yii::$app->response->statusCode = $e->getHttpStatusCode();
|
||||
|
||||
$response = [
|
||||
'error' => $e->getErrorType(),
|
||||
'message' => $e->getMessage(), // TODO: use hint field?
|
||||
];
|
||||
$response = $this->buildIssueErrorResponse($e);
|
||||
}
|
||||
|
||||
return $response;
|
||||
@ -196,22 +198,31 @@ class OauthProcess {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var \common\models\OauthSession|null $session */
|
||||
$session = $account->getOauthSessions()->andWhere(['client_id' => $client->id])->one();
|
||||
if ($session !== null) {
|
||||
$existScopes = $session->getScopes()->members();
|
||||
if (empty(array_diff(array_keys($request->getScopes()), $existScopes))) {
|
||||
return true;
|
||||
}
|
||||
$session = $this->findOauthSession($account, $client);
|
||||
if ($session === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return empty(array_diff($this->getScopesList($request), $session->getScopes()));
|
||||
}
|
||||
|
||||
private function storeOauthSession(Account $account, OauthClient $client, AuthorizationRequest $request): void {
|
||||
$session = $this->findOauthSession($account, $client);
|
||||
if ($session === null) {
|
||||
$session = new OauthSession();
|
||||
$session->account_id = $account->id;
|
||||
$session->client_id = $client->id;
|
||||
}
|
||||
|
||||
$session->scopes = array_unique(array_merge($session->getScopes(), $this->getScopesList($request)));
|
||||
|
||||
Assert::true($session->save());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param OauthClient $client
|
||||
* @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes
|
||||
* @param ScopeEntityInterface[] $scopes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@ -238,7 +249,7 @@ class OauthProcess {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes
|
||||
* @param ScopeEntityInterface[] $scopes
|
||||
* @return array
|
||||
*/
|
||||
private function buildScopesArray(array $scopes): array {
|
||||
@ -250,7 +261,7 @@ class OauthProcess {
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function buildErrorResponse(OAuthServerException $e): array {
|
||||
private function buildCompleteErrorResponse(OAuthServerException $e): array {
|
||||
$hint = $e->getPayload()['hint'] ?? '';
|
||||
if (preg_match('/the `(\w+)` scope/', $hint, $matches)) {
|
||||
$parameter = $matches[1];
|
||||
@ -274,6 +285,35 @@ class OauthProcess {
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw error messages aren't very informative for the end user, as they don't contain
|
||||
* information about the parameter that caused the error.
|
||||
* This method is intended to build a more understandable description.
|
||||
*
|
||||
* Part of the existing texts is a legacy from the previous implementation.
|
||||
*
|
||||
* @param OAuthServerException $e
|
||||
* @return array
|
||||
*/
|
||||
private function buildIssueErrorResponse(OAuthServerException $e): array {
|
||||
$errorType = $e->getErrorType();
|
||||
$message = $e->getMessage();
|
||||
$hint = $e->getHint();
|
||||
switch ($hint) {
|
||||
case 'Invalid redirect URI':
|
||||
$errorType = 'invalid_client';
|
||||
$message = 'Client authentication failed.';
|
||||
break;
|
||||
case 'Cannot decrypt the authorization code':
|
||||
$message .= ' Check the "code" parameter.';
|
||||
}
|
||||
|
||||
return [
|
||||
'error' => $errorType,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
private function getRequest(): ServerRequestInterface {
|
||||
return ServerRequest::fromGlobals();
|
||||
}
|
||||
@ -287,4 +327,16 @@ class OauthProcess {
|
||||
);
|
||||
}
|
||||
|
||||
private function getScopesList(AuthorizationRequest $request): array {
|
||||
// TODO: replace with an arrow function in PHP 7.4
|
||||
return array_map(function(ScopeEntityInterface $scope): string {
|
||||
return $scope->getIdentifier();
|
||||
}, $request->getScopes());
|
||||
}
|
||||
|
||||
private function findOauthSession(Account $account, OauthClient $client): ?OauthSession {
|
||||
/** @noinspection PhpIncompatibleReturnTypeInspection */
|
||||
return $account->getOauthSessions()->andWhere(['client_id' => $client->id])->one();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,31 +9,30 @@ use api\tests\FunctionalTester;
|
||||
|
||||
class OauthSteps extends FunctionalTester {
|
||||
|
||||
public function getAuthCode(array $permissions = []): string {
|
||||
public function obtainAuthCode(array $permissions = []): string {
|
||||
$this->amAuthenticated();
|
||||
$route = new OauthRoute($this);
|
||||
$route->complete([
|
||||
$this->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => implode(',', $permissions),
|
||||
], ['accept' => true]);
|
||||
'scope' => implode(' ', $permissions),
|
||||
]), ['accept' => true]);
|
||||
$this->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
$response = json_decode($this->grabResponse(), true);
|
||||
preg_match('/code=([\w-]+)/', $response['redirectUri'], $matches);
|
||||
[$redirectUri] = $this->grabDataFromResponseByJsonPath('$.redirectUri');
|
||||
preg_match('/code=([\w-]+)/', $redirectUri, $matches);
|
||||
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
public function getAccessToken(array $permissions = []): string {
|
||||
$authCode = $this->getAuthCode($permissions);
|
||||
$authCode = $this->obtainAuthCode($permissions);
|
||||
$response = $this->issueToken($authCode);
|
||||
|
||||
return $response['access_token'];
|
||||
}
|
||||
|
||||
public function getRefreshToken(array $permissions = []): string {
|
||||
$authCode = $this->getAuthCode(array_merge([S::OFFLINE_ACCESS], $permissions));
|
||||
$authCode = $this->obtainAuthCode(array_merge([S::OFFLINE_ACCESS], $permissions));
|
||||
$response = $this->issueToken($authCode);
|
||||
|
||||
return $response['refresh_token'];
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\IdentityInfoRoute;
|
||||
use api\tests\functional\_steps\OauthSteps;
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
@ -1,113 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\oauth;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\functional\_steps\OauthSteps;
|
||||
use api\tests\FunctionalTester;
|
||||
|
||||
class AccessTokenCest {
|
||||
|
||||
/**
|
||||
* @var OauthRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(FunctionalTester $I) {
|
||||
$this->route = new OauthRoute($I);
|
||||
public function successfullyIssueToken(OauthSteps $I) {
|
||||
$I->wantTo('complete oauth flow and obtain access_token');
|
||||
$authCode = $I->obtainAuthCode();
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $authCode,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||
$I->cantSeeResponseJsonMatchesJsonPath('$.refresh_token');
|
||||
}
|
||||
|
||||
public function testIssueTokenWithWrongArgs(OauthSteps $I) {
|
||||
$I->wantTo('check behavior on on request without any credentials');
|
||||
$this->route->issueToken();
|
||||
public function successfullyIssueOfflineToken(OauthSteps $I) {
|
||||
$I->wantTo('complete oauth flow with offline_access scope and obtain access_token and refresh_token');
|
||||
$authCode = $I->obtainAuthCode(['offline_access']);
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $authCode,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.refresh_token');
|
||||
}
|
||||
|
||||
public function callEndpointWithByEmptyRequest(OauthSteps $I) {
|
||||
$I->wantTo('check behavior on on request without any params');
|
||||
$I->sendPOST('/api/oauth2/v1/token');
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_request',
|
||||
'message' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Check the "grant_type" parameter.',
|
||||
'error' => 'unsupported_grant_type',
|
||||
'message' => 'The authorization grant type is not supported by the authorization server.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function issueTokenByPassingInvalidAuthCode(OauthSteps $I) {
|
||||
$I->wantTo('check behavior on passing invalid auth code');
|
||||
$this->route->issueToken($this->buildParams(
|
||||
'wrong-auth-code',
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'http://ely.by'
|
||||
));
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => 'wrong-auth-code',
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_request',
|
||||
'message' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Check the "code" parameter.',
|
||||
]);
|
||||
}
|
||||
|
||||
$authCode = $I->getAuthCode();
|
||||
public function issueTokenByPassingInvalidRedirectUri(OauthSteps $I) {
|
||||
$I->wantTo('check behavior on passing invalid redirect_uri');
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$authCode,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'http://some-other.domain'
|
||||
));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$authCode = $I->obtainAuthCode();
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $authCode,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'redirect_uri' => 'http://some-other.domain',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_client',
|
||||
'message' => 'Client authentication failed.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testIssueToken(OauthSteps $I) {
|
||||
$authCode = $I->getAuthCode();
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$authCode,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'http://ely.by'
|
||||
));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||
$I->cantSeeResponseJsonMatchesJsonPath('$.refresh_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||
}
|
||||
|
||||
public function testIssueTokenWithRefreshToken(OauthSteps $I) {
|
||||
$authCode = $I->getAuthCode(['offline_access']);
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$authCode,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'http://ely.by'
|
||||
));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.refresh_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||
}
|
||||
|
||||
private function buildParams($code = null, $clientId = null, $clientSecret = null, $redirectUri = null) {
|
||||
$params = ['grant_type' => 'authorization_code'];
|
||||
if ($code !== null) {
|
||||
$params['code'] = $code;
|
||||
}
|
||||
|
||||
if ($clientId !== null) {
|
||||
$params['client_id'] = $clientId;
|
||||
}
|
||||
|
||||
if ($clientSecret !== null) {
|
||||
$params['client_secret'] = $clientSecret;
|
||||
}
|
||||
|
||||
if ($redirectUri !== null) {
|
||||
$params['redirect_uri'] = $redirectUri;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ class AuthCodeCest {
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => '',
|
||||
'parameter' => null,
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
@ -90,7 +90,7 @@ class AuthCodeCest {
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => '',
|
||||
'parameter' => null,
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
@ -114,7 +114,7 @@ class AuthCodeCest {
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => '',
|
||||
'parameter' => null,
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ namespace common\components\Redis;
|
||||
use InvalidArgumentException;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
class Key {
|
||||
|
||||
private $key;
|
||||
|
@ -5,6 +5,9 @@ use ArrayIterator;
|
||||
use IteratorAggregate;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
class Set extends Key implements IteratorAggregate {
|
||||
|
||||
public function add($value): self {
|
||||
|
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace common\db\mysql;
|
||||
|
||||
use SamIT\Yii2\MariaDb\QueryBuilder as MysqlQueryBuilder;
|
||||
use yii\db\ExpressionInterface;
|
||||
use yii\db\mysql\QueryBuilder as MysqlQueryBuilder;
|
||||
|
||||
class QueryBuilder extends MysqlQueryBuilder {
|
||||
|
||||
|
@ -1,12 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace common\db\mysql;
|
||||
|
||||
use yii\db\mysql\Schema as MysqlSchema;
|
||||
use SamIT\Yii2\MariaDb\JsonExpressionBuilder;
|
||||
use SamIT\Yii2\MariaDb\Schema as MysqlSchema;
|
||||
|
||||
class Schema extends MysqlSchema {
|
||||
|
||||
public function createQueryBuilder() {
|
||||
return new QueryBuilder($this->db);
|
||||
$result = new QueryBuilder($this->db);
|
||||
$result->setExpressionBuilders([
|
||||
'yii\db\JsonExpression' => JsonExpressionBuilder::class,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ class Account extends ActiveRecord {
|
||||
}
|
||||
|
||||
public function getOauthSessions(): ActiveQuery {
|
||||
return $this->hasMany(OauthSession::class, ['owner_id' => 'id'])->andWhere(['owner_type' => 'user']);
|
||||
return $this->hasMany(OauthSession::class, ['account_id' => 'id']);
|
||||
}
|
||||
|
||||
public function getOauthClients(): OauthClientQuery {
|
||||
|
@ -1,38 +1,32 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace common\models;
|
||||
|
||||
use common\components\Redis\Set;
|
||||
use Yii;
|
||||
use yii\base\NotSupportedException;
|
||||
use yii\behaviors\TimestampBehavior;
|
||||
use yii\db\ActiveQuery;
|
||||
use yii\db\ActiveRecord;
|
||||
|
||||
/**
|
||||
* Fields:
|
||||
* @property integer $id
|
||||
* @property string $owner_type contains one of the OauthOwnerType constants
|
||||
* @property string|null $owner_id
|
||||
* @property int $account_id
|
||||
* @property string $client_id
|
||||
* @property string $client_redirect_uri
|
||||
* @property int $legacy_id
|
||||
* @property array $scopes
|
||||
* @property integer $created_at
|
||||
*
|
||||
* Relations:
|
||||
* @property OauthClient $client
|
||||
* @property Account $account
|
||||
* @property Set $scopes
|
||||
*/
|
||||
class OauthSession extends ActiveRecord {
|
||||
|
||||
public static function tableName(): string {
|
||||
return '{{%oauth_sessions}}';
|
||||
return 'oauth_sessions';
|
||||
}
|
||||
|
||||
public static function find(): OauthSessionQuery {
|
||||
return new OauthSessionQuery(static::class);
|
||||
}
|
||||
|
||||
public function behaviors() {
|
||||
public function behaviors(): array {
|
||||
return [
|
||||
[
|
||||
'class' => TimestampBehavior::class,
|
||||
@ -49,39 +43,28 @@ class OauthSession extends ActiveRecord {
|
||||
return $this->hasOne(Account::class, ['id' => 'owner_id']);
|
||||
}
|
||||
|
||||
public function getScopes(): Set {
|
||||
return new Set(static::getDb()->getSchema()->getRawTableName(static::tableName()), $this->id, 'scopes');
|
||||
public function getScopes(): array {
|
||||
if (empty($this->scopes) && $this->legacy_id !== null) {
|
||||
return Yii::$app->redis->smembers($this->getLegacyRedisScopesKey());
|
||||
}
|
||||
|
||||
public function getAccessTokens() {
|
||||
throw new NotSupportedException('This method is possible, but not implemented');
|
||||
return (array)$this->scopes;
|
||||
}
|
||||
|
||||
public function beforeDelete(): bool {
|
||||
if (!$result = parent::beforeDelete()) {
|
||||
return $result;
|
||||
if (!parent::beforeDelete()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->clearScopes();
|
||||
$this->removeRefreshToken();
|
||||
if ($this->legacy_id !== null) {
|
||||
Yii::$app->redis->del($this->getLegacyRedisScopesKey());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function removeRefreshToken(): void {
|
||||
/** @var \api\components\OAuth2\Repositories\RefreshTokenStorage $refreshTokensStorage */
|
||||
// TODO: rework
|
||||
$refreshTokensStorage = Yii::$app->oauth->getRefreshTokenStorage();
|
||||
$refreshTokensSet = $refreshTokensStorage->sessionHash($this->id);
|
||||
foreach ($refreshTokensSet->members() as $refreshTokenId) {
|
||||
$refreshTokensStorage->delete($refreshTokensStorage->get($refreshTokenId));
|
||||
}
|
||||
|
||||
$refreshTokensSet->delete();
|
||||
}
|
||||
|
||||
public function clearScopes(): void {
|
||||
$this->getScopes()->delete();
|
||||
private function getLegacyRedisScopesKey(): string {
|
||||
return "oauth:sessions:{$this->legacy_id}:scopes";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace common\models;
|
||||
|
||||
use yii\db\ActiveQuery;
|
||||
|
||||
/**
|
||||
* @see \common\models\OauthSession
|
||||
*/
|
||||
class OauthSessionQuery extends ActiveQuery {
|
||||
|
||||
/**
|
||||
* The owner_id field in the oauth_sessions table has a string type.
|
||||
* If you try to search using an integer value, the MariaDB will not apply the index, which will cause
|
||||
* a huge rows scan.
|
||||
*
|
||||
* After examining the query builder logic in Yii2, we managed to find a solution to bring the value
|
||||
* that the builder will use to create a link to the string exactly before the construction
|
||||
* and restore the original value afterwards.
|
||||
*
|
||||
* @param $builder
|
||||
* @return ActiveQuery|\yii\db\Query
|
||||
*/
|
||||
public function prepare($builder) {
|
||||
$idHasBeenCastedToString = false;
|
||||
if ($this->primaryModel instanceof Account && $this->link === ['owner_id' => 'id']) {
|
||||
$this->primaryModel->id = (string)$this->primaryModel->id;
|
||||
$idHasBeenCastedToString = true;
|
||||
}
|
||||
|
||||
$query = parent::prepare($builder);
|
||||
|
||||
if ($idHasBeenCastedToString) {
|
||||
$this->primaryModel->id = (int)$this->primaryModel->id;
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
24
common/tests/fixtures/data/oauth-sessions.php
vendored
24
common/tests/fixtures/data/oauth-sessions.php
vendored
@ -1,35 +1,27 @@
|
||||
<?php
|
||||
return [
|
||||
'admin-test1' => [
|
||||
'id' => 1,
|
||||
'owner_type' => 'user',
|
||||
'owner_id' => 1,
|
||||
'account_id' => 1,
|
||||
'client_id' => 'test1',
|
||||
'client_redirect_uri' => 'http://test1.net/oauth',
|
||||
'scopes' => null,
|
||||
'created_at' => 1479944472,
|
||||
],
|
||||
'banned-account-session' => [
|
||||
'id' => 2,
|
||||
'owner_type' => 'user',
|
||||
'owner_id' => 10,
|
||||
'account_id' => 10,
|
||||
'client_id' => 'test1',
|
||||
'client_redirect_uri' => 'http://test1.net/oauth',
|
||||
'scopes' => null,
|
||||
'created_at' => 1481421663,
|
||||
],
|
||||
'deleted-client-session' => [
|
||||
'id' => 3,
|
||||
'owner_type' => 'user',
|
||||
'owner_id' => 1,
|
||||
'account_id' => 1,
|
||||
'client_id' => 'deleted-oauth-client-with-sessions',
|
||||
'client_redirect_uri' => 'http://not-exists-site.com/oauth/ely',
|
||||
'scopes' => null,
|
||||
'created_at' => 1519510065,
|
||||
],
|
||||
'actual-deleted-client-session' => [
|
||||
'id' => 4,
|
||||
'owner_type' => 'user',
|
||||
'owner_id' => 2,
|
||||
'account_id' => 2,
|
||||
'client_id' => 'deleted-oauth-client-with-sessions',
|
||||
'client_redirect_uri' => 'http://not-exists-site.com/oauth/ely',
|
||||
'scopes' => null,
|
||||
'created_at' => 1519511568,
|
||||
],
|
||||
];
|
||||
|
@ -24,6 +24,7 @@
|
||||
"nesbot/carbon": "^2.22",
|
||||
"paragonie/constant_time_encoding": "^2.0",
|
||||
"ramsey/uuid": "^3.5",
|
||||
"sam-it/yii2-mariadb": "^1.1",
|
||||
"spomky-labs/otphp": "^9.0.2",
|
||||
"webmozart/assert": "^1.2.0",
|
||||
"yiisoft/yii2": "~2.0.20",
|
||||
|
40
composer.lock
generated
40
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "35a16287a6dc45c16e0553022aa34f5b",
|
||||
"content-hash": "7ee1d380684b79ffabf92f115d7de4a8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
@ -1767,6 +1767,44 @@
|
||||
],
|
||||
"time": "2018-07-19T23:38:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sam-it/yii2-mariadb",
|
||||
"version": "v1.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/SAM-IT/yii2-mariadb.git",
|
||||
"reference": "9d0551dc05332e51c4546b070e0cb221217d75d6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/SAM-IT/yii2-mariadb/zipball/9d0551dc05332e51c4546b070e0cb221217d75d6",
|
||||
"reference": "9d0551dc05332e51c4546b070e0cb221217d75d6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.9",
|
||||
"phpunit/phpunit": "^7.2",
|
||||
"yiisoft/yii2-dev": "^2.0.15"
|
||||
},
|
||||
"type": "yii2-extension",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SamIT\\Yii2\\MariaDb\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sam Mousa",
|
||||
"email": "sam@mousa.nl"
|
||||
}
|
||||
],
|
||||
"description": "MariaDB Driver for Yii2",
|
||||
"time": "2019-04-23T13:27:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sentry/sentry",
|
||||
"version": "1.10.0",
|
||||
|
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use console\db\Migration;
|
||||
|
||||
class m190914_181236_rework_oauth_sessions_table extends Migration {
|
||||
|
||||
public function safeUp() {
|
||||
$this->delete('oauth_sessions', ['NOT', ['owner_type' => 'user']]);
|
||||
$this->dropColumn('oauth_sessions', 'owner_type');
|
||||
$this->dropColumn('oauth_sessions', 'client_redirect_uri');
|
||||
$this->execute('
|
||||
DELETE os1
|
||||
FROM oauth_sessions os1,
|
||||
oauth_sessions os2
|
||||
WHERE os1.id > os2.id
|
||||
AND os1.owner_id = os2.owner_id
|
||||
AND os1.client_id = os2.client_id
|
||||
');
|
||||
$this->dropIndex('owner_id', 'oauth_sessions');
|
||||
$this->renameColumn('oauth_sessions', 'owner_id', 'account_id');
|
||||
$this->alterColumn('oauth_sessions', 'account_id', $this->db->getTableSchema('accounts')->getColumn('id')->dbType . ' NOT NULL');
|
||||
$this->alterColumn('oauth_sessions', 'client_id', $this->db->getTableSchema('oauth_clients')->getColumn('id')->dbType . ' NOT NULL');
|
||||
// Change type to be able to remove primary key
|
||||
$this->alterColumn('oauth_sessions', 'id', $this->integer(11)->unsigned()->after('client_id'));
|
||||
$this->dropPrimaryKey('PRIMARY', 'oauth_sessions');
|
||||
// Change type again to make column nullable
|
||||
$this->alterColumn('oauth_sessions', 'id', $this->integer(11)->unsigned()->after('client_id'));
|
||||
$this->renameColumn('oauth_sessions', 'id', 'legacy_id');
|
||||
$this->addPrimaryKey('id', 'oauth_sessions', ['account_id', 'client_id']);
|
||||
$this->dropForeignKey('FK_oauth_session_to_client', 'oauth_sessions');
|
||||
$this->dropIndex('FK_oauth_session_to_client', 'oauth_sessions');
|
||||
$this->addForeignKey('FK_oauth_session_to_account', 'oauth_sessions', 'account_id', 'accounts', 'id', 'CASCADE', 'CASCADE');
|
||||
$this->addForeignKey('FK_oauth_session_to_oauth_client', 'oauth_sessions', 'client_id', 'oauth_clients', 'id', 'CASCADE', 'CASCADE');
|
||||
$this->addColumn('oauth_sessions', 'scopes', $this->json()->toString('scopes') . ' AFTER `legacy_id`');
|
||||
}
|
||||
|
||||
public function safeDown() {
|
||||
$this->dropColumn('oauth_sessions', 'scopes');
|
||||
$this->dropForeignKey('FK_oauth_session_to_oauth_client', 'oauth_sessions');
|
||||
$this->dropForeignKey('FK_oauth_session_to_account', 'oauth_sessions');
|
||||
$this->dropIndex('FK_oauth_session_to_oauth_client', 'oauth_sessions');
|
||||
$this->dropPrimaryKey('PRIMARY', 'oauth_sessions');
|
||||
$this->delete('oauth_sessions', ['legacy_id' => null]);
|
||||
$this->alterColumn('oauth_sessions', 'legacy_id', $this->integer(11)->unsigned()->notNull()->append('AUTO_INCREMENT PRIMARY KEY FIRST'));
|
||||
$this->renameColumn('oauth_sessions', 'legacy_id', 'id');
|
||||
$this->alterColumn('oauth_sessions', 'client_id', $this->db->getTableSchema('oauth_clients')->getColumn('id')->dbType);
|
||||
$this->alterColumn('oauth_sessions', 'account_id', $this->string());
|
||||
$this->renameColumn('oauth_sessions', 'account_id', 'owner_id');
|
||||
$this->createIndex('owner_id', 'oauth_sessions', 'owner_id');
|
||||
$this->addColumn('oauth_sessions', 'owner_type', $this->string()->notNull()->after('id'));
|
||||
$this->update('oauth_sessions', ['owner_type' => 'user']);
|
||||
$this->addColumn('oauth_sessions', 'client_redirect_uri', $this->string()->after('client_id'));
|
||||
$this->addForeignKey('FK_oauth_session_to_client', 'oauth_sessions', 'client_id', 'oauth_clients', 'id', 'CASCADE', 'CASCADE');
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user