├── 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 | [](https://travis-ci.org/lookyman/nette-oauth2-server-doctrine)
7 | [](https://scrutinizer-ci.com/g/lookyman/nette-oauth2-server-doctrine/?branch=master)
8 | [](https://coveralls.io/github/lookyman/nette-oauth2-server-doctrine?branch=master)
9 | [](https://packagist.org/packages/lookyman/nette-oauth2-server-doctrine)
10 | [](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 |
--------------------------------------------------------------------------------