Passed
Push — master ( 81b6b5...5b35ab )
by Tim
03:44
created

Cas20Controller   A

Complexity

Total Complexity 12

Size/Duplication

Total Lines 216
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 216
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 22 3
A serviceValidate() 0 16 1
A proxyValidate() 0 16 1
B proxy() 0 75 6
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;
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...
27
    use TicketValidatorTrait;
28
29
    /** @var Logger */
30
    protected Logger $logger;
31
32
    /** @var Utils\HTTP */
33
    protected Utils\HTTP $httpUtils;
34
35
    /** @var Configuration */
36
    protected Configuration $casConfig;
37
38
    /** @var Cas20 */
39
    protected Cas20 $cas20Protocol;
40
41
    /** @var TicketFactory */
42
    protected TicketFactory $ticketFactory;
43
44
    /** @var TicketStore */
45
    protected TicketStore $ticketStore;
46
47
    /**
48
     * @param   Configuration       $sspConfig
49
     * @param   Configuration|null  $casConfig
50
     * @param   TicketStore|null    $ticketStore
51
     * @param   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
        /* Instantiate ticket factory */
68
        $this->ticketFactory = new TicketFactory($this->casConfig);
69
        /* Instantiate ticket store */
70
        $ticketStoreConfig = $this->casConfig->getOptionalValue(
71
            'ticketstore',
72
            ['class' => 'casserver:FileSystemTicketStore'],
73
        );
74
        $ticketStoreClass = Module::resolveClass($ticketStoreConfig['class'], 'Cas\Ticket');
75
        $this->ticketStore = $ticketStore ?? new $ticketStoreClass($this->casConfig);
76
        $this->httpUtils = $httpUtils ?? new Utils\HTTP();
77
    }
78
79
    /**
80
     * @param   Request      $request
81
     * @param   string|null  $TARGET   Query parameter name for "service" used by older CAS clients'
82
     * @param   bool         $renew    [OPTIONAL] - if this parameter is set, ticket validation will only succeed
83
     *                                 if the service ticket was issued from the presentation of the user’s primary
84
     *                                 credentials. It will fail if the ticket was issued from a single sign-on session.
85
     * @param   string|null  $ticket   [REQUIRED] - the service ticket issued by /login
86
     * @param   string|null  $service  [REQUIRED] - the identifier of the service for which the ticket was issued
87
     * @param   string|null  $pgtUrl   [OPTIONAL] - the URL of the proxy callback
88
     *
89
     * @return XmlResponse
90
     */
91
    public function serviceValidate(
92
        Request $request,
93
        #[MapQueryParameter] ?string $TARGET = null,
94
        #[MapQueryParameter] bool $renew = false,
95
        #[MapQueryParameter] ?string $ticket = null,
96
        #[MapQueryParameter] ?string $service = null,
97
        #[MapQueryParameter] ?string $pgtUrl = null,
98
    ): XmlResponse {
99
        return $this->validate(
100
            request: $request,
101
            method:  'serviceValidate',
102
            renew:   $renew,
103
            target:  $TARGET,
104
            ticket:  $ticket,
105
            service: $service,
106
            pgtUrl:  $pgtUrl,
107
        );
108
    }
109
110
    /**
111
     * /proxy provides proxy tickets to services that have
112
     * acquired proxy-granting tickets and will be proxying authentication to back-end services.
113
     *
114
     * @param   Request      $request
115
     * @param   string|null  $targetService  [REQUIRED] - the service identifier of the back-end service.
116
     * @param   string|null  $pgt            [REQUIRED] - the proxy-granting ticket acquired by the service
117
     *                                       during service ticket or proxy ticket validation.
118
     *
119
     * @return XmlResponse
120
     * @throws \ErrorException
121
     */
122
    public function proxy(
123
        Request $request,
124
        #[MapQueryParameter] ?string $targetService = null,
125
        #[MapQueryParameter] ?string $pgt = null,
126
    ): XmlResponse {
127
        // NOTE: Here we do not override the configuration
128
        $legal_target_service_urls = $this->casConfig->getOptionalValue('legal_target_service_urls', []);
129
        // Fail if
130
        $message = match (true) {
131
            // targetService parameter is not defined
132
            $targetService === null => 'Missing target service parameter [targetService]',
133
            // pgt parameter is not defined
134
            $pgt === null => 'Missing proxy granting ticket parameter: [pgt]',
135
            !$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

135
            !$this->checkServiceURL($this->sanitize(/** @scrutinizer ignore-type */ $targetService), $legal_target_service_urls) =>
Loading history...
136
                "Target service parameter not listed as a legal service: [targetService] = {$targetService}",
137
            default => null,
138
        };
139
140
        if (!empty($message)) {
141
            return new XmlResponse(
142
                (string)$this->cas20Protocol->getValidateFailureResponse(C::ERR_INVALID_REQUEST, $message),
143
                Response::HTTP_BAD_REQUEST,
144
            );
145
        }
146
147
        // Get the ticket
148
        $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

148
        $proxyGrantingTicket = $this->ticketStore->getTicket(/** @scrutinizer ignore-type */ $pgt);
Loading history...
149
        $message = match (true) {
150
            // targetService parameter is not defined
151
            $proxyGrantingTicket === null => "Ticket {$pgt} not recognized",
152
            // pgt parameter is not defined
153
            !$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

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