Skip to content

Commit

Permalink
Refactor authentication; switch to Passports (#275)
Browse files Browse the repository at this point in the history
This will:
- title
  • Loading branch information
jskowronski39 authored Aug 15, 2023
2 parents 762e311 + daff501 commit 1545b13
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 89 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"elao/enum": "^1.7",
"erusev/parsedown": "^1.7",
"friendsofsymfony/jsrouting-bundle": "^2.5",
"knpuniversity/oauth2-client-bundle": "^2.7",
"knpuniversity/oauth2-client-bundle": "^2.15",
"league/csv": "^9.6",
"nelmio/cors-bundle": "^2.1",
"phpdocumentor/reflection-docblock": "^5.3",
Expand Down Expand Up @@ -59,7 +59,7 @@
"twig/markdown-extra": "^3.0",
"twig/string-extra": "^3.0",
"twig/twig": "^2.12|^3.0",
"wohali/oauth2-discord-new": "^1.0"
"wohali/oauth2-discord-new": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 7 additions & 10 deletions config/packages/security.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
security:
enable_authenticator_manager: false
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
Expand All @@ -18,24 +18,21 @@ security:
security: false

api:
lazy: true
stateless: true
pattern: ^/api/attendances
methods: ['POST']
anonymous: true
stateless: true
guard:
authenticators:
- App\Security\Authenticator\ApiKeyAuthenticator
custom_authenticators:
- App\Security\Authenticator\ApiKeyAuthenticator

main:
anonymous: true
lazy: true
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
always_remember_me: true
guard:
authenticators:
- App\Security\Authenticator\DiscordAuthenticator
custom_authenticators:
- App\Security\Authenticator\DiscordAuthenticator
logout:
path: app_security_logout

Expand Down
2 changes: 0 additions & 2 deletions src/Command/PermissionsMakeAdminCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 1;
}

$discordUserId = (int) $discordUserId;

$user = $this->userRepository->findOneByExternalId($discordUserId);
if (!$user) {
$io->error(sprintf('User not found by given id: "%s"!', $discordUserId));
Expand Down
2 changes: 1 addition & 1 deletion src/Repository/User/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function __construct(ManagerRegistry $registry)
parent::__construct($registry, User::class);
}

public function findOneByExternalId(int $externalId): ?User
public function findOneByExternalId(string $externalId): ?User
{
return $this->findOneBy([
'externalId' => $externalId,
Expand Down
44 changes: 15 additions & 29 deletions src/Security/Authenticator/ApiKeyAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,59 +10,45 @@
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
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;

class ApiKeyAuthenticator extends AbstractGuardAuthenticator
class ApiKeyAuthenticator extends AbstractAuthenticator
{
public function __construct(
private string $apiKeyHeaderName,
private string $apiAllowedKeys
) {
}

public function start(Request $request, AuthenticationException $authException = null): Response
{
return new Response(null, 401);
}

public function supports(Request $request): bool
public function supports(Request $request): ?bool
{
return true;
}

public function getCredentials(Request $request): string
{
return $request->headers->get($this->apiKeyHeaderName, '');
}

public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
{
return new ApiTokenUser($credentials);
}

public function checkCredentials($credentials, UserInterface $user): bool
public function authenticate(Request $request): Passport
{
$token = $request->headers->get($this->apiKeyHeaderName, '');
$allowedTokens = explode(',', $this->apiAllowedKeys);
$allowedTokens = array_map('trim', $allowedTokens);
$allowedTokens = array_filter($allowedTokens, static fn (string $allowedToken) => !empty($allowedToken));

return \in_array($credentials, $allowedTokens, true);
}
if (!\in_array($token, $allowedTokens, true)) {
throw new AccessDeniedHttpException('Invalid or missing API key provided!');
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
throw new AccessDeniedHttpException('Invalid or missing API key provided!');
return new SelfValidatingPassport(new UserBadge($token, fn () => new ApiTokenUser($token)));
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}

public function supportsRememberMe(): bool
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return false;
throw $exception;
}
}
84 changes: 40 additions & 44 deletions src/Security/Authenticator/DiscordAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Entity\Permissions\UserPermissions;
use App\Entity\User\User;
use App\Repository\User\UserRepository;
use App\Security\Enum\ConnectionsEnum;
use App\Security\Exception\MultipleRolesFound;
use App\Security\Exception\RequiredRolesNotAssignedException;
Expand All @@ -17,25 +18,26 @@
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use League\OAuth2\Client\Token\AccessToken;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Ramsey\Uuid\Uuid;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
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\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Wohali\OAuth2\Client\Provider\DiscordResourceOwner;

use function React\Async\await;

/**
* @see https://github.com/knpuniversity/oauth2-client-bundle
*/
class DiscordAuthenticator extends SocialAuthenticator
class DiscordAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
{
protected const DISCORD_CLIENT_NAME = 'discord_main';

Expand All @@ -46,6 +48,7 @@ class DiscordAuthenticator extends SocialAuthenticator

public function __construct(
private ClientRegistry $clientRegistry,
private UserRepository $userRepository,
private EntityManagerInterface $em,
private RouterInterface $router,
private DiscordClientFactory $discordClientFactory,
Expand All @@ -67,18 +70,13 @@ public function supports(Request $request): bool
return self::SUPPORTED_ROUTE_NAME === $request->attributes->get('_route');
}

public function getCredentials(Request $request): AccessToken
public function authenticate(Request $request): Passport
{
return $this->fetchAccessToken($this->getDiscordClient());
}
$client = $this->clientRegistry->getClient(self::DISCORD_CLIENT_NAME);
$accessToken = $this->fetchAccessToken($client);

/**
* @param AccessToken $credentials
*/
public function getUser($credentials, UserProviderInterface $userProvider): User
{
/** @var DiscordResourceOwner $discordResourceOwner */
$discordResourceOwner = $this->getDiscordClient()->fetchUserFromToken($credentials);
$discordResourceOwner = $client->fetchUserFromToken($accessToken);

$userId = $discordResourceOwner->getId();
$username = $discordResourceOwner->getUsername();
Expand All @@ -87,7 +85,7 @@ public function getUser($credentials, UserProviderInterface $userProvider): User
$externalId = $discordResourceOwner->getId();

$discordClientAsBot = $this->discordClientFactory->createBotClient($this->botToken);
$discordClientAsUser = $this->discordClientFactory->createUserClient($credentials->getToken());
$discordClientAsUser = $this->discordClientFactory->createUserClient($accessToken->getToken());

$serverResponse = await($discordClientAsBot->get(
Endpoint::bind(Endpoint::GUILD, $this->discordServerId)
Expand Down Expand Up @@ -130,9 +128,8 @@ public function getUser($credentials, UserProviderInterface $userProvider): User

$steamId = $steamConnection ? (int) $steamConnection->id : null;

try {
/** @var User $user */
$user = $userProvider->loadUserByIdentifier($externalId);
$user = $this->userRepository->findOneByExternalId($externalId);
if ($user instanceof User) {
$user->update(
$fullUsername,
$email,
Expand All @@ -142,43 +139,47 @@ public function getUser($credentials, UserProviderInterface $userProvider): User
$discordResourceOwner->getAvatarHash(),
$steamId
);
} catch (UserNotFoundException $ex) {
$permissions = new UserPermissions(Uuid::uuid4());
$user = new User(
Uuid::uuid4(),
$fullUsername,
$email,
$externalId,
$permissions,
[],
$discordResourceOwner->getAvatarHash(),
$steamId
);

$this->em->persist($permissions);
$this->em->persist($user);
$this->em->flush();

return new SelfValidatingPassport(new UserBadge($fullUsername));
}

$permissions = new UserPermissions(Uuid::uuid4());
$user = new User(
Uuid::uuid4(),
$fullUsername,
$email,
$externalId,
$permissions,
[],
$discordResourceOwner->getAvatarHash(),
$steamId
);

$this->em->persist($permissions);
$this->em->persist($user);

$this->em->flush();

return $user;
return new SelfValidatingPassport(new UserBadge($fullUsername));
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?RedirectResponse
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$targetUrl = $this->router->generate(self::HOME_JOIN_US_PAGE_ROUTE_NAME);
$targetUrl = $this->router->generate(self::HOME_INDEX_PAGE_ROUTE_NAME);

return new RedirectResponse($targetUrl);
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?RedirectResponse
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$targetUrl = $this->router->generate(self::HOME_INDEX_PAGE_ROUTE_NAME);
$targetUrl = $this->router->generate(self::HOME_JOIN_US_PAGE_ROUTE_NAME);

return new RedirectResponse($targetUrl);
}

protected function getRoleIdByName(array $roles, string $roleName): string
private function getRoleIdByName(array $roles, string $roleName): string
{
$rolesFound = (new ArrayCollection($roles))->filter(static fn (\stdClass $role) => $role->name === $roleName);

Expand All @@ -188,9 +189,4 @@ protected function getRoleIdByName(array $roles, string $roleName): string
default => throw new MultipleRolesFound(sprintf('Multiple roles found by given name "%s"!', $roleName))
};
}

protected function getDiscordClient(): OAuth2ClientInterface
{
return $this->clientRegistry->getClient(self::DISCORD_CLIENT_NAME);
}
}

0 comments on commit 1545b13

Please sign in to comment.