Skip to content

Instantly share code, notes, and snippets.

@denistouch
Created December 30, 2022 05:08
Show Gist options
  • Save denistouch/9409383510840e71cbce30e8dfeb1e17 to your computer and use it in GitHub Desktop.
Save denistouch/9409383510840e71cbce30e8dfeb1e17 to your computer and use it in GitHub Desktop.
Basic implementation for Telegram OpenAuth for Symfony 6
<?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;
}
}
<?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();
}
}
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 }
<?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;
}
}
<?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