Cas20Controller   A
last analyzed

Complexity

Total Complexity 12

Size/Duplication

Total Lines 218
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 12
eloc 78
c 1
b 0
f 0
dl 0
loc 218
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A proxyValidate() 0 16 1
B proxy() 0 75 6
A __construct() 0 24 3
A serviceValidate() 0 16 1
A getTicketStore() 0 3 1
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;
11
use SimpleSAML\Module\casserver\Cas\Factories\TicketFactory;
12
use SimpleSAML\Module\casserver\Cas\Protocol\Cas20;
13
use SimpleSAML\Module\casserver\Cas\Ticket\TicketStore;
14
use SimpleSAML\Module\casserver\Controller\Traits\TicketValidatorTrait;
15
use SimpleSAML\Module\casserver\Controller\Traits\UrlTrait;
16
use SimpleSAML\Module\casserver\Http\XmlResponse;
17
use SimpleSAML\Utils;
18
use Symfony\Component\HttpFoundation\Request;
19
use Symfony\Component\HttpFoundation\Response;
20
use Symfony\Component\HttpKernel\Attribute\AsController;
21
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
22
23
#[AsController]
24
class Cas20Controller
25
{
26
    use UrlTrait;
27
    use TicketValidatorTrait;
28
29
    /** @var \SimpleSAML\Logger */
30
    protected Logger $logger;
31
32
    /** @var \SimpleSAML\Utils\HTTP */
33
    protected Utils\HTTP $httpUtils;
34
35
    /** @var \SimpleSAML\Configuration */
36
    protected Configuration $casConfig;
37
38
    /** @var \SimpleSAML\Module\casserver\Cas\Protocol\Cas20 */
39
    protected Cas20 $cas20Protocol;
40
41
    /** @var \SimpleSAML\Module\casserver\Cas\Factories\TicketFactory */
42
    protected TicketFactory $ticketFactory;
43
44
    /** @var \SimpleSAML\Module\casserver\Cas\Ticket\TicketStore */
45
    protected TicketStore $ticketStore;
46
47
    /**
48
     * @param \SimpleSAML\Configuration $sspConfig
49
     * @param \SimpleSAML\Configuration|null $casConfig
50
     * @param \SimpleSAML\Module\casserver\Cas\Ticket\TicketStore|null $ticketStore
51
     * @param \SimpleSAML\Utils\HTTP|null $httpUtils
52
     *
53
     * @throws \Exception
54
     */
55
    public function __construct(
56
        private readonly Configuration $sspConfig,
57
        ?Configuration $casConfig = null,
58
        ?TicketStore $ticketStore = null,
59
        ?Utils\HTTP $httpUtils = null,
60
    ) {
61
        // We are using this work around in order to bypass Symfony's autowiring for cas configuration. Since
62
        // the configuration class is the same, it loads the ssp configuration twice. Still, we need the constructor
63
        // argument in order to facilitate testing
64
        $this->casConfig = ($casConfig === null || $casConfig === $sspConfig)
65
            ? Configuration::getConfig('module_casserver.php') : $casConfig;
66
        $this->cas20Protocol = new Cas20($this->casConfig);
67
68
        /* Instantiate ticket factory */
69
        $this->ticketFactory = new TicketFactory($this->casConfig);
70
71
        /* Instantiate ticket store */
72
        $ticketStoreConfig = $this->casConfig->getOptionalValue(
73
            'ticketstore',
74
            ['class' => 'casserver:FileSystemTicketStore'],
75
        );
76
        $ticketStoreClass = Module::resolveClass($ticketStoreConfig['class'], 'Cas\Ticket');
77
        $this->ticketStore = $ticketStore ?? new $ticketStoreClass($this->casConfig);
78
        $this->httpUtils = $httpUtils ?? new Utils\HTTP();
79
    }
80
81
    /**
82
     * @param \Symfony\Component\HttpFoundation\Request $request
83
     * @param string|null $TARGET   Query parameter name for "service" used by older CAS clients'
84
     * @param bool $renew           [OPTIONAL] - if this parameter is set, ticket validation will only succeed
85
     *                                 if the service ticket was issued from the presentation of the user’s primary
86
     *                                 credentials. It will fail if the ticket was issued from a single sign-on session.
87
     * @param string|null $ticket   [REQUIRED] - the service ticket issued by /login
88
     * @param string|null $service  [REQUIRED] - the identifier of the service for which the ticket was issued
89
     * @param string|null $pgtUrl   [OPTIONAL] - the URL of the proxy callback
90
     *
91
     * @return \SimpleSAML\Module\casserver\Http\XmlResponse
92
     */
93
    public function serviceValidate(
94
        Request $request,
95
        #[MapQueryParameter] ?string $TARGET = null,
96
        #[MapQueryParameter] bool $renew = false,
97
        #[MapQueryParameter] ?string $ticket = null,
98
        #[MapQueryParameter] ?string $service = null,
99
        #[MapQueryParameter] ?string $pgtUrl = null,
100
    ): XmlResponse {
101
        return $this->validate(
102
            request: $request,
103
            method:  'serviceValidate',
104
            renew:   $renew,
105
            target:  $TARGET,
106
            ticket:  $ticket,
107
            service: $service,
108
            pgtUrl:  $pgtUrl,
109
        );
110
    }
111
112
    /**
113
     * /proxy provides proxy tickets to services that have
114
     * acquired proxy-granting tickets and will be proxying authentication to back-end services.
115
     *
116
     * @param \Symfony\Component\HttpFoundation\Request $request
117
     * @param string|null $targetService  [REQUIRED] - the service identifier of the back-end service.
118
     * @param string|null $pgt            [REQUIRED] - the proxy-granting ticket acquired by the service
119
     *                                       during service ticket or proxy ticket validation.
120
     *
121
     * @return \SimpleSAML\Module\casserver\Http\XmlResponse
122
     * @throws \ErrorException
123
     */
124
    public function proxy(
125
        Request $request,
126
        #[MapQueryParameter] ?string $targetService = null,
127
        #[MapQueryParameter] ?string $pgt = null,
128
    ): XmlResponse {
129
        // NOTE: Here we do not override the configuration
130
        $legal_target_service_urls = $this->casConfig->getOptionalValue('legal_target_service_urls', []);
131
        // Fail if
132
        $message = match (true) {
133
            // targetService parameter is not defined
134
            $targetService === null => 'Missing target service parameter [targetService]',
135
            // pgt parameter is not defined
136
            $pgt === null => 'Missing proxy granting ticket parameter: [pgt]',
137
            !$this->checkServiceURL($this->sanitize($targetService), $legal_target_service_urls) =>
0 ignored issues
show
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

137
            !$this->checkServiceURL($this->sanitize(/** @scrutinizer ignore-type */ $targetService), $legal_target_service_urls) =>
Loading history...
138
                "Target service parameter not listed as a legal service: [targetService] = {$targetService}",
139
            default => null,
140
        };
141
142
        if (!empty($message)) {
143
            return new XmlResponse(
144
                (string)$this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_REQUEST, $message),
145
                Response::HTTP_BAD_REQUEST,
146
            );
147
        }
148
149
        // Get the ticket
150
        $proxyGrantingTicket = $this->ticketStore->getTicket($pgt);
0 ignored issues
show
Bug introduced by
It seems like $pgt can also be of type null; however, parameter $ticketId of SimpleSAML\Module\casser...icketStore::getTicket() 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

150
        $proxyGrantingTicket = $this->ticketStore->getTicket(/** @scrutinizer ignore-type */ $pgt);
Loading history...
151
        $message = match (true) {
152
            // targetService parameter is not defined
153
            $proxyGrantingTicket === null => "Ticket {$pgt} not recognized",
154
            // pgt parameter is not defined
155
            !$this->ticketFactory->isProxyGrantingTicket($proxyGrantingTicket)
0 ignored issues
show
Bug introduced by
It seems like $proxyGrantingTicket can also be of type null; however, parameter $ticket of SimpleSAML\Module\casser...isProxyGrantingTicket() does only seem to accept array, 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

155
            !$this->ticketFactory->isProxyGrantingTicket(/** @scrutinizer ignore-type */ $proxyGrantingTicket)
Loading history...
156
            => "Not a valid proxy granting ticket id: {$pgt}",
157
            default => null,
158
        };
159
160
        if (!empty($message)) {
161
            return new XmlResponse(
162
                (string)$this->cas20Protocol->getValidateFailureResponse('BAD_PGT', $message),
163
                Response::HTTP_BAD_REQUEST,
164
            );
165
        }
166
167
        // Get the session id from the ticket
168
        $sessionTicket = $this->ticketStore->getTicket($proxyGrantingTicket['sessionId']);
169
170
        if (
171
            $sessionTicket === null
172
            || $this->ticketFactory->isSessionTicket($sessionTicket) === false
173
            || $this->ticketFactory->isExpired($sessionTicket)
174
        ) {
175
            $message = "Ticket {$pgt} has expired";
176
            Logger::debug('casserver:' . $message);
177
178
            return new XmlResponse(
179
                (string)$this->cas20Protocol->getValidateFailureResponse('BAD_PGT', $message),
180
                Response::HTTP_BAD_REQUEST,
181
            );
182
        }
183
184
        $proxyTicket = $this->ticketFactory->createProxyTicket(
185
            [
186
                'service' => $targetService,
187
                'forceAuthn' => $proxyGrantingTicket['forceAuthn'],
188
                'attributes' => $proxyGrantingTicket['attributes'],
189
                'proxies' => $proxyGrantingTicket['proxies'],
190
                'sessionId' => $proxyGrantingTicket['sessionId'],
191
            ],
192
        );
193
194
        $this->ticketStore->addTicket($proxyTicket);
195
196
        return new XmlResponse(
197
            (string)$this->cas20Protocol->getProxySuccessResponse($proxyTicket['id']),
198
            Response::HTTP_OK,
199
        );
200
    }
201
202
    /**
203
     * @param \Symfony\Component\HttpFoundation\Request      $request
204
     * @param string|null $TARGET   Query parameter name for "service" used by older CAS clients'
205
     * @param bool $renew           [OPTIONAL] - if this parameter is set, ticket validation will only succeed
206
     *                                 if the service ticket was issued from the presentation of the user’s primary
207
     *                                 credentials. It will fail if the ticket was issued from a single sign-on session.
208
     * @param string|null $ticket   [REQUIRED] - the service ticket issued by /login
209
     * @param string|null $service  [REQUIRED] - the identifier of the service for which the ticket was issued
210
     * @param string|null $pgtUrl   [OPTIONAL] - the URL of the proxy callback
211
     *
212
     * @return \SimpleSAML\Module\casserver\Http\XmlResponse
213
     */
214
    public function proxyValidate(
215
        Request $request,
216
        #[MapQueryParameter] ?string $TARGET = null,
217
        #[MapQueryParameter] bool $renew = false,
218
        #[MapQueryParameter] ?string $ticket = null,
219
        #[MapQueryParameter] ?string $service = null,
220
        #[MapQueryParameter] ?string $pgtUrl = null,
221
    ): XmlResponse {
222
        return $this->validate(
223
            request: $request,
224
            method:  'proxyValidate',
225
            renew:   $renew,
226
            target:  $TARGET,
227
            ticket:  $ticket,
228
            service: $service,
229
            pgtUrl:  $pgtUrl,
230
        );
231
    }
232
233
    /**
234
     * Used by the unit tests
235
     *
236
     * @return \SimpleSAML\Module\casserver\Cas\Ticket\TicketStore
237
     */
238
    public function getTicketStore(): TicketStore
239
    {
240
        return $this->ticketStore;
241
    }
242
}
243