Passed
Pull Request — master (#45)
by
unknown
02:48
created

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

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

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

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

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