Passed
Pull Request — master (#45)
by
unknown
14:02
created

Cas20Controller::serviceValidate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 14
rs 10
cc 1
nc 1
nop 5
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 Configuration */
31
    protected Configuration $sspConfig;
32
33
    /** @var Cas20 */
34
    protected Cas20 $cas20Protocol;
35
36
    /** @var TicketFactory */
37
    protected TicketFactory $ticketFactory;
38
39
    // this could be any configured ticket store
40
    protected mixed $ticketStore;
41
42
    /**
43
     * @param   Configuration|null  $casConfig
44
     * @param                       $ticketStore
45
     *
46
     * @throws \Exception
47
     */
48
    public function __construct(
49
        Configuration $sspConfig = null,
50
        Configuration $casConfig = null,
51
        $ticketStore = null,
52
    ) {
53
        $this->sspConfig     = $sspConfig ?? Configuration::getInstance();
54
        $this->casConfig     = $casConfig ?? Configuration::getConfig('module_casserver.php');
55
        $this->cas20Protocol = new Cas20($this->casConfig);
56
        /* Instantiate ticket factory */
57
        $this->ticketFactory = new TicketFactory($this->casConfig);
58
        /* Instantiate ticket store */
59
        $ticketStoreConfig = $this->casConfig->getOptionalValue(
60
            'ticketstore',
61
            ['class' => 'casserver:FileSystemTicketStore'],
62
        );
63
        $ticketStoreClass  = 'SimpleSAML\\Module\\casserver\\Cas\\Ticket\\'
64
            . explode(':', $ticketStoreConfig['class'])[1];
65
        /** @psalm-suppress InvalidStringClass */
66
        $this->ticketStore = $ticketStore ?? new $ticketStoreClass($this->casConfig);
67
    }
68
69
    /**
70
     * @param   Request      $request
71
     * @param   bool         $renew
72
     * @param   string|null  $ticket
73
     * @param   string|null  $service
74
     * @param   string|null  $pgtUrl
75
     *
76
     * @return XmlResponse
77
     */
78
    public function serviceValidate(
79
        Request $request,
80
        #[MapQueryParameter] bool $renew = false,
81
        #[MapQueryParameter] ?string $ticket = null,
82
        #[MapQueryParameter] ?string $service = null,
83
        #[MapQueryParameter] ?string $pgtUrl = null,
84
    ): XmlResponse {
85
        return $this->validate(
86
            $request,
87
            'serviceValidate',
88
            $renew,
89
            $ticket,
90
            $service,
91
            $pgtUrl,
92
        );
93
    }
94
95
    /**
96
     * @param   Request      $request
97
     * @param   bool         $renew
98
     * @param   string|null  $ticket
99
     * @param   string|null  $service
100
     * @param   string|null  $pgtUrl
101
     *
102
     * @return XmlResponse
103
     */
104
    public function proxyValidate(
105
        Request $request,
106
        #[MapQueryParameter] bool $renew = false,
107
        #[MapQueryParameter] ?string $ticket = null,
108
        #[MapQueryParameter] ?string $service = null,
109
        #[MapQueryParameter] ?string $pgtUrl = null,
110
    ): XmlResponse {
111
        return $this->validate(
112
            $request,
113
            'proxyValidate',
114
            $renew,
115
            $ticket,
116
            $service,
117
            $pgtUrl,
118
        );
119
    }
120
121
    /**
122
     * @param   Request      $request
123
     * @param   string       $method
124
     * @param   bool         $renew
125
     * @param   string|null  $ticket
126
     * @param   string|null  $service
127
     * @param   string|null  $pgtUrl
128
     *
129
     * @return XmlResponse
130
     */
131
    public function validate(
132
        Request $request,
133
        string $method,
134
        bool $renew = false,
135
        ?string $ticket = null,
136
        ?string $service = null,
137
        ?string $pgtUrl = null,
138
    ): XmlResponse {
139
        $forceAuthn = $renew;
140
        $serviceUrl = $service ?? $_GET['TARGET'] ?? null;
141
142
        // Check if any of the required query parameters are missing
143
        if ($serviceUrl === null || $ticket === null) {
144
            $messagePostfix = $serviceUrl === null ? 'service' : 'ticket';
145
            $message        = "casserver: Missing service parameter: [{$messagePostfix}]";
146
            Logger::debug($message);
147
148
            ob_start();
149
            echo $this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message);
150
            $responseContent = ob_get_clean();
151
152
            return new XmlResponse(
153
                $responseContent,
154
                Response::HTTP_BAD_REQUEST,
155
            );
156
        }
157
158
        try {
159
            // Get the service ticket
160
            // `getTicket` uses the unserializable method and Objects may throw Throwables in their
161
            // unserialization handlers.
162
            $serviceTicket = $this->ticketStore->getTicket($ticket);
163
            // Delete the ticket
164
            $this->ticketStore->deleteTicket($ticket);
165
        } catch (\Exception $e) {
166
            $message = 'casserver:serviceValidate: internal server error. ' . var_export($e->getMessage(), true);
167
            Logger::error($message);
168
169
            ob_start();
170
            echo $this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message);
171
            $responseContent = ob_get_clean();
172
173
            return new XmlResponse(
174
                $responseContent,
175
                Response::HTTP_INTERNAL_SERVER_ERROR,
176
            );
177
        }
178
179
        $failed  = false;
180
        $message = '';
181
        if (empty($serviceTicket)) {
182
            // No ticket
183
            $message = 'ticket: ' . var_export($ticket, true) . ' not recognized';
184
            $failed  = true;
185
        } elseif ($method === 'serviceValidate' && $this->ticketFactory->isProxyTicket($serviceTicket)) {
186
            $message = 'Ticket ' . var_export($_GET['ticket'], true) .
187
                ' is a proxy ticket. Use proxyValidate instead.';
188
            $failed  = true;
189
        } elseif (!$this->ticketFactory->isServiceTicket($serviceTicket)) {
190
            // This is not a service ticket
191
            $message = 'ticket: ' . var_export($ticket, true) . ' is not a service ticket';
192
            $failed  = true;
193
        } elseif ($this->ticketFactory->isExpired($serviceTicket)) {
194
            // the ticket has expired
195
            $message = 'Ticket has ' . var_export($ticket, true) . ' expired';
196
            $failed  = true;
197
        } elseif ($this->sanitize($serviceTicket['service']) !== $this->sanitize($serviceUrl)) {
198
            // The service url we passed to the query parameters does not match the one in the ticket.
199
            $message = 'Mismatching service parameters: expected ' .
200
                var_export($serviceTicket['service'], true) .
201
                ' but was: ' . var_export($serviceUrl, true);
202
            $failed  = true;
203
        } elseif ($forceAuthn && !$serviceTicket['forceAuthn']) {
204
            // If `forceAuthn` is required but not set in the ticket
205
            $message = 'Ticket was issued from single sign on session';
206
            $failed  = true;
207
        }
208
209
        if ($failed) {
210
            $finalMessage = 'casserver:validate: ' . $message;
211
            Logger::error($finalMessage);
212
213
            ob_start();
214
            echo $this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message);
215
            $responseContent = ob_get_clean();
216
217
            return new XmlResponse(
218
                $responseContent,
219
                Response::HTTP_BAD_REQUEST,
220
            );
221
        }
222
223
        $attributes = $serviceTicket['attributes'];
224
        $this->cas20Protocol->setAttributes($attributes);
225
226
        if (isset($pgtUrl)) {
227
            $sessionTicket = $this->ticketStore->getTicket($serviceTicket['sessionId']);
228
            if (
229
                $sessionTicket !== null
230
                && $this->ticketFactory->isSessionTicket($sessionTicket)
231
                && !$this->ticketFactory->isExpired($sessionTicket)
232
            ) {
233
                $proxyGrantingTicket = $this->ticketFactory->createProxyGrantingTicket(
234
                    [
235
                        'userName' => $serviceTicket['userName'],
236
                        'attributes' => $attributes,
237
                        'forceAuthn' => false,
238
                        'proxies' => array_merge(
239
                            [$serviceUrl],
240
                            $serviceTicket['proxies'],
241
                        ),
242
                        'sessionId' => $serviceTicket['sessionId'],
243
                    ],
244
                );
245
                try {
246
                    $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...
247
                        $pgtUrl . '?pgtIou=' . $proxyGrantingTicket['iou'] . '&pgtId=' . $proxyGrantingTicket['id'],
248
                    );
249
250
                    $this->cas20Protocol->setProxyGrantingTicketIOU($proxyGrantingTicket['iou']);
251
252
                    $this->ticketStore->addTicket($proxyGrantingTicket);
253
                } catch (Exception $e) {
0 ignored issues
show
Bug introduced by
The type SimpleSAML\Module\casserver\Controller\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
254
                    // Fall through
255
                }
256
            }
257
        }
258
259
        ob_start();
260
        echo $this->cas20Protocol->getValidateSuccessResponse($serviceTicket['userName']);
261
        $successContent = ob_get_clean();
262
263
        return new XmlResponse(
264
            $successContent,
265
            Response::HTTP_OK,
266
        );
267
    }
268
}
269