Passed
Pull Request — master (#45)
by
unknown
03:32
created

Cas20Controller::proxyValidate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 16
rs 10
cc 1
nc 1
nop 6
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
     */
121
    public function proxy(
122
        Request $request,
123
        #[MapQueryParameter] ?string $targetService = null,
124
        #[MapQueryParameter] ?string $pgt = null,
125
    ): XmlResponse {
126
        $legal_target_service_urls = $this->casConfig->getOptionalValue('legal_target_service_urls', []);
127
        // Fail if
128
        $message = match (true) {
129
            // targetService parameter is not defined
130
            $targetService === null => 'Missing target service parameter [targetService]',
131
            // pgt parameter is not defined
132
            $pgt === null => 'Missing proxy granting ticket parameter: [pgt]',
133
            !$this->checkServiceURL($this->sanitize($targetService), $legal_target_service_urls) =>
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\Module\casser...ller::checkServiceURL() has been deprecated. ( Ignorable by Annotation )

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

133
            !/** @scrutinizer ignore-deprecated */ $this->checkServiceURL($this->sanitize($targetService), $legal_target_service_urls) =>
Loading history...
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

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

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

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