Cas30Controller   A
last analyzed

Complexity

Total Complexity 9

Size/Duplication

Total Lines 109
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 9
eloc 34
c 1
b 0
f 0
dl 0
loc 109
rs 10

2 Methods

Rating   Name   Duplication   Size   Complexity  
B samlValidate() 0 51 6
A __construct() 0 13 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\casserver\Controller;
6
7
use Exception;
8
use RuntimeException;
9
use SimpleSAML\Configuration;
10
use SimpleSAML\Logger;
11
use SimpleSAML\Module\casserver\Cas\Protocol\Cas20;
12
use SimpleSAML\Module\casserver\Cas\Protocol\SamlValidateResponder;
13
use SimpleSAML\Module\casserver\Cas\TicketValidator;
14
use SimpleSAML\Module\casserver\Controller\Traits\UrlTrait;
15
use SimpleSAML\Module\casserver\Http\XmlResponse;
16
use SimpleSAML\SAML11\Exception\ProtocolViolationException;
17
use SimpleSAML\SAML11\XML\samlp\Request as SamlRequest;
18
use SimpleSAML\SOAP\XML\env_200106\Envelope;
19
use SimpleSAML\XML\DOMDocumentFactory;
20
use Symfony\Component\HttpFoundation\Request;
21
use Symfony\Component\HttpFoundation\Response;
22
use Symfony\Component\HttpKernel\Attribute\AsController;
23
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
24
25
use function is_array;
26
27
#[AsController]
28
class Cas30Controller
29
{
30
    use UrlTrait;
31
32
    /** @var \SimpleSAML\Logger */
33
    protected Logger $logger;
34
35
    /** @var \SimpleSAML\Configuration */
36
    protected Configuration $casConfig;
37
38
    /** @var \SimpleSAML\Module\casserver\Cas\Protocol\Cas20 */
39
    protected Cas20 $cas20Protocol;
40
41
    /** @var \SimpleSAML\Module\casserver\Cas\TicketValidator */
42
    protected TicketValidator $ticketValidator;
43
44
    /** @var \SimpleSAML\Module\casserver\Cas\Protocol\SamlValidateResponder */
45
    protected SamlValidateResponder $validateResponder;
46
47
    /**
48
     * @param \SimpleSAML\Configuration $sspConfig
49
     * @param \SimpleSAML\Configuration|null $casConfig
50
     * @param \SimpleSAML\Module\casserver\Cas\TicketValidator|null $ticketValidator
51
     *
52
     * @throws \Exception
53
     */
54
    public function __construct(
55
        private readonly Configuration $sspConfig,
56
        ?Configuration $casConfig = null,
57
        ?TicketValidator $ticketValidator = 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 testin.
62
        $this->casConfig = ($casConfig === null || $casConfig === $sspConfig)
63
            ? Configuration::getConfig('module_casserver.php') : $casConfig;
64
        $this->cas20Protocol = new Cas20($this->casConfig);
65
        $this->ticketValidator   = $ticketValidator ?? new TicketValidator($this->casConfig);
66
        $this->validateResponder = new SamlValidateResponder();
67
    }
68
69
70
    /**
71
     * POST /casserver/samlValidate?TARGET=
72
     * Host: cas.example.com
73
     * Content-Length: 491
74
     * Content-Type: text/xml
75
     *
76
     * @param \Symfony\Component\HttpFoundation\Request $request
77
     * @param string $TARGET  URL encoded service identifier of the back-end service.
78
     *
79
     * @throw SimpleSAML\SAML11\Exception\ProtocolViolationException
80
     * @throw SimpleSAML\XML\Exception\MissingAttributeException
81
     * @throw \RuntimeException
82
     * @return \SimpleSAML\Module\casserver\Http\XmlResponse
83
     * @link https://apereo.github.io/cas/7.1.x/protocol/CAS-Protocol-Specification.html#42-samlvalidate-cas-30
84
     */
85
    public function samlValidate(
86
        Request $request,
87
        #[MapQueryParameter] string $TARGET,
88
    ): XmlResponse {
89
        $postBody = $request->getContent();
90
        if (empty($postBody)) {
91
            throw new RuntimeException('samlValidate expects a soap body.');
92
        }
93
94
        // SAML request values
95
        //
96
        // samlp:Request
97
        //  - RequestID [REQUIRED] - unique identifier for the request
98
        //  - IssueInstant [REQUIRED] - timestamp of the request
99
        // samlp:AssertionArtifact [REQUIRED] - the valid CAS Service
100
101
        $documentBody = DOMDocumentFactory::fromString($postBody);
102
        $envelope = Envelope::fromXML($documentBody->documentElement);
103
104
        // The SOAP Envelope must have only one ticket
105
        $elements = $envelope->getBody()->getElements();
106
        if (count($elements) > 1 || count($elements) < 1) {
107
            throw new ProtocolViolationException('samlValidate expects a soap body with only one ticket.');
108
        }
109
110
        // Request Element
111
        $samlpRequestParsed = SamlRequest::fromXML($elements[0]->getXML());
112
        // Assertion Artifact Element
113
        $assertionArtifactParsed = $samlpRequestParsed->getRequest()[0];
114
115
        $ticketId = $assertionArtifactParsed->getContent();
116
        Logger::debug('samlvalidate: Checking ticket ' . $ticketId);
117
118
        try {
119
            // validateAndDeleteTicket might throw a CasException. In order to avoid third party modules
120
            // dependencies, we will catch and rethrow the Exception.
121
            $ticket = $this->ticketValidator->validateAndDeleteTicket($ticketId, $TARGET);
122
        } catch (Exception $e) {
123
            throw new RuntimeException($e->getMessage());
124
        }
125
126
        if (!is_array($ticket)) {
0 ignored issues
show
introduced by
The condition is_array($ticket) is always true.
Loading history...
127
            throw new RuntimeException('Error loading ticket');
128
        }
129
130
        $response = $this->validateResponder->convertToSaml($ticket);
131
        $soap = $this->validateResponder->wrapInSoap($response);
132
133
        return new XmlResponse(
134
            (string)$soap,
135
            Response::HTTP_OK,
136
        );
137
    }
138
}
139