Passed
Push — master ( 81b6b5...5b35ab )
by Tim
03:44
created

Cas30Controller   A

Complexity

Total Complexity 9

Size/Duplication

Total Lines 108
Duplicated Lines 0 %

Importance

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

2 Methods

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