Реализована логика oAuth авторизации приложений, добавлен Redis, удалены лишние тесты, пофикшены старые.

This commit is contained in:
ErickSkrauch
2016-02-14 20:50:10 +03:00
parent 59addfac07
commit f5f93ddef1
52 changed files with 1752 additions and 317 deletions

View File

@@ -0,0 +1,63 @@
<?php
namespace common\components\oauth;
use common\components\oauth\Storage\Redis\AuthCodeStorage;
use common\components\oauth\Storage\Yii2\AccessTokenStorage;
use common\components\oauth\Storage\Yii2\ClientStorage;
use common\components\oauth\Storage\Yii2\ScopeStorage;
use common\components\oauth\Storage\Yii2\SessionStorage;
use League\OAuth2\Server\AuthorizationServer;
use yii\base\InvalidConfigException;
/**
* @property AuthorizationServer $authServer
*/
class Component extends \yii\base\Component {
/**
* @var AuthorizationServer
*/
private $_authServer;
/**
* @var string[]
*/
public $grantTypes = [];
/**
* @var array grant type => class
*/
public $grantMap = [
'authorization_code' => 'League\OAuth2\Server\Grant\AuthCodeGrant',
'client_credentials' => 'League\OAuth2\Server\Grant\ClientCredentialsGrant',
'password' => 'League\OAuth2\Server\Grant\PasswordGrant',
'refresh_token' => 'League\OAuth2\Server\Grant\RefreshTokenGrant'
];
public function getAuthServer() {
if ($this->_authServer === null) {
$authServer = new AuthorizationServer();
$authServer
->setAccessTokenStorage(new AccessTokenStorage())
->setClientStorage(new ClientStorage())
->setScopeStorage(new ScopeStorage())
->setSessionStorage(new SessionStorage())
->setAuthCodeStorage(new AuthCodeStorage())
->setScopeDelimiter(',');
$this->_authServer = $authServer;
foreach ($this->grantTypes as $grantType) {
if (!array_key_exists($grantType, $this->grantMap)) {
throw new InvalidConfigException('Invalid grant type');
}
$grant = new $this->grantMap[$grantType]();
$this->_authServer->addGrantType($grant);
}
}
return $this->_authServer;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace common\components\oauth\Entity;
use League\OAuth2\Server\Entity\EntityTrait;
use League\OAuth2\Server\Entity\SessionEntity;
class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity {
use EntityTrait;
protected $sessionId;
public function getSessionId() {
return $this->sessionId;
}
/**
* @inheritdoc
* @return static
*/
public function setSession(SessionEntity $session) {
parent::setSession($session);
$this->sessionId = $session->getId();
return $this;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace common\components\oauth\Entity;
use League\OAuth2\Server\Entity\EntityTrait;
use League\OAuth2\Server\Entity\SessionEntity;
class AuthCodeEntity extends \League\OAuth2\Server\Entity\AuthCodeEntity {
use EntityTrait;
protected $sessionId;
public function getSessionId() {
return $this->sessionId;
}
/**
* @inheritdoc
* @return static
*/
public function setSession(SessionEntity $session) {
parent::setSession($session);
$this->sessionId = $session->getId();
return $this;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace common\components\oauth\Entity;
use League\OAuth2\Server\Entity\ClientEntity;
use League\OAuth2\Server\Entity\EntityTrait;
class SessionEntity extends \League\OAuth2\Server\Entity\SessionEntity {
use EntityTrait;
protected $clientId;
public function getClientId() {
return $this->clientId;
}
/**
* @inheritdoc
* @return static
*/
public function associateClient(ClientEntity $client) {
parent::associateClient($client);
$this->clientId = $client->getId();
return $this;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace common\components\oauth\Exception;
use League\OAuth2\Server\Exception\OAuthException;
class AcceptRequiredException extends OAuthException {
public $httpStatusCode = 401;
/**
* {@inheritdoc}
*/
public $errorType = 'accept_required';
/**
* {@inheritdoc}
*/
public function __construct() {
parent::__construct('Client must accept authentication request.');
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace common\components\oauth\Exception;
class AccessDeniedException extends \League\OAuth2\Server\Exception\AccessDeniedException {
public function __construct($redirectUri = null) {
parent::__construct();
$this->redirectUri = $redirectUri;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace common\components\oauth\Storage\Redis;
use common\components\oauth\Entity\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;
class AuthCodeStorage extends AbstractStorage implements AuthCodeInterface {
public $dataTable = 'oauth_auth_codes';
public $ttl = 3600; // 1h
/**
* @inheritdoc
*/
public function get($code) {
$result = (new Key($this->dataTable, $code))->getValue();
if (!$result) {
return null;
}
if ($result['expire_time'] < time()) {
return null;
}
return (new AuthCodeEntity($this->server))->hydrate([
'id' => $result['id'],
'redirectUri' => $result['client_redirect_uri'],
'expireTime' => $result['expire_time'],
'sessionId' => $result['sessionId'],
]);
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $sessionId, $redirectUri) {
$payload = [
'id' => $token,
'expire_time' => $expireTime,
'session_id' => $sessionId,
'client_redirect_uri' => $redirectUri,
];
(new Key($this->dataTable, $token))->setValue($payload)->expire($this->ttl);
}
/**
* @inheritdoc
*/
public function getScopes(OriginalAuthCodeEntity $token) {
$result = (new Set($this->dataTable, $token->getId(), 'scopes'));
$response = [];
foreach ($result as $scope) {
// TODO: нужно проверить все выданные скоупы на их существование
$response[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
return $response;
}
/**
* @inheritdoc
*/
public function associateScope(OriginalAuthCodeEntity $token, ScopeEntity $scope) {
(new Set($this->dataTable, $token->getId(), 'scopes'))->add($scope->getId())->expire($this->ttl);
}
/**
* @inheritdoc
*/
public function delete(OriginalAuthCodeEntity $token) {
// Удаляем ключ
(new Set($this->dataTable, $token->getId()))->delete();
// Удаляем список скоупов для ключа
(new Set($this->dataTable, $token->getId(), 'scopes'))->delete();
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Fahmiardi\OAuth2\Server\Storage\Redis;
use common\components\redis\Key;
use League\OAuth2\Server\Entity\RefreshTokenEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\RefreshTokenInterface;
class RefreshTokenStorage extends AbstractStorage implements RefreshTokenInterface {
public $dataTable = 'oauth_refresh_tokens';
/**
* @inheritdoc
*/
public function get($token) {
$result = (new Key($this->dataTable, $token))->getValue();
if (!$result) {
return null;
}
return (new RefreshTokenEntity($this->server))
->setId($result['id'])
->setExpireTime($result['expire_time'])
->setAccessTokenId($result['access_token_id']);
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $accessToken) {
$payload = [
'id' => $token,
'expire_time' => $expireTime,
'access_token_id' => $accessToken,
];
(new Key($this->dataTable, $token))->setValue($payload);
}
/**
* @inheritdoc
*/
public function delete(RefreshTokenEntity $token) {
(new Key($this->dataTable, $token->getId()))->delete();
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace common\components\oauth\Storage\Yii2;
use common\components\oauth\Entity\AccessTokenEntity;
use common\models\OauthAccessToken;
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\AccessTokenInterface;
use yii\db\Exception;
class AccessTokenStorage extends AbstractStorage implements AccessTokenInterface {
private $cache = [];
/**
* @param string $token
* @return OauthAccessToken|null
*/
private function getTokenModel($token) {
if (isset($this->cache[$token])) {
$this->cache[$token] = OauthAccessToken::findOne($token);
}
return $this->cache[$token];
}
/**
* @inheritdoc
*/
public function get($token) {
$model = $this->getTokenModel($token);
if ($model === null) {
return null;
}
return (new AccessTokenEntity($this->server))->hydrate([
'id' => $model->access_token,
'expireTime' => $model->expire_time,
'sessionId' => $model->session_id,
]);
}
/**
* @inheritdoc
*/
public function getScopes(OriginalAccessTokenEntity $token) {
$entities = [];
foreach($this->getTokenModel($token->getId())->getScopes() as $scope) {
$entities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
return $entities;
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $sessionId) {
$model = new OauthAccessToken([
'access_token' => $token,
'expire_time' => $expireTime,
'session_id' => $sessionId,
]);
if (!$model->save()) {
throw new Exception('Cannot save ' . OauthAccessToken::class . ' model.');
}
}
/**
* @inheritdoc
*/
public function associateScope(OriginalAccessTokenEntity $token, ScopeEntity $scope) {
$this->getTokenModel($token->getId())->getScopes()->add($scope->getId());
}
/**
* @inheritdoc
*/
public function delete(OriginalAccessTokenEntity $token) {
$this->getTokenModel($token->getId())->delete();
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace common\components\oauth\Storage\Yii2;
use common\components\oauth\Entity\SessionEntity;
use common\models\OauthClient;
use League\OAuth2\Server\Entity\ClientEntity;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\ClientInterface;
class ClientStorage extends AbstractStorage implements ClientInterface {
/**
* @inheritdoc
*/
public function get($clientId, $clientSecret = null, $redirectUri = null, $grantType = null) {
$query = OauthClient::find()
->select(['id', 'name', 'secret'])
->where([OauthClient::tableName() . '.id' => $clientId]);
if ($clientSecret !== null) {
$query->andWhere(['secret' => $clientSecret]);
}
if ($redirectUri !== null) {
$query
->addSelect(['redirect_uri'])
->andWhere(['redirect_uri' => $redirectUri]);
}
$model = $query->asArray()->one();
if ($model === null) {
return null;
}
$entity = new ClientEntity($this->server);
$entity->hydrate([
'id' => $model['id'],
'name' => $model['name'],
'secret' => $model['secret'],
]);
if (isset($model['redirect_uri'])) {
$entity->hydrate([
'redirectUri' => $model['redirect_uri'],
]);
}
return $entity;
}
/**
* @inheritdoc
*/
public function getBySession(OriginalSessionEntity $session) {
if (!$session instanceof SessionEntity) {
throw new \ErrorException('This module assumes that $session typeof ' . SessionEntity::class);
}
$model = OauthClient::find()
->select(['id', 'name'])
->andWhere(['id' => $session->getClientId()])
->asArray()
->one();
if ($model === null) {
return null;
}
return (new ClientEntity($this->server))->hydrate($model);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace common\components\oauth\Storage\Yii2;
use common\models\OauthScope;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\ScopeInterface;
class ScopeStorage extends AbstractStorage implements ScopeInterface {
/**
* @inheritdoc
*/
public function get($scope, $grantType = null, $clientId = null) {
$row = OauthScope::find()->andWhere(['id' => $scope])->asArray()->one();
if ($row === null) {
return null;
}
$entity = new ScopeEntity($this->server);
$entity->hydrate($row);
return $entity;
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace common\components\oauth\Storage\Yii2;
use common\components\oauth\Entity\AuthCodeEntity;
use common\components\oauth\Entity\SessionEntity;
use common\models\OauthSession;
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\ActiveQuery;
use yii\db\Exception;
class SessionStorage extends AbstractStorage implements SessionInterface {
private $cache = [];
/**
* @param string $sessionId
* @return OauthSession|null
*/
private function getSessionModel($sessionId) {
if (!isset($this->cache[$sessionId])) {
$this->cache[$sessionId] = OauthSession::findOne($sessionId);
}
return $this->cache[$sessionId];
}
private function hydrateEntity($sessionModel) {
if (!$sessionModel instanceof OauthSession) {
return null;
}
return (new SessionEntity($this->server))->hydrate([
'id' => $sessionModel->id,
'client_id' => $sessionModel->client_id,
])->setOwner($sessionModel->owner_type, $sessionModel->owner_id);
}
/**
* @param string $sessionId
* @return SessionEntity|null
*/
public function getSession($sessionId) {
return $this->hydrateEntity($this->getSessionModel($sessionId));
}
/**
* @inheritdoc
*/
public function getByAccessToken(OriginalAccessTokenEntity $accessToken) {
/** @var OauthSession|null $model */
$model = OauthSession::find()->innerJoinWith([
'accessTokens' => function(ActiveQuery $query) use ($accessToken) {
$query->andWhere(['access_token' => $accessToken->getId()]);
},
])->one();
return $this->hydrateEntity($model);
}
/**
* @inheritdoc
*/
public function getByAuthCode(OriginalAuthCodeEntity $authCode) {
if (!$authCode instanceof AuthCodeEntity) {
throw new \ErrorException('This module assumes that $authCode typeof ' . AuthCodeEntity::class);
}
return $this->getSession($authCode->getSessionId());
}
/**
* {@inheritdoc}
*/
public function getScopes(OriginalSessionEntity $session) {
$result = [];
foreach ($this->getSessionModel($session->getId())->getScopes() as $scope) {
// TODO: нужно проверить все выданные скоупы на их существование
$result[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
return $result;
}
/**
* @inheritdoc
*/
public function create($ownerType, $ownerId, $clientId, $clientRedirectUri = null) {
$sessionId = OauthSession::find()
->select('id')
->andWhere([
'client_id' => $clientId,
'owner_type' => $ownerType,
'owner_id' => $ownerId,
])->scalar();
if ($sessionId === false) {
$model = new OauthSession([
'client_id' => $clientId,
'owner_type' => $ownerType,
'owner_id' => $ownerId,
]);
if (!$model->save()) {
throw new Exception('Cannot save ' . OauthSession::class . ' model.');
}
$sessionId = $model->id;
}
return $sessionId;
}
/**
* @inheritdoc
*/
public function associateScope(OriginalSessionEntity $session, ScopeEntity $scope) {
$this->getSessionModel($session->getId())->getScopes()->add($scope->getId());
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace common\components\redis;
use InvalidArgumentException;
use Yii;
class Key {
protected $key;
/**
* @return \yii\redis\Connection
*/
public function getRedis() {
return Yii::$app->get('redis');
}
public function getKey() {
return $this->key;
}
public function getValue() {
return $this->getRedis()->get(json_decode($this->key));
}
public function setValue($value) {
$this->getRedis()->set($this->key, json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
return $this;
}
public function delete() {
$this->getRedis()->executeCommand('DEL', [$this->key]);
return $this;
}
public function expire($ttl) {
$this->getRedis()->executeCommand('EXPIRE', [$this->key, $ttl]);
return $this;
}
private function buildKey(array $parts) {
$keyParts = [];
foreach($parts as $part) {
$keyParts[] = str_replace('_', ':', $part);
}
return implode(':', $keyParts);
}
public function __construct(...$key) {
if (empty($key)) {
throw new InvalidArgumentException('You must specify at least one key.');
}
$this->key = $this->buildKey($key);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace common\components\redis;
use IteratorAggregate;
use Yii;
class Set extends Key implements IteratorAggregate {
/**
* @return \yii\redis\Connection
*/
public static function getDb() {
return Yii::$app->get('redis');
}
public function add($value) {
$this->getDb()->executeCommand('SADD', [$this->key, $value]);
return $this;
}
public function remove($value) {
$this->getDb()->executeCommand('SREM', [$this->key, $value]);
return $this;
}
public function members() {
return $this->getDb()->executeCommand('SMEMBERS', [$this->key]);
}
public function getValue() {
return $this->members();
}
public function exists($value) {
return !!$this->getDb()->executeCommand('SISMEMBER', [$this->key, $value]);
}
public function diff(array $sets) {
return $this->getDb()->executeCommand('SDIFF', [$this->key, implode(' ', $sets)]);
}
/**
* @inheritdoc
*/
public function getIterator() {
return new \ArrayIterator($this->members());
}
}