Completed
Pull Request — master (#774)
by
unknown
12:17
created

SSOGrant::setVerifyCredentialsCallback()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
/**
3
 * OAuth 2.0 SSO grant
4
 *
5
 * @package     league/oauth2-server
6
 * @author      Luis Cordero H. ([email protected])
7
 * @license     http://mit-license.org/
8
 * @link        https://github.com/thephpleague/oauth2-server
9
 */
10
11
namespace LucaDegasperi\OAuth2Server\Grant;
12
13
use League\OAuth2\Server\Grant\AbstractGrant;
14
use League\OAuth2\Server\Entity\AccessTokenEntity;
15
use League\OAuth2\Server\Entity\ClientEntity;
16
use League\OAuth2\Server\Entity\RefreshTokenEntity;
17
use League\OAuth2\Server\Entity\SessionEntity;
18
use League\OAuth2\Server\Event;
19
use League\OAuth2\Server\Exception;
20
use League\OAuth2\Server\Util\SecureKey;
21
22
/**
23
 * Password grant class
24
 */
25
class SSOGrant extends AbstractGrant
26
{
27
    /**
28
    * Define constants for the name of properties expected into the credentials.
29
    */
30
    const  SSO_IDENTITY_FIELD = 'identity';
31
    const SSO_REDIRECT_URI_FIELD = 'redirect_uri';
32
    const SSO_SIGNATURE_FIELD = 'signature';
33
    const SSO_CLIENT_FIELD = 'client_app';
34
    const SSO_NONCE_FIELD = 'nonce';
35
    const SSO_ORGANIZATION_FIELD = 'organization';
36
37
    /**
38
     * Grant identifier
39
     *
40
     * @var string
41
     */
42
    protected $identifier = 'password';
43
44
    /**
45
     * Response type
46
     *
47
     * @var string
48
     */
49
    protected $responseType;
50
51
    /**
52
     * Callback to authenticate a user's name and password
53
     *
54
     * @var callable
55
     */
56
    protected $callback;
57
58
    /**
59
     * Access token expires in override
60
     *
61
     * @var int
62
     */
63
    protected $accessTokenTTL;
64
65
    /**
66
     * Set the callback to verify a user's username and password
67
     *
68
     * @param callable $callback The callback function
69
     *
70
     * @return void
71
     */
72
    public function setVerifyCredentialsCallback(callable $callback)
73
    {
74
        $this->callback = $callback;
75
    }
76
77
    /**
78
     * Return the callback function
79
     *
80
     * @return callable
81
     *
82
     * @throws
83
     */
84 View Code Duplication
    protected function getVerifyCredentialsCallback()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
85
    {
86
        if (is_null($this->callback) || !is_callable($this->callback))
87
        {
88
            throw new Exception\ServerErrorException('Null or non-callable callback set on SSO grant');
89
        }
90
91
        return $this->callback;
92
    }
93
94
    /**
95
     * Complete the password grant
96
     *
97
     * @return array
98
     *
99
     * @throws
100
     */
101
    public function completeFlow()
102
    {
103
        // Get the required params
104
        $clientId = $this->server->getRequest()->request->get('client_id', $this->server->getRequest()->getUser());
105
        if (is_null($clientId)) {
106
            throw new Exception\InvalidRequestException('client_id');
107
        }
108
109
        $clientSecret = $this->server->getRequest()->request->get('client_secret',
110
            $this->server->getRequest()->getPassword());
111
        if (is_null($clientSecret)) {
112
            throw new Exception\InvalidRequestException('client_secret');
113
        }
114
115
        $identity = $this->server->getRequest()->request->get(self::SSO_IDENTITY_FIELD, null);
116
        if (is_null($identity))
117
        {
118
            throw new Exception\InvalidRequestException('identity');
119
        }
120
121
        $redirect_uri = $this->server->getRequest()->request->get(self::SSO_REDIRECT_URI_FIELD, null);
122
        if ( is_null($redirect_uri) )
123
        {
124
            throw new Exception\InvalidRequestException('redirect_uri');
125
        }
126
127
        $signature = $this->server->getRequest()->request->get(self::SSO_SIGNATURE_FIELD, null);
128
        if ( is_null($signature) )
129
        {
130
            throw new Exception\InvalidRequestException('signature');
131
        }
132
133
        $client_app = $this->server->getRequest()->request->get(self::SSO_CLIENT_FIELD, null);
134
        if ( is_null($client_app) )
135
        {
136
            throw new Exception\InvalidRequestException('client_app');
137
        }
138
139
        $nonce = $this->server->getRequest()->request->get(self::SSO_NONCE_FIELD, null);
140
        if ( is_null($nonce) )
141
        {
142
            throw new Exception\InvalidRequestException('nonce');
143
        }
144
145
        $organization = $this->server->getRequest()->request->get(self::SSO_ORGANIZATION_FIELD, null);
146
        if ( is_null($organization) )
147
        {
148
            throw new Exception\InvalidRequestException('organization');
149
        }
150
151
        // Validate client ID and client secret
152
        $client = $this->server->getClientStorage()->get(
153
            $clientId,
154
            $clientSecret,
155
            null,
156
            $this->getIdentifier()
157
        );
158
159 View Code Duplication
        if (($client instanceof ClientEntity) === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
160
            $this->server->getEventEmitter()->emit(new Event\ClientAuthenticationFailedEvent($this->server->getRequest()));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 123 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
161
            throw new Exception\InvalidClientException();
162
        }
163
164
        $credentials = [
165
            self::SSO_IDENTITY_FIELD => $identity,
166
            self::SSO_REDIRECT_URI_FIELD => $redirect_uri,
167
            self::SSO_SIGNATURE_FIELD => $signature,
168
            self::SSO_CLIENT_FIELD => $client_app,
169
            self::SSO_NONCE_FIELD => $nonce,
170
            self::SSO_ORGANIZATION_FIELD => $organization
171
        ];
172
173
        // Check if user's username and password are correct
174
        $userId = call_user_func($this->getVerifyCredentialsCallback(), $credentials );
175
176
        if ($userId === false) {
177
            $this->server->getEventEmitter()->emit(new Event\UserAuthenticationFailedEvent($this->server->getRequest()));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
178
            throw new Exception\InvalidCredentialsException();
179
        }
180
181
        // Validate any scopes that are in the request
182
        $scopeParam = $this->server->getRequest()->request->get('scope', '');
183
        $scopes = $this->validateScopes($scopeParam, $client);
184
185
        // Create a new session
186
        $session = new SessionEntity($this->server);
187
        $session->setOwner('user', $userId);
188
        $session->associateClient($client);
189
190
        // Generate an access token
191
        $accessToken = new AccessTokenEntity($this->server);
192
        $accessToken->setId(SecureKey::generate());
193
        $accessToken->setExpireTime($this->getAccessTokenTTL() + time());
194
195
        // Associate scopes with the session and access token
196
        foreach ($scopes as $scope) {
197
            $session->associateScope($scope);
198
        }
199
200
        foreach ($session->getScopes() as $scope) {
201
            $accessToken->associateScope($scope);
202
        }
203
204
        $this->server->getTokenType()->setSession($session);
205
        $this->server->getTokenType()->setParam('access_token', $accessToken->getId());
206
        $this->server->getTokenType()->setParam('expires_in', $this->getAccessTokenTTL());
207
208
        // Associate a refresh token if set
209
        if ($this->server->hasGrantType('refresh_token')) {
210
            $refreshToken = new RefreshTokenEntity($this->server);
211
            $refreshToken->setId(SecureKey::generate());
212
            $refreshToken->setExpireTime($this->server->getGrantType('refresh_token')->getRefreshTokenTTL() + time());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface League\OAuth2\Server\Grant\GrantTypeInterface as the method getRefreshTokenTTL() does only exist in the following implementations of said interface: League\OAuth2\Server\Grant\RefreshTokenGrant, LucaDegasperi\OAuth2Server\Grant\RefreshTokenGrant.

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...
213
            $this->server->getTokenType()->setParam('refresh_token', $refreshToken->getId());
214
        }
215
216
        // Save everything
217
        $session->save();
218
        $accessToken->setSession($session);
219
        $accessToken->save();
220
221
        if ($this->server->hasGrantType('refresh_token')) {
222
            $refreshToken->setAccessToken($accessToken);
0 ignored issues
show
Bug introduced by
The variable $refreshToken does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
223
            $refreshToken->save();
224
        }
225
226
        return $this->server->getTokenType()->generateResponse();
227
    }
228
}
229