Passed
Pull Request — master (#45)
by
unknown
12:40
created

Cas20Controller::proxy()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 74
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 41
c 0
b 0
f 0
dl 0
loc 74
rs 8.6417
cc 6
nc 4
nop 3

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\casserver\Controller;
6
7
use SimpleSAML\CAS\Constants as C;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Logger;
10
use SimpleSAML\Module\casserver\Cas\Factories\TicketFactory;
11
use SimpleSAML\Module\casserver\Cas\Protocol\Cas20;
12
use SimpleSAML\Module\casserver\Controller\Traits\UrlTrait;
13
use SimpleSAML\Module\casserver\Http\XmlResponse;
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\HttpFoundation\Response;
16
use Symfony\Component\HttpKernel\Attribute\AsController;
17
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
18
19
#[AsController]
20
class Cas20Controller
21
{
22
    use UrlTrait;
1 ignored issue
show
introduced by
The trait SimpleSAML\Module\casser...troller\Traits\UrlTrait requires some properties which are not provided by SimpleSAML\Module\casser...troller\Cas20Controller: $query, $request
Loading history...
23
24
    /** @var Logger */
25
    protected Logger $logger;
26
27
    /** @var Configuration */
28
    protected Configuration $casConfig;
29
30
    /** @var Cas20 */
31
    protected Cas20 $cas20Protocol;
32
33
    /** @var TicketFactory */
34
    protected TicketFactory $ticketFactory;
35
36
    // this could be any configured ticket store
37
    protected mixed $ticketStore;
38
39
    /**
40
     * @param   Configuration       $sspConfig
41
     * @param   Configuration|null  $casConfig
42
     * @param                       $ticketStore
43
     *
44
     * @throws \Exception
45
     */
46
    public function __construct(
47
        private readonly Configuration $sspConfig,
48
        Configuration $casConfig = null,
49
        $ticketStore = null,
50
    ) {
51
        // We are using this work around in order to bypass Symfony's autowiring for cas configuration. Since
52
        // the configuration class is the same, it loads the ssp configuration twice. Still, we need the constructor
53
        // argument in order to facilitate testing
54
        $this->casConfig = ($casConfig === null || $casConfig === $sspConfig)
55
            ? Configuration::getConfig('module_casserver.php') : $casConfig;
56
        $this->cas20Protocol = new Cas20($this->casConfig);
57
        /* Instantiate ticket factory */
58
        $this->ticketFactory = new TicketFactory($this->casConfig);
59
        /* Instantiate ticket store */
60
        $ticketStoreConfig = $this->casConfig->getOptionalValue(
61
            'ticketstore',
62
            ['class' => 'casserver:FileSystemTicketStore'],
63
        );
64
        $ticketStoreClass  = 'SimpleSAML\\Module\\casserver\\Cas\\Ticket\\'
65
            . explode(':', $ticketStoreConfig['class'])[1];
66
        $this->ticketStore = $ticketStore ?? new $ticketStoreClass($this->casConfig);
67
    }
68
69
    /**
70
     * @param   Request      $request
71
     * @param   string       $TARGET
72
     * @param   bool  $renew  [OPTIONAL] - if this parameter is set, ticket validation will only succeed
73
     *                        if the service ticket was issued from the presentation of the user’s primary
74
     *                        credentials. It will fail if the ticket was issued from a single sign-on session.
75
     * @param   string|null  $ticket  [REQUIRED] - the service ticket issued by /login
76
     * @param   string|null  $service [REQUIRED] - the identifier of the service for which the ticket was issued
77
     * @param   string|null  $pgtUrl  [OPTIONAL] - the URL of the proxy callback
78
     *
79
     * @return XmlResponse
80
     */
81
    public function serviceValidate(
82
        Request $request,
83
        #[MapQueryParameter] string $TARGET = '',
84
        #[MapQueryParameter] bool $renew = false,
85
        #[MapQueryParameter] ?string $ticket = null,
86
        #[MapQueryParameter] ?string $service = null,
87
        #[MapQueryParameter] ?string $pgtUrl = null,
88
    ): XmlResponse {
89
        return $this->validate(
90
            request: $request,
91
            method:  'serviceValidate',
92
            target:  $TARGET,
93
            renew:   $renew,
94
            ticket:  $ticket,
95
            service: $service,
96
            pgtUrl:  $pgtUrl,
97
        );
98
    }
99
100
    /**
101
     * /proxy provides proxy tickets to services that have
102
     * acquired proxy-granting tickets and will be proxying authentication to back-end services.
103
     *
104
     * @param   Request      $request
105
     * @param   string|null  $targetService [REQUIRED] - the service identifier of the back-end service.
106
     * @param   string|null  $pgt  [REQUIRED] - the proxy-granting ticket acquired by the service
107
     *                             during service ticket or proxy ticket validation.
108
     *
109
     * @return XmlResponse
110
     */
111
    public function proxy(
112
        Request $request,
113
        #[MapQueryParameter] ?string $targetService = null,
114
        #[MapQueryParameter] ?string $pgt = null,
115
    ): XmlResponse {
116
        $legal_target_service_urls = $this->casConfig->getOptionalValue('legal_target_service_urls', []);
117
        // Fail if
118
        $message = match (true) {
119
            // targetService pareameter is not defined
120
            $targetService === null => 'Missing target service parameter [targetService]',
121
            // pgt parameter is not defined
122
            $pgt === null => 'Missing proxy granting ticket parameter: [pgt]',
123
            !$this->checkServiceURL($this->sanitize($targetService), $legal_target_service_urls) =>
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\Module\casser...ller::checkServiceURL() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

123
            !/** @scrutinizer ignore-deprecated */ $this->checkServiceURL($this->sanitize($targetService), $legal_target_service_urls) =>
Loading history...
Bug introduced by
It seems like $targetService can also be of type null; however, parameter $parameter of SimpleSAML\Module\casser...0Controller::sanitize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

123
            !$this->checkServiceURL($this->sanitize(/** @scrutinizer ignore-type */ $targetService), $legal_target_service_urls) =>
Loading history...
124
                "Target service parameter not listed as a legal service: [targetService] = {$targetService}",
125
            default => null,
126
        };
127
128
        if (!empty($message)) {
129
            return new XmlResponse(
130
                (string)$this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_REQUEST, $message),
131
                Response::HTTP_BAD_REQUEST,
132
            );
133
        }
134
135
        // Get the ticket
136
        $proxyGrantingTicket = $this->ticketStore->getTicket($pgt);
137
        $message = match (true) {
138
            // targetService parameter is not defined
139
            $proxyGrantingTicket === null => "Ticket {$pgt} not recognized",
140
            // pgt parameter is not defined
141
            !$this->ticketFactory->isProxyGrantingTicket($proxyGrantingTicket)
142
            => "Not a valid proxy granting ticket id: {$pgt}",
143
            default => null,
144
        };
145
146
        if (!empty($message)) {
147
            return new XmlResponse(
148
                (string)$this->cas20Protocol->getValidateFailureResponse('BAD_PGT', $message),
149
                Response::HTTP_BAD_REQUEST,
150
            );
151
        }
152
153
        // Get the session id from the ticket
154
        $sessionTicket = $this->ticketStore->getTicket($proxyGrantingTicket['sessionId']);
155
156
        if (
157
            $sessionTicket === null
158
            || $this->ticketFactory->isSessionTicket($sessionTicket) === false
159
            || $this->ticketFactory->isExpired($sessionTicket)
160
        ) {
161
            $message = "Ticket {$pgt} has expired";
162
            Logger::debug('casserver:' . $message);
163
164
            return new XmlResponse(
165
                (string)$this->cas20Protocol->getValidateFailureResponse('BAD_PGT', $message),
166
                Response::HTTP_BAD_REQUEST,
167
            );
168
        }
169
170
        $proxyTicket = $this->ticketFactory->createProxyTicket(
171
            [
172
                'service' => $targetService,
173
                'forceAuthn' => $proxyGrantingTicket['forceAuthn'],
174
                'attributes' => $proxyGrantingTicket['attributes'],
175
                'proxies' => $proxyGrantingTicket['proxies'],
176
                'sessionId' => $proxyGrantingTicket['sessionId'],
177
                ],
178
        );
179
180
        $this->ticketStore->addTicket($proxyTicket);
181
182
        return new XmlResponse(
183
            (string)$this->cas20Protocol->getProxySuccessResponse($proxyTicket['id']),
184
            Response::HTTP_OK,
185
        );
186
    }
187
188
    /**
189
     * @param   Request      $request
190
     * @param   string       $TARGET  // todo: this should go away???
191
     * @param   bool  $renew  [OPTIONAL] - if this parameter is set, ticket validation will only succeed
192
     *                        if the service ticket was issued from the presentation of the user’s primary
193
     *                        credentials. It will fail if the ticket was issued from a single sign-on session.
194
     * @param   string|null  $ticket  [REQUIRED] - the service ticket issued by /login
195
     * @param   string|null  $service  [REQUIRED] - the identifier of the service for which the ticket was issued
196
     * @param   string|null  $pgtUrl  [OPTIONAL] - the URL of the proxy callback
197
     * @return XmlResponse
198
     */
199
    public function proxyValidate(
200
        Request $request,
201
        #[MapQueryParameter] string $TARGET = '',
202
        #[MapQueryParameter] bool $renew = false,
203
        #[MapQueryParameter] ?string $ticket = null,
204
        #[MapQueryParameter] ?string $service = null,
205
        #[MapQueryParameter] ?string $pgtUrl = null,
206
    ): XmlResponse {
207
        return $this->validate(
208
            request: $request,
209
            method:  'proxyValidate',
210
            target:  $TARGET,
211
            renew:   $renew,
212
            ticket:  $ticket,
213
            service: $service,
214
            pgtUrl:  $pgtUrl,
215
        );
216
    }
217
218
    /**
219
     * @param   Request      $request
220
     * @param   string       $method
221
     * @param   string       $target
222
     * @param   bool         $renew
223
     * @param   string|null  $ticket
224
     * @param   string|null  $service
225
     * @param   string|null  $pgtUrl
226
     *
227
     * @return XmlResponse
228
     */
229
    public function validate(
230
        Request $request,
231
        string $method,
232
        string $target,
233
        bool $renew = false,
234
        ?string $ticket = null,
235
        ?string $service = null,
236
        ?string $pgtUrl = null,
237
    ): XmlResponse {
238
        $forceAuthn = $renew;
239
        // todo: According to the protocol, there is no target??? Why are we using it?
240
        $serviceUrl = $service ?? $target ?? null;
241
242
        // Check if any of the required query parameters are missing
243
        if ($serviceUrl === null || $ticket === null) {
244
            $messagePostfix = $serviceUrl === null ? 'service' : 'ticket';
245
            $message        = "casserver: Missing service parameter: [{$messagePostfix}]";
246
            Logger::debug($message);
247
248
            return new XmlResponse(
249
                (string)$this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message),
250
                Response::HTTP_BAD_REQUEST,
251
            );
252
        }
253
254
        try {
255
            // Get the service ticket
256
            // `getTicket` uses the unserializable method and Objects may throw Throwables in their
257
            // unserialization handlers.
258
            $serviceTicket = $this->ticketStore->getTicket($ticket);
259
            // Delete the ticket
260
            $this->ticketStore->deleteTicket($ticket);
261
        } catch (\Exception $e) {
262
            $message = 'casserver:serviceValidate: internal server error. ' . var_export($e->getMessage(), true);
263
            Logger::error($message);
264
265
            return new XmlResponse(
266
                (string)$this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message),
267
                Response::HTTP_INTERNAL_SERVER_ERROR,
268
            );
269
        }
270
271
        $failed  = false;
272
        $message = '';
273
        if (empty($serviceTicket)) {
274
            // No ticket
275
            $message = 'ticket: ' . var_export($ticket, true) . ' not recognized';
276
            $failed  = true;
277
        } elseif ($method === 'serviceValidate' && $this->ticketFactory->isProxyTicket($serviceTicket)) {
278
            $message = 'Ticket ' . var_export($_GET['ticket'], true) .
279
                ' is a proxy ticket. Use proxyValidate instead.';
280
            $failed  = true;
281
        } elseif (!$this->ticketFactory->isServiceTicket($serviceTicket)) {
282
            // This is not a service ticket
283
            $message = 'ticket: ' . var_export($ticket, true) . ' is not a service ticket';
284
            $failed  = true;
285
        } elseif ($this->ticketFactory->isExpired($serviceTicket)) {
286
            // the ticket has expired
287
            $message = 'Ticket has ' . var_export($ticket, true) . ' expired';
288
            $failed  = true;
289
        } elseif ($this->sanitize($serviceTicket['service']) !== $this->sanitize($serviceUrl)) {
290
            // The service url we passed to the query parameters does not match the one in the ticket.
291
            $message = 'Mismatching service parameters: expected ' .
292
                var_export($serviceTicket['service'], true) .
293
                ' but was: ' . var_export($serviceUrl, true);
294
            $failed  = true;
295
        } elseif ($forceAuthn && !$serviceTicket['forceAuthn']) {
296
            // If `forceAuthn` is required but not set in the ticket
297
            $message = 'Ticket was issued from single sign on session';
298
            $failed  = true;
299
        }
300
301
        if ($failed) {
302
            $finalMessage = 'casserver:validate: ' . $message;
303
            Logger::error($finalMessage);
304
305
            return new XmlResponse(
306
                (string)$this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message),
307
                Response::HTTP_BAD_REQUEST,
308
            );
309
        }
310
311
        $attributes = $serviceTicket['attributes'];
312
        $this->cas20Protocol->setAttributes($attributes);
313
314
        if (isset($pgtUrl)) {
315
            $sessionTicket = $this->ticketStore->getTicket($serviceTicket['sessionId']);
316
            if (
317
                $sessionTicket !== null
318
                && $this->ticketFactory->isSessionTicket($sessionTicket)
319
                && !$this->ticketFactory->isExpired($sessionTicket)
320
            ) {
321
                $proxyGrantingTicket = $this->ticketFactory->createProxyGrantingTicket(
322
                    [
323
                        'userName' => $serviceTicket['userName'],
324
                        'attributes' => $attributes,
325
                        'forceAuthn' => false,
326
                        'proxies' => array_merge(
327
                            [$serviceUrl],
328
                            $serviceTicket['proxies'],
329
                        ),
330
                        'sessionId' => $serviceTicket['sessionId'],
331
                    ],
332
                );
333
                try {
334
                    $this->httpUtils->fetch(
0 ignored issues
show
Bug Best Practice introduced by
The property httpUtils does not exist on SimpleSAML\Module\casser...troller\Cas20Controller. Did you maybe forget to declare it?
Loading history...
335
                        $pgtUrl . '?pgtIou=' . $proxyGrantingTicket['iou'] . '&pgtId=' . $proxyGrantingTicket['id'],
336
                    );
337
338
                    $this->cas20Protocol->setProxyGrantingTicketIOU($proxyGrantingTicket['iou']);
339
340
                    $this->ticketStore->addTicket($proxyGrantingTicket);
341
                } catch (\Exception $e) {
342
                    // Fall through
343
                }
344
            }
345
        }
346
347
        return new XmlResponse(
348
            (string)$this->cas20Protocol->getValidateSuccessResponse($serviceTicket['userName']),
349
            Response::HTTP_OK,
350
        );
351
    }
352
}
353