44
55namespace SimpleSAML \Module \exampleattributeserver \Controller ;
66
7- use SAML2 \Assertion ;
8- use SAML2 \AttributeQuery ;
9- use SAML2 \Binding ;
10- use SAML2 \Constants ;
11- use SAML2 \HTTPPost ;
12- use SAML2 \Response ;
13- use SAML2 \XML \saml \Issuer ;
14- use SAML2 \XML \saml \SubjectConfirmation ;
15- use SAML2 \XML \saml \SubjectConfirmationData ;
16- use SimpleSAML \Configuration ;
17- use SimpleSAML \Error ;
7+ use DateInterval ;
8+ use Nyholm \Psr7 \Factory \Psr17Factory ;
9+ use SimpleSAML \{Configuration , Error , Logger };
1810use SimpleSAML \HTTP \RunnableResponse ;
19- use SimpleSAML \Logger ;
2011use SimpleSAML \Metadata \MetaDataStorageHandler ;
21- use SimpleSAML \Module \saml \Message ;
12+ use SimpleSAML \SAML2 \Binding ;
13+ use SimpleSAML \SAML2 \Binding \HTTPPost ;
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 };
2237use Symfony \Component \HttpFoundation \Request ;
2338
39+ use function array_filter ;
40+
2441/**
2542 * Controller class for the exampleattributeserver module.
2643 *
@@ -67,19 +84,23 @@ public function setMetadataStorageHandler(MetaDataStorageHandler $handler): void
6784 */
6885 public function main (/** @scrutinizer ignore-unused */ Request $ request ): RunnableResponse
6986 {
70- $ binding = Binding::getCurrentBinding ();
71- $ query = $ binding ->receive ();
72- if (!($ query instanceof AttributeQuery)) {
87+ $ psr17Factory = new Psr17Factory ();
88+ $ psrHttpFactory = new PsrHttpFactory ($ psr17Factory , $ psr17Factory , $ psr17Factory , $ psr17Factory );
89+ $ psrRequest = $ psrHttpFactory ->createRequest ($ request );
90+
91+ $ binding = Binding::getCurrentBinding ($ psrRequest );
92+ $ message = $ binding ->receive ($ psrRequest );
93+ if (!($ message instanceof AttributeQuery)) {
7394 throw new Error \BadRequest ('Invalid message received to AttributeQuery endpoint. ' );
7495 }
7596
7697 $ idpEntityId = $ this ->metadataHandler ->getMetaDataCurrentEntityID ('saml20-idp-hosted ' );
7798
78- $ issuer = $ query ->getIssuer ();
99+ $ issuer = $ message ->getIssuer ();
79100 if ($ issuer === null ) {
80101 throw new Error \BadRequest ('Missing <saml:Issuer> in <samlp:AttributeQuery>. ' );
81102 } else {
82- $ spEntityId = $ issuer ->getValue ();
103+ $ spEntityId = $ issuer ->getContent ();
83104 if ($ spEntityId === '' ) {
84105 throw new Error \BadRequest ('Empty <saml:Issuer> in <samlp:AttributeQuery>. ' );
85106 }
@@ -93,73 +114,195 @@ public function main(/** @scrutinizer ignore-unused */ Request $request): Runnab
93114
94115 // The attributes we will return
95116 $ attributes = [
96- 'name ' => ['value1 ' , 'value2 ' , 'value3 ' ],
97- 'test ' => ['test ' ],
117+ new Attribute (
118+ 'name ' ,
119+ C::NAMEFORMAT_UNSPECIFIED ,
120+ null ,
121+ [
122+ new AttributeValue ('value1 ' ),
123+ new AttributeValue ('value2 ' ),
124+ new AttributeValue ('value3 ' ),
125+ ],
126+ ),
127+ new Attribute (
128+ 'test ' ,
129+ C::NAMEFORMAT_UNSPECIFIED ,
130+ null ,
131+ [
132+ new AttributeValue ('test ' ),
133+ ],
134+ ),
98135 ];
99136
100- // The name format of the attributes
101- $ attributeNameFormat = Constants::NAMEFORMAT_UNSPECIFIED ;
102-
103137 // Determine which attributes we will return
104- $ returnAttributes = array_keys ( $ query -> getAttributes ());
105- if (count ($ returnAttributes ) === 0 ) {
138+ // @phpstan-ignore identical.alwaysFalse
139+ if (count ($ attributes ) === 0 ) {
106140 Logger::debug ('No attributes requested - return all attributes. ' );
107- $ returnAttributes = $ attributes ;
108- } elseif ($ query ->getAttributeNameFormat () !== $ attributeNameFormat ) {
109- Logger::debug ('Requested attributes with wrong NameFormat - no attributes returned. ' );
110- $ returnAttributes = [];
141+ $ attributeStatement = null ;
111142 } else {
112- /** @var array<mixed>$values */
113- foreach ($ returnAttributes as $ name => $ values ) {
114- if (!array_key_exists ($ name , $ attributes )) {
115- // We don't have this attribute
116- unset($ returnAttributes [$ name ]);
117- continue ;
118- }
119- if (count ($ values ) === 0 ) {
120- // Return all attributes
121- $ returnAttributes [$ name ] = $ attributes [$ name ];
122- continue ;
123- }
143+ $ returnAttributes = [];
144+ foreach ($ message ->getAttributes () as $ reqAttr ) {
145+ foreach ($ attributes as $ attr ) {
146+ if (
147+ $ attr ->getName () === $ reqAttr ->getName ()
148+ && $ attr ->getNameFormat () === $ reqAttr ->getNameFormat ()
149+ ) {
150+ // The requested attribute is available
151+ if ($ reqAttr ->getAttributeValues () === []) {
152+ // If no specific values are requested, return all
153+ $ returnAttributes [] = $ attr ;
154+ } else {
155+ $ returnValues = $ this ->filterAttributeValues (
156+ $ reqAttr ->getAttributeValues (),
157+ $ attr ->getAttributeValues (),
158+ );
124159
125- // Filter which attribute values we should return
126- $ returnAttributes [$ name ] = array_intersect ($ values , $ attributes [$ name ]);
160+ $ returnAttributes [] = new Attribute (
161+ $ attr ->getName (),
162+ $ attr ->getNameFormat (),
163+ null ,
164+ $ returnValues ,
165+ $ attr ->getAttributesNS (),
166+ );
167+ }
168+ }
169+ }
127170 }
171+
172+ $ attributeStatement = $ returnAttributes ? (new AttributeStatement ($ returnAttributes )) : null ;
128173 }
129174
130175 // $returnAttributes contains the attributes we should return. Send them
131- $ issuer = new Issuer ();
132- $ issuer ->setValue ($ idpEntityId );
133-
134- $ assertion = new Assertion ();
135- $ assertion ->setIssuer ($ issuer );
136- $ assertion ->setNameId ($ query ->getNameId ());
137- $ assertion ->setNotBefore (time ());
138- $ assertion ->setNotOnOrAfter (time () + 300 ); // 60*5 = 5min
139- $ assertion ->setValidAudiences ([$ spEntityId ]);
140- $ assertion ->setAttributes ($ returnAttributes );
141- $ assertion ->setAttributeNameFormat ($ attributeNameFormat );
142-
143- $ sc = new SubjectConfirmation ();
144- $ sc ->setMethod (Constants::CM_BEARER );
145-
146- $ scd = new SubjectConfirmationData ();
147- $ scd ->setNotOnOrAfter (time () + 300 ); // 60*5 = 5min
148- $ scd ->setRecipient ($ endpoint );
149- $ scd ->setInResponseTo ($ query ->getId ());
150- $ sc ->setSubjectConfirmationData ($ scd );
151- $ assertion ->setSubjectConfirmation ([$ sc ]);
152-
153- Message::addSign ($ idpMetadata , $ spMetadata , $ assertion );
154-
155- $ response = new Response ();
156- $ response ->setRelayState ($ query ->getRelayState ());
157- $ response ->setDestination ($ endpoint );
158- $ response ->setIssuer ($ issuer );
159- $ response ->setInResponseTo ($ query ->getId ());
160- $ response ->setAssertions ([$ assertion ]);
161- Message::addSign ($ idpMetadata , $ spMetadata , $ response );
162-
163- return new RunnableResponse ([new HTTPPost (), 'send ' ], [$ response ]);
176+ $ clock = SAML2_Utils::getContainer ()->getClock ();
177+
178+ $ statements = array_filter ([$ attributeStatement ]);
179+ $ assertion = new Assertion (
180+ issuer: new Issuer ($ idpEntityId ),
181+ issueInstant: $ clock ->now (),
182+ id: (new Random ())->generateID (),
183+ subject: new Subject (
184+ identifier: $ message ->getSubject ()->getIdentifier (),
185+ subjectConfirmation: [
186+ new SubjectConfirmation (
187+ method: C::CM_BEARER ,
188+ subjectConfirmationData: new SubjectConfirmationData (
189+ notOnOrAfter: $ clock ->now ()->add (new DateInterval ('PT300S ' )),
190+ recipient: $ endpoint ,
191+ inResponseTo: $ message ->getId (),
192+ ),
193+ ),
194+ ],
195+ ),
196+ conditions: new Conditions (
197+ notBefore: $ clock ->now (),
198+ notOnOrAfter: $ clock ->now ()->add (new DateInterval ('PT300S ' )),
199+ audienceRestriction: [
200+ new AudienceRestriction ([
201+ new Audience ($ spEntityId ),
202+ ]),
203+ ],
204+ ),
205+ statements: $ statements ,
206+ );
207+
208+ self ::addSign ($ idpMetadata , $ spMetadata , $ assertion );
209+
210+ $ response = new Response (
211+ status: new Status (
212+ new StatusCode (C::STATUS_SUCCESS ),
213+ ),
214+ issueInstant: $ clock ->now (),
215+ issuer: $ issuer ,
216+ id: (new Random ())->generateID (),
217+ version: '2.0 ' ,
218+ inResponseTo: $ message ->getId (),
219+ destination: $ endpoint ,
220+ assertions: [$ assertion ],
221+ );
222+
223+ self ::addSign ($ idpMetadata , $ spMetadata , $ response );
224+
225+ /** @var \SimpleSAML\SAML2\Binding\HTTPPost $httpPost */
226+ $ httpPost = new HTTPPost ();
227+ $ httpPost ->setRelayState ($ binding ->getRelayState ());
228+
229+ return new RunnableResponse ([$ httpPost , 'send ' ], [$ response ]);
230+ }
231+
232+
233+ /**
234+ * @param array<\SimpleSAML\SAML2\XML\saml\AttributeValue> $reqValues
235+ * @param array<\SimpleSAML\SAML2\XML\saml\AttributeValue> $values
236+ *
237+ * @return array<\SimpleSAML\SAML2\XML\saml\AttributeValue>
238+ */
239+ private function filterAttributeValues (array $ reqValues , array $ values ): array
240+ {
241+ $ result = [];
242+
243+ foreach ($ reqValues as $ x ) {
244+ foreach ($ values as $ y ) {
245+ if ($ x ->getValue () === $ y ->getValue ()) {
246+ $ result [] = $ y ;
247+ }
248+ }
249+ }
250+
251+ return $ result ;
252+ }
253+
254+
255+ /**
256+ * @deprecated This method is a modified version of \SimpleSAML\Module\saml\Message::addSign and
257+ * should be replaced with a call to a future ServiceProvider-class in the saml2-library
258+ *
259+ * Add signature key and sender certificate to an element (Message or Assertion).
260+ *
261+ * @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
262+ * @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient.
263+ * @param \SimpleSAML\XMLSecurity\XML\SignableElementInterface $element The element we should add the data to.
264+ */
265+ private static function addSign (
266+ Configuration $ srcMetadata ,
267+ Configuration $ dstMetadata ,
268+ SignableElementInterface &$ element ,
269+ ): void {
270+ $ dstPrivateKey = $ dstMetadata ->getOptionalString ('signature.privatekey ' , null );
271+ $ cryptoUtils = new Utils \Crypto ();
272+
273+ if ($ dstPrivateKey !== null ) {
274+ /** @var string[] $keyArray */
275+ $ keyArray = $ cryptoUtils ->loadPrivateKey ($ dstMetadata , true , 'signature. ' );
276+ $ certArray = $ cryptoUtils ->loadPublicKey ($ dstMetadata , false , 'signature. ' );
277+ } else {
278+ /** @var string[] $keyArray */
279+ $ keyArray = $ cryptoUtils ->loadPrivateKey ($ srcMetadata , true );
280+ $ certArray = $ cryptoUtils ->loadPublicKey ($ srcMetadata , false );
281+ }
282+
283+ $ algo = $ dstMetadata ->getOptionalString ('signature.algorithm ' , null );
284+ if ($ algo === null ) {
285+ $ algo = $ srcMetadata ->getOptionalString ('signature.algorithm ' , C::SIG_RSA_SHA256 );
286+ }
287+
288+ $ privateKey = PrivateKey::fromFile ($ keyArray ['PEM ' ], $ keyArray ['password ' ]);
289+
290+ $ keyInfo = null ;
291+ if ($ certArray !== null ) {
292+ $ keyInfo = new KeyInfo ([
293+ new X509Data (
294+ [
295+ new X509Certificate ($ certArray ['PEM ' ]),
296+ ],
297+ ),
298+ ]);
299+ }
300+
301+ $ signer = (new SignatureAlgorithmFactory ())->getAlgorithm (
302+ $ algo ,
303+ $ privateKey ,
304+ );
305+
306+ $ element ->sign ($ signer , C::C14N_EXCLUSIVE_WITHOUT_COMMENTS , $ keyInfo );
164307 }
165308}
0 commit comments