Skip to content

Commit

Permalink
Add Discord SSO (#887)
Browse files Browse the repository at this point in the history
  • Loading branch information
debounced authored Jul 10, 2024
1 parent a4692e3 commit 7b97ec2
Show file tree
Hide file tree
Showing 14 changed files with 311 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ OAUTH_FACEBOOK_ID=
OAUTH_FACEBOOK_SECRET=
OAUTH_GOOGLE_ID=
OAUTH_GOOGLE_SECRET=
OAUTH_DISCORD_ID=
OAUTH_DISCORD_SECRET=
OAUTH_GITHUB_ID=
OAUTH_GITHUB_SECRET=
OAUTH_PRIVACYPORTAL_ID=
Expand Down
2 changes: 2 additions & 0 deletions .env.example_docker
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ OAUTH_FACEBOOK_ID=
OAUTH_FACEBOOK_SECRET=
OAUTH_GOOGLE_ID=
OAUTH_GOOGLE_SECRET=
OAUTH_DISCORD_ID=
OAUTH_DISCORD_SECRET=
OAUTH_GITHUB_ID=
OAUTH_GITHUB_SECRET=
OAUTH_KEYCLOAK_ID=
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@
"twig/html-extra": "^3.5",
"twig/intl-extra": "^3.2",
"twig/twig": "^2.12 || ^3.0",
"webmozart/assert": "^1.9"
"webmozart/assert": "^1.9",
"wohali/oauth2-discord-new": "^1.2"
},
"require-dev": {
"dama/doctrine-test-bundle": "^8.0.2",
Expand Down
67 changes: 66 additions & 1 deletion composer.lock

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

10 changes: 10 additions & 0 deletions config/kbin_routes/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ oauth_google_verify:
path: /oauth/google/verify
methods: [ GET ]

oauth_discord_connect:
controller: App\Controller\Security\DiscordController::connect
path: /oauth/discord/connect
methods: [ GET ]

oauth_discord_verify:
controller: App\Controller\Security\DiscordController::verify
path: /oauth/discord/verify
methods: [ GET ]

oauth_github_connect:
controller: App\Controller\Security\GithubController::connect
path: /oauth/github/connect
Expand Down
6 changes: 6 additions & 0 deletions config/packages/knpu_oauth2_client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ knpu_oauth2_client:
client_secret: '%oauth_google_secret%'
redirect_route: oauth_google_verify
redirect_params: { }
discord:
type: discord
client_id: '%oauth_discord_id%'
client_secret: '%oauth_discord_secret%'
redirect_route: oauth_discord_verify
redirect_params: { }
github:
type: github
client_id: '%oauth_github_id%'
Expand Down
1 change: 1 addition & 0 deletions config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ security:
custom_authenticators:
- App\Security\KbinAuthenticator
- App\Security\AzureAuthenticator
- App\Security\DiscordAuthenticator
- App\Security\FacebookAuthenticator
- App\Security\GoogleAuthenticator
- App\Security\GithubAuthenticator
Expand Down
3 changes: 3 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ parameters:
oauth_google_id: "%env(default::OAUTH_GOOGLE_ID)%"
oauth_google_secret: "%env(OAUTH_GOOGLE_SECRET)%"

oauth_discord_id: "%env(default::OAUTH_DISCORD_ID)%"
oauth_discord_secret: "%env(OAUTH_DISCORD_SECRET)%"

oauth_github_id: "%env(default::OAUTH_GITHUB_ID)%"
oauth_github_secret: "%env(OAUTH_GITHUB_SECRET)%"

Expand Down
31 changes: 31 additions & 0 deletions migrations/Version20240706005744.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240706005744 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add Discord SSO';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD oauth_discord_id VARCHAR(255) DEFAULT NULL');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" DROP oauth_discord_id');
}
}
24 changes: 24 additions & 0 deletions src/Controller/Security/DiscordController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace App\Controller\Security;

use App\Controller\AbstractController;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class DiscordController extends AbstractController
{
public function connect(ClientRegistry $clientRegistry): Response
{
return $clientRegistry
->getClient('discord')
->redirect();
}

public function verify(Request $request, ClientRegistry $client)
{
}
}
4 changes: 3 additions & 1 deletion src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Visibil
#[Column(type: 'string', nullable: true)]
public ?string $oauthSimpleLoginId = null;
#[Column(type: 'string', nullable: true)]
public ?string $oauthDiscordId = null;
#[Column(type: 'string', nullable: true)]
public ?string $oauthZitadelId = null;
#[Column(type: 'string', nullable: true)]
public ?string $oauthAuthentikId = null;
Expand Down Expand Up @@ -762,7 +764,7 @@ public function removeOAuth2UserConsent(OAuth2UserConsent $oAuth2UserConsent): s

public function isSsoControlled(): bool
{
return $this->oauthAzureId || $this->oauthGithubId || $this->oauthGoogleId || $this->oauthFacebookId || $this->oauthKeycloakId || $this->oauthSimpleLoginId || $this->oauthZitadelId || $this->oauthAuthentikId || $this->oauthPrivacyPortalId;
return $this->oauthAzureId || $this->oauthGithubId || $this->oauthGoogleId || $this->oauthDiscordId || $this->oauthFacebookId || $this->oauthKeycloakId || $this->oauthSimpleLoginId || $this->oauthZitadelId || $this->oauthAuthentikId || $this->oauthPrivacyPortalId;
}

public function getCustomCss(): ?string
Expand Down
149 changes: 149 additions & 0 deletions src/Security/DiscordAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

declare(strict_types=1);

namespace App\Security;

use App\DTO\UserDto;
use App\Entity\User;
use App\Factory\ImageFactory;
use App\Repository\ImageRepository;
use App\Service\ImageManager;
use App\Service\IpResolver;
use App\Service\SettingsManager;
use App\Service\UserManager;
use App\Utils\Slugger;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
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\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
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 Wohali\OAuth2\Client\Provider\DiscordResourceOwner;

class DiscordAuthenticator extends OAuth2Authenticator
{
public function __construct(
private readonly ClientRegistry $clientRegistry,
private readonly RouterInterface $router,
private readonly EntityManagerInterface $entityManager,
private readonly UserManager $userManager,
private readonly ImageManager $imageManager,
private readonly ImageFactory $imageFactory,
private readonly ImageRepository $imageRepository,
private readonly RequestStack $requestStack,
private readonly IpResolver $ipResolver,
private readonly Slugger $slugger,
private readonly SettingsManager $settingsManager
) {
}

public function supports(Request $request): ?bool
{
return 'oauth_discord_verify' === $request->attributes->get('_route');
}

public function authenticate(Request $request): Passport
{
$client = $this->clientRegistry->getClient('discord');
$slugger = $this->slugger;
$session = $this->requestStack->getSession();

$accessToken = $this->fetchAccessToken($client, ['prompt' => 'consent', 'accessType' => 'offline']);
$session->set('access_token', $accessToken);

$accessToken = $session->get('access_token');

if ($accessToken->hasExpired()) {
$accessToken = $client->refreshAccessToken($accessToken->getRefreshToken());
$session->set('access_token', $accessToken);
}

$rememberBadge = new RememberMeBadge();
$rememberBadge = $rememberBadge->enable();

return new SelfValidatingPassport(
new UserBadge($accessToken->getToken(), function () use ($accessToken, $client, $slugger) {
/** @var DiscordResourceOwner $discordUser */
$discordUser = $client->fetchUserFromToken($accessToken);

$existingUser = $this->entityManager->getRepository(User::class)->findOneBy(
['oauthDiscordId' => $discordUser->getId()]
);

if ($existingUser) {
return $existingUser;
}

$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $discordUser->getEmail()]
);

if ($user) {
$user->oauthDiscordId = $discordUser->getId();

$this->entityManager->persist($user);
$this->entityManager->flush();

return $user;
}

if (false === $this->settingsManager->get('MBIN_SSO_REGISTRATIONS_ENABLED')) {
throw new CustomUserMessageAuthenticationException('MBIN_SSO_REGISTRATIONS_ENABLED');
}

$dto = (new UserDto())->create(
$slugger->slug($discordUser->getUsername()).rand(1, 999),
$discordUser->getEmail()
);

$dto->plainPassword = bin2hex(random_bytes(20));
$dto->ip = $this->ipResolver->resolve();

$user = $this->userManager->create($dto, false);
$user->oauthDiscordId = $discordUser->getId();
$user->isVerified = true;

$this->entityManager->persist($user);
$this->entityManager->flush();

return $user;
}),
[
$rememberBadge,
]
);
}

public function onAuthenticationSuccess(
Request $request,
TokenInterface $token,
string $firewallName
): ?Response {
$targetUrl = $this->router->generate('front');

return new RedirectResponse($targetUrl);
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());

if ('MBIN_SSO_REGISTRATIONS_ENABLED' === $message) {
$session = $request->getSession();
$session->getFlashBag()->add('error', 'sso_registrations_enabled.error');

return new RedirectResponse($this->router->generate('app_login'));
}

return new Response($message, Response::HTTP_FORBIDDEN);
}
}
Loading

0 comments on commit 7b97ec2

Please sign in to comment.