Update auth, decouple user tokens from access tokens
This commit is contained in:
@@ -4,7 +4,10 @@ namespace App\Dto;
|
||||
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
final class AccessTokenLoginDto
|
||||
/**
|
||||
* What the fuck?
|
||||
*/
|
||||
final class UserTokenLoginDto
|
||||
{
|
||||
/**
|
||||
* The username of the User to be authenticated.
|
||||
+15
-15
@@ -78,11 +78,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
private ?Accounting $accounting = null;
|
||||
|
||||
/**
|
||||
* The AccessTokens owned by this user. Owner only property.
|
||||
* The UserTokens owned by this user. Owner only property.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false, readableLink: true, security: 'is_granted("AUTH_OWNER", object)')]
|
||||
#[ORM\OneToMany(mappedBy: 'ownedBy', targetEntity: AccessToken::class, orphanRemoval: true)]
|
||||
private Collection $accessTokens;
|
||||
#[ORM\OneToMany(mappedBy: 'ownedBy', targetEntity: UserToken::class, orphanRemoval: true)]
|
||||
private Collection $tokens;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $active = null;
|
||||
@@ -101,7 +101,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
$this->accounting = new Accounting();
|
||||
$this->accounting->setOwnerClass(User::class);
|
||||
|
||||
$this->accessTokens = new ArrayCollection();
|
||||
$this->tokens = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -227,29 +227,29 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, AccessToken>
|
||||
* @return Collection<int, UserToken>
|
||||
*/
|
||||
public function getAccessTokens(): Collection
|
||||
public function getTokens(): Collection
|
||||
{
|
||||
return $this->accessTokens;
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
public function addAccessToken(AccessToken $accessToken): static
|
||||
public function addToken(UserToken $token): static
|
||||
{
|
||||
if (!$this->accessTokens->contains($accessToken)) {
|
||||
$this->accessTokens->add($accessToken);
|
||||
$accessToken->setOwnedBy($this);
|
||||
if (!$this->tokens->contains($token)) {
|
||||
$this->tokens->add($token);
|
||||
$token->setOwnedBy($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeAccessToken(AccessToken $accessToken): static
|
||||
public function removeToken(UserToken $token): static
|
||||
{
|
||||
if ($this->accessTokens->removeElement($accessToken)) {
|
||||
if ($this->tokens->removeElement($token)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($accessToken->getOwnedBy() === $this) {
|
||||
$accessToken->setOwnedBy(null);
|
||||
if ($token->getOwnedBy() === $this) {
|
||||
$token->setOwnedBy(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Dto\AccessTokenLoginDto;
|
||||
use App\Repository\AccessTokenRepository;
|
||||
use App\State\AccessTokenLoginProcessor;
|
||||
use App\Dto\UserTokenLoginDto;
|
||||
use App\Repository\UserTokenRepository;
|
||||
use App\State\UserTokenLoginProcessor;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* AccessTokens authenticate requests on behalf of the User who owns them.\
|
||||
* UserTokens authenticates requests on behalf of the User who owns them.\
|
||||
* \
|
||||
* When an AccessToken is created the system generates a SHA-256 hash that is unique for the User it represents.
|
||||
* When a UserToken is created v4 generates a SHA-256 hash that is unique for that Token and the User it represents.
|
||||
* The value of a token comes preceded by a 4-digit-length prefix based on the type of token it is.\
|
||||
* \
|
||||
* `oat_` means the token was created via an OAuth flow.\
|
||||
* `pat_` means the token was created via a login flow.
|
||||
*/
|
||||
#[API\Post(input: AccessTokenLoginDto::class, processor: AccessTokenLoginProcessor::class)]
|
||||
#[API\Post(input: UserTokenLoginDto::class, processor: UserTokenLoginProcessor::class)]
|
||||
#[API\Delete(security: 'is_granted("AUTH_OWNER")')]
|
||||
#[ORM\Entity(repositoryClass: AccessTokenRepository::class)]
|
||||
class AccessToken
|
||||
#[ORM\Entity(repositoryClass: UserTokenRepository::class)]
|
||||
class UserToken
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -34,9 +34,9 @@ class AccessToken
|
||||
private ?string $token = null;
|
||||
|
||||
/**
|
||||
* The User on behalf of which this Token authenticates.
|
||||
* The User on behalf of which this UserToken authenticates.
|
||||
*/
|
||||
#[ORM\ManyToOne(inversedBy: 'accessTokens')]
|
||||
#[ORM\ManyToOne(inversedBy: 'tokens')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $ownedBy = null;
|
||||
|
||||
@@ -12,15 +12,17 @@ This API is still in early development and is not set to have backward compatibi
|
||||
|
||||
# Authentication
|
||||
|
||||
The v4 API uses AccessTokens to authenticate requests. AccessTokens exist under an User's scope, when you obtain a token this will only grant you as much permissions as the User under which it was created.
|
||||
|
||||
To obtain an AccessToken you must send a POST request to [/v4/access_tokens](/v4/access_tokens) with the User login credentials (username and password) in the payload. If the credentials are correct an AccessToken will be created, the `token` property value of which you must include in future requests Authorization header using the [Bearer](https://swagger.io/docs/specification/authentication/bearer-authentication/) strategy.
|
||||
The v4 API uses Access Tokens to authenticate requests. To use a Token, include it in the Authorization header using the [Bearer](https://swagger.io/docs/specification/authentication/bearer-authentication/) strategy.
|
||||
|
||||
```shell
|
||||
curl -X 'GET' \
|
||||
'https://api.goteo.org/v4/projects?page=1' \
|
||||
-H 'accept: application/json' \
|
||||
-H 'Authorization: Bearer <YOUR-ACCESS-TOKEN-HERE>'
|
||||
-H 'Authorization: Bearer <THE-TOKEN-HERE>'
|
||||
```
|
||||
|
||||
Users can delete AccessTokens owned by them at any moment, revoking your application's access to the v4 API on their behalf. When an AccessToken fails to authenticate a request you will receive a 401 Unauthorized response from the API, at which point your application must look to get a new AccessToken from the User.
|
||||
UserTokens exist under an User's scope, when you obtain a token this will only grant you as much permissions as the User under which it was created.
|
||||
|
||||
To obtain a UserToken you must send a POST request to [/v4/user_tokens](/v4/user_tokens) with the User login credentials (username and password) in the payload. If the credentials are correct a UserToken will be created, the `token` property value of which you must include in future requests.
|
||||
|
||||
Users can delete UserTokens owned by them at any moment, revoking your application's access to the v4 API on their behalf. When a UserToken fails to authenticate a request you will receive a 401 Unauthorized response from the API, at which point your application must look to get a new UserToken from the User.
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AccessToken;
|
||||
use App\Entity\UserToken;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<AccessToken>
|
||||
* @extends ServiceEntityRepository<UserToken>
|
||||
*
|
||||
* @method AccessToken|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method AccessToken|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method AccessToken[] findAll()
|
||||
* @method AccessToken[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
* @method UserToken|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method UserToken|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method UserToken[] findAll()
|
||||
* @method UserToken[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class AccessTokenRepository extends ServiceEntityRepository
|
||||
class UserTokenRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AccessToken::class);
|
||||
parent::__construct($registry, UserToken::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return AccessToken[] Returns an array of AccessToken objects
|
||||
// * @return UserToken[] Returns an array of UserToken objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
@@ -36,7 +36,7 @@ class AccessTokenRepository extends ServiceEntityRepository
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?AccessToken
|
||||
// public function findOneBySomeField($value): ?UserToken
|
||||
// {
|
||||
// return $this->createQueryBuilder('a')
|
||||
// ->andWhere('a.exampleField = :val')
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Repository\AccessTokenRepository;
|
||||
use App\Repository\UserTokenRepository;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
|
||||
class AccessTokenHandler implements AccessTokenHandlerInterface
|
||||
{
|
||||
public function __construct(private AccessTokenRepository $accessTokenRepository)
|
||||
public function __construct(private UserTokenRepository $userTokenRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge
|
||||
{
|
||||
$token = $this->accessTokenRepository->findOneBy(['token' => $accessToken]);
|
||||
$token = $this->userTokenRepository->findOneBy(['token' => $accessToken]);
|
||||
|
||||
if (!$token) {
|
||||
throw new BadCredentialsException();
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Auth;
|
||||
|
||||
enum AccessTokenType: string
|
||||
{
|
||||
/**
|
||||
* An OAuth type represents an Acces Token created via an OAuth flow.
|
||||
* OAuth Access Tokens represent 3rd party apps acting on behalf of a User.
|
||||
*/
|
||||
case OAuth = 'oat';
|
||||
|
||||
/**
|
||||
* A Personal type represents an Access Token created via direct login.
|
||||
* Personal Access Token represents the User itself.
|
||||
*/
|
||||
case Personal = 'pat';
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Service\Auth;
|
||||
|
||||
use App\Entity\AccessToken;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserToken;
|
||||
|
||||
class AuthService
|
||||
{
|
||||
@@ -24,9 +24,9 @@ class AuthService
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function generateAccessToken(User $user, AccessTokenType $type): AccessToken
|
||||
public function generateUserToken(User $user, AuthTokenType $type): UserToken
|
||||
{
|
||||
$token = new AccessToken;
|
||||
$token = new UserToken;
|
||||
|
||||
$token->setOwnedBy($user);
|
||||
$token->setToken(sprintf('%s_%s', $type->value, hash(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Auth;
|
||||
|
||||
enum AuthTokenType: string
|
||||
{
|
||||
/**
|
||||
* An OAuth type represents an Authentication Token created via an OAuth flow.
|
||||
* OAuth Tokens represent 3rd party apps acting on behalf of a User.
|
||||
*/
|
||||
case OAuth = 'oat';
|
||||
|
||||
/**
|
||||
* A Personal type represents an Authentication Token created via direct login.
|
||||
* Personal Access Tokens represent the User itself.
|
||||
*/
|
||||
case Personal = 'pat';
|
||||
}
|
||||
@@ -4,17 +4,17 @@ namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Dto\AccessTokenLoginDto;
|
||||
use App\Entity\AccessToken;
|
||||
use App\Dto\UserTokenLoginDto;
|
||||
use App\Entity\UserToken;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Service\Auth\AccessTokenType;
|
||||
use App\Service\Auth\AuthTokenType;
|
||||
use App\Service\Auth\AuthService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
|
||||
class AccessTokenLoginProcessor implements ProcessorInterface
|
||||
class UserTokenLoginProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AuthService $authService,
|
||||
@@ -24,11 +24,11 @@ class AccessTokenLoginProcessor implements ProcessorInterface
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): AccessToken
|
||||
/**
|
||||
* @param UserTokenLoginDto $data
|
||||
*/
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): UserToken
|
||||
{
|
||||
/** @var AccessTokenLoginDto */
|
||||
$data;
|
||||
|
||||
$user = $this->userRepository->findOneBy(['username' => $data->username]);
|
||||
|
||||
if (!$user) {
|
||||
@@ -39,7 +39,7 @@ class AccessTokenLoginProcessor implements ProcessorInterface
|
||||
throw new BadCredentialsException();
|
||||
}
|
||||
|
||||
$token = $this->authService->generateAccessToken($user, AccessTokenType::Personal);
|
||||
$token = $this->authService->generateUserToken($user, AuthTokenType::Personal);
|
||||
|
||||
$this->entityManager->persist($token);
|
||||
$this->entityManager->flush();
|
||||
Reference in New Issue
Block a user