1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2012-2025
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID\Protocols\Connect;
24:
25: use Psr\Log\LogLevel;
26: use SimpleID\ModuleManager;
27: use SimpleID\Auth\AuthManager;
28: use SimpleID\Base\ScopeInfoCollectionEvent;
29: use SimpleID\Protocols\HTTPResponse;
30: use SimpleID\Protocols\ProtocolResult;
31: use SimpleID\Protocols\OAuth\OAuthEvent;
32: use SimpleID\Protocols\OAuth\OAuthProtectedResource;
33: use SimpleID\Protocols\OAuth\OAuthAuthRequestEvent;
34: use SimpleID\Protocols\OAuth\OAuthAuthGrantEvent;
35: use SimpleID\Protocols\OAuth\OAuthTokenGrantEvent;
36: use SimpleID\Protocols\OAuth\Response;
37: use SimpleID\Store\StoreManager;
38: use SimpleID\Util\Events\BaseDataCollectionEvent;
39: use SimpleJWT\Util\Helper;
40: use SimpleJWT\JWT;
41: use SimpleJWT\InvalidTokenException;
42: use SimpleJWT\Crypt\AlgorithmFactory;
43: use SimpleJWT\Crypt\AlgorithmInterface;
44: use SimpleJWT\Keys\KeySet;
45: use \Web;
46:
47: /**
48: * Module for authenticating with OpenID Connect.
49: */
50: class ConnectModule extends OAuthProtectedResource implements ProtocolResult {
51: /**
52: * @see SimpleID\Protocols\OAuth\OAuthProtectedResource::$oauth_include_request_body
53: */
54: protected $oauth_include_request_body = true;
55:
56: static function init($f3) {
57: $f3->redirect('GET /.well-known/openid-configuration', '@oauth_metadata');
58: $f3->route('GET|POST @connect_userinfo: /connect/userinfo', 'SimpleID\Protocols\Connect\ConnectModule->userinfo');
59: $f3->route('GET @connect_jwks: /connect/jwks', 'SimpleID\Protocols\Connect\ConnectModule->jwks');
60: }
61:
62:
63: public function __construct() {
64: parent::__construct();
65:
66: $mgr = ModuleManager::instance();
67: $mgr->loadModule('SimpleID\Protocols\OAuth\OAuthModule');
68:
69: $this->checkConfig();
70: }
71:
72: /**
73: * @return void
74: */
75: protected function checkConfig() {
76: $config = $this->f3->get('config');
77:
78: if (!is_readable($config['public_jwks_file'])) {
79: $this->logger->log(LogLevel::CRITICAL, 'Public JSON web key file not found.');
80: $this->f3->error(500, $this->f3->get('intl.core.connect.missing_public_jwk', 'http://simpleid.org/docs/2/installing/#keys'));
81: }
82:
83: if (!is_readable($config['private_jwks_file'])) {
84: $this->logger->log(LogLevel::CRITICAL, 'Private JSON web key file not found.');
85: $this->f3->error(500, $this->f3->get('intl.core.connect.missing_private_jwk', 'http://simpleid.org/docs/2/installing/#keys'));
86: }
87: }
88:
89: /**
90: * Resolves an OpenID Connect authorisation request by decoding any
91: * `request` and `request_uri` parameters.
92: *
93: * @return void
94: */
95: public function onOauthAuthResolve(OAuthEvent $event) {
96: $store = StoreManager::instance();
97: $web = Web::instance();
98: $request = $event->getRequest();
99: $response = $event->getResponse();
100:
101: // 1. Check if request_uri parameter is present. If so, fetch the JWT
102: // from this URL and place it in the request parameter
103: if (isset($request['request_uri'])) {
104: $this->logger->log(LogLevel::INFO, 'OpenID request object: getting object from ' . $request['request_uri']);
105:
106: $parts = parse_url($request['request_uri']);
107:
108: $http_response = new HTTPResponse($web->request($request['request_uri'], [ 'headers' => [ 'Accept' => 'application/jwt,text/plain,application/octet-stream' ] ]));
109:
110: if ($http_response->isHTTPError()) {
111: $this->logger->log(LogLevel::ERROR, 'Cannot retrieve request file from request_uri:' . $request['request_uri']);
112: $response->setError('invalid_request_uri', 'cannot retrieve request file from request_uri');
113: return;
114: }
115:
116: $request['request'] = $http_response->getBody();
117: unset($request['request_uri']);
118: }
119:
120: // 2. Check if the request parameter is present. If so, we are dealing with
121: // an additional OpenID Connect request object. We need to parse this object
122: if (isset($request['request'])) {
123: $this->logger->log(LogLevel::INFO, 'OpenID request object token: ' . $request['request']);
124:
125: /** @var \SimpleID\Protocols\OAuth\OAuthClient $client */
126: $client = $store->loadClient($request['client_id'], 'SimpleID\Protocols\OAuth\OAuthClient');
127:
128: if (!isset($client['connect']['request_object_signing_alg'])) {
129: $this->logger->log(LogLevel::ERROR, 'Invalid OpenID request object: signature algorithm not registered');
130: $response->setError('invalid_openid_request_object', 'signature algorithm not registered');
131: return;
132: }
133:
134: $jwt_alg = (isset($client['connect']['request_object_signing_alg'])) ? $client['connect']['request_object_signing_alg'] : null;
135: $jwe_alg = (isset($client['connect']['request_object_encryption_alg'])) ? $client['connect']['request_object_encryption_alg'] : null;
136: $builder = new KeySetBuilder($client);
137: $set = $builder->addClientSecret()->addClientPublicKeys()->addServerPrivateKeys()->toKeySet();
138: try {
139: AlgorithmFactory::addNoneAlg();
140: $helper = new Helper($request['request']);
141: $jwt = $helper->decodeFully($set, $jwe_alg, $jwt_alg);
142: $request->loadData($jwt->getClaims());
143: } catch (\UnexpectedValueException $e) {
144: $this->logger->log(LogLevel::ERROR, 'Invalid OpenID request object: ' . $e->getMessage());
145: $response->setError('invalid_openid_request_object', $e->getMessage());
146: return;
147: }
148: }
149: AlgorithmFactory::removeNoneAlg();
150:
151: // 3. nonce
152: if ($request->paramContains('scope', 'openid') && $request->paramContains('response_type', 'token') && !isset($request['nonce'])) {
153: $this->logger->log(LogLevel::ERROR, 'Protocol Error: nonce not set when using implicit flow');
154: $response->setError('invalid_request', 'nonce not set when using implicit flow')->renderRedirect();
155: return;
156: }
157: }
158:
159: /**
160: * Processes an OpenID Connect authorisation request.
161: *
162: * This hook is called as part of the OAuth authorisation process. This
163: * function performs additional checks required by the OpenID Connect
164: * protocol, including processing the `prompt`, `max_age` and
165: * `acr` paramters.
166: *
167: * @return void
168: * @see SimpleID\Protocols\OAuth\OAuthProcessAuthRequestEvent
169: */
170: function onOAuthAuthRequestEvent(OAuthAuthRequestEvent $event) {
171: $request = $event->getRequest();
172: $store = StoreManager::instance();
173: $auth = AuthManager::instance();
174:
175: $client = $store->loadClient($request['client_id'], 'SimpleID\Protocols\OAuth\OAuthClient');
176:
177: // Check 1: Check whether the prompt parameter is present in the request
178: $request->setImmediate($request->paramContains('prompt', 'none'));
179:
180: if ($request->paramContains('prompt', 'login')) {
181: $this->f3->set('message', $this->f3->get('intl.common.reenter_credentials'));
182: $request->paramRemove('prompt', 'login');
183: $event->setResult(self::CHECKID_REENTER_CREDENTIALS);
184: return;
185: }
186:
187: if ($request->paramContains('prompt', 'consent')) {
188: $request->paramRemove('prompt', 'consent');
189: $event->setResult(self::CHECKID_APPROVAL_REQUIRED);
190: return;
191: }
192:
193: // Check 2: If id_token_hint is provided, check that it refers to the current logged-in user
194: if (isset($request['id_token_hint'])) {
195: try {
196: $jwt = JWT::deserialise($request['id_token_hint']);
197: $claims = $jwt['claims'];
198: $user_match = ($claims['sub'] == self::getSubject($auth->getUser(), $client));
199: } catch (InvalidTokenException $e) {
200: $user_match = false;
201: }
202: if (!$user_match) {
203: $auth->logout();
204: $event->setResult(self::CHECKID_LOGIN_REQUIRED);
205: return;
206: }
207: }
208:
209: // Check 3: Check whether the max_age or acr parameters are present in the client defaults
210: // or the request parameters
211: if (isset($request['max_age'])) {
212: $max_age = $request['max_age'];
213: } elseif (isset($client['connect']) && isset($client['connect']['default_max_age'])) {
214: $max_age = $client['connect']['default_max_age'];
215: } else {
216: $max_age = -1;
217: }
218: // If the relying party provides a max_auth_age
219: if (($max_age > -1) && $auth->isLoggedIn()) {
220: $auth_level = $auth->getAuthLevel();
221: if ($auth_level == null) $auth_level = AuthManager::AUTH_LEVEL_SESSION;
222:
223: $auth_time = $auth->getAuthTime();
224: if ($auth_time == null) $auth_time = 0;
225:
226: // If the last time we logged on actively (i.e. using a password) is greater than
227: // max_age, we then require the user to log in again
228: if (($auth_level < AuthManager::AUTH_LEVEL_CREDENTIALS)
229: || ((time() - $auth->getAuthTime()) > $max_age)) {
230: $this->f3->set('message', $this->f3->get('intl.common.reenter_credentials'));
231: $event->setResult(self::CHECKID_REENTER_CREDENTIALS);
232: return;
233: }
234: }
235:
236: if (isset($request['acr'])) {
237: $acr = $request['acr'];
238: } elseif (isset($client['connect']) && isset($client['connect']['default_acr'])) {
239: $acr = $client['connect']['default_acr'];
240: } else {
241: $acr = -1;
242: }
243:
244: if ($acr > -1) {
245: $event->setResult(self::CHECKID_INSUFFICIENT_TRUST);
246: return;
247: }
248: }
249:
250:
251: /**
252: * Builds the OpenID Connect authentication response on a successful
253: * authentication.
254: *
255: * The OpenID Connect authentication response is built on top of the OAuth
256: * authorisation response and token responses. It may include an ID token
257: * containing the claims requested by the OpenID Connect client.
258: *
259: * This function prepares the OpenID Connect claims to be returned by calling
260: * the {@link buildClaims()} function with an `id_token` parameter. This
261: * function will then:
262: *
263: * - encode the claims in an ID token and return it as part of the authorisation
264: * response; and/or
265: * - save the claims to be returned as part of the token response.
266: *
267: * @return void
268: * @see SimpleID\Protocols\OAuth\OAuthAuthGrantEvent
269: */
270: function onOAuthAuthGrantEvent(OAuthAuthGrantEvent $event) {
271: // code: ?code / id_token
272: // id_token: #id_token
273: // id_token token: #access_token #id_token
274: // code id_token: #code #id_token[c_hash] / id_token
275: // code token: #code #access_token / id_token
276: // code id_token token: #code #access_token #id_token[c_hash, at_hash] / id_token
277: $request = $event->getRequest();
278: $response = $event->getResponse();
279: $authorization = $event->getAuthorization();
280: $scopes = $event->getRequestedScope();
281:
282: if ($request->paramContains('scope', 'openid')) {
283: $user = AuthManager::instance()->getUser();
284: /** @var \SimpleID\Protocols\OAuth\OAuthClient $client */
285: $client = StoreManager::instance()->loadClient($request['client_id'], 'SimpleID\Protocols\OAuth\OAuthClient');
286:
287: if (isset($request['claims']) && is_string($request['claims'])) $request['claims'] = json_decode($request['claims'], true);
288:
289: // 1. Build claims
290: $claims_requested = (isset($request['claims']['id_token'])) ? $request['claims']['id_token'] : null;
291: $claims = $this->buildClaims($user, $client, 'id_token', $scopes, $claims_requested);
292:
293: if (isset($request['nonce'])) $claims['nonce'] = $request['nonce'];
294:
295: // 2. Encode claims as jwt
296: if ($request->paramContains('response_type', 'id_token')) {
297: // response_type = id_token, code id_token, id_token token, code id_token token
298:
299: // Response is always fragment
300: $response->setResponseMode(Response::FRAGMENT_RESPONSE_MODE);
301:
302: // Build authorisation response ID token
303: $jose = new JOSEResponse($this->getCanonicalHost(), $client, 'connect.id_token', $claims, 'RS256');
304:
305: if (isset($response['code'])) $jose->setShortHashClaim('c_hash', $response['code']);
306: if (isset($response['access_token'])) $jose->setShortHashClaim('at_hash', $response['access_token']);
307:
308: $response['id_token'] = $jose->buildJOSE();
309: }
310:
311: if ($request->paramContains('response_type', 'code')) {
312: // response_type = code, code token
313:
314: // Save the id token for token endpoint
315: $authorization->additional['id_token_claims'] = $claims;
316: }
317: // Note response_type = token is not defined
318:
319: // 3. Save claims for UserInfo endpoint
320: if (isset($request['claims'])) {
321: $authorization->additional['claims'] = $request['claims'];
322: }
323: }
324: }
325:
326: /**
327: * Processes an OpenID Connect token response. An OpenID Connect token
328: * response may contain an ID token containing the claims that the
329: * OpenID Connect client requested earlier.
330: *
331: * @return void
332: * @see SimpleID\Protocols\OAuth\OAuthTokenGrantEvent
333: */
334: function onOAuthTokenGrantEvent(OAuthTokenGrantEvent $event) {
335: $auth = $event->getAuthorization();
336: $response = $event->getResponse();
337:
338: if (($event->getGrantType() == 'authorization_code') && isset($auth->additional['id_token_claims'])) {
339: $client = $this->oauth->getClient();
340: $claims = $auth->additional['id_token_claims'];
341: $access_token = $response['access_token'];
342:
343: // Build token response ID token
344: $jose = new JOSEResponse($this->getCanonicalHost(), $client, 'connect.id_token', $claims, 'RS256');
345: $jose->setShortHashClaim('at_hash', $access_token);
346:
347: $response['id_token'] = $jose->buildJOSE();
348: unset($auth->additional['id_token_claims']);
349: }
350: }
351:
352: /**
353: * @return void
354: */
355: public function onOauthResponseTypes(BaseDataCollectionEvent $event) {
356: $event->addResult('id_token');
357: }
358:
359: /**
360: * The UserInfo endpoint. The UserInfo endpoint returns a set
361: * of claims requested by the OpenID Connect client.
362: *
363: * @return void
364: */
365: public function userinfo() {
366: $this->checkHttps('error');
367:
368: $error = '';
369: if (!$this->isTokenAuthorized('openid', $error)) {
370: $this->unAuthorizedError($error, null, [], 'json');
371: }
372:
373: $authorization = $this->getAuthorization();
374: /** @var \SimpleID\Models\User $user */
375: $user = $authorization->getOwner();
376: /** @var \SimpleID\Protocols\OAuth\OAuthClient $client */
377: $client = $authorization->getClient('SimpleID\Protocols\OAuth\OAuthClient');
378: $scope = $this->getAccessToken()->getScope();
379:
380: $claims_requested = (isset($authorization->additional['claims']['userinfo'])) ? $authorization->additional['claims']['userinfo'] : null;
381: $claims = $this->buildClaims($user, $client, 'userinfo', $scope, $claims_requested);
382: if (count($claims) == 0) $this->unAuthorizedError('invalid_request');
383:
384: $response = new JOSEResponse($this->getCanonicalHost(), $client, 'connect.userinfo', $claims);
385: $response->render();
386: }
387:
388: /**
389: * Build a set of claims to be included in an ID token or UserInfo response
390: *
391: * @param \SimpleID\Models\User $user the user about which the ID
392: * token is created
393: * @param \SimpleID\Models\Client $client the client to which the
394: * ID token will be sent
395: * @param string $context the context, either `id_token` or `userinfo`
396: * @param array<string> $scopes the scope
397: * @param array<string, mixed>|null $claims_requested the claims requested in the request object,
398: * or null if the request object is not present
399: * @return array<string, mixed> an array of claims
400: */
401: private function buildClaims($user, $client, $context, $scopes, $claims_requested = NULL) {
402: $auth = AuthManager::instance();
403: $mgr = ModuleManager::instance();
404: $dispatcher = \Events::instance();
405:
406: $scope_info_event = new ScopeInfoCollectionEvent();
407: $dispatcher->dispatch($scope_info_event);
408: $scope_info = $scope_info_event->getScopeInfoForType('oauth');
409:
410: $claims = [];
411: $claims['sub'] = self::getSubject($user, $client);
412:
413: if ($claims_requested != null) {
414: foreach ($claims_requested as $claim => $properties) {
415: switch ($claim) {
416: case 'acr':
417: // Processed later
418: break;
419: case 'updated_at':
420: // Not supported
421: break;
422: default:
423: $consent_scope = null;
424: /** @var array<string, mixed> $settings */
425: foreach ($scope_info as $scope => $settings) {
426: if (!isset($settings['claims'])) continue;
427: if (in_array($claim, $settings['claims'])) $consent_scope = $scope;
428: }
429: if ($consent_scope == null) break; // No consent given for this claim
430:
431: if (isset($user['userinfo'][$claim])) {
432: $claims[$claim] = $user['userinfo'][$claim];
433: if ($claim == 'email') $claims['email_verified'] = false;
434: if ($claim == 'phone_number') $claims['phone_number_verified'] = false;
435: }
436: break;
437: }
438: }
439: } else {
440: foreach ([ 'profile', 'email', 'address', 'phone' ] as $scope) {
441: if (in_array($scope, $scopes)) {
442: if (isset($scope_info[$scope]['claims'])) {
443: foreach ($scope_info[$scope]['claims'] as $claim) {
444: if (isset($user['userinfo'][$claim])) $claims[$claim] = $user['userinfo'][$claim];
445: if ($claim == 'email') $claims['email_verified'] = false;
446: if ($claim == 'phone_number') $claims['phone_number_verified'] = false;
447: }
448: }
449: }
450: }
451: }
452:
453: if ($context == 'id_token') {
454: $now = time();
455: $claims['exp'] = $now + SIMPLEID_LONG_TOKEN_EXPIRES_IN - SIMPLEID_LONG_TOKEN_EXPIRES_BUFFER;
456: $claims['iat'] = $now;
457: $claims['auth_time'] = $auth->getAuthTime();
458: $claims['acr'] = $auth->getACR();
459: }
460:
461: $build_claims_event = new ConnectBuildClaimsEvent($user, $client, $context, $scopes, $claims_requested);
462: $build_claims_event->addResult($claims);
463: $dispatcher->dispatch($build_claims_event);
464:
465: return $build_claims_event->getResults();
466: }
467:
468: /**
469: * Obtains a `sub` (subject) claim for a user and client.
470: *
471: * The subject type can be public (which reflect the user's ID)
472: * or pairwise. The type chosen is dependent on the client's
473: * registration settings.
474: *
475: * @param \SimpleID\Models\User $user the user about which the ID
476: * token is created
477: * @param \SimpleID\Models\Client $client the client to which the
478: * ID token will be sent
479: * @return string|null the subject
480: */
481: public static function getSubject($user, $client) {
482: if (isset($client['connect']['sector_identifier_uri'])) {
483: $sector_id = parse_url($client['connect']['sector_identifier_uri'], PHP_URL_HOST);
484: } elseif (is_string($client['oauth']['redirect_uris'])) {
485: $sector_id = parse_url($client['oauth']['redirect_uris'], PHP_URL_HOST);
486: } elseif (is_array($client['oauth']['redirect_uris']) && (count($client['oauth']['redirect_uris']) == 1)) {
487: $sector_id = parse_url($client['oauth']['redirect_uris'][0], PHP_URL_HOST);
488: } else {
489: $sector_id = $client->getStoreID();
490: }
491: if (!$sector_id) return null;
492:
493: $claims = [];
494:
495: $subject_type = (isset($client['connect']['subject_type'])) ? $client['connect']['subject_type'] : 'pairwise';
496: switch ($subject_type) {
497: case 'public':
498: return $user->getStoreID();
499: case 'pairwise':
500: return $user->getPairwiseIdentity($sector_id);
501: default:
502: return null;
503: }
504: }
505:
506: /**
507: * Returns the OpenID Connect scopes supported by this server.
508: *
509: * @see ScopeInfoCollectionEvent
510: * @return void
511: */
512: public function onScopeInfoCollectionEvent(ScopeInfoCollectionEvent $event) {
513: $event->addScopeInfo('oauth', [
514: 'openid' => [
515: 'description' => $this->f3->get('intl.common.scope.id'),
516: 'weight' => -1
517: ],
518: 'profile' => [
519: 'description' => $this->f3->get('intl.common.scope.profile'),
520: 'claims' => ['name', 'family_name', 'given_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'gender', 'birthdate', 'zoneinfo', 'locale' ]
521: ],
522: 'email' => [
523: 'description' => $this->f3->get('intl.common.scope.email'),
524: 'claims' => [ 'email' ]
525: ],
526: 'address' => [
527: 'description' => $this->f3->get('intl.common.scope.address'),
528: 'claims' => [ 'address' ]
529: ],
530: 'phone' => [
531: 'description' => $this->f3->get('intl.common.scope.phone'),
532: 'claims' => [ 'phone_number' ]
533: ]
534: ]);
535: }
536:
537:
538: /**
539: * Add the OpenID Connect configuration data to the OAuth metadata endpoint.
540: *
541: * @return void
542: */
543: public function onOauthMetadata(BaseDataCollectionEvent $event) {
544: $dispatcher = \Events::instance();
545:
546: $scope_info_event = new ScopeInfoCollectionEvent();
547: $dispatcher->dispatch($scope_info_event);
548: $scopes = $scope_info_event->getScopeInfoForType('oauth');
549:
550: $jwt_signing_algs = AlgorithmFactory::getSupportedAlgs(AlgorithmInterface::SIGNATURE_ALGORITHM);
551: $jwt_encryption_algs = AlgorithmFactory::getSupportedAlgs(AlgorithmInterface::KEY_ALGORITHM);
552: $jwt_encryption_enc_algs = AlgorithmFactory::getSupportedAlgs(AlgorithmInterface::ENCRYPTION_ALGORITHM);
553:
554: $claims_supported = [ 'sub', 'iss', 'auth_time', 'acr' ];
555: foreach ($scopes as $scope => $settings) {
556: if (isset($settings['claims'])) {
557: $claims_supported = array_merge($claims_supported, $settings['claims']);
558: }
559: }
560:
561: $config = [
562: 'response_types_supported' => [ 'id_token', 'id_token token', 'code id_token', 'code id_token token' ],
563: 'userinfo_endpoint' => $this->getCanonicalURL('@connect_userinfo', '', 'https'),
564: 'jwks_uri' => $this->getCanonicalURL('@connect_jwks', '', 'https'),
565: 'acr_values_supported' => [],
566: 'subject_types_supported' => [ 'public', 'pairwise' ],
567: 'userinfo_signing_alg_values_supported' => $jwt_signing_algs,
568: 'userinfo_encryption_alg_values_supported' => $jwt_encryption_algs,
569: 'userinfo_encryption_enc_alg_values_supported' => $jwt_encryption_enc_algs,
570: 'id_token_signing_alg_values_supported' => $jwt_signing_algs,
571: 'id_token_encrpytion_alg_values_supported' => $jwt_encryption_algs,
572: 'id_token_encrpytion_enc_alg_values_supported' => $jwt_encryption_enc_algs,
573: 'request_object_signing_alg_values_supported' => array_merge($jwt_signing_algs, [ 'none' ]),
574: 'request_object_encryption_alg_values_supported' => $jwt_encryption_algs,
575: 'request_object_encryption_enc_alg_values_supported' => $jwt_encryption_enc_algs,
576: 'claim_types_supported' => [ 'normal' ],
577: 'claims_supported' => $claims_supported,
578: 'claims_parameter_supported' => true,
579: 'request_parameter_supported' => true,
580: 'request_uri_parameter_supported' => true,
581: 'require_request_uri_registration' => false,
582: ];
583:
584: $event->addResult($config);
585: }
586:
587:
588: /**
589: * Displays the JSON web key for this installation.
590: *
591: * @return void
592: */
593: public function jwks() {
594: $config = $this->f3->get('config');
595:
596: if (!isset($config['public_jwks_file'])) {
597: $this->fatalError($this->f3->get('intl.core.connect.missing_jwks'), 404);
598: }
599:
600: $set = new KeySet();
601: $file = file_get_contents($config['public_jwks_file']);
602: if ($file) $set->load($file);
603:
604: if (!$set->isPublic()) {
605: $this->fatalError($this->f3->get('intl.core.connect.jwks_not_public'), 401);
606: }
607:
608: header('Content-Type: application/jwk-set+json');
609: header('Content-Disposition: inline; filename=jwks.json');
610: print $set->toJWKS();
611: }
612: }
613: ?>
614: