@@ -202,6 +202,71 @@ public function validateRequestUris(Form $form): void
202202 }
203203 }
204204
205+ /**
206+ * Validate the per-client Authentication Processing Filters. The value is
207+ * expected to be a JSON object/array in the same shape as the global
208+ * ModuleConfig::OPTION_AUTH_PROCESSING_FILTERS option, i.e. a list keyed by
209+ * priority where each filter is either a class string or an array with a
210+ * 'class' key. JSON syntax itself is validated in getValues().
211+ *
212+ * @throws \Exception
213+ */
214+ public function validateAuthProcFilters (Form $ form ): void
215+ {
216+ $ values = $ form ->getValues (self ::TYPE_ARRAY );
217+
218+ $ authProcFilters = $ values [ClientEntity::KEY_AUTH_PROC_FILTERS ] ?? [];
219+
220+ if (!is_array ($ authProcFilters )) {
221+ $ this ->addError ('Authentication Processing Filters must be a JSON object. ' );
222+ return ;
223+ }
224+
225+ /**
226+ * @var mixed $filter
227+ */
228+ foreach ($ authProcFilters as $ filter ) {
229+ if (is_string ($ filter )) {
230+ continue ;
231+ }
232+
233+ if (!is_array ($ filter )) {
234+ $ this ->addError (
235+ 'Each Authentication Processing Filter must be a class string or an object: ' .
236+ var_export ($ filter , true ),
237+ );
238+ continue ;
239+ }
240+
241+ if (!isset ($ filter ['class ' ]) || !is_string ($ filter ['class ' ])) {
242+ $ this ->addError (
243+ "Each Authentication Processing Filter object must have a string 'class' property: " .
244+ var_export ($ filter , true ),
245+ );
246+ }
247+ }
248+ }
249+
250+ /**
251+ * Cast integer-like string array keys to int, leaving all other keys (and
252+ * the values) untouched. Only the top level is processed, which is where
253+ * authproc filter priority keys live. Used to normalize priorities coming
254+ * from the JSON-encoded authproc filters field.
255+ */
256+ protected function castNumericKeysToInt (array $ array ): array
257+ {
258+ $ result = [];
259+ /** @var mixed $value */
260+ foreach ($ array as $ key => $ value ) {
261+ if (is_string ($ key ) && preg_match ('/^-?\d+$/ ' , $ key ) === 1 ) {
262+ $ key = (int ) $ key ;
263+ }
264+ $ result [$ key ] = $ value ;
265+ }
266+
267+ return $ result ;
268+ }
269+
205270 public function validateJwks (mixed $ jwks ): void
206271 {
207272 if (is_null ($ jwks )) {
@@ -319,6 +384,24 @@ public function getValues(string|object|bool|null $returnType = null, ?array $co
319384 array_keys ($ this ->getAllowedResponseModesValues ()),
320385 );
321386
387+ $ authProcFilters = trim ((string )($ values [ClientEntity::KEY_AUTH_PROC_FILTERS ] ?? '' ));
388+ try {
389+ /** @psalm-suppress MixedAssignment */
390+ $ decodedAuthProcFilters = $ authProcFilters === '' ?
391+ [] :
392+ json_decode ($ authProcFilters , true , 512 , JSON_THROW_ON_ERROR );
393+ // Normalize numeric priority keys to integers. PHP already casts
394+ // canonical integer string keys (e.g. "60") to int, but not forms
395+ // like "08", so we make the priority type predictable for the
396+ // SimpleSAMLphp ProcessingChain.
397+ $ values [ClientEntity::KEY_AUTH_PROC_FILTERS ] = is_array ($ decodedAuthProcFilters ) ?
398+ $ this ->castNumericKeysToInt ($ decodedAuthProcFilters ) :
399+ $ decodedAuthProcFilters ;
400+ } catch (\JsonException $ e ) {
401+ $ this ->addError ('Authentication Processing Filters JSON error: ' . $ e ->getMessage ());
402+ $ values [ClientEntity::KEY_AUTH_PROC_FILTERS ] = [];
403+ }
404+
322405 return $ values ;
323406 }
324407
@@ -390,6 +473,11 @@ public function setDefaults(object|array $values, bool $erase = false): static
390473 $ values [ClientEntity::KEY_ALLOWED_RESPONSE_MODES ],
391474 ) ? $ values [ClientEntity::KEY_ALLOWED_RESPONSE_MODES ] : [];
392475
476+ $ authProcFilters = $ values [ClientEntity::KEY_AUTH_PROC_FILTERS ] ?? null ;
477+ $ values [ClientEntity::KEY_AUTH_PROC_FILTERS ] = (is_array ($ authProcFilters ) && $ authProcFilters !== []) ?
478+ (string )json_encode ($ authProcFilters , JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) :
479+ '' ;
480+
393481 parent ::setDefaults ($ values , $ erase );
394482
395483 return $ this ;
@@ -413,6 +501,7 @@ protected function buildForm(): void
413501 $ this ->onValidate [] = $ this ->validateProtocolJwks (...);
414502 $ this ->onValidate [] = $ this ->validateJwksUri (...);
415503 $ this ->onValidate [] = $ this ->validateRequestUris (...);
504+ $ this ->onValidate [] = $ this ->validateAuthProcFilters (...);
416505
417506 $ this ->setMethod ('POST ' );
418507 $ this ->addComponent ($ this ->csrfProtection , Form::ProtectorId);
@@ -494,6 +583,13 @@ protected function buildForm(): void
494583 $ this ->addCheckbox (ClaimsEnum::RequireSignedRequestObject->value , 'Require Signed Request Object ' );
495584 $ this ->addTextArea (ClaimsEnum::RequestUris->value , 'Request URIs (OIDC Core / JAR, one per line) ' , null , 5 )
496585 ->setHtmlAttribute ('class ' , 'full-width ' );
586+
587+ $ this ->addTextArea (
588+ ClientEntity::KEY_AUTH_PROC_FILTERS ,
589+ Translate::noop ('Authentication Processing Filters ' ),
590+ null ,
591+ 5 ,
592+ )->setHtmlAttribute ('class ' , 'full-width ' );
497593 }
498594
499595 /**
0 commit comments