Completed
Push — fallback_language_fields ( 531860 )
by André
37:34
created

RestAuthenticator::addLogoutHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
/**
4
 * File containing the RestSessionBasedAuthenticationListener class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 *
9
 * @version //autogentag//
10
 */
11
namespace eZ\Publish\Core\REST\Server\Security;
12
13
use eZ\Publish\Core\MVC\ConfigResolverInterface;
14
use eZ\Publish\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface;
15
use eZ\Publish\Core\MVC\Symfony\Security\UserInterface as EzUser;
16
use eZ\Publish\Core\REST\Server\Exceptions\InvalidUserTypeException;
17
use eZ\Publish\Core\REST\Server\Exceptions\UserConflictException;
18
use Psr\Log\LoggerInterface;
19
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
20
use Symfony\Component\HttpFoundation\Request;
21
use Symfony\Component\HttpFoundation\Response;
22
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface;
23
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
24
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
25
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
26
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
27
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
28
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
29
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
30
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
31
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
32
use Symfony\Component\Security\Http\Logout\SessionLogoutHandler;
33
use Symfony\Component\Security\Http\SecurityEvents;
34
35
/**
36
 * Authenticator for REST API, mainly used for session based authentication (session creation resource).
37
 *
38
 * Implements \Symfony\Component\Security\Http\Firewall\ListenerInterface to be able to receive the provider key
39
 * (firewall identifier from configuration).
40
 */
41
class RestAuthenticator implements ListenerInterface, AuthenticatorInterface
42
{
43
    /**
44
     * @var \Psr\Log\LoggerInterface
45
     */
46
    private $logger;
47
48
    /**
49
     * @var \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface
50
     */
51
    private $authenticationManager;
52
    /**
53
     * @var string
54
     */
55
    private $providerKey;
56
57
    /**
58
     * @var \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface
59
     */
60
    private $tokenStorage;
61
62
    /**
63
     * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
64
     */
65
    private $dispatcher;
66
67
    /**
68
     * @var \eZ\Publish\Core\MVC\ConfigResolverInterface
69
     */
70
    private $configResolver;
71
72
    /**
73
     * @var \Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface
74
     */
75
    private $sessionStorage;
76
77
    /**
78
     * @var \Symfony\Component\Security\Http\Logout\LogoutHandlerInterface[]
79
     */
80
    private $logoutHandlers = array();
81
82 View Code Duplication
    public function __construct(
83
        TokenStorageInterface $tokenStorage,
84
        AuthenticationManagerInterface $authenticationManager,
85
        $providerKey,
86
        EventDispatcherInterface $dispatcher,
87
        ConfigResolverInterface $configResolver,
88
        SessionStorageInterface $sessionStorage,
89
        LoggerInterface $logger = null
90
    ) {
91
        $this->tokenStorage = $tokenStorage;
92
        $this->authenticationManager = $authenticationManager;
93
        $this->providerKey = $providerKey;
94
        $this->dispatcher = $dispatcher;
95
        $this->configResolver = $configResolver;
96
        $this->sessionStorage = $sessionStorage;
97
        $this->logger = $logger;
98
    }
99
100
    /**
101
     * Doesn't do anything as we don't use this service with main Firewall listener.
102
     *
103
     * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
104
     */
105
    public function handle(GetResponseEvent $event)
106
    {
107
        return;
108
    }
109
110
    public function authenticate(Request $request)
111
    {
112
        // If a token already exists and username is the same as the one we request authentication for,
113
        // then return it and mark it as coming from session.
114
        $previousToken = $this->tokenStorage->getToken();
115
        if (
116
            $previousToken instanceof TokenInterface
117
            && $previousToken->getUsername() === $request->attributes->get('username')
118
        ) {
119
            $previousToken->setAttribute('isFromSession', true);
120
121
            return $previousToken;
122
        }
123
124
        $token = $this->attemptAuthentication($request);
125
        if (!$token instanceof TokenInterface) {
126
            if ($this->logger) {
127
                $this->logger->error('REST: No token could be found in SecurityContext');
128
            }
129
130
            throw new TokenNotFoundException();
131
        }
132
133
        $this->tokenStorage->setToken($token);
134
        $this->dispatcher->dispatch(
135
            SecurityEvents::INTERACTIVE_LOGIN,
136
            new InteractiveLoginEvent($request, $token)
137
        );
138
139
        // Re-fetch token from SecurityContext since an INTERACTIVE_LOGIN listener might have changed it
140
        // i.e. when using multiple user providers.
141
        // @see \eZ\Publish\Core\MVC\Symfony\Security\EventListener\SecurityListener::onInteractiveLogin()
142
        $token = $this->tokenStorage->getToken();
143
        $user = $token->getUser();
144
        if (!$user instanceof EzUser) {
145
            if ($this->logger) {
146
                $this->logger->error('REST: Authenticated user must be eZ\Publish\Core\MVC\Symfony\Security\User, got ' . is_string($user) ? $user : get_class($user));
147
            }
148
149
            $e = new InvalidUserTypeException('Authenticated user is not an eZ User.');
150
            $e->setToken($token);
0 ignored issues
show
Bug introduced by
It seems like $token defined by $this->tokenStorage->getToken() on line 142 can be null; however, Symfony\Component\Securi...onException::setToken() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
151
            throw $e;
152
        }
153
154
        // Check if newly logged in user differs from previous one.
155
        if ($this->isUserConflict($user, $previousToken)) {
156
            $this->tokenStorage->setToken($previousToken);
157
            throw new UserConflictException();
158
        }
159
160
        return $token;
161
    }
162
163
    /**
164
     * @param \Symfony\Component\HttpFoundation\Request $request
165
     *
166
     * @return \Symfony\Component\Security\Core\Authentication\Token\TokenInterface
167
     */
168
    private function attemptAuthentication(Request $request)
169
    {
170
        return $this->authenticationManager->authenticate(
171
            new UsernamePasswordToken(
172
                $request->attributes->get('username'),
173
                $request->attributes->get('password'),
174
                $this->providerKey
175
            )
176
        );
177
    }
178
179
    /**
180
     * Checks if newly matched user is conflicting with previously non-anonymous logged in user, if any.
181
     *
182
     * @param EzUser $user
183
     * @param TokenInterface $previousToken
184
     *
185
     * @return bool
186
     */
187
    private function isUserConflict(EzUser $user, TokenInterface $previousToken = null)
188
    {
189
        if ($previousToken === null || !$previousToken instanceof UsernamePasswordToken) {
190
            return false;
191
        }
192
193
        $previousUser = $previousToken->getUser();
194
        if (!$previousUser instanceof EzUser) {
195
            return false;
196
        }
197
198
        $wasAnonymous = $previousUser->getAPIUser()->id == $this->configResolver->getParameter('anonymous_user_id');
199
        // TODO: isEqualTo is not on the interface
200
        return (!$wasAnonymous && !$user->isEqualTo($previousUser));
201
    }
202
203
    public function addLogoutHandler(LogoutHandlerInterface $handler)
204
    {
205
        $this->logoutHandlers[] = $handler;
206
    }
207
208
    public function logout(Request $request)
209
    {
210
        $response = new Response();
211
212
        // Manually clear the session through session storage.
213
        // Session::invalidate() is not called on purpose, to avoid unwanted session migration that would imply
214
        // generation of a new session id.
215
        // REST logout must indeed clear the session cookie.
216
        // See \eZ\Publish\Core\REST\Server\Security\RestLogoutHandler
217
        $this->sessionStorage->clear();
218
219
        $token = $this->tokenStorage->getToken();
220
        foreach ($this->logoutHandlers as $handler) {
221
            // Explicitly ignore SessionLogoutHandler as we do session invalidation manually here,
222
            // through the session storage, to avoid unwanted session migration.
223
            if ($handler instanceof SessionLogoutHandler) {
224
                continue;
225
            }
226
227
            $handler->logout($request, $response, $token);
0 ignored issues
show
Bug introduced by
It seems like $token defined by $this->tokenStorage->getToken() on line 219 can be null; however, Symfony\Component\Securi...dlerInterface::logout() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
228
        }
229
230
        return $response;
231
    }
232
}
233