Completed
Push — master ( e7801a...b0ac52 )
by
unknown
13s
created

TicketValidatorTrait::validateServiceTicket()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 16
rs 8.8333
cc 7
nc 4
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\casserver\Controller\Traits;
6
7
use Exception;
8
use SimpleSAML\CAS\Constants as C;
9
use SimpleSAML\Logger;
10
use SimpleSAML\Module\casserver\Http\XmlResponse;
11
use Symfony\Component\HttpFoundation\Request;
12
use Symfony\Component\HttpFoundation\Response;
13
14
use function array_merge;
15
use function var_export;
16
17
trait TicketValidatorTrait
18
{
19
    /**
20
     * @param \Symfony\Component\HttpFoundation\Request $request
21
     * @param string $method
22
     * @param bool $renew
23
     * @param string|null $target
24
     * @param string|null $ticket
25
     * @param string|null $service
26
     * @param string|null $pgtUrl
27
     *
28
     * @return \SimpleSAML\Module\casserver\Http\XmlResponse
29
     */
30
    public function validate(
31
        Request $request,
32
        string $method,
33
        bool $renew = false,
34
        ?string $target = null,
35
        ?string $ticket = null,
36
        ?string $service = null,
37
        ?string $pgtUrl = null,
38
    ): XmlResponse {
39
        $forceAuthn = $renew;
40
        $serviceUrl = $service ?? $target ?? null;
41
42
        // Check if any of the required query parameters are missing
43
        if ($serviceUrl === null || $ticket === null) {
44
            $messagePostfix = $serviceUrl === null ? 'service' : 'ticket';
45
            $message = "casserver: Missing {$messagePostfix} parameter: [{$messagePostfix}]";
46
            Logger::debug($message);
47
48
            return new XmlResponse(
49
                (string)$this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message),
50
                Response::HTTP_BAD_REQUEST,
51
            );
52
        }
53
54
        try {
55
            // Get the service ticket
56
            // `getTicket` uses the unserializable method and Objects may throw "Throwables" in their
57
            // un-serialization handlers.
58
            $serviceTicket = $this->ticketStore->getTicket($ticket);
59
        } catch (Exception $e) {
60
            $messagePostfix = '';
61
            if (!empty($e->getMessage())) {
62
                $messagePostfix = ': ' . var_export($e->getMessage(), true);
63
            }
64
            $message = 'casserver:serviceValidate: internal server error' . $messagePostfix;
65
            Logger::error(__METHOD__ . '::' . $message);
66
67
            return new XmlResponse(
68
                (string)$this->cas20Protocol->getValidateFailureResponse(C::ERR_INTERNAL_ERROR, $message),
69
                Response::HTTP_INTERNAL_SERVER_ERROR,
70
            );
71
        }
72
73
        $failed  = false;
74
        $message = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $message is dead and can be removed.
Loading history...
75
        // Below, we do not have a ticket or the ticket does not meet the very basic criteria that allow
76
        // any further handling
77
        if ($message = $this->validateServiceTicket($serviceTicket, $ticket, $method)) {
78
            $finalMessage = 'casserver:validate: ' . $message;
79
            Logger::error(__METHOD__ . '::' . $finalMessage);
80
81
            return new XmlResponse(
82
                (string)$this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message),
83
                Response::HTTP_BAD_REQUEST,
84
            );
85
        }
86
87
        // Delete the ticket
88
        $this->ticketStore->deleteTicket($ticket);
89
90
        // Check if the ticket
91
        // - has expired
92
        // - does not pass sanitization
93
        // - forceAutnn criteria are not met
94
        if ($this->ticketFactory->isExpired($serviceTicket)) {
95
            // the ticket has expired
96
            $message = 'Ticket ' . var_export($ticket, true) . ' has expired';
97
            $failed  = true;
98
        } elseif ($this->sanitize($serviceTicket['service']) !== $this->sanitize($serviceUrl)) {
0 ignored issues
show
Bug introduced by
It seems like sanitize() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

98
        } elseif ($this->/** @scrutinizer ignore-call */ sanitize($serviceTicket['service']) !== $this->sanitize($serviceUrl)) {
Loading history...
99
            // The service url we passed to the query parameters does not match the one in the ticket.
100
            $message = 'Mismatching service parameters: expected ' .
101
                var_export($serviceTicket['service'], true) .
102
                ' but was: ' . var_export($serviceUrl, true);
103
            $failed  = true;
104
        } elseif ($forceAuthn && !$serviceTicket['forceAuthn']) {
105
            // If `forceAuthn` is required but not set in the ticket
106
            $message = 'Ticket was issued from single sign on session';
107
            $failed  = true;
108
        }
109
110
        if ($failed) {
111
            $finalMessage = 'casserver:validate: ' . $message;
112
            Logger::error(__METHOD__ . '::' . $finalMessage);
113
114
            return new XmlResponse(
115
                (string)$this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_SERVICE, $message),
116
                Response::HTTP_BAD_REQUEST,
117
            );
118
        }
119
120
        $attributes = $serviceTicket['attributes'];
121
        $this->cas20Protocol->setAttributes($attributes);
122
123
        if (isset($pgtUrl)) {
124
            $sessionTicket = $this->ticketStore->getTicket($serviceTicket['sessionId']);
125
            if (
126
                $sessionTicket !== null
127
                && $this->ticketFactory->isSessionTicket($sessionTicket)
128
                && !$this->ticketFactory->isExpired($sessionTicket)
129
            ) {
130
                $proxyGrantingTicket = $this->ticketFactory->createProxyGrantingTicket(
131
                    [
132
                        'userName' => $serviceTicket['userName'],
133
                        'attributes' => $attributes,
134
                        'forceAuthn' => false,
135
                        'proxies' => array_merge(
136
                            [$serviceUrl],
137
                            $serviceTicket['proxies'],
138
                        ),
139
                        'sessionId' => $serviceTicket['sessionId'],
140
                    ],
141
                );
142
                try {
143
                    // Here we assume that the fetch will throw on any error.
144
                    // The generation of the proxy-granting-ticket or the corresponding proxy granting ticket IOU may
145
                    // fail due to the proxy callback url failing to meet the minimum security requirements such as
146
                    // failure to establish trust between peers or unresponsiveness of the endpoint, etc.
147
                    // In case of failure, no proxy-granting ticket will be issued and the CAS service response
148
                    // as described in Section 2.5.2 MUST NOT contain a <proxyGrantingTicket> block.
149
                    // At this point, the issuance of a proxy-granting ticket is halted and service ticket
150
                    // validation will fail.
151
                    $data = $this->httpUtils->fetch(
152
                        $pgtUrl . '?pgtIou=' . $proxyGrantingTicket['iou'] . '&pgtId=' . $proxyGrantingTicket['id'],
153
                    );
154
                    Logger::debug(__METHOD__ . '::data: ' . var_export($data, true));
155
                    $this->cas20Protocol->setProxyGrantingTicketIOU($proxyGrantingTicket['iou']);
156
                    $this->ticketStore->addTicket($proxyGrantingTicket);
157
                } catch (Exception $e) {
158
                    return new XmlResponse(
159
                        (string)$this->cas20Protocol->getValidateFailureResponse(
160
                            C::ERR_INVALID_SERVICE,
161
                            'Proxy callback url is failing.',
162
                        ),
163
                        Response::HTTP_BAD_REQUEST,
164
                    );
165
                }
166
            }
167
        }
168
169
        return new XmlResponse(
170
            (string)$this->cas20Protocol->getValidateSuccessResponse($serviceTicket['userName']),
171
            Response::HTTP_OK,
172
        );
173
    }
174
175
    /**
176
     * @param array{'id': string}|null $serviceTicket
177
     *
178
     * @return ?string Message on failure, null on success
179
     */
180
    private function validateServiceTicket(?array $serviceTicket, string $ticket, string $method): ?string
181
    {
182
        if (empty($serviceTicket)) {
183
            return 'Ticket ' . var_export($ticket, true) . ' not recognized';
184
        }
185
186
        $isServiceTicket = $this->ticketFactory->isServiceTicket($serviceTicket);
187
        if ($method === 'serviceValidate' && !$isServiceTicket) {
188
            return 'Ticket ' . var_export($ticket, true) . ' is not a service ticket.';
189
        }
190
191
        if ($method === 'proxyValidate' && !$isServiceTicket && !$this->ticketFactory->isProxyTicket($serviceTicket)) {
192
            return 'Ticket ' . var_export($ticket, true) . ' is not a proxy ticket.';
193
        }
194
195
        return null;
196
    }
197
}
198