Issues (4)

src/Controller/AttributeServer.php (4 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\exampleattributeserver\Controller;
6
7
use DateInterval;
8
use Nyholm\Psr7\Factory\Psr17Factory;
9
use SimpleSAML\{Configuration, Error, Logger};
10
use SimpleSAML\HTTP\RunnableResponse;
11
use SimpleSAML\Metadata\MetaDataStorageHandler;
12
use SimpleSAML\SAML2\Binding;
13
use SimpleSAML\SAML2\Binding\{SynchronousBindingInterface, SOAP};
14
use SimpleSAML\SAML2\Constants as C;
15
use SimpleSAML\SAML2\Utils as SAML2_Utils;
16
use SimpleSAML\SAML2\XML\saml\{
17
    Assertion,
18
    Attribute,
19
    AttributeStatement,
20
    AttributeValue,
21
    Audience,
22
    AudienceRestriction,
23
    Conditions,
24
    Issuer,
25
    Subject,
26
    SubjectConfirmation,
27
    SubjectConfirmationData,
28
};
29
use SimpleSAML\SAML2\XML\samlp\{AttributeQuery, Response, Status, StatusCode};
30
use SimpleSAML\Utils;
31
use SimpleSAML\XML\Utils\Random;
32
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
33
use SimpleSAML\XMLSecurity\Key\PrivateKey;
34
use SimpleSAML\XMLSecurity\XML\ds\{KeyInfo, X509Certificate, X509Data};
35
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
36
use Symfony\Bridge\PsrHttpMessage\Factory\{HttpFoundationFactory, PsrHttpFactory};
37
use Symfony\Component\HttpFoundation\Request;
38
39
use function array_filter;
40
41
/**
42
 * Controller class for the exampleattributeserver module.
43
 *
44
 * This class serves the attribute server available in the module.
45
 *
46
 * @package SimpleSAML\Module\exampleattributeserver
47
 */
48
class AttributeServer
49
{
50
    /** @var \SimpleSAML\Configuration */
51
    protected Configuration $config;
52
53
    /** @var \SimpleSAML\Metadata\MetaDataStorageHandler|null */
54
    protected ?MetaDataStorageHandler $metadataHandler = null;
55
56
57
    /**
58
     * ConfigController constructor.
59
     *
60
     * @param \SimpleSAML\Configuration $config The configuration to use.
61
     */
62
    public function __construct(Configuration $config)
63
    {
64
        $this->config = $config;
65
    }
66
67
68
    /**
69
     * Inject the \SimpleSAML\Metadata\MetaDataStorageHandler dependency.
70
     *
71
     * @param \SimpleSAML\Metadata\MetaDataStorageHandler $handler
72
     */
73
    public function setMetadataStorageHandler(MetaDataStorageHandler $handler): void
74
    {
75
        $this->metadataHandler = $handler;
76
    }
77
78
79
    /**
80
     * @param \Symfony\Component\HttpFoundation\Request $request The current request.
81
     *
82
     * @return \SimpleSAML\HTTP\RunnableResponse
83
     * @throws \SimpleSAML\Error\BadRequest
84
     */
85
    public function main(/** @scrutinizer ignore-unused */ Request $request): RunnableResponse
86
    {
87
        $psr17Factory = new Psr17Factory();
88
        $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
89
        $psrRequest = $psrHttpFactory->createRequest($request);
90
91
        $binding = Binding::getCurrentBinding($psrRequest);
92
        if (!($binding instanceof SynchronousBindingInterface)) {
93
            throw new Error\BadRequest('Invalid binding; MUST use a synchronous binding.');
94
        }
95
96
        $message = $binding->receive($psrRequest);
97
        if (!($message instanceof AttributeQuery)) {
98
            throw new Error\BadRequest('Invalid message received to AttributeQuery endpoint.');
99
        }
100
101
        $idpEntityId = $this->metadataHandler->getMetaDataCurrentEntityID('saml20-idp-hosted');
0 ignored issues
show
The method getMetaDataCurrentEntityID() 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

101
        /** @scrutinizer ignore-call */ 
102
        $idpEntityId = $this->metadataHandler->getMetaDataCurrentEntityID('saml20-idp-hosted');

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...
102
103
        $issuer = $message->getIssuer();
104
        if ($issuer === null) {
105
            throw new Error\BadRequest('Missing <saml:Issuer> in <samlp:AttributeQuery>.');
106
        } else {
107
            $spEntityId = $issuer->getContent();
108
            if ($spEntityId === '') {
109
                throw new Error\BadRequest('Empty <saml:Issuer> in <samlp:AttributeQuery>.');
110
            }
111
        }
112
113
        $idpMetadata = $this->metadataHandler->getMetaDataConfig($idpEntityId, 'saml20-idp-hosted');
114
        $spMetadata = $this->metadataHandler->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
115
116
        // The endpoint we should deliver the message to
117
        $endpoint = $spMetadata->getString('testAttributeEndpoint');
118
119
        // The attributes we will return
120
        $attributes = [
121
            new Attribute(
122
                'name',
123
                C::NAMEFORMAT_UNSPECIFIED,
124
                null,
125
                [
126
                    new AttributeValue('value1'),
127
                    new AttributeValue('value2'),
128
                    new AttributeValue('value3'),
129
                ],
130
            ),
131
            new Attribute(
132
                'test',
133
                C::NAMEFORMAT_UNSPECIFIED,
134
                null,
135
                [
136
                    new AttributeValue('test'),
137
                ],
138
            ),
139
        ];
140
141
        // Determine which attributes we will return
142
        // @phpstan-ignore identical.alwaysFalse
143
        if (count($attributes) === 0) {
144
            Logger::debug('No attributes requested - return all attributes.');
145
            $attributeStatement = null;
146
        } else {
147
            $returnAttributes = [];
148
            foreach ($message->getAttributes() as $reqAttr) {
149
                foreach ($attributes as $attr) {
150
                    if (
151
                        $attr->getName() === $reqAttr->getName()
152
                        && $attr->getNameFormat() === $reqAttr->getNameFormat()
153
                    ) {
154
                        // The requested attribute is available
155
                        if ($reqAttr->getAttributeValues() === []) {
156
                            // If no specific values are requested, return all
157
                            $returnAttributes[] = $attr;
158
                        } else {
159
                            $returnValues = $this->filterAttributeValues(
160
                                $reqAttr->getAttributeValues(),
161
                                $attr->getAttributeValues(),
162
                            );
163
164
                            $returnAttributes[] = new Attribute(
165
                                $attr->getName(),
166
                                $attr->getNameFormat(),
167
                                null,
168
                                $returnValues,
169
                                $attr->getAttributesNS(),
170
                            );
171
                        }
172
                    }
173
                }
174
            }
175
176
            $attributeStatement = $returnAttributes ? (new AttributeStatement($returnAttributes)) : null;
177
        }
178
179
        // $returnAttributes contains the attributes we should return. Send them
180
        $clock = SAML2_Utils::getContainer()->getClock();
181
182
        $statements = array_filter([$attributeStatement]);
183
        $assertion = new Assertion(
184
            issuer: new Issuer($idpEntityId),
185
            issueInstant: $clock->now(),
186
            id: (new Random())->generateID(),
187
            subject: new Subject(
188
                identifier: $message->getSubject()->getIdentifier(),
189
                subjectConfirmation: [
190
                    new SubjectConfirmation(
191
                        method: C::CM_BEARER,
192
                        subjectConfirmationData: new SubjectConfirmationData(
193
                            notOnOrAfter: $clock->now()->add(new DateInterval('PT300S')),
194
                            recipient: $endpoint,
195
                            inResponseTo: $message->getId(),
196
                        ),
197
                    ),
198
                ],
199
            ),
200
            conditions: new Conditions(
201
                notBefore: $clock->now(),
202
                notOnOrAfter: $clock->now()->add(new DateInterval('PT300S')),
203
                audienceRestriction: [
204
                    new AudienceRestriction([
205
                        new Audience($spEntityId),
206
                    ]),
207
                ],
208
            ),
209
            statements: $statements,
210
        );
211
212
        self::addSign($idpMetadata, $spMetadata, $assertion);
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\Module\exampl...ributeServer::addSign() has been deprecated: This method is a modified version of \SimpleSAML\Module\saml\Message::addSign and should be replaced with a call to a future ServiceProvider-class in the saml2-library ( Ignorable by Annotation )

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

212
        /** @scrutinizer ignore-deprecated */ self::addSign($idpMetadata, $spMetadata, $assertion);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
213
214
        $response = new Response(
215
            status: new Status(
216
                new StatusCode(C::STATUS_SUCCESS),
217
            ),
218
            issueInstant: $clock->now(),
219
            issuer: $issuer,
220
            id: (new Random())->generateID(),
221
            version: '2.0',
222
            inResponseTo: $message->getId(),
223
            destination: $endpoint,
224
            assertions: [$assertion],
225
        );
226
227
        self::addSign($idpMetadata, $spMetadata, $response);
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\Module\exampl...ributeServer::addSign() has been deprecated: This method is a modified version of \SimpleSAML\Module\saml\Message::addSign and should be replaced with a call to a future ServiceProvider-class in the saml2-library ( Ignorable by Annotation )

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

227
        /** @scrutinizer ignore-deprecated */ self::addSign($idpMetadata, $spMetadata, $response);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
228
229
        $soap = new SOAP();
230
        return new RunnableResponse([$soap, 'send'], [$response]);
231
    }
232
233
234
    /**
235
     * @param array<\SimpleSAML\SAML2\XML\saml\AttributeValue> $reqValues
236
     * @param array<\SimpleSAML\SAML2\XML\saml\AttributeValue> $values
237
     *
238
     * @return array<\SimpleSAML\SAML2\XML\saml\AttributeValue>
239
     */
240
    private function filterAttributeValues(array $reqValues, array $values): array
241
    {
242
        $result = [];
243
244
        foreach ($reqValues as $x) {
245
            foreach ($values as $y) {
246
                if ($x->getValue() === $y->getValue()) {
247
                    $result[] = $y;
248
                }
249
            }
250
        }
251
252
        return $result;
253
    }
254
255
256
    /**
257
     * @deprecated This method is a modified version of \SimpleSAML\Module\saml\Message::addSign and
258
     *  should be replaced with a call to a future ServiceProvider-class in the saml2-library
259
     *
260
     * Add signature key and sender certificate to an element (Message or Assertion).
261
     *
262
     * @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
263
     * @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient.
264
     * @param \SimpleSAML\XMLSecurity\XML\SignableElementInterface $element The element we should add the data to.
265
     */
266
    private static function addSign(
267
        Configuration $srcMetadata,
268
        Configuration $dstMetadata,
269
        SignableElementInterface &$element,
270
    ): void {
271
        $dstPrivateKey = $dstMetadata->getOptionalString('signature.privatekey', null);
272
        $cryptoUtils = new Utils\Crypto();
273
274
        if ($dstPrivateKey !== null) {
275
            /** @var string[] $keyArray */
276
            $keyArray = $cryptoUtils->loadPrivateKey($dstMetadata, true, 'signature.');
277
            $certArray = $cryptoUtils->loadPublicKey($dstMetadata, false, 'signature.');
278
        } else {
279
            /** @var string[] $keyArray */
280
            $keyArray = $cryptoUtils->loadPrivateKey($srcMetadata, true);
281
            $certArray = $cryptoUtils->loadPublicKey($srcMetadata, false);
282
        }
283
284
        $algo = $dstMetadata->getOptionalString('signature.algorithm', null);
285
        if ($algo === null) {
286
            $algo = $srcMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256);
287
        }
288
289
        $privateKey = PrivateKey::fromFile($keyArray['PEM'], $keyArray['password']);
290
291
        $keyInfo = null;
292
        if ($certArray !== null) {
293
            $keyInfo = new KeyInfo([
294
                new X509Data(
295
                    [
296
                        new X509Certificate($certArray['PEM']),
297
                    ],
298
                ),
299
            ]);
300
        }
301
302
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(
303
            $algo,
0 ignored issues
show
It seems like $algo can also be of type null; however, parameter $algId of SimpleSAML\XMLSecurity\A...Factory::getAlgorithm() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

303
            /** @scrutinizer ignore-type */ $algo,
Loading history...
304
            $privateKey,
305
        );
306
307
        $element->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo);
308
    }
309
}
310