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