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

Cas10Controller::validate()   C

Complexity

Conditions 13
Paths 22

Size

Total Lines 88
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 50
c 3
b 0
f 0
dl 0
loc 88
rs 6.6166
cc 13
nc 22
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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