Mezzio Example: Doctrine Entities and Repositories


Overview

This post brings several pieces of supporting libraries together.

Entities to Support OAuth2 Requirements

We’ll start by defining a minimum domain model via PHP8 Attributes and Doctrine’s new attribute mapping driver. In the short-term, we only need to model what’s necessary for our application’s authentication layer using OAuth2. For now, we’re creating a minimal set of objects to comply with the needs of our supporting libraries' interfaces. We need a minimum of 6 entities and their corresponding repositories:

  • User
  • OAuthClient
  • OAuthAuthCode
  • OAuthAccessToken
  • OAuthRefreshToken
  • OAuthScope

Mezzio includes an implementation of Alex Bilbie’s wonderful, spec-compliant OAuth2 Server as an adapter to the Mezzio authentication abstraction middleware component. Both the server library and the Mezzio authentication component provide various interfaces for which we’ll need to provide concrete implementations. We’ll do that while building our domain model with Entities and Repositories.

Psalm Changes Required for Error Level 1

Entity Changes

TLDR; All errors required suppression notation to pass. Check this diff for the details: https://github.com/marcguyer/mezzio-doctrine-oauth2-example/commit/5640b45bb942d577af0f4e3606f30bc91b6105c5

Repository Changes

https://github.com/marcguyer/mezzio-doctrine-oauth2-example/commit/df7e770f32137ec1c26bad02eb971421058f5a63

Entities

An Entity is a persist-able PHP data object with an identity. Generally speaking, an entity represents a database table as a native PHP object with properties that correspond with the table columns.

We’ll create a set of entities fulfilling the minimum requirements of both Mezzio Authentication and the corresponding OAuth2 library.

User Entity

A User represents an unique individual person in our domain model. The User entity will need to implement both the OAuth2 Server library user interface and Mezzio Authentication component’s user interface:

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Entities;

interface UserEntityInterface
{
    /**
     * Return the user's identifier.
     *
     * @return mixed
     */
    public function getIdentifier();
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

/**
 * @see       https://github.com/mezzio/mezzio-authentication for the canonical source repository
 * @copyright https://github.com/mezzio/mezzio-authentication/blob/master/COPYRIGHT.md
 * @license   https://github.com/mezzio/mezzio-authentication/blob/master/LICENSE.md New BSD License
 */

declare(strict_types=1);

namespace Mezzio\Authentication;

interface UserInterface
{
    /**
     * Get the unique user identity (id, username, email address or ...)
     */
    public function getIdentity(): string;

    /**
     * Get all user roles
     *
     * @psalm-return iterable<int|string, string>
     */
    public function getRoles(): iterable;

    /**
     * Get a detail $name if present, $default otherwise
     *
     * @param null|mixed $default
     * @return mixed
     */
    public function getDetail(string $name, $default = null);

    /**
     * Get all the details, if any
     *
     * @psalm-return array<string, mixed>
     */
    public function getDetails(): array;
}

Implementation

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<?php
declare(strict_types=1);

namespace Data\Entity;

use Doctrine\ORM\Mapping as ORM;
use Mezzio\Authentication\UserInterface as MezzioUserInterface;
use League\OAuth2\Server\Entities\UserEntityInterface as OAuth2UserInterface;

#[ORM\Table(name: "users")]
#[ORM\Entity]
class User implements MezzioUserInterface, OAuth2UserInterface
{
    #[ORM\Column(name: "id", type: "integer", options: ["unsigned" => true])]
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: "IDENTITY")]
    private int $id;

    #[ORM\Column(type: "string", length: 255)]
    private string $email;

    #[ORM\Column(type: "string", length: 255)]
    private string $password;

    #[ORM\Column(name: "active", type: "boolean", options: ["default" => 0])]
    private bool $isActive = false;

    private array $roles = [];

    private array $details = [];

    public function __construct()
    {
        $this->id = 0;
        $this->email = '';
        $this->password = '';
    }

    public function setId(int $id): self
    {
        $this->id = $id;

        return $this;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function setEmail(string $email): self
    {
        $this->email = $email;
        return $this;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setPassword(string $password): self
    {
        $this->password = $password;
        return $this;
    }

    public function getPassword(): string
    {
        return $this->password;
    }

    public function setIsActive(bool $isActive = true): self
    {
        $this->isActive = $isActive;

        return $this;
    }

    public function isActive(): bool
    {
        return $this->isActive;
    }

    public function getIdentifier(): string
    {
        return $this->getEmail();
    }

    public function getIdentity(): string
    {
        return $this->getEmail();
    }

    public function setRoles(array $roles): self
    {
        $this->roles = $roles;

        return $this;
    }

    public function getRoles(): array
    {
        return $this->roles;
    }

    public function getDetail(string $name, $default = null)
    {
        return $this->details[$name] ?? $default;
    }

    public function setDetails(array $details): self
    {
        $this->details = $details;

        return $this;
    }

    /** @psalm-suppress MixedReturnTypeCoercion */
    public function getDetails(): array
    {
        /** @psalm-suppress MixedReturnTypeCoercion */
        return $this->details;
    }
}

OAuthClient Entity

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Entities;

interface ClientEntityInterface
{
    /**
     * Get the client's identifier.
     *
     * @return string
     */
    public function getIdentifier();

    /**
     * Get the client's name.
     *
     * @return string
     */
    public function getName();

    /**
     * Returns the registered redirect URI (as a string).
     *
     * Alternatively return an indexed array of redirect URIs.
     *
     * @return string|string[]
     */
    public function getRedirectUri();

    /**
     * Returns true if the client is confidential.
     *
     * @return bool
     */
    public function isConfidential();
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

/**
 * @see       https://github.com/mezzio/mezzio-authentication for the canonical source repository
 * @copyright https://github.com/mezzio/mezzio-authentication/blob/master/COPYRIGHT.md
 * @license   https://github.com/mezzio/mezzio-authentication/blob/master/LICENSE.md New BSD License
 */

declare(strict_types=1);

namespace Mezzio\Authentication;

interface UserInterface
{
    /**
     * Get the unique user identity (id, username, email address or ...)
     */
    public function getIdentity(): string;

    /**
     * Get all user roles
     *
     * @psalm-return iterable<int|string, string>
     */
    public function getRoles(): iterable;

    /**
     * Get a detail $name if present, $default otherwise
     *
     * @param null|mixed $default
     * @return mixed
     */
    public function getDetail(string $name, $default = null);

    /**
     * Get all the details, if any
     *
     * @psalm-return array<string, mixed>
     */
    public function getDetails(): array;
}

Implementation

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<?php
declare(strict_types=1);

namespace Data\Entity;

use Doctrine\ORM\Mapping as ORM;
use League\OAuth2\Server\Entities\ClientEntityInterface as OAuthClientInterface;
use Mezzio\Authentication\UserInterface as MezzioUserInterface;

#[ORM\Table(name: "oauth_clients")]
#[ORM\Entity]
class OAuthClient implements MezzioUserInterface, OAuthClientInterface
{
    #[ORM\Column(name: "id", type: "integer", options: ["unsigned" => true])]
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: "IDENTITY")]
    private int $id;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(name: "user_id", referencedColumnName: "user_id", nullable: true)]
    private ?User $user = null;

    #[ORM\Column(length: 40)]
    private string $name;

    #[ORM\Column(length: 100, nullable: true)]
    private ?string $secret;

    #[ORM\Column(length: 255)]
    private string $redirect;

    #[ORM\Column(type: "boolean", options: ["default" => 0])]
    private bool $isRevoked = false;

    #[ORM\Column(type: "boolean", options: ["default" => 0])]
    private bool $isConfidential = false;

    private array $roles = [];
    private array $details = [];

    public function __construct()
    {
        $this->id = 0;
        $this->name = '';
        $this->secret = null;
        $this->redirect = '';
    }

    public function setId(int $id): self
    {
        $this->id = $id;

        return $this;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function setUser(?User $user = null): self
    {
        $this->user = $user;

        return $this;
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function getIdentity(): string
    {
        return $this->getName();
    }

    public function setRoles(array $roles): self
    {
        $this->roles = $roles;
        return $this;
    }

    public function getRoles(): array
    {
        return $this->roles;
    }

    public function setDetails(array $details): self
    {
        $this->details = $details;
        return $this;
    }

    /** @psalm-suppress MixedReturnTypeCoercion */
    public function getDetails(): array
    {
        return $this->details;
    }

    public function getDetail(string $name, $default = null)
    {
        return $this->details[$name] ?? $default;
    }

    public function getIdentifier()
    {
        return $this->getName();
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setSecret(?string $secret = null): self
    {
        $this->secret = $secret;

        return $this;
    }

    public function getSecret(): ?string
    {
        return $this->secret;
    }

    public function setRedirect(string $redirect): self
    {
        $this->redirect = $redirect;

        return $this;
    }

    public function getRedirect(): string
    {
        return $this->redirect;
    }

    public function getRedirectUri(): ?string
    {
        return $this->getRedirect();
    }

    public function setIsRevoked(bool $isRevoked): self
    {
        $this->isRevoked = $isRevoked;

        return $this;
    }

    public function getIsRevoked(): bool
    {
        return $this->isRevoked;
    }

    public function setIsConfidential(bool $isConfidential): self
    {
        $this->isConfidential = $isConfidential;

        return $this;
    }

    public function getIsConfidential(): bool
    {
        return $this->isConfidential;
    }

    public function isConfidential(): bool
    {
        return $this->getIsConfidential();
    }
}

OAuthAuthCode Entity

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Entities;

interface AuthCodeEntityInterface extends TokenInterface
{
    /**
     * @return string|null
     */
    public function getRedirectUri();

    /**
     * @param string $uri
     */
    public function setRedirectUri($uri);
}

Implementation

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<?php

declare(strict_types=1);

namespace Data\Entity;

use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use League\OAuth2\Server\Entities as OAuth;
use DateTimeImmutable;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;

#[ORM\Table(name: "oauth_auth_codes")]
#[ORM\Entity]
class OAuthAuthCode implements OAuth\AuthCodeEntityInterface
{
    use OAuth\Traits\AuthCodeTrait;

    #[ORM\Column(name: "id", type: "integer", options: ["unsigned" => true])]
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: "IDENTITY")]
    private int $id;

    #[ORM\ManyToOne(targetEntity: OAuthClient::class)]
    #[ORM\JoinColumn(name: "client_id", referencedColumnName: "id")]
    private OAuth\ClientEntityInterface $client;

    #[ORM\Column(type: "boolean", options: ["default" => 0])]
    private bool $isRevoked = false;

    #[ORM\ManyToMany(targetEntity: OAuthScope::class, inversedBy: "authCodes", indexBy: "id")]
    #[ORM\JoinTable(name: "oauth_auth_code_scopes")]
    #[ORM\JoinColumn(name: "auth_code_id", referencedColumnName: "id")]
    #[ORM\InverseJoinColumn(name: "scope_id", referencedColumnName: "id")]
    protected Collection $scopes;

    #[ORM\Column(type: "datetime_immutable", nullable: true)]
    private DateTimeImmutable $expiresDatetime;

    public function __construct()
    {
        $this->id = 0;
        $this->expiresDatetime = new DateTimeImmutable();
        $this->scopes = new ArrayCollection();
    }

    public function setId(int $id): self
    {
        $this->id = $id;

        return $this;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function setClient(OAuth\ClientEntityInterface $client): self
    {
        $this->client = $client;

        return $this;
    }

    public function getClient(): OAuth\ClientEntityInterface
    {
        return $this->client;
    }

    public function getIdentifier(): ?int
    {
        return $this->getId();
    }

    public function setIdentifier(mixed $identifier): self
    {
        /** @psalm-suppress MixedArgument */
        $this->setId($identifier);

        return $this;
    }

    /**
     * @return static
     * @psalm-suppress MissingParamType
     */
    public function setUserIdentifier($identifier)
    {
        return $this;
    }

    public function getUserIdentifier()
    {
        /** @var OAuthClient $client */
        $client = $this->getClient();

        if (null === $user = $client->getUser()) {
            return null;
        }

        return $user->getIdentifier();
    }

    public function setIsRevoked(bool $isRevoked): self
    {
        $this->isRevoked = $isRevoked;

        return $this;
    }

    public function getIsRevoked(): bool
    {
        return $this->isRevoked;
    }

    public function addScope(OAuth\ScopeEntityInterface $scope): self
    {
        if ($this->scopes->contains($scope)) {
            return $this;
        }

        $this->scopes->add($scope);

        return $this;
    }

    public function removeScope(OAuthScope $scope): self
    {
        $this->scopes->removeElement($scope);

        return $this;
    }

    /**
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnTypeCoercion
     */
    public function getScopes(?Criteria $criteria = null): array
    {
        if ($criteria === null) {
            /** @psalm-suppress MixedReturnStatement */
            return $this->scopes->toArray();
        }

        /**
         * @psalm-suppress UndefinedInterfaceMethod
         * @psalm-suppress MixedReturnStatement
         * @psalm-suppress MixedMethodCall
         */
        return $this->scopes->matching($criteria)->toArray();
    }

    public function setExpiresDatetime(DateTimeImmutable $expiresDatetime): self
    {
        $this->expiresDatetime = $expiresDatetime;

        return $this;
    }

    public function getExpiresDatetime(): DateTimeImmutable
    {
        return $this->expiresDatetime;
    }

    public function getExpiryDateTime(): DateTimeImmutable
    {
        return $this->getExpiresDatetime();
    }

    public function setExpiryDateTime(DateTimeImmutable $dateTime): self
    {
        return $this->setExpiresDatetime($dateTime);
    }
}


OAuthAccessToken Entity

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Entities;

use League\OAuth2\Server\CryptKey;

interface AccessTokenEntityInterface extends TokenInterface
{
    /**
     * Set a private key used to encrypt the access token.
     */
    public function setPrivateKey(CryptKey $privateKey);

    /**
     * Generate a string representation of the access token.
     */
    public function __toString();
}

Implementation

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
<?php
declare(strict_types=1);

namespace Data\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Entities as OAuth;

#[ORM\Table(name: "oauth_access_tokens")]
#[ORM\Entity]
class OAuthAccessToken implements OAuth\AccessTokenEntityInterface
{
    #[ORM\Column(name: "id", type: "integer", options: ["unsigned" => true])]
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: "IDENTITY")]
    private int $id;

    #[ORM\ManyToOne(targetEntity: OAuthClient::class)]
    #[ORM\JoinColumn(name: "client_id", referencedColumnName: "id")]
    private OAuth\ClientEntityInterface $client;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(name: "user_id", referencedColumnName: "user_id", nullable: true)]
    private ?User $user;

    #[ORM\Column(length: 100)]
    private string $token;

    #[ORM\Column(type: "boolean", options: ["default" => 0])]
    private bool $isRevoked = false;

    #[ORM\ManyToMany(targetEntity: OAuthScope::class, inversedBy: "accessTokens", indexBy: "id")]
    #[ORM\JoinTable(name: "oauth_access_token_scopes")]
    #[ORM\JoinColumn(name: "access_token_id", referencedColumnName: "id")]
    #[ORM\InverseJoinColumn(name: "scope_id", referencedColumnName: "id")]
    protected Collection $scopes;

    #[ORM\Column(type: "datetime_immutable")]
    private \DateTimeImmutable $expiresDatetime;

    private ?CryptKey $privateKey = null;

    private ?Configuration $jwtConfiguration = null;

    public function __construct()
    {
        $this->id = 0;
        $this->expiresDatetime = new \DateTimeImmutable();
        $this->user = null;
        $this->token = '';
        $this->scopes = new ArrayCollection();
    }

    public function setId(int $id): self
    {
        $this->id = $id;

        return $this;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function setClient(OAuth\ClientEntityInterface $client): self
    {
        $this->client = $client;

        return $this;
    }

    public function getClient(): OAuth\ClientEntityInterface
    {
        return $this->client;
    }

    public function setUser(?User $user = null): self
    {
        $this->user = $user;

        return $this;
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function getToken(): string
    {
        return $this->token;
    }

    public function setToken(string $token): self
    {
        $this->token = $token;

        return $this;
    }

    public function setIsRevoked(bool $isRevoked): self
    {
        $this->isRevoked = $isRevoked;

        return $this;
    }

    public function getIsRevoked(): bool
    {
        return $this->isRevoked;
    }

    public function getIdentifier(): string
    {
        return $this->getToken();
    }

    /** @psalm-suppress MixedArgument */
    public function setIdentifier($identifier): self
    {
        return $this->setToken($identifier);
    }

    public function setUserIdentifier($identifier): self
    {
        // not sure what this is for
        // just making the interface happy for now
        return $this;
    }

    public function getUserIdentifier()
    {
        if (null === $user = $this->getUser()) {
            return '';
        }

        return $user->getIdentifier();
    }

    public function addScope(OAuth\ScopeEntityInterface $scope): self
    {
        if ($this->scopes->contains($scope)) {
            return $this;
        }

        $this->scopes->add($scope);

        return $this;
    }

    public function removeScope(OAuthScope $scope): self
    {
        $this->scopes->removeElement($scope);

        return $this;
    }

    /** @psalm-suppress MixedInferredReturnType */
    public function getScopes(?Criteria $criteria = null): array
    {
        if ($criteria === null) {
            /** @psalm-suppress MixedReturnTypeCoercion */
            return $this->scopes->toArray();
        }

        /**
         * @psalm-suppress UndefinedInterfaceMethod
         * @psalm-suppress MixedReturnStatement
         * @psalm-suppress MixedMethodCall
         */
        return $this->scopes->matching($criteria)->toArray();
    }

    public function setExpiresDatetime(\DateTimeImmutable $expiresDatetime): self
    {
        $this->expiresDatetime = $expiresDatetime;

        return $this;
    }

    public function getExpiresDatetime(): \DateTimeImmutable
    {
        return $this->expiresDatetime;
    }

    public function getExpiryDateTime(): \DateTimeImmutable
    {
        return $this->getExpiresDatetime();
    }

    public function setExpiryDateTime(\DateTimeImmutable $dateTime): self
    {
        return $this->setExpiresDatetime($dateTime);
    }

    /** Set key used to encrypt token */
    public function setPrivateKey(CryptKey $privateKey): self
    {
        $this->privateKey = $privateKey;

        return $this;
    }

    /** Initialise the JWT Configuration. */
    public function initJwtConfiguration(): self
    {
        if (null === $this->privateKey) {
            throw new \RuntimeException('Unable to init JWT without private key');
        }
        $this->jwtConfiguration = Configuration::forAsymmetricSigner(
            new Sha256(),
            InMemory::plainText(
                $this->privateKey->getKeyContents(),
                $this->privateKey->getPassPhrase() ?? ''
            ),
            InMemory::plainText('')
        );

        return $this;
    }

    /**
     * Generate a JWT from the access token
     */
    private function convertToJWT(): Token
    {
        $this->initJwtConfiguration();

        if (null === $this->jwtConfiguration) {
            throw new \RuntimeException('Unable to convert to JWT without config');
        }

        return $this->jwtConfiguration->builder()
            ->permittedFor($this->getClient()->getIdentifier())
            ->identifiedBy($this->getIdentifier())
            ->issuedAt(new \DateTimeImmutable())
            ->canOnlyBeUsedAfter(new \DateTimeImmutable())
            ->expiresAt($this->getExpiryDateTime())
            ->relatedTo((string) $this->getUserIdentifier())
            ->withClaim('scopes', $this->getScopes())
            ->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey());
    }

    /**
     * Generate a string representation from the access token
     */
    public function __toString(): string
    {
        return $this->convertToJWT()->toString();
    }
}

OAuthRefreshToken Entity

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Entities;

use DateTimeImmutable;

interface RefreshTokenEntityInterface
{
    /**
     * Get the token's identifier.
     *
     * @return string
     */
    public function getIdentifier();

    /**
     * Set the token's identifier.
     *
     * @param mixed $identifier
     */
    public function setIdentifier($identifier);

    /**
     * Get the token's expiry date time.
     *
     * @return DateTimeImmutable
     */
    public function getExpiryDateTime();

    /**
     * Set the date time when the token expires.
     *
     * @param DateTimeImmutable $dateTime
     */
    public function setExpiryDateTime(DateTimeImmutable $dateTime);

    /**
     * Set the access token that the refresh token was associated with.
     *
     * @param AccessTokenEntityInterface $accessToken
     */
    public function setAccessToken(AccessTokenEntityInterface $accessToken);

    /**
     * Get the access token that the refresh token was originally associated with.
     *
     * @return AccessTokenEntityInterface
     */
    public function getAccessToken();
}

Implementation

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
<?php

declare(strict_types=1);

namespace Data\Entity;

use Doctrine\ORM\Mapping as ORM;
use League\OAuth2\Server\Entities as OAuth;
use DateTimeImmutable;

#[ORM\Table(name: "oauth_refresh_tokens")]
#[ORM\Entity]
class OAuthRefreshToken implements OAuth\RefreshTokenEntityInterface
{
    #[ORM\Column(name: "id", type: "integer", options: ["unsigned" => true])]
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: "IDENTITY")]
    private int $id;

    #[ORM\ManyToOne(targetEntity: "OAuthAccessToken")]
    #[ORM\JoinColumn(name: "access_token_id", referencedColumnName: "id")]
    private OAuthAccessToken $accessToken;

    #[ORM\Column(type: "boolean", options: ["default" => 0])]
    private bool $isRevoked = false;

    #[ORM\Column(type: "datetime_immutable")]
    private DateTimeImmutable $expiresDatetime;

    public function __construct()
    {
        $this->id = 0;
        $this->expiresDatetime = new DateTimeImmutable();
    }

    public function setId(int $id): self
    {
        $this->id = $id;

        return $this;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getIdentifier(): string
    {
        return (string)$this->getId();
    }

    /**
     * @return static
     */
    public function setIdentifier(mixed $identifier)
    {
        $this->setId((int)$identifier);

        return $this;
    }

    public function setAccessToken(OAuth\AccessTokenEntityInterface $accessToken): self
    {
        /** @psalm-suppress PropertyTypeCoercion */
        $this->accessToken = $accessToken;

        return $this;
    }

    public function getAccessToken(): OAuthAccessToken
    {
        return $this->accessToken;
    }

    public function setIsRevoked(bool $isRevoked): self
    {
        $this->isRevoked = $isRevoked;

        return $this;
    }

    public function getIsRevoked(): bool
    {
        return $this->isRevoked;
    }

    public function setExpiresDatetime(DateTimeImmutable $expiresDatetime): self
    {
        $this->expiresDatetime = $expiresDatetime;

        return $this;
    }

    public function getExpiresDatetime(): DateTimeImmutable
    {
        return $this->expiresDatetime;
    }

    public function getExpiryDateTime(): DateTimeImmutable
    {
        return $this->getExpiresDatetime();
    }

    public function setExpiryDateTime(DateTimeImmutable $dateTime): self
    {
        return $this->setExpiresDatetime($dateTime);
    }
}

OAuthScope Entity

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Entities;

use JsonSerializable;

interface ScopeEntityInterface extends JsonSerializable
{
    /**
     * Get the scope's identifier.
     *
     * @return string
     */
    public function getIdentifier();
}

Implementation

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php
declare(strict_types=1);

namespace Data\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use League\OAuth2\Server\Entities as OAuth;

#[ORM\Table(name: "oauth_scopes")]
#[ORM\Entity]
class OAuthScope implements OAuth\ScopeEntityInterface
{
    use OAuth\Traits\ScopeTrait;

    #[ORM\Column(name: "id", type: "integer", options: ["unsigned" => true])]
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: "IDENTITY")]
    private int $id;

    #[ORM\Column(length: 255)]
    private string $scope;

    #[ORM\ManyToMany(targetEntity: OAuthAccessToken::class, mappedBy: "scopes")]
    protected Collection $accessTokens;

    #[ORM\ManyToMany(targetEntity: OAuthAuthCode::class, mappedBy: "scopes")]
    protected Collection $authCodes;

    public function __construct()
    {
        $this->id = 0;
        $this->scope = '';
        $this->accessTokens = new ArrayCollection();
        $this->authCodes = new ArrayCollection();
    }

    public function setId(int $id): self
    {
        $this->id = $id;

        return $this;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getIdentifier(): string
    {
        return $this->getScope();
    }

    public function setScope(string $scope): self
    {
        $this->scope = $scope;

        return $this;
    }

    public function getScope(): string
    {
        return $this->scope;
    }

    public function addAccessToken(OAuthAccessToken $accessToken): self
    {
        if ($this->accessTokens->contains($accessToken)) {
            return $this;
        }

        $this->accessTokens->add($accessToken);

        return $this;
    }

    public function removeAccessToken(OAuthAccessToken $accessToken): self
    {
        $this->accessTokens->removeElement($accessToken);

        return $this;
    }

    /** @psalm-suppress MixedInferredReturnType */
    public function getAccessTokens(?Criteria $criteria = null): Collection
    {
        if ($criteria === null) {
            return $this->accessTokens;
        }

        /**
         * @psalm-suppress UndefinedInterfaceMethod
         * @psalm-suppress MixedReturnStatement
         */
        return $this->accessTokens->matching($criteria);
    }

    public function addAuthCode(OAuthAuthCode $authCode): self
    {
        if ($this->authCodes->contains($authCode)) {
            return $this;
        }

        $this->authCodes->add($authCode);

        return $this;
    }

    public function removeAuthCode(OAuthAuthCode $authCode): self
    {
        $this->authCodes->removeElement($authCode);

        /** @psalm-suppress MixedInferredReturnType */
        return $this;
    }

    /** @psalm-suppress MixedInferredReturnType */
    public function getAuthCodes(?Criteria $criteria = null): Collection
    {
        if ($criteria === null) {
            /** @psalm-suppress MixedInferredReturnType */
            return $this->authCodes;
        }

        /**
         * @psalm-suppress UndefinedInterfaceMethod
         * @psalm-suppress MixedReturnStatement
         */
        return $this->authCodes->matching($criteria);
    }
}

Repositories

User Repository

The User repository will need to implement both the OAuth2 Server library user repository interface and Mezzio Authentication component’s user repository interface:

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Repositories;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;

interface UserRepositoryInterface extends RepositoryInterface
{
    /**
     * Get a user entity.
     *
     * @param string                $username
     * @param string                $password
     * @param string                $grantType    The grant type used
     * @param ClientEntityInterface $clientEntity
     *
     * @return UserEntityInterface|null
     */
    public function getUserEntityByUserCredentials(
        $username,
        $password,
        $grantType,
        ClientEntityInterface $clientEntity
    );
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

/**
 * @see       https://github.com/mezzio/mezzio-authentication for the canonical source repository
 * @copyright https://github.com/mezzio/mezzio-authentication/blob/master/COPYRIGHT.md
 * @license   https://github.com/mezzio/mezzio-authentication/blob/master/LICENSE.md New BSD License
 */

declare(strict_types=1);

namespace Mezzio\Authentication;

interface UserRepositoryInterface
{
    /**
     * Authenticate the identity (id, username, email ...) using a password
     * or using only a credential string (e.g. token based credential)
     * It returns the authenticated user or null.
     *
     * @param string $credential can be also a token
     */
    public function authenticate(string $credential, ?string $password = null): ?UserInterface;
}

Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php

declare(strict_types=1);

namespace Data\Repository;

use Data\Entity\User;
use Doctrine\ORM\EntityRepository;
use League\OAuth2\Server\Entities\UserEntityInterface;
use Mezzio\Authentication\UserInterface;
use Mezzio\Authentication\UserRepositoryInterface as MezzioAuthInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface as OAuthUserRepositoryInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;

class UserRepository extends EntityRepository implements MezzioAuthInterface, OAuthUserRepositoryInterface
{
    public function getUserEntityByUserCredentials(
        $username,
        $password,
        $grantType,
        ClientEntityInterface $clientEntity
    ): ?UserEntityInterface {

        /** @var ?User $user */
        $user = $this->findOneBy(['email' => $username]);

        if ($user === null) {
            return null;
        }

        if (password_verify($password, $user->getPassword())) {
            return $user;
        }

        return null;
    }

    public function authenticate(string $credential, string $password = null): ?UserInterface
    {
        /** @var ?User $user */
        $user = $this->findOneBy(['email' => $credential]);

        if ($user === null) {
            return null;
        }

        // If password is null, we're authenticating without password.
        // For example, when using Google or GitHub or Facebook as OAuth2 identity provider
        if ($password === null) {
            return $user;
        }

        if (password_verify($password, $user->getPassword())) {
            return $user;
        }

        return null;
    }
}

OAuthClient Repository

The Client repository will need to implement only the OAuth2 Server library client repository interface:

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Repositories;

use League\OAuth2\Server\Entities\ClientEntityInterface;

/**
 * Client storage interface.
 */
interface ClientRepositoryInterface extends RepositoryInterface
{
    /**
     * Get a client.
     *
     * @param string $clientIdentifier The client's identifier
     *
     * @return ClientEntityInterface|null
     */
    public function getClientEntity($clientIdentifier);

    /**
     * Validate a client's secret.
     *
     * @param string      $clientIdentifier The client's identifier
     * @param null|string $clientSecret     The client's secret (if sent)
     * @param null|string $grantType        The type of grant the client is using (if sent)
     *
     * @return bool
     */
    public function validateClient($clientIdentifier, $clientSecret, $grantType);
}

Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php

declare(strict_types=1);

namespace Data\Repository;

use Data\Entity\OAuthClient;
use Doctrine\ORM\EntityRepository;
use League\OAuth2\Server as OAuth;

class OAuthClientRepository extends EntityRepository implements OAuth\Repositories\ClientRepositoryInterface
{
    public function getClientEntity($clientIdentifier): ?OAuth\Entities\ClientEntityInterface
    {
        /** @var ?OAuthClient */
        return $this->findOneBy(['name' => $clientIdentifier]);
    }

    public function validateClient($clientIdentifier, $clientSecret, $grantType): bool
    {
        $client = $this->getClientEntity($clientIdentifier);

        if (null === $client) {
            return false;
        }

        if (!$this->isGranted($client, $grantType)) {
            return false;
        }

        /** @psalm-suppress UndefinedInterfaceMethod */
        if (empty($client->getSecret())) {
            return false;
        }

        /**
         * @psalm-suppress UndefinedInterfaceMethod
         * @psalm-suppress MixedArgument
         */
        return password_verify((string)$clientSecret, $client->getSecret());
    }

    /** @psalm-suppress UnusedParam */
    private function isGranted(OAuth\Entities\ClientEntityInterface $client, string $grantType = null): bool
    {
        return match ($grantType) {
            null, 'client_credentials', 'authorization_code', 'refresh_token' => true,
            default => false,
        };
    }

}

OAuthAuthCode Repository

The AuthCode repository will need to implement only the OAuth2 Server library authcode repository interface:

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Repositories;

use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;

/**
 * Auth code storage interface.
 */
interface AuthCodeRepositoryInterface extends RepositoryInterface
{
    /**
     * Creates a new AuthCode
     *
     * @return AuthCodeEntityInterface
     */
    public function getNewAuthCode();

    /**
     * Persists a new auth code to permanent storage.
     *
     * @param AuthCodeEntityInterface $authCodeEntity
     *
     * @throws UniqueTokenIdentifierConstraintViolationException
     */
    public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity);

    /**
     * Revoke an auth code.
     *
     * @param string $codeId
     */
    public function revokeAuthCode($codeId);

    /**
     * Check if the auth code has been revoked.
     *
     * @param string $codeId
     *
     * @return bool Return true if this code has been revoked
     */
    public function isAuthCodeRevoked($codeId);
}

Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php
declare(strict_types=1);

namespace Data\Repository;

use Data\Entity\OAuthAuthCode;
use Doctrine\ORM;
use League\OAuth2\Server as OAuth;

class OAuthAuthCodeRepository extends ORM\EntityRepository implements OAuth\Repositories\AuthCodeRepositoryInterface
{
    public function getNewAuthCode(): OAuthAuthCode
    {
        return new OAuthAuthCode();
    }

    /**
     * @throws ORM\OptimisticLockException
     * @throws ORM\ORMException
     *
     * @return void
     */
    public function persistNewAuthCode(OAuth\Entities\AuthCodeEntityInterface $authCodeEntity)
    {
        $this->getEntityManager()->persist($authCodeEntity);
        $this->getEntityManager()->flush();
    }

    /**
     * @throws ORM\OptimisticLockException
     * @throws ORM\ORMException
     *
     * @return void
     *
     * @param string $codeId
     */
    public function revokeAuthCode($codeId)
    {
        /** @var ?OAuthAuthCode $authCodeEntity */
        $authCodeEntity = $this->find($codeId);
        if (null === $authCodeEntity) {
            return;
        }
        $authCodeEntity->setIsRevoked(true);
        $this->getEntityManager()->persist($authCodeEntity);
        $this->getEntityManager()->flush();
    }

    public function isAuthCodeRevoked($codeId): bool
    {
        /** @var ?OAuthAuthCode $authCodeEntity */
        $authCodeEntity = $this->find($codeId);
        if (null === $authCodeEntity) {
            return true;
        }

        return $authCodeEntity->getIsRevoked();
    }
}

OAuthAccessToken Repository

The AccessToken repository will need to implement only the OAuth2 Server library accesstoken repository interface:

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Repositories;

use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;

/**
 * Access token interface.
 */
interface AccessTokenRepositoryInterface extends RepositoryInterface
{
    /**
     * Create a new access token
     *
     * @param ClientEntityInterface  $clientEntity
     * @param ScopeEntityInterface[] $scopes
     * @param mixed                  $userIdentifier
     *
     * @return AccessTokenEntityInterface
     */
    public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null);

    /**
     * Persists a new access token to permanent storage.
     *
     * @param AccessTokenEntityInterface $accessTokenEntity
     *
     * @throws UniqueTokenIdentifierConstraintViolationException
     */
    public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity);

    /**
     * Revoke an access token.
     *
     * @param string $tokenId
     */
    public function revokeAccessToken($tokenId);

    /**
     * Check if the access token has been revoked.
     *
     * @param string $tokenId
     *
     * @return bool Return true if this token has been revoked
     */
    public function isAccessTokenRevoked($tokenId);
}

Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php

declare(strict_types=1);

namespace Data\Repository;

use Data\Entity\User;
use Data\Entity\OAuthAccessToken;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use League\OAuth2\Server as OAuth;

class OAuthAccessTokenRepository extends EntityRepository implements OAuth\Repositories\AccessTokenRepositoryInterface
{
    /**
     * @throws ORMException
     */
    public function getNewToken(
        OAuth\Entities\ClientEntityInterface $clientEntity,
        array $scopes,
        $userIdentifier = null
    ): OAuthAccessToken {
        $accessToken = new OAuthAccessToken();
        $accessToken->setClient($clientEntity);
        foreach ($scopes as $scope) {
            $accessToken->addScope($scope);
        }
        if ($userIdentifier !== null) {
            $accessToken->setUser($this->getEntityManager()->getReference(User::class, $userIdentifier));
        }
        return $accessToken;
    }

    /**
     * @return void
     * @throws ORMException
     *
     * @throws OptimisticLockException
     */
    public function persistNewAccessToken(OAuth\Entities\AccessTokenEntityInterface $accessTokenEntity)
    {
        $this->getEntityManager()->persist($accessTokenEntity);
        $this->getEntityManager()->flush();
    }

    /**
     * @param string $tokenId
     *
     * @return void
     * @throws ORMException
     *
     */
    public function revokeAccessToken($tokenId)
    {
        /** @var ?OAuthAccessToken $accessTokenEntity */
        $accessTokenEntity = $this->findOneBy(['token' => $tokenId]);
        if (null === $accessTokenEntity) {
            return;
        }
        $accessTokenEntity->setIsRevoked(true);
        $this->getEntityManager()->persist($accessTokenEntity);
        $this->getEntityManager()->flush();
    }

    public function isAccessTokenRevoked($tokenId): bool
    {
        /** @var ?OAuthAccessToken $accessTokenEntity */
        $accessTokenEntity = $this->findOneBy(['token' => $tokenId]);

        if (null === $accessTokenEntity) {
            return true;
        }

        return $accessTokenEntity->getIsRevoked();
    }
}

OAuthRefreshToken Repository

The RefreshToken repository will need to implement only the OAuth2 Server library refreshtoken repository interface:

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Repositories;

use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;

/**
 * Refresh token interface.
 */
interface RefreshTokenRepositoryInterface extends RepositoryInterface
{
    /**
     * Creates a new refresh token
     *
     * @return RefreshTokenEntityInterface|null
     */
    public function getNewRefreshToken();

    /**
     * Create a new refresh token_name.
     *
     * @param RefreshTokenEntityInterface $refreshTokenEntity
     *
     * @throws UniqueTokenIdentifierConstraintViolationException
     */
    public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity);

    /**
     * Revoke the refresh token.
     *
     * @param string $tokenId
     */
    public function revokeRefreshToken($tokenId);

    /**
     * Check if the refresh token has been revoked.
     *
     * @param string $tokenId
     *
     * @return bool Return true if this token has been revoked
     */
    public function isRefreshTokenRevoked($tokenId);
}

Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php
declare(strict_types=1);

namespace Data\Repository;

use Data\Entity\OAuthRefreshToken;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use League\OAuth2\Server as OAuth;

class OAuthRefreshTokenRepository extends EntityRepository implements OAuth\Repositories\RefreshTokenRepositoryInterface
{
    public function getNewRefreshToken(): OAuthRefreshToken
    {
        return new OAuthRefreshToken();
    }

    /**
     * @throws OptimisticLockException
     * @throws ORMException
     */
    public function persistNewRefreshToken(OAuth\Entities\RefreshTokenEntityInterface $refreshTokenEntity): void
    {
        $em = $this->getEntityManager();
        $em->persist($refreshTokenEntity);
        $em->flush();
    }

    /**
     * @throws OptimisticLockException
     * @throws ORMException
     */
    public function revokeRefreshToken($tokenId): void
    {
        /** @var ?OAuthRefreshToken $refreshTokenEntity */
        $refreshTokenEntity = $this->find($tokenId);

        if (null === $refreshTokenEntity) {
            return;
        }

        $refreshTokenEntity->setIsRevoked(true);
        $em = $this->getEntityManager();
        $em->persist($refreshTokenEntity);
        $em->flush();
    }

    public function isRefreshTokenRevoked($tokenId): bool
    {
        /** @var ?OAuthRefreshToken $refreshTokenEntity */
        $refreshTokenEntity = $this->find($tokenId);

        if (null === $refreshTokenEntity) {
            return true;
        }

        return $refreshTokenEntity->getIsRevoked();
    }
}

OAuthScope Repository

The Scope repository will need to implement only the OAuth2 Server library scope repository interface:

Implemented Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace League\OAuth2\Server\Repositories;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;

/**
 * Scope interface.
 */
interface ScopeRepositoryInterface extends RepositoryInterface
{
    /**
     * Return information about a scope.
     *
     * @param string $identifier The scope identifier
     *
     * @return ScopeEntityInterface|null
     */
    public function getScopeEntityByIdentifier($identifier);

    /**
     * Given a client, grant type and optional user identifier validate the set of scopes requested are valid and optionally
     * append additional scopes or remove requested scopes.
     *
     * @param ScopeEntityInterface[] $scopes
     * @param string                 $grantType
     * @param ClientEntityInterface  $clientEntity
     * @param null|string            $userIdentifier
     *
     * @return ScopeEntityInterface[]
     */
    public function finalizeScopes(
        array $scopes,
        $grantType,
        ClientEntityInterface $clientEntity,
        $userIdentifier = null
    );
}

Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

declare(strict_types=1);

namespace Data\Repository;

use Data\Entity\OAuthScope;
use Doctrine\ORM\EntityRepository;
use League\OAuth2\Server as OAuth;

class OAuthScopeRepository extends EntityRepository implements OAuth\Repositories\ScopeRepositoryInterface
{
    public function getScopeEntityByIdentifier($identifier): ?OAuth\Entities\ScopeEntityInterface
    {
        /** @var ?OAuthScope $scope */
        $scope = $this->findOneBy(['scope' => $identifier]);

        return $scope;
    }

    public function finalizeScopes(
        array $scopes,
        $grantType,
        OAuth\Entities\ClientEntityInterface $clientEntity,
        $userIdentifier = null
    ): array {
        return $scopes;
    }
}


Series: Mezzio Example

  1. Mezzio Example: Introduction
  2. Mezzio Example: Functional and Unit Testing
  3. Mezzio Example: Psalm Introduction
  4. Mezzio Example: Doctrine Entities and Repositories
comments powered by Disqus