Passed
Pull Request — master (#45)
by
unknown
15:00
created

Cas20Controller::validate()   D

Complexity

Conditions 19
Paths 53

Size

Total Lines 138
Code Lines 84

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 84
c 1
b 0
f 0
dl 0
loc 138
rs 4.5166
cc 19
nc 53
nop 7

How to fix   Long Method    Complexity   

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 // todo: this should go away
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
     * @param   Request      $request
102
     * @param   string       $TARGET  // todo: this should go away???
103
     * @param   bool  $renew  [OPTIONAL] - if this parameter is set, ticket validation will only succeed
104
     *                        if the service ticket was issued from the presentation of the user’s primary
105
     *                        credentials. It will fail if the ticket was issued from a single sign-on session.
106
     * @param   string|null  $ticket  [REQUIRED] - the service ticket issued by /login
107
     * @param   string|null  $service  [REQUIRED] - the identifier of the service for which the ticket was issued
108
     * @param   string|null  $pgtUrl  [OPTIONAL] - the URL of the proxy callback
109
     * @return XmlResponse
110
     */
111
    public function proxyValidate(
112
        Request $request,
113
        #[MapQueryParameter] string $TARGET = '',
114
        #[MapQueryParameter] bool $renew = false,
115
        #[MapQueryParameter] ?string $ticket = null,
116
        #[MapQueryParameter] ?string $service = null,
117
        #[MapQueryParameter] ?string $pgtUrl = null,
118
    ): XmlResponse {
119
        return $this->validate(
120
            request: $request,
121
            method:  'proxyValidate',
122
            target:  $TARGET,
123
            renew:   $renew,
124
            ticket:  $ticket,
125
            service: $service,
126
            pgtUrl:  $pgtUrl,
127
        );
128
    }
129
130
    /**
131
     * @param   Request      $request
132
     * @param   string       $method
133
     * @param   string       $target
134
     * @param   bool         $renew
135
     * @param   string|null  $ticket
136
     * @param   string|null  $service
137
     * @param   string|null  $pgtUrl
138
     *
139
     * @return XmlResponse
140
     */
141
    public function validate(
142
        Request $request,
143
        string $method,
144
        string $target,
145
        bool $renew = false,
146
        ?string $ticket = null,
147
        ?string $service = null,
148
        ?string $pgtUrl = null,
149
    ): XmlResponse {
150
        $forceAuthn = $renew;
151
        // todo: According to the protocol, there is no target??? Why are we using it?
152
        $serviceUrl = $service ?? $target ?? null;
153
154
        // Check if any of the required query parameters are missing
155
        if ($serviceUrl === null || $ticket === null) {
156
            $messagePostfix = $serviceUrl === null ? 'service' : 'ticket';
157
            $message        = "casserver: Missing service parameter: [{$messagePostfix}]";
158
            Logger::debug($message);
159
160
            ob_start();
161
            echo $this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message);
162
            $responseContent = ob_get_clean();
163
164
            return new XmlResponse(
165
                $responseContent,
166
                Response::HTTP_BAD_REQUEST,
167
            );
168
        }
169
170
        try {
171
            // Get the service ticket
172
            // `getTicket` uses the unserializable method and Objects may throw Throwables in their
173
            // unserialization handlers.
174
            $serviceTicket = $this->ticketStore->getTicket($ticket);
175
            // Delete the ticket
176
            $this->ticketStore->deleteTicket($ticket);
177
        } catch (\Exception $e) {
178
            $message = 'casserver:serviceValidate: internal server error. ' . var_export($e->getMessage(), true);
179
            Logger::error($message);
180
181
            ob_start();
182
            echo $this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message);
183
            $responseContent = ob_get_clean();
184
185
            return new XmlResponse(
186
                $responseContent,
187
                Response::HTTP_INTERNAL_SERVER_ERROR,
188
            );
189
        }
190
191
        $failed  = false;
192
        $message = '';
193
        if (empty($serviceTicket)) {
194
            // No ticket
195
            $message = 'ticket: ' . var_export($ticket, true) . ' not recognized';
196
            $failed  = true;
197
        } elseif ($method === 'serviceValidate' && $this->ticketFactory->isProxyTicket($serviceTicket)) {
198
            $message = 'Ticket ' . var_export($_GET['ticket'], true) .
199
                ' is a proxy ticket. Use proxyValidate instead.';
200
            $failed  = true;
201
        } elseif (!$this->ticketFactory->isServiceTicket($serviceTicket)) {
202
            // This is not a service ticket
203
            $message = 'ticket: ' . var_export($ticket, true) . ' is not a service ticket';
204
            $failed  = true;
205
        } elseif ($this->ticketFactory->isExpired($serviceTicket)) {
206
            // the ticket has expired
207
            $message = 'Ticket has ' . var_export($ticket, true) . ' expired';
208
            $failed  = true;
209
        } elseif ($this->sanitize($serviceTicket['service']) !== $this->sanitize($serviceUrl)) {
210
            // The service url we passed to the query parameters does not match the one in the ticket.
211
            $message = 'Mismatching service parameters: expected ' .
212
                var_export($serviceTicket['service'], true) .
213
                ' but was: ' . var_export($serviceUrl, true);
214
            $failed  = true;
215
        } elseif ($forceAuthn && !$serviceTicket['forceAuthn']) {
216
            // If `forceAuthn` is required but not set in the ticket
217
            $message = 'Ticket was issued from single sign on session';
218
            $failed  = true;
219
        }
220
221
        if ($failed) {
222
            $finalMessage = 'casserver:validate: ' . $message;
223
            Logger::error($finalMessage);
224
225
            ob_start();
226
            echo $this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message);
227
            $responseContent = ob_get_clean();
228
229
            return new XmlResponse(
230
                $responseContent,
231
                Response::HTTP_BAD_REQUEST,
232
            );
233
        }
234
235
        $attributes = $serviceTicket['attributes'];
236
        $this->cas20Protocol->setAttributes($attributes);
237
238
        if (isset($pgtUrl)) {
239
            $sessionTicket = $this->ticketStore->getTicket($serviceTicket['sessionId']);
240
            if (
241
                $sessionTicket !== null
242
                && $this->ticketFactory->isSessionTicket($sessionTicket)
243
                && !$this->ticketFactory->isExpired($sessionTicket)
244
            ) {
245
                $proxyGrantingTicket = $this->ticketFactory->createProxyGrantingTicket(
246
                    [
247
                        'userName' => $serviceTicket['userName'],
248
                        'attributes' => $attributes,
249
                        'forceAuthn' => false,
250
                        'proxies' => array_merge(
251
                            [$serviceUrl],
252
                            $serviceTicket['proxies'],
253
                        ),
254
                        'sessionId' => $serviceTicket['sessionId'],
255
                    ],
256
                );
257
                try {
258
                    $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...
259
                        $pgtUrl . '?pgtIou=' . $proxyGrantingTicket['iou'] . '&pgtId=' . $proxyGrantingTicket['id'],
260
                    );
261
262
                    $this->cas20Protocol->setProxyGrantingTicketIOU($proxyGrantingTicket['iou']);
263
264
                    $this->ticketStore->addTicket($proxyGrantingTicket);
265
                } catch (\Exception $e) {
266
                    // Fall through
267
                }
268
            }
269
        }
270
271
        // TODO: Replace with string casting
272
        ob_start();
273
        echo $this->cas20Protocol->getValidateSuccessResponse($serviceTicket['userName']);
274
        $successContent = ob_get_clean();
275
276
        return new XmlResponse(
277
            $successContent,
278
            Response::HTTP_OK,
279
        );
280
    }
281
}
282