├── phpcs.xml.dist ├── phpstan.neon ├── src ├── Scope │ ├── ScopeQuery.php │ ├── ScopeEntity.php │ └── ScopeRepository.php ├── Client │ ├── ClientQuery.php │ ├── ClientRepository.php │ └── ClientEntity.php ├── AuthCode │ ├── AuthCodeQuery.php │ ├── AuthCodeRepository.php │ └── AuthCodeEntity.php ├── AccessToken │ ├── AccessTokenQuery.php │ ├── AccessTokenRepository.php │ └── AccessTokenEntity.php ├── RefreshToken │ ├── RefreshTokenQuery.php │ ├── RefreshTokenRepository.php │ └── RefreshTokenEntity.php ├── AuthorizationRequestSerializer.php ├── TablePrefixSubscriber.php └── NetteOAuth2ServerDoctrineExtension.php ├── LICENSE ├── composer.json └── README.md /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | excludes_analyse: 3 | - %rootDir%/../../../tests/temp/* 4 | ignoreErrors: 5 | - "#Casting to string something that's already string#" 6 | includes: 7 | - vendor/phpstan/phpstan-nette/extension.neon 8 | - vendor/phpstan/phpstan-nette/rules.neon 9 | - vendor/phpstan/phpstan-phpunit/extension.neon 10 | - vendor/phpstan/phpstan-phpunit/rules.neon 11 | - vendor/phpstan/phpstan-strict-rules/rules.neon 12 | -------------------------------------------------------------------------------- /src/Scope/ScopeQuery.php: -------------------------------------------------------------------------------- 1 | filters[] = function (QueryBuilder $queryBuilder) use ($identifier): void { 21 | $queryBuilder->andWhere('s.identifier = :identifier')->setParameter('identifier', $identifier); 22 | }; 23 | return $this; 24 | } 25 | 26 | protected function doCreateQuery(Queryable $repository): QueryBuilder 27 | { 28 | $queryBuilder = $repository->createQueryBuilder()->select('s')->from(ScopeEntity::class, 's'); 29 | foreach ($this->filters as $filter) { 30 | $filter($queryBuilder); 31 | } 32 | return $queryBuilder; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Client/ClientQuery.php: -------------------------------------------------------------------------------- 1 | filters[] = function (QueryBuilder $queryBuilder) use ($identifier): void { 21 | $queryBuilder->andWhere('c.identifier = :identifier')->setParameter('identifier', $identifier); 22 | }; 23 | return $this; 24 | } 25 | 26 | protected function doCreateQuery(Queryable $repository): QueryBuilder 27 | { 28 | $queryBuilder = $repository->createQueryBuilder()->select('c')->from(ClientEntity::class, 'c'); 29 | foreach ($this->filters as $filter) { 30 | $filter($queryBuilder); 31 | } 32 | return $queryBuilder; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/AuthCode/AuthCodeQuery.php: -------------------------------------------------------------------------------- 1 | filters[] = function (QueryBuilder $queryBuilder) use ($identifier): void { 21 | $queryBuilder->andWhere('ac.identifier = :identifier')->setParameter('identifier', $identifier); 22 | }; 23 | return $this; 24 | } 25 | 26 | protected function doCreateQuery(Queryable $repository): QueryBuilder 27 | { 28 | $queryBuilder = $repository->createQueryBuilder()->select('ac')->from(AuthCodeEntity::class, 'ac'); 29 | foreach ($this->filters as $filter) { 30 | $filter($queryBuilder); 31 | } 32 | return $queryBuilder; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/AccessToken/AccessTokenQuery.php: -------------------------------------------------------------------------------- 1 | filters[] = function (QueryBuilder $queryBuilder) use ($identifier): void { 21 | $queryBuilder->andWhere('at.identifier = :identifier')->setParameter('identifier', $identifier); 22 | }; 23 | return $this; 24 | } 25 | 26 | protected function doCreateQuery(Queryable $repository): QueryBuilder 27 | { 28 | $queryBuilder = $repository->createQueryBuilder()->select('at')->from(AccessTokenEntity::class, 'at'); 29 | foreach ($this->filters as $filter) { 30 | $filter($queryBuilder); 31 | } 32 | return $queryBuilder; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/RefreshToken/RefreshTokenQuery.php: -------------------------------------------------------------------------------- 1 | filters[] = function (QueryBuilder $queryBuilder) use ($identifier): void { 21 | $queryBuilder->andWhere('rt.identifier = :identifier')->setParameter('identifier', $identifier); 22 | }; 23 | return $this; 24 | } 25 | 26 | protected function doCreateQuery(Queryable $repository): QueryBuilder 27 | { 28 | $queryBuilder = $repository->createQueryBuilder()->select('rt')->from(RefreshTokenEntity::class, 'rt'); 29 | foreach ($this->filters as $filter) { 30 | $filter($queryBuilder); 31 | } 32 | return $queryBuilder; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Scope/ScopeEntity.php: -------------------------------------------------------------------------------- 1 | id; 33 | } 34 | 35 | public function __clone() 36 | { 37 | $this->id = null; 38 | } 39 | 40 | public function getIdentifier(): string 41 | { 42 | return $this->identifier; 43 | } 44 | 45 | public function setIdentifier(string $identifier): void 46 | { 47 | $this->identifier = $identifier; 48 | } 49 | 50 | public function jsonSerialize(): string 51 | { 52 | return $this->getIdentifier(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Lukáš Unger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Client/ClientRepository.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 25 | $this->secretValidator = $secretValidator ?: function ($expected, $actual) { 26 | return hash_equals($expected, $actual); 27 | }; 28 | } 29 | 30 | /** 31 | * @param string $clientIdentifier 32 | * @param string|null $grantType 33 | * @param string|null $clientSecret 34 | * @param bool $mustValidateSecret 35 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 36 | */ 37 | public function getClientEntity($clientIdentifier, $grantType = null, $clientSecret = null, $mustValidateSecret = true): ?ClientEntity 38 | { 39 | /** @var ClientEntity|null $clientEntity */ 40 | $clientEntity = $this->registry->getManager()->getRepository(ClientEntity::class)->fetchOne($this->createQuery()->byIdentifier($clientIdentifier)); 41 | return $clientEntity !== null 42 | && $mustValidateSecret 43 | && $clientEntity->getSecret() !== null 44 | && !call_user_func($this->secretValidator, $clientEntity->getSecret(), $clientSecret) 45 | ? null 46 | : $clientEntity; 47 | } 48 | 49 | protected function createQuery(): ClientQuery 50 | { 51 | return new ClientQuery(); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lookyman/nette-oauth2-server-doctrine", 3 | "description": "Integration of The League of Extraordinary Packages' OAuth 2.0 Server into Nette Framework - Kdyby/Doctrine storage implementation", 4 | "keywords": ["Nette", "League", "OAuth 2.0", "Kdyby", "Doctrine"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Lukáš Unger", 9 | "email": "looky.msc@gmail.com", 10 | "homepage": "https://lookyman.net" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.1", 15 | "kdyby/doctrine": "^3.3", 16 | "kdyby/events": "^3.1, >=3.1.2", 17 | "league/oauth2-server": "^7.0", 18 | "nette/application": "^2.4, >=2.4.9", 19 | "nette/di": "^2.4, >=2.4.10", 20 | "lookyman/nette-oauth2-server": "^3.0", 21 | "nette/caching": "^2.5, >=2.5.6" 22 | }, 23 | "require-dev": { 24 | "nette/bootstrap": "^2.4, >=2.4.5", 25 | "phpunit/phpunit": "^7.0", 26 | "phpstan/phpstan": "^0.9.2", 27 | "lookyman/coding-standard": "^0.1.0", 28 | "phpstan/phpstan-nette": "^0.9.0", 29 | "phpstan/phpstan-phpunit": "^0.9.4", 30 | "phpstan/phpstan-strict-rules": "^0.9.0", 31 | "jakub-onderka/php-parallel-lint": "^1.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Lookyman\\NetteOAuth2Server\\Storage\\Doctrine\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Lookyman\\NetteOAuth2Server\\Storage\\Doctrine\\Tests\\": "tests/" 41 | } 42 | }, 43 | "scripts": { 44 | "lint": "parallel-lint src tests", 45 | "cs": "phpcs --extensions=php --encoding=utf-8 -sp --ignore=*/tests/temp/* src tests", 46 | "tests": "phpunit --coverage-text", 47 | "stan": "phpstan analyse -c phpstan.neon -l 5 src tests", 48 | "check": ["@lint", "@cs", "@tests", "@stan"] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Scope/ScopeRepository.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 26 | $this->scopeFinalizer = $scopeFinalizer ?: function (array $scopes): array { 27 | return $scopes; 28 | }; 29 | } 30 | 31 | /** 32 | * @param string $identifier 33 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 34 | */ 35 | public function getScopeEntityByIdentifier($identifier): ?ScopeEntity 36 | { 37 | /** @var ScopeEntity $entity */ 38 | $entity = $this->registry->getManager()->getRepository(ScopeEntity::class)->fetchOne($this->createQuery()->byIdentifier($identifier)); 39 | return $entity; 40 | } 41 | 42 | /** 43 | * @param ScopeEntity[] $scopes 44 | * @param string $grantType 45 | * @param string|null $userIdentifier 46 | * @return ScopeEntity[] 47 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 48 | */ 49 | public function finalizeScopes( 50 | array $scopes, 51 | $grantType, 52 | ClientEntityInterface $clientEntity, 53 | $userIdentifier = null 54 | ): array { 55 | return call_user_func($this->scopeFinalizer, $scopes, $grantType, $clientEntity, $userIdentifier); 56 | } 57 | 58 | protected function createQuery(): ScopeQuery 59 | { 60 | return new ScopeQuery(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/AuthorizationRequestSerializer.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 22 | } 23 | 24 | public function serialize(AuthorizationRequest $authorizationRequest): string 25 | { 26 | $manager = $this->registry->getManager(); 27 | /** @var ClientEntity|null $client */ 28 | $client = $authorizationRequest->getClient(); 29 | if ($client !== null) { 30 | $manager->detach($authorizationRequest->getClient()); 31 | } 32 | foreach ($authorizationRequest->getScopes() as $scope) { 33 | $manager->detach($scope); 34 | } 35 | return serialize($authorizationRequest); 36 | } 37 | 38 | public function unserialize(string $data): AuthorizationRequest 39 | { 40 | $manager = $this->registry->getManager(); 41 | /** @var AuthorizationRequest $authorizationRequest */ 42 | $authorizationRequest = unserialize($data); 43 | /** @var ClientEntity|null $client */ 44 | $client = $authorizationRequest->getClient(); 45 | if ($client !== null) { 46 | /** @var ClientEntity $client */ 47 | $client = $manager->merge($client); 48 | $authorizationRequest->setClient($client); 49 | } 50 | $scopes = []; 51 | foreach ($authorizationRequest->getScopes() as $scope) { 52 | $scopes[] = $manager->merge($scope); 53 | } 54 | $authorizationRequest->setScopes($scopes); 55 | return $authorizationRequest; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/AuthCode/AuthCodeRepository.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 21 | } 22 | 23 | public function getNewAuthCode(): AuthCodeEntity 24 | { 25 | return new AuthCodeEntity(); 26 | } 27 | 28 | public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): void 29 | { 30 | if ($authCodeEntity instanceof AuthCodeEntity) { 31 | $manager = $this->registry->getManager(); 32 | $manager->persist($authCodeEntity); 33 | $manager->flush(); 34 | } 35 | } 36 | 37 | /** 38 | * @param string $codeId 39 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 40 | */ 41 | public function revokeAuthCode($codeId): void 42 | { 43 | $manager = $this->registry->getManager(); 44 | /** @var AuthCodeEntity|null $authCodeEntity */ 45 | $authCodeEntity = $manager->getRepository(AuthCodeEntity::class)->fetchOne($this->createQuery()->byIdentifier($codeId)); 46 | if ($authCodeEntity !== null) { 47 | $authCodeEntity->setRevoked(true); 48 | $manager->flush(); 49 | } 50 | } 51 | 52 | /** 53 | * @param string $codeId 54 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 55 | */ 56 | public function isAuthCodeRevoked($codeId): bool 57 | { 58 | /** @var AuthCodeEntity|null $authCodeEntity */ 59 | $authCodeEntity = $this->registry->getManager()->getRepository(AuthCodeEntity::class)->fetchOne($this->createQuery()->byIdentifier($codeId)); 60 | return $authCodeEntity !== null ? $authCodeEntity->isRevoked() : true; 61 | } 62 | 63 | protected function createQuery(): AuthCodeQuery 64 | { 65 | return new AuthCodeQuery(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/Client/ClientEntity.php: -------------------------------------------------------------------------------- 1 | id; 51 | } 52 | 53 | public function __clone() 54 | { 55 | $this->id = null; 56 | } 57 | 58 | public function getSecret(): ?string 59 | { 60 | return $this->secret; 61 | } 62 | 63 | /** 64 | * @param string|null $secret 65 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 66 | */ 67 | public function setSecret($secret): void 68 | { 69 | $this->secret = $secret; 70 | } 71 | 72 | public function getIdentifier(): string 73 | { 74 | return $this->identifier; 75 | } 76 | 77 | public function setIdentifier(string $identifier): void 78 | { 79 | $this->identifier = $identifier; 80 | } 81 | 82 | public function getName(): string 83 | { 84 | return $this->name; 85 | } 86 | 87 | public function setName(string $name): void 88 | { 89 | $this->name = $name; 90 | } 91 | 92 | public function getRedirectUri(): string 93 | { 94 | return $this->redirectUri; 95 | } 96 | 97 | public function setRedirectUri(string $uri): void 98 | { 99 | $this->redirectUri = $uri; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/RefreshToken/RefreshTokenRepository.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 21 | } 22 | 23 | public function getNewRefreshToken(): RefreshTokenEntity 24 | { 25 | return new RefreshTokenEntity(); 26 | } 27 | 28 | public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void 29 | { 30 | if ($refreshTokenEntity instanceof RefreshTokenEntity) { 31 | $manager = $this->registry->getManager(); 32 | $manager->persist($refreshTokenEntity); 33 | $manager->flush(); 34 | } 35 | } 36 | 37 | /** 38 | * @param string $tokenId 39 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 40 | */ 41 | public function revokeRefreshToken($tokenId): void 42 | { 43 | $manager = $this->registry->getManager(); 44 | /** @var RefreshTokenEntity|null $refreshTokenEntity */ 45 | $refreshTokenEntity = $manager->getRepository(RefreshTokenEntity::class)->fetchOne($this->createQuery()->byIdentifier($tokenId)); 46 | if ($refreshTokenEntity !== null) { 47 | $refreshTokenEntity->setRevoked(true); 48 | $manager->flush(); 49 | } 50 | } 51 | 52 | /** 53 | * @param string $tokenId 54 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 55 | */ 56 | public function isRefreshTokenRevoked($tokenId): bool 57 | { 58 | /** @var RefreshTokenEntity|null $refreshTokenEntity */ 59 | $refreshTokenEntity = $this->registry->getManager()->getRepository(RefreshTokenEntity::class)->fetchOne($this->createQuery()->byIdentifier($tokenId)); 60 | return $refreshTokenEntity !== null ? $refreshTokenEntity->isRevoked() : true; 61 | } 62 | 63 | protected function createQuery(): RefreshTokenQuery 64 | { 65 | return new RefreshTokenQuery(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/TablePrefixSubscriber.php: -------------------------------------------------------------------------------- 1 | prefix = $prefix; 38 | } 39 | 40 | public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void 41 | { 42 | /** @var ClassMetadata $metadata */ 43 | $metadata = $eventArgs->getClassMetadata(); 44 | if (in_array($metadata->getName(), self::ENTITIES, true)) { 45 | $metadata->setPrimaryTable([ 46 | 'name' => self::getPrefixedName($this->prefix, $metadata->getTableName()), 47 | ]); 48 | } 49 | foreach ($metadata->getAssociationMappings() as $name => $mapping) { 50 | if ($mapping['type'] === ClassMetadataInfo::MANY_TO_MANY 51 | && $mapping['isOwningSide'] 52 | && in_array($mapping['targetEntity'], self::ENTITIES, true) 53 | ) { 54 | $metadata->associationMappings[$name]['joinTable']['name'] = self::getPrefixedName($this->prefix, $mapping['joinTable']['name']); 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * @return string[] 61 | */ 62 | public function getSubscribedEvents(): array 63 | { 64 | return [Events::loadClassMetadata]; 65 | } 66 | 67 | protected static function getPrefixedName(string $prefix, string $name): string 68 | { 69 | return $prefix . $name; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/RefreshToken/RefreshTokenEntity.php: -------------------------------------------------------------------------------- 1 | id; 53 | } 54 | 55 | public function __clone() 56 | { 57 | $this->id = null; 58 | } 59 | 60 | public function isRevoked(): bool 61 | { 62 | return $this->revoked; 63 | } 64 | 65 | public function setRevoked(bool $revoked): void 66 | { 67 | $this->revoked = $revoked; 68 | } 69 | 70 | public function getIdentifier(): string 71 | { 72 | return $this->identifier; 73 | } 74 | 75 | /** 76 | * @param string $identifier 77 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 78 | */ 79 | public function setIdentifier($identifier): void 80 | { 81 | $this->identifier = $identifier; 82 | } 83 | 84 | public function getExpiryDateTime(): \DateTime 85 | { 86 | return $this->expiryDateTime; 87 | } 88 | 89 | public function setExpiryDateTime(\DateTime $dateTime): void 90 | { 91 | $this->expiryDateTime = $dateTime; 92 | } 93 | 94 | /** 95 | * @param AccessTokenEntity $accessToken 96 | */ 97 | public function setAccessToken(AccessTokenEntityInterface $accessToken): void 98 | { 99 | $this->accessToken = $accessToken; 100 | } 101 | 102 | public function getAccessToken(): AccessTokenEntityInterface 103 | { 104 | return $this->accessToken; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/AccessToken/AccessTokenRepository.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 23 | } 24 | 25 | /** 26 | * @param ScopeEntityInterface[] $scopes 27 | * @param string|null $userIdentifier 28 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 29 | */ 30 | public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null): AccessTokenEntity 31 | { 32 | $accessToken = new AccessTokenEntity(); 33 | $accessToken->setClient($clientEntity); 34 | foreach ($scopes as $scope) { 35 | $accessToken->addScope($scope); 36 | } 37 | $accessToken->setUserIdentifier($userIdentifier); 38 | return $accessToken; 39 | } 40 | 41 | public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void 42 | { 43 | if ($accessTokenEntity instanceof AccessTokenEntity) { 44 | $manager = $this->registry->getManager(); 45 | $manager->persist($accessTokenEntity); 46 | $manager->flush(); 47 | } 48 | } 49 | 50 | /** 51 | * @param string $tokenId 52 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 53 | */ 54 | public function revokeAccessToken($tokenId): void 55 | { 56 | $manager = $this->registry->getManager(); 57 | /** @var AccessTokenEntity|null $accessTokenEntity */ 58 | $accessTokenEntity = $manager->getRepository(AccessTokenEntity::class)->fetchOne($this->createQuery()->byIdentifier($tokenId)); 59 | if ($accessTokenEntity !== null) { 60 | $accessTokenEntity->setRevoked(true); 61 | $manager->flush(); 62 | } 63 | } 64 | 65 | /** 66 | * @param string $tokenId 67 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 68 | */ 69 | public function isAccessTokenRevoked($tokenId): bool 70 | { 71 | /** @var AccessTokenEntity|null $accessTokenEntity */ 72 | $accessTokenEntity = $this->registry->getManager()->getRepository(AccessTokenEntity::class)->fetchOne($this->createQuery()->byIdentifier($tokenId)); 73 | return $accessTokenEntity !== null ? $accessTokenEntity->isRevoked() : true; 74 | } 75 | 76 | protected function createQuery(): AccessTokenQuery 77 | { 78 | return new AccessTokenQuery(); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/AccessToken/AccessTokenEntity.php: -------------------------------------------------------------------------------- 1 | scopes = new ArrayCollection(); 74 | } 75 | 76 | public function getId(): ?int 77 | { 78 | return $this->id; 79 | } 80 | 81 | public function __clone() 82 | { 83 | $this->id = null; 84 | } 85 | 86 | public function isRevoked(): bool 87 | { 88 | return $this->revoked; 89 | } 90 | 91 | public function setRevoked(bool $revoked): void 92 | { 93 | $this->revoked = $revoked; 94 | } 95 | 96 | public function getClient(): ClientEntityInterface 97 | { 98 | return $this->client; 99 | } 100 | 101 | public function getExpiryDateTime(): \DateTime 102 | { 103 | return $this->expiryDateTime; 104 | } 105 | 106 | /** 107 | * @return int|string|null 108 | */ 109 | public function getUserIdentifier() 110 | { 111 | return $this->userIdentifier; 112 | } 113 | 114 | /** 115 | * @return ScopeEntityInterface[] 116 | */ 117 | public function getScopes(): array 118 | { 119 | return $this->scopes->toArray(); 120 | } 121 | 122 | public function getIdentifier(): string 123 | { 124 | return $this->identifier; 125 | } 126 | 127 | /** 128 | * @param string $identifier 129 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 130 | */ 131 | public function setIdentifier($identifier): void 132 | { 133 | $this->identifier = $identifier; 134 | } 135 | 136 | public function setExpiryDateTime(\DateTime $dateTime): void 137 | { 138 | $this->expiryDateTime = $dateTime; 139 | } 140 | 141 | /** 142 | * @param string $identifier 143 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 144 | */ 145 | public function setUserIdentifier($identifier): void 146 | { 147 | $this->userIdentifier = $identifier; 148 | } 149 | 150 | public function setClient(ClientEntityInterface $client): void 151 | { 152 | if ($client instanceof ClientEntity) { 153 | $this->client = $client; 154 | } 155 | } 156 | 157 | public function addScope(ScopeEntityInterface $scope): void 158 | { 159 | if ($scope instanceof ScopeEntity && !$this->scopes->contains($scope)) { 160 | $this->scopes->add($scope); 161 | } 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /src/AuthCode/AuthCodeEntity.php: -------------------------------------------------------------------------------- 1 | scopes = new ArrayCollection(); 77 | } 78 | 79 | public function getId(): ?int 80 | { 81 | return $this->id; 82 | } 83 | 84 | public function __clone() 85 | { 86 | $this->id = null; 87 | } 88 | 89 | public function isRevoked(): bool 90 | { 91 | return $this->revoked; 92 | } 93 | 94 | public function setRevoked(bool $revoked): void 95 | { 96 | $this->revoked = $revoked; 97 | } 98 | 99 | public function getRedirectUri(): string 100 | { 101 | return $this->redirectUri; 102 | } 103 | 104 | /** 105 | * @param string $uri 106 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 107 | */ 108 | public function setRedirectUri($uri): void 109 | { 110 | $this->redirectUri = (string) $uri; 111 | } 112 | 113 | public function getIdentifier(): string 114 | { 115 | return $this->identifier; 116 | } 117 | 118 | /** 119 | * @param string $identifier 120 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 121 | */ 122 | public function setIdentifier($identifier): void 123 | { 124 | $this->identifier = $identifier; 125 | } 126 | 127 | public function getExpiryDateTime(): \DateTime 128 | { 129 | return $this->expiryDateTime; 130 | } 131 | 132 | public function setExpiryDateTime(\DateTime $dateTime): void 133 | { 134 | $this->expiryDateTime = $dateTime; 135 | } 136 | 137 | /** 138 | * @param string $identifier 139 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint 140 | */ 141 | public function setUserIdentifier($identifier): void 142 | { 143 | $this->userIdentifier = $identifier; 144 | } 145 | 146 | /** 147 | * @return int|string|null 148 | */ 149 | public function getUserIdentifier() 150 | { 151 | return $this->userIdentifier; 152 | } 153 | 154 | public function getClient(): ClientEntityInterface 155 | { 156 | return $this->client; 157 | } 158 | 159 | public function setClient(ClientEntityInterface $client): void 160 | { 161 | if ($client instanceof ClientEntity) { 162 | $this->client = $client; 163 | } 164 | } 165 | 166 | public function addScope(ScopeEntityInterface $scope): void 167 | { 168 | if ($scope instanceof ScopeEntity && !$this->scopes->contains($scope)) { 169 | $this->scopes->add($scope); 170 | } 171 | } 172 | 173 | /** 174 | * @return ScopeEntityInterface[] 175 | */ 176 | public function getScopes(): array 177 | { 178 | return $this->scopes->toArray(); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/NetteOAuth2ServerDoctrineExtension.php: -------------------------------------------------------------------------------- 1 | [ 40 | 'authCode' => false, 41 | 'clientCredentials' => false, 42 | 'implicit' => false, 43 | 'password' => false, 44 | 'refreshToken' => false, 45 | ], 46 | 'privateKey' => null, 47 | 'publicKey' => null, 48 | 'encryptionKey' => null, 49 | 'approveDestination' => null, 50 | 'loginDestination' => null, 51 | 'tablePrefix' => TablePrefixSubscriber::DEFAULT_PREFIX, 52 | 'loginEventPriority' => 0, 53 | ]; 54 | 55 | public function loadConfiguration(): void 56 | { 57 | $builder = $this->getContainerBuilder(); 58 | $config = $this->validateConfig($this->defaults); 59 | 60 | // Table mapping & Login redirection 61 | Validators::assertField($config, 'tablePrefix', 'string'); 62 | $builder->addDefinition($this->prefix('tablePrefixSubscriber')) 63 | ->setClass(TablePrefixSubscriber::class, [$config['tablePrefix']]) 64 | ->addTag(EventsExtension::TAG_SUBSCRIBER); 65 | Validators::assertField($config, 'loginEventPriority', 'integer'); 66 | $builder->addDefinition($this->prefix('loginSubscriber')) 67 | ->setClass(LoginSubscriber::class, ['priority' => $config['loginEventPriority']]) 68 | ->addTag(EventsExtension::TAG_SUBSCRIBER); 69 | 70 | // Common repositories 71 | $builder->addDefinition($this->prefix('repository.accessToken')) 72 | ->setClass(AccessTokenRepository::class); 73 | $builder->addDefinition($this->prefix('repository.authCode')) 74 | ->setClass(AuthCodeRepository::class); 75 | $builder->addDefinition($this->prefix('repository.client')) 76 | ->setClass(ClientRepository::class); 77 | $builder->addDefinition($this->prefix('repository.refreshToken')) 78 | ->setClass(RefreshTokenRepository::class); 79 | $builder->addDefinition($this->prefix('repository.scope')) 80 | ->setClass(ScopeRepository::class); 81 | $builder->addDefinition($this->prefix('repository.user')) 82 | ->setClass(UserRepository::class); 83 | 84 | // Encryption keys 85 | Validators::assertField($config, 'encryptionKey', 'string'); 86 | Validators::assertField($config, 'publicKey', 'string'); 87 | Validators::assertField($config, 'privateKey', 'string|array'); 88 | if (is_array($config['privateKey'])) { 89 | Validators::assertField($config['privateKey'], 'keyPath', 'string'); 90 | Validators::assertField($config['privateKey'], 'passPhrase', 'string'); 91 | $privateKey = new Statement(CryptKey::class, [$config['privateKey']['keyPath'], $config['privateKey']['passPhrase']]); 92 | 93 | } else { 94 | $privateKey = $config['privateKey']; 95 | } 96 | 97 | // Authorization & resource server 98 | $authorizationServer = $builder->addDefinition($this->prefix('authorizationServer')) 99 | ->setClass(AuthorizationServer::class, [ 100 | 'privateKey' => $privateKey, 101 | 'encryptionKey' => $config['encryptionKey'], 102 | ]); 103 | $builder->addDefinition($this->prefix('resourceServer')) 104 | ->setClass(ResourceServer::class, [ 105 | 'publicKey' => $config['publicKey'], 106 | ]); 107 | 108 | // Grants 109 | Validators::assertField($config, 'grants', 'array'); 110 | foreach ($config['grants'] as $grant => $options) { 111 | Validators::assert($options, 'boolean|array'); 112 | if ($options === false) { 113 | continue; 114 | 115 | } else { 116 | $options = (array) $options; 117 | } 118 | 119 | $definition = $builder->addDefinition($this->prefix('grant.' . $grant)); 120 | 121 | switch ($grant) { 122 | case 'authCode': 123 | if (!array_key_exists('authCodeTtl', $options)) { 124 | $options['authCodeTtl'] = 'PT10M'; 125 | } 126 | $definition->setClass(AuthCodeGrant::class, ['authCodeTTL' => $this->createDateIntervalStatement($options['authCodeTtl'])]); 127 | if (array_key_exists('pkce', $options)) { 128 | Validators::assertField($options, 'pkce', 'boolean'); 129 | if ($options['pkce']) { 130 | $definition->addSetup('enableCodeExchangeProof'); 131 | } 132 | } 133 | break; 134 | case 'clientCredentials': 135 | $definition->setClass(ClientCredentialsGrant::class); 136 | break; 137 | case 'implicit': 138 | if (!array_key_exists('accessTokenTtl', $options)) { 139 | $options['accessTokenTtl'] = 'PT10M'; 140 | } 141 | $definition->setClass(ImplicitGrant::class, ['accessTokenTTL' => $this->createDateIntervalStatement($options['accessTokenTtl'])]); 142 | break; 143 | case 'password': 144 | $definition->setClass(PasswordGrant::class); 145 | break; 146 | case 'refreshToken': 147 | $definition->setClass(RefreshTokenGrant::class); 148 | break; 149 | default: 150 | throw new \InvalidArgumentException(sprintf('Unknown grant %s', $grant)); 151 | } 152 | 153 | $args = [$this->prefix('@grant.' . $grant)]; 154 | if (array_key_exists('ttl', $options)) { 155 | $args[] = $this->createDateIntervalStatement($options['ttl']); 156 | } 157 | $authorizationServer->addSetup('enableGrantType', $args); 158 | } 159 | 160 | // Presenter, Control factory, Serializer 161 | $builder->addDefinition($this->prefix('presenter')) 162 | ->setClass(OAuth2Presenter::class); 163 | $builder->addDefinition($this->prefix('approveControlFactory')) 164 | ->setClass(ApproveControlFactory::class); 165 | $builder->addDefinition($this->prefix('serializer')) 166 | ->setClass(IAuthorizationRequestSerializer::class) 167 | ->setFactory(AuthorizationRequestSerializer::class); 168 | 169 | // Redirect config 170 | Validators::assertField($config, 'approveDestination', 'string|null'); 171 | Validators::assertField($config, 'loginDestination', 'string|null'); 172 | $builder->addDefinition($this->prefix('redirectConfig')) 173 | ->setClass(RedirectConfig::class, [ 174 | 'approveDestination' => $config['approveDestination'], 175 | 'loginDestination' => $config['loginDestination'], 176 | ]); 177 | } 178 | 179 | public function beforeCompile(): void 180 | { 181 | $builder = $this->getContainerBuilder(); 182 | 183 | // Mapping 184 | $presenterFactory = $builder->getDefinition($builder->getByType(IPresenterFactory::class)); 185 | $presenterFactory->addSetup('if (!? instanceof \Nette\Application\PresenterFactory) { throw new \RuntimeException(\'Cannot set OAuth2Server mapping\'); } else { ?->setMapping(?); }', [ 186 | '@self', 187 | '@self', 188 | ['NetteOAuth2Server' => 'Lookyman\NetteOAuth2Server\UI\*Presenter'], 189 | ]); 190 | } 191 | 192 | /** 193 | * @return string[] 194 | */ 195 | public function getEntityMappings(): array 196 | { 197 | return ['Lookyman\NetteOAuth2Server\Storage\Doctrine' => __DIR__]; 198 | } 199 | 200 | private function createDateIntervalStatement(string $interval): Statement 201 | { 202 | new \DateInterval($interval); // throw early 203 | return new Statement(\DateInterval::class, [$interval]); 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lookyman/NetteOAuth2Server Doctrine 2 | =================================== 3 | 4 | Integration of [The League of Extraordinary Packages](https://thephpleague.com)' [OAuth 2.0 Server](https://oauth2.thephpleague.com) into [Nette Framework](https://nette.org) - [Kdyby/Doctrine](https://github.com/Kdyby/Doctrine) storage implementation. 5 | 6 | [![Build Status](https://travis-ci.org/lookyman/nette-oauth2-server-doctrine.svg?branch=master)](https://travis-ci.org/lookyman/nette-oauth2-server-doctrine) 7 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/lookyman/nette-oauth2-server-doctrine/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/lookyman/nette-oauth2-server-doctrine/?branch=master) 8 | [![Coverage Status](https://coveralls.io/repos/github/lookyman/nette-oauth2-server-doctrine/badge.svg?branch=master)](https://coveralls.io/github/lookyman/nette-oauth2-server-doctrine?branch=master) 9 | [![Downloads](https://img.shields.io/packagist/dt/lookyman/nette-oauth2-server-doctrine.svg)](https://packagist.org/packages/lookyman/nette-oauth2-server-doctrine) 10 | [![Latest stable](https://img.shields.io/packagist/v/lookyman/nette-oauth2-server-doctrine.svg)](https://packagist.org/packages/lookyman/nette-oauth2-server-doctrine) 11 | 12 | 13 | Installation 14 | ------------ 15 | 16 | ### The boring part 17 | 18 | Read [this](https://oauth2.thephpleague.com). All of it. Seriously, don't just skip it and then come back complaining that something doesn't work. 19 | 20 | Don't forget to install and configure [Kdyby/Doctrine](https://github.com/Kdyby/Doctrine) if you haven't already. 21 | 22 | ### Install using [Composer](https://getcomposer.org/): 23 | 24 | ```sh 25 | composer require lookyman/nette-oauth2-server-doctrine 26 | ``` 27 | 28 | ### Setup routes 29 | 30 | Depending on which grants you want to support, you will have to setup routes to either `access_token`, `authorize`, or both endpoints. 31 | 32 | - For grants other than `Implicit` setup the `access_token` endpoint route. 33 | - For `Authorization Code` or `Implicit` grants setup the `authorize` endpoint route. 34 | 35 | The endpoints are located at `NetteOAuth2Server:OAuth2:accessToken` and `NetteOAuth2Server:OAuth2:authorize` mapping respectively, and the setup should look something like this: 36 | 37 | ```php 38 | class RouterFactory 39 | { 40 | /** 41 | * @return IRouter 42 | */ 43 | public static function createRouter() 44 | { 45 | $router = new RouteList(); 46 | $router[] = new Route('oauth2/', 'NetteOAuth2Server:OAuth2:default'); 47 | // ... 48 | return $router; 49 | } 50 | } 51 | ``` 52 | 53 | You can then access those endpoints via `https://myapp.com/oauth2/access-token` and `https://myapp.com/oauth2/authorize` URLs respectively. 54 | 55 | ### Config 56 | 57 | ```yaml 58 | extensions: 59 | oauth2: Lookyman\NetteOAuth2Server\Storage\Doctrine\NetteOAuth2ServerDoctrineExtension 60 | 61 | oauth2: 62 | grants: 63 | authCode: on 64 | clientCredentials: on 65 | implicit: on 66 | password: on 67 | refreshToken: on 68 | privateKey: /path/to/private.key 69 | publicKey: /path/to/public.key 70 | encryptionKey: '32 base64encoded random bytes' 71 | approveDestination: :Approve: 72 | loginDestination: :Sign:in 73 | tablePrefix: nette_oauth2_server_ 74 | loginEventPriority: 0 75 | ``` 76 | 77 | The `grants` section contains grants that you want to enable. By default they are all disabled, so you just have to enter those you want to use. Each value doesn't have to just be a boolean. You can specify a token TTL like this: `[ttl: PT1H]`. Two of the grants also have additional settings. The `Authorization Code` grant has the `authCodeTtl` option, and the `Implicit` grant has the `accessTokenTtl` option. In each of these cases, the format for specifying the intervals follows the format described [here](https://secure.php.net/manual/en/dateinterval.construct.php). 78 | 79 | The `Authorization Code` grant also has another option to enable support for [RFC 7636](https://tools.ietf.org/html/rfc7636). You can turn it on by specifying `[pkce: on]`. 80 | 81 | Next, you're going to need a pair of private/public keys. If you didn't skip the first step you should know how to do that. If you did, now is the time. Go read it, come back when you have the keys, and enter the paths in the `privateKey` and `publicKey` options. If your private key is protected with a passphrase, specify it like this: `privateKey: [keyPath: /path/to/private.key, passPhrase: passphrase]`. 82 | 83 | Additionaly, you need to provide an encryption key. The easiest way to do that would be to run `php -r 'echo base64_encode(random_bytes(32));'` from the terminal and paste the result in the `encryptionKey` option. 84 | 85 | If you are using either `Authorization Code` or `Implicit` grants, you need to setup the redirect destinations. These should be normal strings you would use in `$presenter->redirect()` method. The `approveDestination` is discussed in detail below. The `loginDestination` should point to the presenter/action where your application has it's login form. Both paths should be absolute (with module). 86 | 87 | You can omit `approveDestination` and `loginDestination` options if you are not using `Authorization Code` or `Implicit` grants. 88 | 89 | The `tablePrefix` option lets you set the prefix for the generated tables. Default value is `nette_oauth2_server_`. 90 | 91 | Finally, when using `Authorization Code` or `Implicit` grants, the user is at some point redirected to the login page. This redirection is done by a subscriber listening for `Nette\Security\User::onLoggedIn` event, but if you already have some other subscribers listening on it, you might want to tweak the event priority. You can do it with the `loginEventPriority` option. 92 | 93 | ### Update database schema 94 | 95 | ```sh 96 | php www/index.php orm:schema-tool:update --force 97 | ``` 98 | 99 | You might want to use `--dump-sql` instead of `--force` and run the resulting SQL queries manually. But if your database schema was previously in sync with your mappings, this should be safe. 100 | 101 | It will generate 7 new tables in the database: 102 | 103 | - `nette_oauth2_server_access_token` 104 | - `nette_oauth2_server_access_token_scope` 105 | - `nette_oauth2_server_auth_code` 106 | - `nette_oauth2_server_auth_code_scope` 107 | - `nette_oauth2_server_client` 108 | - `nette_oauth2_server_refresh_token` 109 | - `nette_oauth2_server_scope` 110 | 111 | ### Implement trait 112 | 113 | The last part (and the most fun one) is to hook this all up into your application. For this there's a handy trait ready, so the process should be fairly smooth. Also, this step is only necessary if you want to use `Authorization Code` or `Implicit` grants, so if you don't, you are already done. Yay! 114 | 115 | You will have to create an approve presenter. Remember that `approveDestination` option in config? This is where it comes to play. The presenter should use the `Lookyman\NetteOAuth2Server\UI\ApprovePresenterTrait` trait and call `$this['approve']` in the action the `approveDestination` option leads to. It should look something like this: 116 | 117 | ```php 118 | class ApprovePresenter extends Presenter 119 | { 120 | use ApprovePresenterTrait; 121 | 122 | // ... 123 | 124 | public function actionDefault() 125 | { 126 | $this['approve']; 127 | } 128 | } 129 | ``` 130 | 131 | Of course, you don't have to create a new presenter just for this. If you want, use the trait in one of your existing ones. Just make sure to set the correct `approveDestination` in the config and initialize the component with `$this['approve']` in the action. 132 | 133 | Finally, that action needs a template. So create a [Latte](https://latte.nette.org) template file in the correct destination for the presenter's action to pick it up, and put a single line somewhere into it: 134 | 135 | ```latte 136 | {control approve} 137 | ``` 138 | 139 | As you can see, this whole process is highly configurable. This is done to let you have a complete control over your application, and just leave the hard work to the package. 140 | 141 | 142 | Finalizing the setup 143 | -------------------- 144 | 145 | This package does not provide ways to manage client applications, access tokens, or scopes. You have to implement those yourself. You can, however, use the entities and repositories provided by this package. 146 | 147 | - AccessToken 148 | - `Lookyman\NetteOAuth2Server\Storage\Doctrine\AccessToken\AccessTokenEntity` 149 | - `Lookyman\NetteOAuth2Server\Storage\Doctrine\AccessToken\AccessTokenRepository` 150 | - AuthCode 151 | - `Lookyman\NetteOAuth2Server\Storage\Doctrine\AuthCode\AuthCodeEntity` 152 | - `Lookyman\NetteOAuth2Server\Storage\Doctrine\AuthCode\AuthCodeRepository` 153 | - Client 154 | - `Lookyman\NetteOAuth2Server\Storage\Doctrine\Client\ClientEntity` 155 | - `Lookyman\NetteOAuth2Server\Storage\Doctrine\Client\ClientRepository` 156 | - RefreshToken 157 | - `Lookyman\NetteOAuth2Server\Storage\Doctrine\RefreshToken\RefreshTokenEntity` 158 | - `Lookyman\NetteOAuth2Server\Storage\Doctrine\RefreshToken\RefreshTokenRepository` 159 | - Scope 160 | - `Lookyman\NetteOAuth2Server\Storage\Doctrine\Scope\ScopeEntity` 161 | - `Lookyman\NetteOAuth2Server\Storage\Doctrine\Scope\ScopeRepository` 162 | 163 | At minimum, you should create a way to register the client applications. Unless of course you just want do it manually in the database. 164 | 165 | 166 | Protecting resources 167 | -------------------- 168 | 169 | This package provides an abstract `Lookyman\NetteOAuth2Server\UI\ResourcePresenter` that you can use to protect your resources. It's `checkRequirements()` method validates the access token and fires an `onAuthorized` event with the modified `Psr\Http\Message\ServerRequestInterface` object. The following attributes will be set on it in case of successful validation: 170 | 171 | - `oauth_access_token_id` - the access token identifier, 172 | - `oauth_client_id` - the client identifier, 173 | - `oauth_user_id` - the user identifier represented by the access token, 174 | - `oauth_scopes` - an array of string scope identifiers. 175 | 176 | 177 | Advanced usage 178 | -------------- 179 | 180 | ### Custom approve template 181 | 182 | The template of the approve component is [Bootstrap](https://getbootstrap.com) ready, but can be changed using some trait magic: 183 | 184 | ```php 185 | class ApprovePresenter extends Presenter 186 | { 187 | use ApprovePresenterTrait { 188 | createComponentApprove as ___createComponentApprove; 189 | } 190 | 191 | // ... 192 | 193 | /** 194 | * @return ApproveControl 195 | */ 196 | protected function createComponentApprove() 197 | { 198 | $control = $this->___createComponentApprove(); 199 | $control->setTemplateFile(__DIR__ . '/path/to/template.latte'); 200 | return $control; 201 | } 202 | } 203 | ``` 204 | 205 | The template gets passed a single variable `$authorizationRequest` with a `League\OAuth2\Server\RequestTypes\AuthorizationRequest` object inside containing information about the request being approved. 206 | 207 | ### Custom grants 208 | 209 | Custom grants have to implement `League\OAuth2\Server\Grant\GrantTypeInterface`. Enable them in your `config.neon` like this: 210 | 211 | ```yaml 212 | services: 213 | - MyCustomGrant 214 | oauth2.authorizationServer: 215 | setup: 216 | - enableGrantType(@MyCustomGrant) 217 | ``` 218 | 219 | ### Logging 220 | 221 | This package supports standard [PSR-3](http://www.php-fig.org/psr/psr-3) logging. If you have a compliant logger registered as a service, the easiest way to enable it is via `config.neon`: 222 | 223 | ```yaml 224 | decorator: 225 | Psr\Log\LoggerAwareInterface: 226 | setup: 227 | - setLogger 228 | ``` 229 | 230 | ### Client secret validation 231 | 232 | By default, the `Lookyman\NetteOAuth2Server\Storage\Doctrine\Client\ClientRepository` uses a simple `hash_equals` function to validate the client secret. This means that it expects the secrets in the database to be stored in plaintext, which might not be the best of ideas for obvious reasons. It is therefore **STRONGLY** recommended that you store the secrets hashed (for example with `password_hash()`), and implement your custom secret validator: 233 | 234 | ```php 235 | class SecretValidator 236 | { 237 | public function __invoke($expected, $actual) 238 | { 239 | return password_verify($actual, $expected); 240 | } 241 | } 242 | ``` 243 | 244 | Then register it in the config: 245 | 246 | ```yaml 247 | services: 248 | - SecretValidator 249 | oauth2.repository.client: 250 | arguments: [secretValidator: @SecretValidator] 251 | ``` 252 | 253 | ### User credentials validation 254 | 255 | `Lookyman\NetteOAuth2Server\User\UserRepository` validates user credentials by trying to log the user in. However, if your login process is somehow modified, this can easily fail in unexpected ways. In that case you might need to reimplement the credentials validator. Just get the correct user ID the way your application does it, and return `Lookyman\NetteOAuth2Server\User\UserEntity` (or `null` in case of bad credentials). 256 | 257 | ```php 258 | class CredentialsValidator 259 | { 260 | public function __invoke($username, $password, $grantType, ClientEntityInterface $clientEntity) 261 | { 262 | // get the user ID from your application, and 263 | return new UserEntity($userId); 264 | } 265 | } 266 | ``` 267 | 268 | Then register it in the config: 269 | 270 | ```yaml 271 | services: 272 | - CredentialsValidator 273 | oauth2.repository.user: 274 | arguments: [credentialsValidator: @CredentialsValidator] 275 | ``` 276 | 277 | ### Modifying scopes 278 | 279 | Just before an access token is issued, you can modify the requested scopes. By default the token is issued with exactly the same scopes that were requested, but you can change that with a custom finalizer: 280 | 281 | ```php 282 | class ScopeFinalizer 283 | { 284 | public function __invoke(array $scopes, $grantType, ClientEntityInterface $clientEntity, $userIdentifier) 285 | { 286 | return $scopes; // this is the default behavior 287 | } 288 | } 289 | ``` 290 | 291 | Then register it in the config: 292 | 293 | ```yaml 294 | services: 295 | - ScopeFinalizer 296 | oauth2.repository.scope: 297 | arguments: [scopeFinalizer: @ScopeFinalizer] 298 | ``` 299 | --------------------------------------------------------------------------------