Passed
Pull Request — master (#214)
by
unknown
02:26
created

LogoutRequest::setReason()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SAML2;
6
7
use DOMElement;
8
use RobRichards\XMLSecLibs\XMLSecEnc;
9
use RobRichards\XMLSecLibs\XMLSecurityKey;
10
use SAML2\XML\saml\NameID;
11
use Webmozart\Assert\Assert;
12
13
/**
14
 * Class for SAML 2 logout request messages.
15
 *
16
 * @package SimpleSAMLphp
17
 */
18
class LogoutRequest extends Request
19
{
20
    /**
21
     * The expiration time of this request.
22
     *
23
     * @var int|null
24
     */
25
    private $notOnOrAfter = null;
26
27
    /**
28
     * The encrypted NameID in the request.
29
     *
30
     * If this is not null, the NameID needs decryption before it can be accessed.
31
     *
32
     * @var \DOMElement|null
33
     */
34
    private $encryptedNameId = null;
35
36
    /**
37
     * The name identifier of the session that should be terminated.
38
     *
39
     * @var \SAML2\XML\saml\NameID|null
40
     */
41
    private $nameId = null;
42
43
    /**
44
     * The SessionIndexes of the sessions that should be terminated.
45
     *
46
     * @var array
47
     */
48
    private $sessionIndexes = [];
49
50
    /**
51
     * The optional reason for the logout, typically a URN
52
     * See \SAML2\Constants::LOGOUT_REASON_*
53
     * From the standard section 3.7.3: "other values MAY be agreed on between participants"
54
     *
55
     * @var string|null
56
     */
57
    protected $reason = null;
58
59
60
    /**
61
     * Constructor for SAML 2 logout request messages.
62
     *
63
     * @param \DOMElement|null $xml The input message.
64
     * @throws \Exception
65
     */
66
    public function __construct(DOMElement $xml = null)
67
    {
68
        parent::__construct('LogoutRequest', $xml);
69
70
        if ($xml === null) {
71
            return;
72
        }
73
74
        if ($xml->hasAttribute('NotOnOrAfter')) {
75
            $this->notOnOrAfter = Utils::xsDateTimeToTimestamp($xml->getAttribute('NotOnOrAfter'));
76
        }
77
78
        if ($xml->hasAttribute('Reason')) {
79
            $this->reason = $xml->getAttribute('Reason');
80
        }
81
82
        /** @var \DOMElement[] $nameId */
83
        $nameId = Utils::xpQuery($xml, './saml_assertion:NameID | ./saml_assertion:EncryptedID/xenc:EncryptedData');
84
        if (empty($nameId)) {
85
            throw new \Exception('Missing <saml:NameID> or <saml:EncryptedID> in <samlp:LogoutRequest>.');
86
        } elseif (count($nameId) > 1) {
87
            throw new \Exception('More than one <saml:NameID> or <saml:EncryptedD> in <samlp:LogoutRequest>.');
88
        }
89
        if ($nameId[0]->localName === 'EncryptedData') {
90
            /* The NameID element is encrypted. */
91
            $this->encryptedNameId = $nameId[0];
92
        } else {
93
            $this->nameId = new NameID($nameId[0]);
94
        }
95
96
        /** @var \DOMElement[] $sessionIndexes */
97
        $sessionIndexes = Utils::xpQuery($xml, './saml_protocol:SessionIndex');
98
        foreach ($sessionIndexes as $sessionIndex) {
99
            $this->sessionIndexes[] = trim($sessionIndex->textContent);
100
        }
101
    }
102
103
104
    /**
105
     * Retrieve the expiration time of this request.
106
     *
107
     * @return int|null The expiration time of this request.
108
     */
109
    public function getNotOnOrAfter(): ?int
110
    {
111
        return $this->notOnOrAfter;
112
    }
113
114
115
    /**
116
     * Set the expiration time of this request.
117
     *
118
     * @param int|null $notOnOrAfter The expiration time of this request.
119
     * @return void
120
     */
121
    public function setNotOnOrAfter(int $notOnOrAfter = null): void
122
    {
123
        $this->notOnOrAfter = $notOnOrAfter;
124
    }
125
126
    /**
127
     * Retrieve the reason for this request.
128
     *
129
     * @return string|null The reason for this request.
130
     */
131
    public function getReason(): ?string
132
    {
133
        return $this->reason;
134
    }
135
136
137
    /**
138
     * Set the reason for this request.
139
     *
140
     * @param string|null $reason The optional reason for this request in URN format
141
     * @return void
142
     */
143
    public function setReason($reason = null): void
144
    {
145
        $this->reason = $reason;
146
    }
147
148
149
    /**
150
     * Check whether the NameId is encrypted.
151
     *
152
     * @return bool True if the NameId is encrypted, false if not.
153
     */
154
    public function isNameIdEncrypted(): bool
155
    {
156
        if ($this->encryptedNameId !== null) {
157
            return true;
158
        }
159
160
        return false;
161
    }
162
163
164
    /**
165
     * Encrypt the NameID in the LogoutRequest.
166
     *
167
     * @param XMLSecurityKey $key The encryption key.
168
     * @return void
169
     *
170
     * @throws \InvalidArgumentException if assertions are false
171
     */
172
    public function encryptNameId(XMLSecurityKey $key): void
173
    {
174
        Assert::notEmpty($this->nameId, 'Cannot encrypt NameID without a NameID set.');
175
176
        /* First create a XML representation of the NameID. */
177
        $doc = DOMDocumentFactory::create();
178
        $root = $doc->createElement('root');
179
        $doc->appendChild($root);
180
        $this->nameId->toXML($root);
0 ignored issues
show
Bug introduced by
The method toXML() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

180
        $this->nameId->/** @scrutinizer ignore-call */ 
181
                       toXML($root);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
181
        /** @var \DOMElement $nameId */
182
        $nameId = $root->firstChild;
183
184
        Utils::getContainer()->debugMessage($nameId, 'encrypt');
185
186
        /* Encrypt the NameID. */
187
        $enc = new XMLSecEnc();
188
        $enc->setNode($nameId);
189
        $enc->type = XMLSecEnc::Element;
190
191
        $symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES128_CBC);
192
        $symmetricKey->generateSessionKey();
193
        $enc->encryptKey($key, $symmetricKey);
194
195
        /**
196
         * @var \DOMElement encryptedNameId
197
         * @psalm-suppress UndefinedClass
198
         */
199
        $this->encryptedNameId = $enc->encryptNode($symmetricKey);
200
        $this->nameId = null;
201
    }
202
203
204
    /**
205
     * Decrypt the NameID in the LogoutRequest.
206
     *
207
     * @param XMLSecurityKey $key The decryption key.
208
     * @param array $blacklist Blacklisted decryption algorithms.
209
     * @return void
210
     */
211
    public function decryptNameId(XMLSecurityKey $key, array $blacklist = []): void
212
    {
213
        if ($this->encryptedNameId === null) {
214
            /* No NameID to decrypt. */
215
            return;
216
        }
217
218
        $nameId = Utils::decryptElement($this->encryptedNameId, $key, $blacklist);
219
        Utils::getContainer()->debugMessage($nameId, 'decrypt');
220
        $this->nameId = new NameID($nameId);
221
        $this->encryptedNameId = null;
222
    }
223
224
225
    /**
226
     * Retrieve the name identifier of the session that should be terminated.
227
     *
228
     * @return \SAML2\XML\saml\NameID The name identifier of the session that should be terminated.
229
     *
230
     * @throws \InvalidArgumentException if assertions are false
231
     */
232
    public function getNameId(): NameID
233
    {
234
        Assert::null($this->encryptedNameId, 'Attempted to retrieve encrypted NameID without decrypting it first.');
235
        Assert::notNull($this->nameId);
236
237
        return $this->nameId;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->nameId could return the type null which is incompatible with the type-hinted return SAML2\XML\saml\NameID. Consider adding an additional type-check to rule them out.
Loading history...
238
    }
239
240
241
    /**
242
     * Set the name identifier of the session that should be terminated.
243
     *
244
     * @param \SAML2\XML\saml\NameID $nameId The name identifier of the session that should be terminated.
245
     * @return void
246
     */
247
    public function setNameId(NameID $nameId): void
248
    {
249
        $this->nameId = $nameId;
250
    }
251
252
253
    /**
254
     * Retrieve the SessionIndexes of the sessions that should be terminated.
255
     *
256
     * @return array The SessionIndexes, or an empty array if all sessions should be terminated.
257
     */
258
    public function getSessionIndexes(): array
259
    {
260
        return $this->sessionIndexes;
261
    }
262
263
264
    /**
265
     * Set the SessionIndexes of the sessions that should be terminated.
266
     *
267
     * @param array $sessionIndexes The SessionIndexes, or an empty array if all sessions should be terminated.
268
     * @return void
269
     */
270
    public function setSessionIndexes(array $sessionIndexes): void
271
    {
272
        $this->sessionIndexes = $sessionIndexes;
273
    }
274
275
276
    /**
277
     * Retrieve the sesion index of the session that should be terminated.
278
     *
279
     * @return string|null The sesion index of the session that should be terminated.
280
     */
281
    public function getSessionIndex(): ?string
282
    {
283
        if (empty($this->sessionIndexes)) {
284
            return null;
285
        }
286
287
        return $this->sessionIndexes[0];
288
    }
289
290
291
    /**
292
     * Set the sesion index of the session that should be terminated.
293
     *
294
     * @param string|null $sessionIndex The sesion index of the session that should be terminated.
295
     * @return void
296
     */
297
    public function setSessionIndex(string $sessionIndex = null): void
298
    {
299
        if (is_null($sessionIndex)) {
300
            $this->sessionIndexes = [];
301
        } else {
302
            $this->sessionIndexes = [$sessionIndex];
303
        }
304
    }
305
306
307
    /**
308
     * Convert this logout request message to an XML element.
309
     *
310
     * @return \DOMElement This logout request.
311
     */
312
    public function toXML(): DOMElement
313
    {
314
        if ($this->encryptedNameId === null && $this->nameId === null) {
315
            throw new \Exception('Cannot convert LogoutRequest to XML without a NameID set.');
316
        }
317
318
        $root = parent::toXML();
319
320
        if ($this->notOnOrAfter !== null) {
321
            $root->setAttribute('NotOnOrAfter', gmdate('Y-m-d\TH:i:s\Z', $this->notOnOrAfter));
322
        }
323
324
        if ($this->reason !== null) {
325
            $root->setAttribute('Reason', $this->reason);
326
        }
327
328
        if ($this->encryptedNameId === null) {
329
            $this->nameId->toXML($root);
330
        } else {
331
            $eid = $root->ownerDocument->createElementNS(Constants::NS_SAML, 'saml:EncryptedID');
332
            $root->appendChild($eid);
333
            $eid->appendChild($root->ownerDocument->importNode($this->encryptedNameId, true));
334
        }
335
336
        foreach ($this->sessionIndexes as $sessionIndex) {
337
            Utils::addString($root, Constants::NS_SAMLP, 'SessionIndex', $sessionIndex);
338
        }
339
340
        return $root;
341
    }
342
}
343