├── .gitignore
├── Exception
├── SteamAuthenticationException.php
└── InvalidCallbackPayloadException.php
├── Event
├── FirstLoginEvent.php
├── PayloadValidEvent.php
├── CommunityIdAwareEvent.php
├── AuthenticateUserEvent.php
└── CallbackReceivedEvent.php
├── Validator
├── MatchesLoginCallbackRoute.php
└── MatchesLoginCallbackRouteValidator.php
├── KnojectorSteamAuthenticationBundle.php
├── Resources
├── views
│ └── button.html.twig
└── config
│ └── services.xml
├── DependencyInjection
├── Configuration.php
└── KnojectorSteamAuthenticationExtension.php
├── composer.json
├── LICENSE
├── ArgumentResolver
└── SteamCallbackResolver.php
├── Controller
└── SteamController.php
├── Subscriber
├── LoadUserSubscriber.php
├── AuthenticateUserSubscriber.php
└── ValidateCallbackReceivedSubscriber.php
├── DTO
└── SteamCallback.php
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/Exception/SteamAuthenticationException.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | abstract class SteamAuthenticationException extends \Exception
9 | {
10 | }
--------------------------------------------------------------------------------
/Exception/InvalidCallbackPayloadException.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class InvalidCallbackPayloadException extends SteamAuthenticationException
9 | {
10 | }
--------------------------------------------------------------------------------
/Event/FirstLoginEvent.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class FirstLoginEvent extends CommunityIdAwareEvent
9 | {
10 | CONST NAME = 'knojector.steam_authentication_bundle.first_login';
11 | }
--------------------------------------------------------------------------------
/Event/PayloadValidEvent.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class PayloadValidEvent extends CommunityIdAwareEvent
9 | {
10 | CONST NAME = 'knojector.steam_authentication_bundle.payload_valid';
11 | }
--------------------------------------------------------------------------------
/Validator/MatchesLoginCallbackRoute.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | #[\Attribute]
11 | class MatchesLoginCallbackRoute extends Constraint
12 | {
13 | public string $message = 'The parameter "openid_return_to" with value "{{ url }}" does not match original callback url "{{ expected }}".';
14 | }
--------------------------------------------------------------------------------
/Event/CommunityIdAwareEvent.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | abstract class CommunityIdAwareEvent extends Event
11 | {
12 | public function __construct(private string $communityId)
13 | {}
14 |
15 | public function getCommunityId(): string
16 | {
17 | return $this->communityId;
18 | }
19 | }
--------------------------------------------------------------------------------
/KnojectorSteamAuthenticationBundle.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class KnojectorSteamAuthenticationBundle extends Bundle
12 | {
13 | /**
14 | * @inheritDoc
15 | */
16 | public function build(ContainerBuilder $container)
17 | {
18 | parent::build($container);
19 | }
20 | }
--------------------------------------------------------------------------------
/Event/AuthenticateUserEvent.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class AuthenticateUserEvent extends Event
12 | {
13 | CONST NAME = 'knojector.steam_authentication_bundle.authenticate_user';
14 |
15 | public function __construct(protected UserInterface $user)
16 | {}
17 |
18 | public function getUser(): UserInterface
19 | {
20 | return $this->user;
21 | }
22 | }
--------------------------------------------------------------------------------
/Event/CallbackReceivedEvent.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class CallbackReceivedEvent extends Event
12 | {
13 | CONST NAME = 'knojector.steam_authentication_bundle.callback_received';
14 |
15 | public function __construct(protected SteamCallback $steamCallback)
16 | {}
17 |
18 | public function getSteamCallback(): SteamCallback
19 | {
20 | return $this->steamCallback;
21 | }
22 | }
--------------------------------------------------------------------------------
/Resources/views/button.html.twig:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class Configuration implements ConfigurationInterface
12 | {
13 | /**
14 | * @inheritDoc
15 | */
16 | public function getConfigTreeBuilder(): TreeBuilder
17 | {
18 | $treeBuilder = new TreeBuilder('knojector_steam_authentication');
19 |
20 | $treeBuilder->getRootNode()
21 | ->children()
22 | ->scalarNode('login_failure_redirect')->isRequired()->end()
23 | ->scalarNode('login_success_redirect')->isRequired()->end()
24 | ->end()
25 | ;
26 |
27 | return $treeBuilder;
28 | }
29 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "knojector/steam-authentication-bundle",
3 | "license": "MIT",
4 | "type": "symfony-bundle",
5 | "description": "Symfony Bundle to integrate Steam authentication",
6 | "authors": [
7 | {
8 | "name": "knojector",
9 | "email": "dev@knojector.xyz"
10 | }
11 | ],
12 | "autoload": {
13 | "psr-4": { "Knojector\\SteamAuthenticationBundle\\": "" }
14 | },
15 | "require": {
16 | "php": ">=8.2",
17 | "symfony/config": "^5.3|^6.0|^7.0",
18 | "symfony/dependency-injection": "^5.3|^6.0|^7.0",
19 | "symfony/event-dispatcher": "^5.3|^6.0|^7.0",
20 | "symfony/expression-language": "^5.3|^6.0|^7.0",
21 | "symfony/framework-bundle": "^5.3|^6.0|^7.0",
22 | "symfony/http-client": "^5.3|^6.0|^7.0",
23 | "symfony/security-bundle": "^5.3|^6.0|^7.0",
24 | "symfony/routing": "^5.3|^6.0|^7.0",
25 | "symfony/validator": "^5.3|^6.0|^7.0"
26 | }
27 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/DependencyInjection/KnojectorSteamAuthenticationExtension.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class KnojectorSteamAuthenticationExtension extends Extension
14 | {
15 | /**
16 | * @inheritDoc
17 | */
18 | public function load(array $configs, ContainerBuilder $container)
19 | {
20 | $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
21 | $loader->load('services.xml');
22 |
23 | $configuration = new Configuration();
24 | $config = $this->processConfiguration($configuration, $configs);
25 |
26 | $container->setParameter('knojector.steam_authentication.login_failure_redirect', $config['login_failure_redirect']);
27 | $container->setParameter('knojector.steam_authentication.login_success_redirect', $config['login_success_redirect']);
28 | }
29 | }
--------------------------------------------------------------------------------
/ArgumentResolver/SteamCallbackResolver.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class SteamCallbackResolver implements ArgumentValueResolverInterface
16 | {
17 | public function __construct(private ValidatorInterface $validator)
18 | {}
19 |
20 | /**
21 | * @inheritDoc
22 | */
23 | public function supports(Request $request, ArgumentMetadata $argument): bool
24 | {
25 | return $argument->getType() === SteamCallback::class;
26 | }
27 |
28 | /**
29 | * @inheritDoc
30 | */
31 | public function resolve(Request $request, ArgumentMetadata $argument): iterable
32 | {
33 | $steamCallback = SteamCallback::fromRequest($request);
34 |
35 | $errors = $this->validator->validate($steamCallback);
36 | if (count($errors) > 0) {
37 | throw new BadRequestHttpException((string) $errors);
38 | }
39 |
40 | yield $steamCallback;
41 | }
42 | }
--------------------------------------------------------------------------------
/Resources/config/services.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Validator/MatchesLoginCallbackRouteValidator.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class MatchesLoginCallbackRouteValidator extends ConstraintValidator
15 | {
16 | public function __construct(private UrlGeneratorInterface $urlGenerator)
17 | {}
18 |
19 | /**
20 | * @inheritDoc
21 | */
22 | public function validate($value, Constraint $constraint): void
23 | {
24 | if (!$constraint instanceof MatchesLoginCallbackRoute) {
25 | throw new UnexpectedTypeException($constraint, MatchesLoginCallbackRoute::class);
26 | }
27 |
28 | if (null === $value || '' === $value) {
29 | return;
30 | }
31 |
32 | if (!is_string($value)) {
33 | throw new UnexpectedValueException($value, 'string');
34 | }
35 |
36 | $expected = $this->urlGenerator->generate('steam_authentication_callback', [], UrlGeneratorInterface::ABSOLUTE_URL);
37 | if ($expected !== $value) {
38 | $this->context->buildViolation($constraint->message)
39 | ->setParameter('{{ url }}', $value)
40 | ->setParameter('{{ expected }}', $expected)
41 | ->addViolation();
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/Controller/SteamController.php:
--------------------------------------------------------------------------------
1 |
17 | *
18 | */
19 | #[Route(path: '/steam')]
20 | class SteamController extends AbstractController
21 | {
22 | public function __construct(
23 | private EventDispatcherInterface $eventDispatcher,
24 | private UrlGeneratorInterface $urlGenerator
25 | ) {}
26 |
27 | #[Route(path: '/callback')]
28 | public function callback(SteamCallback $callback): Response
29 | {
30 | try {
31 | $this->eventDispatcher->dispatch(new CallbackReceivedEvent($callback), CallbackReceivedEvent::NAME);
32 | } catch (SteamAuthenticationException $e) {
33 | return new RedirectResponse(
34 | $this->urlGenerator->generate($this->getParameter('knojector.steam_authentication.login_failure_redirect'))
35 | );
36 | }
37 |
38 | return new RedirectResponse(
39 | $this->urlGenerator->generate($this->getParameter('knojector.steam_authentication.login_success_redirect'))
40 | );
41 | }
42 | }
--------------------------------------------------------------------------------
/Subscriber/LoadUserSubscriber.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class LoadUserSubscriber implements EventSubscriberInterface
17 | {
18 | public function __construct(
19 | private EventDispatcherInterface $eventDispatcher,
20 | private UserProviderInterface $userProvider
21 | ) {}
22 |
23 | /**
24 | * @inheritDoc
25 | */
26 | public static function getSubscribedEvents(): array
27 | {
28 | return [
29 | PayloadValidEvent::NAME => [
30 | ['onPayloadValid', 10]
31 | ]
32 | ];
33 | }
34 |
35 | public function onPayloadValid(PayloadValidEvent $event): void
36 | {
37 | $communityId = $event->getCommunityId();
38 |
39 | try {
40 | $user = $this->userProvider->loadUserByIdentifier($communityId);
41 | } catch (UserNotFoundException $e) {
42 | $this->eventDispatcher->dispatch(new FirstLoginEvent($communityId), FirstLoginEvent::NAME);
43 |
44 | return;
45 | }
46 |
47 | $this->eventDispatcher->dispatch(new AuthenticateUserEvent($user), AuthenticateUserEvent::NAME);
48 | }
49 | }
--------------------------------------------------------------------------------
/Subscriber/AuthenticateUserSubscriber.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class AuthenticateUserSubscriber implements EventSubscriberInterface
17 | {
18 | public function __construct(
19 | private EventDispatcherInterface $eventDispatcher,
20 | private TokenStorageInterface $tokenStorage,
21 | private RequestStack $requestStack,
22 | ) {}
23 |
24 | /**
25 | * @inheritDoc
26 | */
27 | public static function getSubscribedEvents(): array
28 | {
29 | return [
30 | AuthenticateUserEvent::NAME => [
31 | ['onAuthenticateUser', 10]
32 | ]
33 | ];
34 | }
35 |
36 | public function onAuthenticateUser(AuthenticateUserEvent $event): void
37 | {
38 | $user = $event->getUser();
39 |
40 | $token = new UsernamePasswordToken($user, 'steam', $user->getRoles());
41 | $this->tokenStorage->setToken($token);
42 | $this->requestStack->getSession()->set('_security_steam', serialize($token));
43 |
44 | $event = new InteractiveLoginEvent($this->requestStack->getCurrentRequest(), $token);
45 | $this->eventDispatcher->dispatch($event, 'security.interactive_login');
46 | }
47 | }
--------------------------------------------------------------------------------
/Subscriber/ValidateCallbackReceivedSubscriber.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class ValidateCallbackReceivedSubscriber implements EventSubscriberInterface
16 | {
17 | const STEAM_VALIDATION_URL = 'https://steamcommunity.com/openid/login';
18 |
19 | public function __construct(
20 | private EventDispatcherInterface $eventDispatcher,
21 | private HttpClientInterface $client
22 | ) {}
23 |
24 | /**
25 | * @inheritDoc
26 | */
27 | public static function getSubscribedEvents(): array
28 | {
29 | return [
30 | CallbackReceivedEvent::NAME => [
31 | ['onCallbackReceived', 10]
32 | ]
33 | ];
34 | }
35 |
36 | public function onCallbackReceived(CallbackReceivedEvent $event): void
37 | {
38 | $callback = $event->getSteamCallback();
39 | $callback->openid_mode = 'check_authentication';
40 |
41 | $response = $this->client->request(
42 | 'POST',
43 | self::STEAM_VALIDATION_URL,
44 | [
45 | 'body' => (array) $callback
46 | ]
47 | );
48 |
49 | if (false === str_contains($response->getContent(), 'is_valid:true')) {
50 | throw new InvalidCallbackPayloadException();
51 | }
52 |
53 | $this->eventDispatcher->dispatch(new PayloadValidEvent($callback->getCommunityId()), PayloadValidEvent::NAME);
54 | }
55 | }
--------------------------------------------------------------------------------
/DTO/SteamCallback.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class SteamCallback
13 | {
14 | #[Assert\NotBlank]
15 | #[Assert\EqualTo('http://specs.openid.net/auth/2.0')]
16 | public string $openid_ns;
17 |
18 | #[Assert\NotBlank]
19 | public string $openid_mode;
20 |
21 | #[Assert\NotBlank]
22 | #[Assert\EqualTo('https://steamcommunity.com/openid/login')]
23 | public string $openid_op_endpoint;
24 |
25 | #[Assert\NotBlank]
26 | #[Assert\Expression('this.openid_claimed_id === this.openid_identity')]
27 | public string $openid_claimed_id;
28 |
29 | #[Assert\NotBlank]
30 | public string $openid_identity;
31 |
32 | #[Assert\NotBlank]
33 | #[SteamAssert\MatchesLoginCallbackRoute]
34 | public string $openid_return_to;
35 |
36 | #[Assert\NotBlank]
37 | public string $openid_response_nonce;
38 |
39 | #[Assert\NotBlank]
40 | public string $openid_assoc_handle;
41 |
42 | #[Assert\NotBlank]
43 | public string $openid_signed;
44 |
45 | #[Assert\NotBlank]
46 | public $openid_sig;
47 |
48 | public function getCommunityId(): string
49 | {
50 | return str_replace('https://steamcommunity.com/openid/id/', '', $this->openid_identity);
51 | }
52 |
53 | public static function fromRequest(Request $request): self
54 | {
55 | $steamCallback = new self;
56 | $steamCallback->openid_ns = $request->get('openid_ns');
57 | $steamCallback->openid_mode = $request->get('openid_mode');
58 | $steamCallback->openid_op_endpoint = $request->get('openid_op_endpoint');
59 | $steamCallback->openid_claimed_id = $request->get('openid_claimed_id');
60 | $steamCallback->openid_identity = $request->get('openid_identity');
61 | $steamCallback->openid_return_to = $request->get('openid_return_to');
62 | $steamCallback->openid_response_nonce = $request->get('openid_response_nonce');
63 | $steamCallback->openid_assoc_handle = $request->get('openid_assoc_handle');
64 | $steamCallback->openid_signed = $request->get('openid_signed');
65 | $steamCallback->openid_sig = $request->get('openid_sig');
66 |
67 | return $steamCallback;
68 | }
69 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/knojector/SteamAuthenticationBundle/blob/main/LICENSE)
2 | [](https://github.com/knojector/SteamAuthenticationBundle/issues)
3 | [](https://github.com/knojector/SteamAuthenticationBundle/pulls)
4 | [](https://github.com/knojector/SteamAuthenticationBundle/stargazers)
5 | 
6 |
7 | # SteamAuthenticationBundle - Steam authentication for Symfony
8 |
9 | The SteamAuthenticationBundle provides an easy way to integrate Steams OpenID login for your application.
10 |
11 | ## Table of Contents
12 | - [Installation](#installation)
13 | - [Usage](#usage)
14 | - [Bugs and ideas?](#bugs-and-ideas)
15 | - [Requirements](#requirements)
16 |
17 | ## Installation
18 |
19 | ### Step 1 - Install the bundle
20 | ```shell
21 | composer require knojector/steam-authentication-bundle
22 | ```
23 |
24 | ### Step 2 - Configuration
25 | ```yaml
26 | knojector_steam_authentication:
27 | login_success_redirect: 'app.protected_route'
28 | login_failure_redirect: 'app.error_route'
29 | ```
30 | As you can see there are only two options available for configuration. Both are very self-explanatory. The option `login_success_redirect` contains the name of the route the user should be redirected to if the login was successful. The option `login_failure_redirect` contains the route the user is redirected to if the login fails.
31 |
32 |
33 | Furthermore you have to adjust your `security.yml`. In the following snippet you can see an example with a basic configuration. There are only two important things to consider:
34 | - The firewall name must be `steam`
35 | - You can use any user provider as long as the Steam CommunityId is the property to query for
36 | ```yaml
37 | security:
38 | providers:
39 | users:
40 | entity:
41 | class: 'App\Entity\User'
42 | property: 'username'
43 | firewalls:
44 | dev:
45 | pattern: ^/(_(profiler|wdt)|css|images|js)/
46 | security: false
47 | steam:
48 | pattern: ^/
49 | lazy: true
50 | provider: users
51 | ```
52 |
53 | The final step is to enable the bundle's controller in your `routes.yaml`
54 | ```yaml
55 | steam_authentication_callback:
56 | path: /steam/login_check
57 | controller: Knojector\SteamAuthenticationBundle\Controller\SteamController::callback
58 | ```
59 | ## Usage
60 |
61 | ### Step 1 - Create your own registration subscriber
62 | Technically there is no registration available in this bundle. The bundle receives an ID from Steam and tries to load a user via the configured user provider. If no user exists you can assume that the user logged in for the first time. Instead of throwing an exception, the bundle dispatches an event you can subscribe to. A simple example for your subscriber is shown below
63 |
64 | ```php
65 | 'onFirstLogin'
87 | ];
88 | }
89 |
90 | public function onFirstLogin(FirstLoginEvent $event)
91 | {
92 | $communityId = $event->getCommunityId();
93 |
94 | $user = new User();
95 | $user->setUsername($communityId);
96 |
97 | // e.g. call the Steam API to fetch more profile information
98 | // e.g. create user entity and persist it
99 |
100 | // dispatch the authenticate event in order to sign in the new created user.
101 | $this->eventDispatcher->dispatch(new AuthenticateUserEvent($user), AuthenticateUserEvent::NAME);
102 | }
103 | }
104 | ```
105 |
106 | ### Step 2 - Place the login button
107 | To place the "Sign in through Steam" button you can include the following snippet in your template
108 | ```
109 | {% include '@KnojectorSteamAuthentication/button.html.twig' %}
110 | ```
111 |
112 | ## Bugs and ideas?
113 | Feel free to open an issue or submit a pull request :wink:
114 |
115 | ## Requirements
116 | The bundle requires:
117 | - PHP 8.0.0+
118 | - Symfony 5.3/6
119 |
--------------------------------------------------------------------------------