Completed
Push — master ( c1a2e8...3115b3 )
by Tobias
03:42
created

WsseProvider   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 159
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 14
lcom 1
cbo 9
dl 0
loc 159
ccs 0
cts 68
cp 0
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A authenticate() 0 13 2
B validateDigest() 0 44 5
A supports() 0 4 1
A setLogger() 0 6 1
A log() 0 7 2
A getUser() 0 10 2
1
<?php
2
3
namespace Happyr\ApiBundle\Security\Authentication\Provider;
4
5
use Psr\Cache\CacheItemPoolInterface;
6
use Psr\Log\LoggerInterface;
7
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
8
use Symfony\Component\Security\Core\User\UserProviderInterface;
9
use Symfony\Component\Security\Core\Exception\AuthenticationException;
10
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
11
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
12
use Happyr\ApiBundle\Security\Authentication\Token\WsseUserToken;
13
14
/**
15
 * The authentication provider will do the verification of the WsseUserToken. Namely, the provider will verify the
16
 * Created header value is valid within the specified lifetime, the Nonce header value is unique within the
17
 * specified lifetime, and the PasswordDigest header value matches with the user's password.
18
 *
19
 * @author Toby Ryuk
20
 */
21
class WsseProvider implements AuthenticationProviderInterface
22
{
23
    /**
24
     * @var UserProviderInterface
25
     */
26
    private $userProvider;
27
28
    /**
29
     * @var CacheItemPoolInterface
30
     */
31
    private $cacheService;
32
33
    /**
34
     * @var int
35
     */
36
    private $lifetime;
37
38
    /**
39
     * @var LoggerInterface
40
     */
41
    private $logger;
42
43
    /**
44
     * WsseProvider constructor.
45
     *
46
     * @param UserProviderInterface  $userProvider
47
     * @param CacheItemPoolInterface $cacheService
48
     * @param $lifetime
49
     */
50
    public function __construct(UserProviderInterface $userProvider, CacheItemPoolInterface $cacheService, $lifetime)
51
    {
52
        $this->userProvider = $userProvider;
53
        $this->cacheService = $cacheService;
54
        $this->lifetime = $lifetime;
55
    }
56
57
    /**
58
     * @param WsseUserToken $token
59
     *
60
     * @return WsseUserToken
61
     */
62
    public function authenticate(TokenInterface $token)
63
    {
64
        $user = $this->getUser($token);
65
66
        if ($this->validateDigest($token->getDigest(), $token->getNonce(), $token->getCreated(), $user->getPassword())) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Securi...on\Token\TokenInterface as the method getDigest() does only exist in the following implementations of said interface: Happyr\ApiBundle\Securit...ion\Token\WsseUserToken.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Securi...on\Token\TokenInterface as the method getNonce() does only exist in the following implementations of said interface: Happyr\ApiBundle\Securit...ion\Token\WsseUserToken.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Securi...on\Token\TokenInterface as the method getCreated() does only exist in the following implementations of said interface: Happyr\ApiBundle\Securit...ion\Token\WsseUserToken.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
67
            $authenticatedToken = new WsseUserToken($user->getRoles());
68
            $authenticatedToken->setUser($user);
69
70
            return $authenticatedToken;
71
        }
72
73
        throw new AuthenticationException('The WSSE authentication failed, invalid token.');
74
    }
75
76
    /**
77
     * This function is specific to Wsse authentication and is only used to help this example.
78
     *
79
     * For more information specific to the logic here, see
80
     * https://github.com/symfony/symfony-docs/pull/3134#issuecomment-27699129
81
     */
82
    protected function validateDigest($digest, $nonce, $created, $secret)
83
    {
84
        // Check created time is not in the future
85
        if (strtotime($created) > time()) {
86
            $this->log('error', 'Digest not valid. Created timestamp was in the future');
87
88
            return false;
89
        }
90
91
        // Expire timestamp after time chosen in the configuration
92
        if (time() - strtotime($created) > $this->lifetime) {
93
            $this->log('error', 'Digest not valid. Created timestamp has expired', [
94
                'lifetime' => $this->lifetime,
95
                'created' => $created,
96
                'current_time' => time(),
97
            ]);
98
99
            return false;
100
        }
101
102
        $cacheItem = $this->cacheService->getItem(md5($nonce));
103
        // Validate that the nonce have not been used in it's lifetime
104
        // if it has, this could be a replay attack
105
        if ($cacheItem->isHit()) {
106
            $this->log('error', 'Digest not valid. Nonce already used');
107
108
            throw new NonceExpiredException('Previously used nonce detected');
109
        }
110
111
        // If cache item does not exist, create it
112
        $cacheItem->set(null)->expiresAfter($this->lifetime - (time() - strtotime($created)));
113
        $this->cacheService->save($cacheItem);
114
115
        // Validate Secret
116
        $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));
117
118
        $result = hash_equals($expected, $digest);
119
120
        if (!$result) {
121
            $this->log('error', 'Digest not valid. Wrong data');
122
        }
123
124
        return $result;
125
    }
126
127
    /**
128
     * @param TokenInterface $token
129
     *
130
     * @return bool
131
     */
132
    public function supports(TokenInterface $token)
133
    {
134
        return $token instanceof WsseUserToken;
135
    }
136
137
    /**
138
     * @param LoggerInterface $logger
139
     *
140
     * @return WsseProvider
141
     */
142
    public function setLogger(LoggerInterface $logger)
143
    {
144
        $this->logger = $logger;
145
146
        return $this;
147
    }
148
149
    /**
150
     * @param string $level
151
     * @param string $message
152
     * @param array  $context
153
     */
154
    private function log($level, $message, array $context = [])
155
    {
156
        if ($this->logger === null) {
157
            return;
158
        }
159
        $this->logger->log($level, $message, $context);
160
    }
161
162
    /**
163
     * @param TokenInterface $token
164
     *
165
     * @return \Symfony\Component\Security\Core\User\UserInterface
166
     *
167
     * @throws AuthenticationException
168
     */
169
    protected function getUser(TokenInterface $token)
170
    {
171
        $user = $this->userProvider->loadUserByUsername($token->getUsername());
172
173
        if (null === $user) {
174
            throw new AuthenticationException('User not found.');
175
        }
176
177
        return $user;
178
    }
179
}
180