首页 > 解决方案 > 带有 Open ID 的 Steam Auth (Symfony 4.x)

问题描述

我正在尝试使用两种方法在我的网站中进行身份验证:

  1. 使用电子邮件地址/密码(工作)

  2. 使用 Steam(我有问题的地方)

这是我的services.yaml

security:
  encoders:
    App\Entity\User\User:
      algorithm: bcrypt
  providers:
    user_provider:
      entity:
        class: App\Entity\User\User
    steam_provider:
      id: App\Security\SteamAuth\User\SteamUserProvider
    chain_provider:
      chain:
        providers: [user_provider, steam_provider]
  firewalls:
    dev:
      pattern: ^/(_(profiler|wdt)|css|images|js)/
      security: false
    steam:
      anonymous: ~
      pattern: /steam/login
      user_checker: App\Security\User\UserChecker
      provider: steam_provider
      steam: true
      logout:
        path:   /logout
        target: /
    classic:
      anonymous: true
      user_checker: App\Security\User\UserChecker
      provider: user_provider
      form_login:
        provider: user_provider
        default_target_path: /servers
        use_referer: true
        login_path: /login
        check_path: /login
        username_parameter: login_form[emailAddress]
        password_parameter: login_form[password]
      remember_me:
        remember_me_parameter: login_form[remember_me]
        secret: '%kernel.secret%'
        lifetime: 1209600 # 2 week in seconds
        path: /
      logout:
        target: /
  access_control:
    # Users
    - { path: '^/steam/login', roles: IS_AUTHENTICATED_ANONYMOUSLY }

与其他服务相比,Steam 仅提供使用 OpenID 的身份验证。没问题,我更新了我的代码以使用 OpenID。

在我看来,一切正常,甚至令牌也是正确的。主要问题是重新加载页面后。身份验证令牌丢失,我被验证为匿名用户。如您所见,我/login无论如何都会将用户重定向到(如果用户经过身份验证,我有逻辑从该页面重定向)。

这是我的代码:

SteamListener.php

<?php declare(strict_types = 1);

namespace App\Security\SteamAuth\Firewall;

use App\Security\SteamAuth\Authentication\Token\SteamUserToken;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

/**
 * Class SteamListener
 * @package App\Security\SteamAuth\Firewall
 */
class SteamListener
{
    /**
     * @var AuthenticationManagerInterface
     */
    private $authenticationManager;

    /**
     * @var RouterInterface
     */
    private $router;

    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * SteamListener constructor.
     *
     * @param AuthenticationManagerInterface $authenticationManager
     * @param TokenStorageInterface          $tokenStorage
     * @param RouterInterface                $router
     */
    public function __construct(AuthenticationManagerInterface $authenticationManager, TokenStorageInterface $tokenStorage, RouterInterface $router)
    {
        $this->authenticationManager = $authenticationManager;
        $this->tokenStorage          = $tokenStorage;
        $this->router                = $router;
    }

    /**
     * Try to authenticate user based on SteamID.
     *
     * @param RequestEvent $event
     */
    public function __invoke(RequestEvent $event)
    {
        $request   = $event->getRequest();
        $claimedId = str_replace('https://steamcommunity.com/openid/id/', '', $request->query->get('openid_claimed_id'));

        try {
            $token = new SteamUserToken($claimedId);
            $token->setAttributes($request->query->all());

            $authToken = $this->authenticationManager->authenticate($token);
            $this->tokenStorage->setToken($authToken);
        } catch (AuthenticationException $exception) {
            $token = $this->tokenStorage->getToken();
            if ($token instanceof SteamUserToken) {
                $this->tokenStorage->setToken(null);
            }
        }

        $event->setResponse(new RedirectResponse($this->router->generate('login')));
    }
}

SteamProvider.php

<?php declare(strict_types = 1);

namespace App\Security\SteamAuth\Authentication\Provider;

use App\Security\SteamAuth\Authentication\Token\SteamUserToken;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

/**
 * Class SteamProvider
 * @package App\Security\SteamAuth\Provider
 */
class SteamProvider implements AuthenticationProviderInterface
{
    const ACCEPTED_RESPONSE = "ns:http://specs.openid.net/auth/2.0\nis_valid:true\n";

    /**
     * This actually points to UserRepository.
     *
     * @var UserProviderInterface
     */
    private $userProvider;

    /**
     * SteamProvider constructor.
     *
     * @param UserProviderInterface $userProvider
     */
    public function __construct(UserProviderInterface $userProvider)
    {
        $this->userProvider = $userProvider;
    }

    /**
     * {@inheritDoc}
     *
     * Note: Token is an instance of SteamUserToken.
     */
    public function authenticate(TokenInterface $token)
    {
        if (!$user = $this->userProvider->loadUserByUsername($token->getUsername())) {
            throw new AuthenticationException('Steam auth is invalid!');
        }

        if ($token->getAttribute('openid_ns') !== 'http://specs.openid.net/auth/2.0') {
            throw new AuthenticationException('Steam token is invalid!');
        }

        // Validate SteamID before authenticating user.
        $checkAuth                = $token->getAttributes();
        $checkAuth['openid_mode'] = 'check_authentication';

        try {
            $request  = HttpClient::create();
            $response = $request->request(Request::METHOD_GET, $checkAuth['openid_op_endpoint'], ['query' => $checkAuth]);
            if ($response->getContent() !== self::ACCEPTED_RESPONSE) {
                throw new AuthenticationException('Steam token is invalid!');
            }

            $authToken = new SteamUserToken($token->getUsername(), $user->getRoles());
            $authToken->setUser($user);

            return $authToken;
        } catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface $e) {
            throw new AuthenticationException('Steam token is invalid!');
        }
    }

    /**
     * {@inheritDoc}
     */
    public function supports(TokenInterface $token)
    {
        return $token instanceof SteamUserToken;
    }
}

SteamUserToken.php

<?php declare(strict_types = 1);

namespace App\Security\SteamAuth\Authentication\Token;

use App\Entity\User\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Class SteamUserToken
 * @package App\Security\SteamAuth\Authentication
 */
class SteamUserToken implements TokenInterface
{
    /**
     * @var array
     */
    private $attributes = [];

    /**
     * @var bool
     */
    private $authenticated = false;

    /**
     * @var User|null
     */
    private $user = null;

    /**
     * @var array
     */
    private $roles = [];

    /**
     * @var string|null
     */
    private $steamId = null;

    /**
     * SteamUserToken constructor.
     *
     * @param string $steamId
     * @param array  $roles
     */
    public function __construct(string $steamId, array $roles = [])
    {
        $this->steamId       = $steamId;
        $this->roles         = $roles;
        $this->authenticated = \count($roles) > 0;
    }

    /**
     * {@inheritDoc}
     */
    public function getUser()
    {
        return $this->user;
    }

    /**
     * {@inheritDoc}
     * @var $user UserInterface
     */
    public function setUser($user)
    {
        $this->user = $user;
    }


    /**
     * {@inheritDoc}
     */
    public function getUsername()
    {
        return $this->steamId;
    }

    /**
     * {@inheritDoc}
     */
    public function getRoles()
    {
        if (!$this->user) {
            return [];
        }

        return $this->roles;
    }

    /**
     * @param array $roles
     */
    public function setRoles(array $roles = [])
    {
        $this->roles = $roles;
    }

    /**
     * {@inheritDoc}
     */
    public function getRoleNames()
    {
        return array_map(function ($role) {return $role;}, $this->user ? $this->user->getRoles() : []);
    }

    /**
     * {@inheritDoc}
     */
    public function getCredentials()
    {
        return '';
    }

    /**
     * {@inheritDoc}
     */
    public function isAuthenticated()
    {
        return $this->authenticated;
    }

    /**
     * {@inheritDoc}
     */
    public function setAuthenticated($authenticated)
    {
        $this->authenticated = $authenticated;
    }

    /**
     * {@inheritDoc}
     */
    public function eraseCredentials()
    {
        if ($this->getUser() instanceof UserInterface) {
            $this->getUser()->eraseCredentials();
        }
    }

    /**
     * {@inheritDoc}
     */
    public function serialize()
    {
        return serialize($this->__serialize());
    }

    /**
     * {@inheritDoc}
     */
    public function __serialize(): array
    {
        return [$this->attributes, $this->authenticated, $this->steamId, $this->roles, $this->user];
    }

    /**
     * {@inheritDoc}
     */
    public function unserialize($serialized)
    {
        $this->__unserialize(unserialize($serialized));
    }

    /**
     * {@inheritDoc}
     */
    public function __unserialize(array $data): void
    {
        [$this->attributes, $this->authenticated, $this->steamId, $this->roles, $this->user] = $data;
    }

    /**
     * {@inheritDoc}
     */
    public function getAttributes()
    {
        return $this->attributes;
    }

    /**
     * {@inheritDoc}
     */
    public function setAttributes(array $attributes)
    {
        $this->attributes = $attributes;
    }

    /**
     * {@inheritDoc}
     */
    public function hasAttribute($name)
    {
        return \array_key_exists($name, $this->attributes);
    }

    /**
     * {@inheritDoc}
     */
    public function getAttribute($name)
    {
        if (!\array_key_exists($name, $this->attributes)) {
            throw new \InvalidArgumentException(sprintf('This token has no "%s" attribute.', $name));
        }

        return $this->attributes[$name];
    }

    /**
     * {@inheritDoc}
     */
    public function setAttribute($name, $value)
    {
        $this->attributes[$name] = $value;
    }

    /**
     * {@inheritDoc}
     */
    public function __toString()
    {
        if (!$this->user) {
            return '-';
        }

        return $this->user->getUsername();
    }
}

如果我return;在设置$this->tokenStorage->setToken($authToken)inside之后写SteamListener,我会得到一个错误,但这是奇怪的事情:我在那个页面中被正确地验证了(见附图)。否则,我将被重定向到登录页面。(我没有在该方法中抛出错误,而是调用$this->getUser()并返回了经过身份验证的用户。此外,我尝试将用户重定向到另一个页面,但问题仍然存在)。

错误

编辑:这是我的控制台日志:

2019-10-30T14:32:47+01:00 [debug] Stored the security token in the session.
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.response" to listener "Sonata\BlockBundle\Cache\HttpCacheHandler::onKernelResponse".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\EventListener\ResponseListener::onKernelResponse".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\DataCollector\RequestDataCollector::onKernelResponse".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.response" to listener "Symfony\Component\Security\Http\RememberMe\ResponseListener::onKernelResponse".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.response" to listener "Sensio\Bundle\FrameworkExtraBundle\EventListener\HttpCacheListener::onKernelResponse".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelResponse".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.response" to listener "Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener::onKernelResponse".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener::onResponse".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\EventListener\SessionListener::onKernelResponse".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\EventListener\StreamedResponseListener::onKernelResponse".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.finish_request" to listener "Symfony\Component\HttpKernel\EventListener\LocaleListener::onKernelFinishRequest".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.finish_request" to listener "Symfony\Component\HttpKernel\EventListener\SessionListener::onFinishRequest".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.finish_request" to listener "Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelFinishRequest".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.finish_request" to listener "Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener::onKernelFinishRequest".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.finish_request" to listener "Symfony\Component\HttpKernel\EventListener\LocaleAwareListener::onKernelFinishRequest".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.terminate" to listener "Symfony\Bundle\SwiftmailerBundle\EventListener\EmailSenderListener::onTerminate".
2019-10-30T14:32:47+01:00 [debug] Notified event "kernel.terminate" to listener "Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelTerminate".
[Wed Oct 30 15:32:47 2019] 127.0.0.1:52654 [302]: /steam/login?openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.mode=id_res&openid.op_endpoint=https%3A%2F%2Fsteamcommunity.com%2Fopenid%2Flogin&openid.claimed_id=https%3A%2F%2Fsteamcommunity.com%2Fopenid%2Fid%2F
76561198062372939&openid.identity=https%3A%2F%2Fsteamcommunity.com%2Fopenid%2Fid%2F76561198062372939&openid.return_to=http%3A%2F%2Flocalhost%3A8000%2Fsteam%2Flogin&openid.response_nonce=2019-10-30T13%3A31%3A49ZSGUwWiQNPgQRCry3p3AjonwB7rg%3D&openid.assoc_handle=1234567890
&openid.signed=signed%2Cop_endpoint%2Cclaimed_id%2Cidentity%2Creturn_to%2Cresponse_nonce%2Cassoc_handle&openid.sig=3ysTtN3Cc3FbC8b0oWi8ZTCrc0U%3D
2019-10-30T14:32:49+01:00 [info] User Deprecated: The Symfony\Bundle\TwigBundle\Loader\FilesystemLoader class is deprecated since version 4.3 and will be removed in 5.0; use Twig notation for templates instead.
2019-10-30T14:32:49+01:00 [info] Matched route "login".
2019-10-30T14:32:49+01:00 [info] Populated the TokenStorage with an anonymous Token.

标签: symfonysteam

解决方案


推荐阅读