1717use Psr \Http \Message \ResponseInterface ;
1818use Psr \Http \Message \ServerRequestInterface ;
1919use SimpleSAML \Module \oidc \Bridges \PsrHttpBridge ;
20- use SimpleSAML \Module \oidc \Entities \PushedAuthorizationRequestEntity ;
20+ use SimpleSAML \Module \oidc \Factories \ Entities \PushedAuthorizationRequestEntityFactory ;
2121use SimpleSAML \Module \oidc \Helpers ;
22- use SimpleSAML \Module \oidc \ModuleConfig ;
2322use SimpleSAML \Module \oidc \Repositories \PushedAuthorizationRequestRepository ;
2423use SimpleSAML \Module \oidc \Server \Exceptions \OidcServerException ;
24+ use SimpleSAML \Module \oidc \Server \RequestRules \Interfaces \ResultBagInterface ;
2525use SimpleSAML \Module \oidc \Server \RequestRules \RequestRulesManager ;
2626use SimpleSAML \Module \oidc \Server \RequestRules \Result ;
2727use SimpleSAML \Module \oidc \Server \RequestRules \ResultBag ;
2828use SimpleSAML \Module \oidc \Server \RequestRules \Rules \ClientRedirectUriRule ;
2929use SimpleSAML \Module \oidc \Server \RequestRules \Rules \ClientRule ;
3030use SimpleSAML \Module \oidc \Server \RequestRules \Rules \CodeChallengeMethodRule ;
3131use SimpleSAML \Module \oidc \Server \RequestRules \Rules \CodeChallengeRule ;
32+ use SimpleSAML \Module \oidc \Server \RequestRules \Rules \RequestObjectRule ;
3233use SimpleSAML \Module \oidc \Server \RequestRules \Rules \RequiredOpenIdScopeRule ;
3334use SimpleSAML \Module \oidc \Server \RequestRules \Rules \ResponseModeRule ;
3435use SimpleSAML \Module \oidc \Server \RequestRules \Rules \ScopeRule ;
3738use SimpleSAML \Module \oidc \Services \ErrorResponder ;
3839use SimpleSAML \Module \oidc \Services \LoggerService ;
3940use SimpleSAML \Module \oidc \Utils \AuthenticatedOAuth2ClientResolver ;
40- use SimpleSAML \Module \oidc \Utils \JwksResolver ;
4141use SimpleSAML \OpenID \Codebooks \HttpMethodsEnum ;
42- use SimpleSAML \OpenID \RequestObject ;
42+ use SimpleSAML \OpenID \Codebooks \ ParamsEnum ;
4343use Symfony \Component \HttpFoundation \Request ;
4444use Symfony \Component \HttpFoundation \Response ;
4545
@@ -48,31 +48,33 @@ class PushedAuthorizationController
4848 public function __construct (
4949 private readonly AuthenticatedOAuth2ClientResolver $ authenticatedOAuth2ClientResolver ,
5050 private readonly PushedAuthorizationRequestRepository $ pushedAuthorizationRequestRepository ,
51+ private readonly PushedAuthorizationRequestEntityFactory $ pushedAuthorizationRequestEntityFactory ,
5152 private readonly RequestRulesManager $ requestRulesManager ,
52- private readonly JwksResolver $ jwksResolver ,
53- private readonly RequestObject $ requestObject ,
54- private readonly ModuleConfig $ moduleConfig ,
5553 private readonly PsrHttpBridge $ psrHttpBridge ,
5654 private readonly ErrorResponder $ errorResponder ,
5755 private readonly Helpers $ helpers ,
5856 private readonly LoggerService $ logger ,
5957 ) {
6058 }
6159
60+ /**
61+ * @throws \League\OAuth2\Server\Exception\OAuthServerException
62+ * @throws \Throwable
63+ */
6264 public function __invoke (ServerRequestInterface $ request ): ResponseInterface
6365 {
6466 $ this ->logger ->debug ('PushedAuthorizationController::__invoke ' );
6567
66- if (strtoupper ($ request ->getMethod ()) !== ' POST ' ) {
68+ if (strtoupper ($ request ->getMethod ()) !== HttpMethodsEnum:: POST -> value ) {
6769 return $ this ->psrHttpBridge ->getResponseFactory ()->createResponse ()
6870 ->withStatus (405 )
69- ->withHeader ('Allow ' , ' POST ' );
71+ ->withHeader ('Allow ' , HttpMethodsEnum:: POST -> value );
7072 }
7173
72- // 1. Authenticate client
74+ // Authenticate the client in the same way as at the token endpoint.
7375 $ resolvedAuth = $ this ->authenticatedOAuth2ClientResolver ->forAnySupportedMethod ($ request );
7476 if (is_null ($ resolvedAuth )) {
75- throw OidcServerException::accessDenied ('Client authentication failed ' );
77+ throw OidcServerException::accessDenied ('Client authentication failed. ' );
7678 }
7779
7880 $ client = $ resolvedAuth ->getClient ();
@@ -81,98 +83,63 @@ public function __invoke(ServerRequestInterface $request): ResponseInterface
8183 throw OidcServerException::accessDenied ('Confidential client must authenticate. ' );
8284 }
8385
84- // 2. Parse request params
8586 $ bodyParams = $ request ->getParsedBody ();
86- $ params = is_array ($ bodyParams ) ? $ bodyParams : [];
87+ $ bodyParams = is_array ($ bodyParams ) ? $ bodyParams : [];
8788
88- // 3. Reject request_uri in PAR body
89- if (isset ( $ params [ ' request_uri ' ] )) {
89+ // The request_uri authorization request parameter must not be used in pushed authorization requests.
90+ if (array_key_exists (ParamsEnum::RequestUri-> value , $ bodyParams )) {
9091 throw OidcServerException::invalidRequest (
91- ' request_uri ' ,
92- 'The request_uri parameter MUST NOT be provided in pushed authorization requests. ' ,
92+ ParamsEnum::RequestUri-> value ,
93+ 'The request_uri parameter must not be used in pushed authorization requests. ' ,
9394 );
9495 }
9596
96- // 4. Handle JAR in PAR (request parameter)
97- if (isset ($ params ['request ' ])) {
98- try {
99- $ requestObject = $ this ->requestObject ->jarRequestObjectFactory ()->fromToken ((string )$ params ['request ' ]);
100- $ jwks = $ this ->jwksResolver ->forClient ($ client );
101- if (is_null ($ jwks )) {
102- throw OidcServerException::invalidRequest (
103- 'request ' ,
104- 'Client JWKS not available for signature verification. ' ,
105- );
106- }
107- $ requestObject ->verifyWithKeySet ($ jwks );
108-
109- if ($ requestObject ->getClientId () !== $ client ->getIdentifier ()) {
110- throw OidcServerException::invalidRequest (
111- 'request ' ,
112- 'Client ID in request object does not match authenticated client. ' ,
113- );
114- }
115-
116- $ params = array_merge ($ params , $ requestObject ->getPayload ());
117- unset($ params ['request ' ]);
118- } catch (\Throwable $ t ) {
119- throw OidcServerException::invalidRequest ('request ' , 'Invalid request object: ' . $ t ->getMessage ());
120- }
121- }
122-
123- // 5. Build mock request with merged params and run validation rules
124- $ psrRequest = $ request ->withParsedBody ($ params )->withQueryParams ([]);
125-
97+ // Validate the pushed params as we would an authorization request sent to the authorization endpoint.
98+ // Note that the rules transparently take the Request Object (request param) into account, with
99+ // RequestObjectRule doing its validation (signature, signed-required policy...).
126100 $ resultBag = new ResultBag ();
127101 $ resultBag ->add (new Result (ClientRule::class, $ client ));
128-
129102 $ this ->requestRulesManager ->predefineResultBag ($ resultBag );
130103
104+ $ this ->requestRulesManager ->setData ('default_scope ' , '' );
105+ $ this ->requestRulesManager ->setData ('scope_delimiter_string ' , ' ' );
106+
131107 $ rulesToExecute = [
132108 StateRule::class,
133109 ClientRedirectUriRule::class,
110+ RequestObjectRule::class,
134111 ResponseModeRule::class,
135112 ScopeRule::class,
136113 RequiredOpenIdScopeRule::class,
137114 CodeChallengeRule::class,
138115 CodeChallengeMethodRule::class,
139116 ];
140117
141- $ this ->requestRulesManager ->setData ('default_scope ' , '' );
142- $ this ->requestRulesManager ->setData ('scope_delimiter_string ' , ' ' );
143-
144- $ this ->requestRulesManager ->check (
145- $ psrRequest ,
118+ $ resultBag = $ this ->requestRulesManager ->check (
119+ $ request ,
146120 $ rulesToExecute ,
147121 new QueryResponseMode (),
148122 [HttpMethodsEnum::POST ],
149123 );
150124
151- // 6. Generate request_uri
152- $ hex = bin2hex (random_bytes (32 ));
153- $ requestUri = 'urn:ietf:params:oauth:request_uri: ' . $ hex ;
154-
155- // 7. Persist entity
156- $ ttl = $ this ->moduleConfig ->getParRequestUriTtl ();
157- $ expiresAt = $ this ->helpers ->dateTime ()->getUtc ()->add ($ ttl );
158-
159- // Make sure we carry forward all validated params
160- $ entity = new PushedAuthorizationRequestEntity (
161- requestUri: $ requestUri ,
162- clientId: $ client ->getIdentifier (),
163- parameters: $ params ,
164- expiresAt: \DateTimeImmutable::createFromInterface ($ expiresAt ),
165- isConsumed: false ,
125+ $ parameters = $ this ->resolveParametersToPersist ($ resultBag , $ bodyParams , $ client ->getIdentifier ());
126+
127+ $ parEntity = $ this ->pushedAuthorizationRequestEntityFactory ->buildNew (
128+ $ client ->getIdentifier (),
129+ $ parameters ,
166130 );
167131
168- $ this ->pushedAuthorizationRequestRepository ->persist ($ entity );
132+ $ this ->pushedAuthorizationRequestRepository ->persist ($ parEntity );
169133
170- // 8. Respond
171- $ expiresIn = $ this ->helpers ->dateTime ()->getSecondsToExpirationTime ($ expiresAt ->getTimestamp ());
172- $ responseBody = json_encode ([
173- 'request_uri ' => $ requestUri ,
174- 'expires_in ' => $ expiresIn ,
175- ], JSON_THROW_ON_ERROR );
134+ $ responseBody = json_encode (
135+ [
136+ 'request_uri ' => $ parEntity ->getRequestUri (),
137+ 'expires_in ' => $ this ->helpers ->dateTime ()->getSecondsToExpirationTime (
138+ $ parEntity ->getExpiresAt ()->getTimestamp (),
139+ ),
140+ ],
141+ JSON_THROW_ON_ERROR ,
142+ );
176143
177144 $ response = $ this ->psrHttpBridge ->getResponseFactory ()->createResponse ()
178145 ->withStatus (201 )
@@ -184,17 +151,85 @@ public function __invoke(ServerRequestInterface $request): ResponseInterface
184151 return $ response ;
185152 }
186153
154+ /**
155+ * Resolve the authorization request parameters which are to be persisted for later use at the
156+ * authorization endpoint.
157+ *
158+ * @param mixed[] $bodyParams
159+ * @return mixed[]
160+ * @throws \League\OAuth2\Server\Exception\OAuthServerException
161+ */
162+ protected function resolveParametersToPersist (
163+ ResultBagInterface $ resultBag ,
164+ array $ bodyParams ,
165+ string $ clientId ,
166+ ): array {
167+ // If a body client_id param was provided, it must match the authenticated client.
168+ if (
169+ array_key_exists (ParamsEnum::ClientId->value , $ bodyParams ) &&
170+ $ bodyParams [ParamsEnum::ClientId->value ] !== $ clientId
171+ ) {
172+ throw OidcServerException::invalidRequest (
173+ ParamsEnum::ClientId->value ,
174+ 'The client_id parameter does not match the authenticated client. ' ,
175+ );
176+ }
177+
178+ $ requestObjectResult = $ resultBag ->get (RequestObjectRule::class);
179+
180+ if ($ requestObjectResult !== null ) {
181+ // Request Object (JAR) was used. Per RFC 9126, all authorization request parameters must appear
182+ // as claims of the Request Object, so only use its (validated) payload.
183+ /** @psalm-suppress MixedAssignment */
184+ $ parameters = $ resultBag ->getOrFail (RequestObjectRule::class)->getValue ();
185+ $ parameters = is_array ($ parameters ) ? $ parameters : [];
186+
187+ /** @psalm-suppress MixedAssignment */
188+ $ clientIdClaim = $ parameters [ParamsEnum::ClientId->value ] ?? null ;
189+ if (!is_null ($ clientIdClaim ) && $ clientIdClaim !== $ clientId ) {
190+ throw OidcServerException::invalidRequest (
191+ ParamsEnum::ClientId->value ,
192+ 'The client_id claim in request object does not match the authenticated client. ' ,
193+ );
194+ }
195+ } else {
196+ // Plain pushed authorization request. Make sure not to persist client authentication related
197+ // params (they are not part of the authorization request itself).
198+ $ parameters = $ bodyParams ;
199+ unset(
200+ $ parameters [ParamsEnum::ClientSecret->value ],
201+ $ parameters [ParamsEnum::ClientAssertion->value ],
202+ $ parameters [ParamsEnum::ClientAssertionType->value ],
203+ );
204+ }
205+
206+ unset(
207+ $ parameters [ParamsEnum::Request->value ],
208+ $ parameters [ParamsEnum::RequestUri->value ],
209+ );
210+
211+ // Bind the parameters to the authenticated client.
212+ $ parameters [ParamsEnum::ClientId->value ] = $ clientId ;
213+
214+ return $ parameters ;
215+ }
216+
187217 public function par (Request $ request ): Response
188218 {
189219 try {
190220 $ psrRequest = $ this ->psrHttpBridge ->getPsrHttpFactory ()->createRequest ($ request );
191221 $ psrResponse = $ this ->__invoke ($ psrRequest );
192222 return $ this ->psrHttpBridge ->getHttpFoundationFactory ()->createResponse ($ psrResponse );
193223 } catch (OAuthServerException $ exception ) {
194- return $ this ->errorResponder ->forException ($ exception );
224+ // Per RFC 9126, the error response format is the one specified for the token endpoint, so make
225+ // sure we never redirect (regardless of any redirect URI contained in the exception).
226+ return $ this ->errorResponder ->forExceptionJson ($ exception );
195227 } catch (\Throwable $ exception ) {
196- return $ this ->errorResponder ->forException (
197- OidcServerException::invalidRequest ('request ' , $ exception ->getMessage ()),
228+ $ this ->logger ->error (
229+ 'PushedAuthorizationController: error processing request: ' . $ exception ->getMessage (),
230+ );
231+ return $ this ->errorResponder ->forExceptionJson (
232+ OidcServerException::serverError ('Unable to process pushed authorization request. ' ),
198233 );
199234 }
200235 }
0 commit comments