Cas10Controller::__construct()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
dl 0
loc 20
rs 9.9666
c 1
b 0
f 0
cc 3
nc 4
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\casserver\Controller;
6
7
use Exception;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Logger;
10
use SimpleSAML\Module;
11
use SimpleSAML\Module\casserver\Cas\Factories\TicketFactory;
12
use SimpleSAML\Module\casserver\Cas\Protocol\Cas10;
13
use SimpleSAML\Module\casserver\Cas\Ticket\TicketStore;
14
use SimpleSAML\Module\casserver\Controller\Traits\UrlTrait;
15
use Symfony\Component\HttpFoundation\Request;
16
use Symfony\Component\HttpFoundation\Response;
17
use Symfony\Component\HttpKernel\Attribute\AsController;
18
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
19
20
use function var_export;
21
22
#[AsController]
23
class Cas10Controller
24
{
25
    use UrlTrait;
26
27
    /** @var \SimpleSAML\Logger */
28
    protected Logger $logger;
29
30
    /** @var \SimpleSAML\Configuration */
31
    protected Configuration $casConfig;
32
33
    /** @var \SimpleSAML\Module\casserver\Cas\Protocol\Cas10 */
34
    protected Cas10 $cas10Protocol;
35
36
    /** @var \SimpleSAML\Module\casserver\Cas\Factories\TicketFactory */
37
    protected TicketFactory $ticketFactory;
38
39
    /** @var \SimpleSAML\Module\casserver\Cas\Ticket\TicketStore */
40
    protected TicketStore $ticketStore;
41
42
    /**
43
     * @param \SimpleSAML\Configuration $sspConfig
44
     * @param \SimpleSAML\Configuration|null $casConfig
45
     * @param \SimpleSAML\Module\casserver\Cas\Ticket\TicketStore|null $ticketStore
46
     *
47
     * @throws \Exception
48
     */
49
    public function __construct(
50
        private readonly Configuration $sspConfig,
51
        ?Configuration $casConfig = null,
52
        ?TicketStore $ticketStore = null,
53
    ) {
54
        // We are using this work around in order to bypass Symfony's autowiring for cas configuration. Since
55
        // the configuration class is the same, it loads the ssp configuration twice. Still, we need the constructor
56
        // argument in order to facilitate testin.
57
        $this->casConfig = ($casConfig === null || $casConfig === $sspConfig)
58
            ? Configuration::getConfig('module_casserver.php') : $casConfig;
59
        $this->cas10Protocol = new Cas10($this->casConfig);
60
        /* Instantiate ticket factory */
61
        $this->ticketFactory = new TicketFactory($this->casConfig);
62
        /* Instantiate ticket store */
63
        $ticketStoreConfig = $this->casConfig->getOptionalValue(
64
            'ticketstore',
65
            ['class' => 'casserver:FileSystemTicketStore'],
66
        );
67
        $ticketStoreClass = Module::resolveClass($ticketStoreConfig['class'], 'Cas\Ticket');
68
        $this->ticketStore = $ticketStore ?? new $ticketStoreClass($this->casConfig);
69
    }
70
71
    /**
72
     * @param \Symfony\Component\HttpFoundation\Request $request
73
     * @param bool $renew          [OPTIONAL] - if this parameter is set, ticket validation will only succeed if the
74
     *                             service ticket was issued from the presentation of the user’s primary credentials.
75
     *                             It will fail if the ticket was issued from a single sign-on session.
76
     * @param string|null $ticket  [REQUIRED] - the service ticket issued by /login.
77
     * @param string|null $service [REQUIRED] - the identifier of the service for which the ticket was issued
78
     *
79
     * @return Response
80
     */
81
    public function validate(
82
        Request $request,
83
        #[MapQueryParameter] ?string $ticket = null,
84
        #[MapQueryParameter] bool $renew = false,
85
        #[MapQueryParameter] ?string $service = null,
86
    ): Response {
87
        $forceAuthn = $renew;
88
        // Check if any of the required query parameters are missing
89
        // Even though we can delegate the check to Symfony's `MapQueryParameter` we cannot return
90
        // the failure response needed. As a result, we allow a default value, and we handle the missing
91
        // values afterward.
92
        if ($service === null || $ticket === null) {
93
            $messagePostfix = $service === null ? 'service' : 'ticket';
94
            Logger::debug("casserver: Missing service parameter: [{$messagePostfix}]");
95
            return new Response(
96
                $this->cas10Protocol->getValidateFailureResponse(),
97
                Response::HTTP_BAD_REQUEST,
98
            );
99
        }
100
101
        try {
102
            // Get the service ticket
103
            // `getTicket` uses the unserializable method and Objects may throw Throwables in their
104
            // unserialization handlers.
105
            $serviceTicket = $this->ticketStore->getTicket($ticket);
106
            // Delete the ticket
107
            $this->ticketStore->deleteTicket($ticket);
108
        } catch (Exception $e) {
109
            Logger::error('casserver:validate: internal server error. ' . var_export($e->getMessage(), true));
110
            return new Response(
111
                $this->cas10Protocol->getValidateFailureResponse(),
112
                Response::HTTP_INTERNAL_SERVER_ERROR,
113
            );
114
        }
115
116
        $failed = false;
117
        $message = '';
118
        if (empty($serviceTicket)) {
119
            // No ticket
120
            $message = 'ticket: ' . var_export($ticket, true) . ' not recognized';
121
            $failed = true;
122
        } elseif (!$this->ticketFactory->isServiceTicket($serviceTicket)) {
123
            // This is not a service ticket
124
            $message = 'ticket: ' . var_export($ticket, true) . ' is not a service ticket';
125
            $failed = true;
126
        } elseif ($this->ticketFactory->isExpired($serviceTicket)) {
127
            // the ticket has expired
128
            $message = 'Ticket has ' . var_export($ticket, true) . ' expired';
129
            $failed = true;
130
        } elseif ($this->sanitize($serviceTicket['service']) !== $this->sanitize($service)) {
131
            // The service url we passed to the query parameters does not match the one in the ticket.
132
            $message = 'Mismatching service parameters: expected ' .
133
                var_export($serviceTicket['service'], true) .
134
                ' but was: ' . var_export($service, true);
135
            $failed = true;
136
        } elseif ($forceAuthn && !$serviceTicket['forceAuthn']) {
137
            // If `forceAuthn` is required but not set in the ticket
138
            $message = 'Ticket was issued from single sign on session';
139
            $failed = true;
140
        }
141
142
        if ($failed) {
143
            Logger::error('casserver:validate: ' . $message);
144
            return new Response(
145
                $this->cas10Protocol->getValidateFailureResponse(),
146
                Response::HTTP_BAD_REQUEST,
147
            );
148
        }
149
150
        // Fail if the username is not present in the ticket
151
        if (empty($serviceTicket['userName'])) {
152
            return new Response(
153
                $this->cas10Protocol->getValidateFailureResponse(),
154
                Response::HTTP_BAD_REQUEST,
155
            );
156
        }
157
158
        // Successful validation
159
        return new Response(
160
            $this->cas10Protocol->getValidateSuccessResponse($serviceTicket['userName']),
161
            Response::HTTP_OK,
162
        );
163
    }
164
165
    /**
166
     * Used by the unit tests
167
     *
168
     * @return \SimpleSAML\Module\casserver\Cas\Ticket\TicketStore
169
     */
170
    public function getTicketStore(): TicketStore
171
    {
172
        return $this->ticketStore;
173
    }
174
}
175