Created
December 30, 2022 05:08
-
-
Save denistouch/9409383510840e71cbce30e8dfeb1e17 to your computer and use it in GitHub Desktop.
Basic implementation for Telegram OpenAuth for Symfony 6
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Entity; | |
use App\Repository\ClientRepository; | |
use Doctrine\Common\Collections\Collection; | |
use Doctrine\ORM\Mapping as ORM; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
#[ORM\Entity(repositoryClass: ClientRepository::class)] | |
class Client implements UserInterface | |
{ | |
#[ORM\Id] | |
#[ORM\GeneratedValue] | |
#[ORM\Column] | |
private ?int $id = null; | |
#[ORM\Column(length: 255, unique: true)] | |
private ?string $code = null; | |
#[ORM\Column(length: 255)] | |
private ?string $name = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $surName = null; | |
#[ORM\Column(length: 255)] | |
private ?string $status = null; | |
#[ORM\OneToOne(mappedBy: 'client', cascade: ['persist', 'remove'])] | |
private ?TelegramAccount $telegramAccount = null; | |
public function getRoles(): array | |
{ | |
return [User::ROLE_USER]; | |
} | |
public function getUserIdentifier(): string | |
{ | |
return $this->telegramAccount->getTelegramId(); | |
} | |
public function eraseCredentials(): void | |
{ | |
// TODO: Implement eraseCredentials() method. | |
} | |
public function getId(): ?int | |
{ | |
return $this->id; | |
} | |
public function getCode(): ?string | |
{ | |
return $this->code; | |
} | |
public function setCode(string $code): self | |
{ | |
$this->code = $code; | |
return $this; | |
} | |
public function getName(): ?string | |
{ | |
return $this->name; | |
} | |
public function setName(string $name): self | |
{ | |
$this->name = $name; | |
return $this; | |
} | |
public function getSurName(): ?string | |
{ | |
return $this->surName; | |
} | |
public function setSurName(?string $surName): self | |
{ | |
$this->surName = $surName; | |
return $this; | |
} | |
public function getStatus(): ?string | |
{ | |
return $this->status; | |
} | |
public function setStatus(string $status): self | |
{ | |
$this->status = $status; | |
return $this; | |
} | |
public function getTelegramAccount(): ?TelegramAccount | |
{ | |
return $this->telegramAccount; | |
} | |
public function setTelegramAccount(TelegramAccount $telegramAccount): self | |
{ | |
// set the owning side of the relation if necessary | |
if ($telegramAccount->getClient() !== $this) { | |
$telegramAccount->setClient($this); | |
} | |
$this->telegramAccount = $telegramAccount; | |
return $this; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Repository; | |
use App\Entity\Client; | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
use Doctrine\ORM\QueryBuilder; | |
use Doctrine\Persistence\ManagerRegistry; | |
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface; | |
/** | |
* @extends ServiceEntityRepository<Client> | |
* | |
* @method Client|null find($id, $lockMode = null, $lockVersion = null) | |
* @method Client|null findOneBy(array $criteria, array $orderBy = null) | |
* @method Client[] findAll() | |
* @method Client[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) | |
*/ | |
class ClientRepository extends ServiceEntityRepository implements UserLoaderInterface | |
{ | |
public function __construct(ManagerRegistry $registry) | |
{ | |
parent::__construct($registry, Client::class); | |
} | |
public function add(Client $entity, bool $flush = false): void | |
{ | |
$this->getEntityManager()->persist($entity); | |
if ($flush) { | |
$this->getEntityManager()->flush(); | |
} | |
} | |
public function remove(Client $entity, bool $flush = false): void | |
{ | |
$this->getEntityManager()->remove($entity); | |
if ($flush) { | |
$this->getEntityManager()->flush(); | |
} | |
} | |
public function loadUserByIdentifier(string $identifier): ?Client | |
{ | |
$qb = $this->createQueryBuilder('c') | |
$qb->where('ta.telegramId = :telegramId') | |
->setParameter('telegramId', $telegramId); | |
$query = $qb->getQuery(); | |
return $query->getOneOrNullResult(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
security: | |
providers: | |
app_telegram_provider: | |
entity: | |
class: App\Entity\Client | |
firewalls: | |
player: | |
pattern: ^/telegram/ | |
provider: app_telegram_provider | |
entry_point: App\Security\TelegramEntryPoint | |
custom_authenticators: | |
- App\Security\TelegramAuthenticator | |
# Easy way to control access for large sections of your site | |
# Note: Only the *first* access control that matches will be used | |
access_control: | |
- { path: ^/telegram/auth, roles: PUBLIC_ACCESS } | |
- { path: ^/telegram/, roles: IS_AUTHENTICATED } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Security; | |
use App\Exception\TelegramAuthenticationException; | |
use Psr\Log\LoggerInterface; | |
use Symfony\Component\HttpFoundation\RedirectResponse; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
use Symfony\Component\Security\Core\Exception\AuthenticationException; | |
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | |
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |
use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | |
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; | |
use Symfony\Component\Translation\TranslatableMessage; | |
class TelegramAuthenticator extends AbstractAuthenticator | |
{ | |
private const REQUIRED_PARAMS = ['id', 'auth_date', 'hash']; | |
public function __construct( | |
private readonly string $botToken, | |
) { | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function supports(Request $request): ?bool | |
{ | |
$authPath = '/telegram/'; | |
$isSupports = str_starts_with($request->getRequestUri(), $authPath); | |
$allowedKeys = self::REQUIRED_PARAMS; | |
$parameters = collect($request->query->all()) | |
->filter(function ($value, $key) use ($allowedKeys) { | |
return in_array($key, $allowedKeys); | |
}); | |
return count($allowedKeys) === $parameters->count() && $isSupports; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function authenticate(Request $request): Passport | |
{ | |
$parameters = $request->query->all(); | |
if (count($parameters) === 0) { | |
throw new TelegramAuthenticationException(new TranslatableMessage('telegram.request.invalid')); | |
} | |
$checkHash = $request->query->get('hash'); | |
$authDate = $request->get('auth_date'); | |
$data = collect($parameters) | |
->filter(function ($parameter, $key) { | |
return $key !== 'hash'; | |
}) | |
->map(function ($parameter, $key) { | |
return "$key=$parameter"; | |
}) | |
->sort() | |
->all(); | |
$dataAsString = implode("\n", $data); | |
$secret_key = hash('sha256', $this->botToken, true); | |
$hash = hash_hmac('sha256', $dataAsString, $secret_key); | |
if (strcmp($hash, $checkHash) !== 0) { | |
throw new TelegramAuthenticationException(new TranslatableMessage('telegram.request.invalid')); | |
} | |
if ((time() - $authDate) > 86400) { | |
throw new TelegramAuthenticationException(new TranslatableMessage('telegram.request.expired')); | |
} | |
return new SelfValidatingPassport(new UserBadge($request->get('id'))); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |
{ | |
$pathInfo = $request->getPathInfo() ? new RedirectResponse($request->getPathInfo()) : null; | |
if ($pathInfo) { | |
return $pathInfo; | |
} | |
return null; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | |
{ | |
throw $exception; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Security; | |
use Symfony\Component\HttpFoundation\RedirectResponse; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
use Symfony\Component\Security\Core\Exception\AuthenticationException; | |
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; | |
class TelegramEntryPoint implements AuthenticationEntryPointInterface | |
{ | |
public function __construct( | |
private readonly UrlGeneratorInterface $urlGenerator, | |
) { | |
} | |
public function start(Request $request, AuthenticationException $authException = null): ?RedirectResponse | |
{ | |
return new RedirectResponse($this->urlGenerator->generate('app_telegram_auth')); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment