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

Cas10Controller   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 156
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 15
eloc 70
c 3
b 0
f 0
dl 0
loc 156
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
C validate() 0 86 13
A getTicketStore() 0 3 1
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 Configuration */
29
    protected Configuration $sspConfig;
30
31
    /** @var Cas10 */
32
    protected Cas10 $cas10Protocol;
33
34
    /** @var TicketFactory */
35
    protected TicketFactory $ticketFactory;
36
37
    // this could be any configured ticket store
38
    protected mixed $ticketStore;
39
40
    /**
41
     * @param   Configuration|null  $sspConfig
42
     * @param   Configuration|null  $casConfig
43
     * @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...
44
     *
45
     * @throws \Exception
46
     */
47
    public function __construct(
48
        Configuration $sspConfig = null,
49
        Configuration $casConfig = null,
50
        $ticketStore = null,
51
    ) {
52
        $this->sspConfig = $sspConfig ?? Configuration::getInstance();
53
        $this->casConfig = $casConfig ?? Configuration::getConfig('module_casserver.php');
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
71
     * @param   string|null  $ticket
72
     * @param   string|null  $service
73
     *
74
     * @return Response
75
     */
76
    public function validate(
77
        Request $request,
78
        #[MapQueryParameter] bool $renew = false,
79
        #[MapQueryParameter] ?string $ticket = null,
80
        #[MapQueryParameter] ?string $service = null,
81
    ): Response {
82
83
        $forceAuthn = $renew;
84
        // Check if any of the required query parameters are missing
85
        if ($service === null || $ticket === null) {
86
            $messagePostfix = $service === null ? 'service' : 'ticket';
87
            Logger::debug("casserver: Missing service parameter: [{$messagePostfix}]");
88
            return new Response(
89
                $this->cas10Protocol->getValidateFailureResponse(),
90
                Response::HTTP_BAD_REQUEST,
91
            );
92
        }
93
94
        try {
95
            // Get the service ticket
96
            // `getTicket` uses the unserializable method and Objects may throw Throwables in their
97
            // unserialization handlers.
98
            $serviceTicket = $this->ticketStore->getTicket($ticket);
99
            // Delete the ticket
100
            $this->ticketStore->deleteTicket($ticket);
101
        } catch (\Exception $e) {
102
            Logger::error('casserver:validate: internal server error. ' . var_export($e->getMessage(), true));
103
            return new Response(
104
                $this->cas10Protocol->getValidateFailureResponse(),
105
                Response::HTTP_INTERNAL_SERVER_ERROR,
106
            );
107
        }
108
109
        $failed = false;
110
        $message = '';
111
        if (empty($serviceTicket)) {
112
            // No ticket
113
            $message = 'ticket: ' . var_export($ticket, true) . ' not recognized';
114
            $failed = true;
115
        } elseif (!$this->ticketFactory->isServiceTicket($serviceTicket)) {
116
            // This is not a service ticket
117
            $message = 'ticket: ' . var_export($ticket, true) . ' is not a service ticket';
118
            $failed = true;
119
        } elseif ($this->ticketFactory->isExpired($serviceTicket)) {
120
            // the ticket has expired
121
            $message = 'Ticket has ' . var_export($ticket, true) . ' expired';
122
            $failed = true;
123
        } elseif ($this->sanitize($serviceTicket['service']) !== $this->sanitize($service)) {
124
            // The service url we passed to the query parameters does not match the one in the ticket.
125
            $message = 'Mismatching service parameters: expected ' .
126
                var_export($serviceTicket['service'], true) .
127
                ' but was: ' . var_export($service, true);
128
            $failed = true;
129
        } elseif ($forceAuthn && !$serviceTicket['forceAuthn']) {
130
            // If `forceAuthn` is required but not set in the ticket
131
            $message = 'Ticket was issued from single sign on session';
132
            $failed = true;
133
        }
134
135
        if ($failed) {
136
            Logger::error('casserver:validate: ' . $message);
137
            return new Response(
138
                $this->cas10Protocol->getValidateFailureResponse(),
139
                Response::HTTP_BAD_REQUEST,
140
            );
141
        }
142
143
        // Get the username field
144
        $usernameField = $this->casConfig->getOptionalValue('attrname', 'eduPersonPrincipalName');
145
146
        // Fail if the username field is not present in the attribute list
147
        if (!\array_key_exists($usernameField, $serviceTicket['attributes'])) {
148
            Logger::error(
149
                'casserver:validate: internal server error. Missing user name attribute: '
150
                . var_export($usernameField, true),
151
            );
152
            return new Response(
153
                $this->cas10Protocol->getValidateFailureResponse(),
154
                Response::HTTP_BAD_REQUEST,
155
            );
156
        }
157
158
        // Successful validation
159
        return new Response(
160
            $this->cas10Protocol->getValidateSuccessResponse($serviceTicket['attributes'][$usernameField][0]),
161
            Response::HTTP_OK,
162
        );
163
    }
164
165
    /**
166
     * Used by the unit tests
167
     *
168
     * @return mixed
169
     */
170
    public function getTicketStore(): mixed
171
    {
172
        return $this->ticketStore;
173
    }
174
}
175