Skip to content

Commit bfc8b4f

Browse files
JovanSimonoskiPeterBolhacicnavi
authored
Add OAuth2 Token Introspection endpoint (#331)
* Add token introspect controller * Add client authentication * Add note about cache busting after upgrade * Add access token introspection support * Add refresh token introspection support * Include introspection endpoint in OAuth2 metadata * Add unit tests * Update docs * Update upgrade log --------- Co-authored-by: peterbolha <bolha@cesnet.cz> Co-authored-by: Marko Ivančić <mivanci@srce.hr>
1 parent fa20eb5 commit bfc8b4f

33 files changed

Lines changed: 2569 additions & 199 deletions

config/module_oidc.php.dist

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1037,10 +1037,27 @@ $config = [
10371037

10381038
/**
10391039
* (optional) Enable or disable API capabilities. Default is disabled
1040-
* (false).
1040+
* (false). If API capabilities are enabled, you can enable or disable
1041+
* specific API endpoints as needed and set up API tokens to allow
1042+
* access to those endpoints. If API capabilities are disabled, all API
1043+
* endpoints will be inaccessible regardless of the settings for
1044+
* specific endpoints and API tokens.
1045+
*
10411046
*/
10421047
ModuleConfig::OPTION_API_ENABLED => false,
10431048

1049+
/**
1050+
* (optional) API Enable VCI Credential Offer API endpoint. Default is
1051+
* disabled (false). Only relevant if API capabilities are enabled.
1052+
*/
1053+
ModuleConfig::OPTION_API_VCI_CREDENTIAL_OFFER_ENDPOINT_ENABLED => false,
1054+
1055+
/**
1056+
* (optional) API Enable OAuth2 Token Introspection API endpoint. Default
1057+
* is disabled (false). Only relevant if API capabilities are enabled.
1058+
*/
1059+
ModuleConfig::OPTION_API_OAUTH2_TOKEN_INTROSPECTION_ENDPOINT_ENABLED => false,
1060+
10441061
/**
10451062
* List of API tokens which can be used to access API endpoints based on
10461063
* given scopes. The format is: ['token' => [ApiScopesEnum]]
@@ -1050,6 +1067,11 @@ $config = [
10501067
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::All, // Gives access to the whole API.
10511068
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::VciAll, // Gives access to all VCI-related endpoints.
10521069
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::VciCredentialOffer, // Gives access to the credential offer endpoint.
1070+
// ],
1071+
// 'strong-random-token-string-2' => [
1072+
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::All, // Gives access to the whole API.
1073+
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::OAuth2All, // Gives access to all OAuth2-related endpoints.
1074+
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::OAuth2TokenIntrospection, // Gives access to the token introspection endpoint.
10531075
// ],
10541076
],
10551077
];

docker/ssp/config-override.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
'database.dsn' => getenv('DB.DSN') ?: 'sqlite:/var/simplesamlphp/data/mydb.sq3',
1212
'database.username' => getenv('DB.USERNAME') ?: 'user',
1313
'database.password' => getenv('DB.PASSWORD') ?: 'password',
14-
'language.i18n.backend' => 'gettext/gettext',
1514
'logging.level' => 7,
16-
'usenewui' => false,
15+
1716
] + $config;

docker/ssp/module_oidc.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,16 @@
128128
ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS => [
129129
// Use defaults
130130
],
131+
132+
ModuleConfig::OPTION_API_ENABLED => true,
133+
134+
ModuleConfig::OPTION_API_VCI_CREDENTIAL_OFFER_ENDPOINT_ENABLED => true,
135+
136+
ModuleConfig::OPTION_API_OAUTH2_TOKEN_INTROSPECTION_ENDPOINT_ENABLED => true,
137+
138+
ModuleConfig::OPTION_API_TOKENS => [
139+
'strong-random-token-string' => [
140+
\SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::All, // Gives access to the whole API.
141+
],
142+
],
131143
];

docs/1-oidc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,4 @@ Upgrading? See the [upgrade guide](6-oidc-upgrade.md).
7777
- Conformance tests: [OpenID Conformance](5-oidc-conformance.md)
7878
- Upgrading between versions: [Upgrade guide](6-oidc-upgrade.md)
7979
- Common questions: [FAQ](7-oidc-faq.md)
80+
- API documentation: [API](8-api.md)

docs/6-oidc-upgrade.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
This is an upgrade guide from versions 1 → 7. Review the changes and
44
apply those relevant to your deployment.
55

6+
In general, when upgrading any of the SimpleSAMLphp modules or the
7+
SimpleSAMLphp instance itself, you should clear the SimpleSAMLphp
8+
cache after the upgrade. In newer versions of SimpleSAMLphp, the
9+
following command is available to do that:
10+
11+
```shell
12+
composer clear-symfony-cache
13+
```
14+
615
## Version 6 to 7
716

817
As the database schema has been updated, you will have to run the DB migrations
@@ -15,6 +24,8 @@ keys for protocol (Connect), Federation, and VCI purposes. This was introduced
1524
to support signature algorithm negotiation with the clients.
1625
- Clients can now be configured with new properties:
1726
- ID Token Signing Algorithm (`id_token_signed_response_alg`)
27+
- Optional OAuth2 Token Introspection endpoint, as per RFC7662. Check the API
28+
documentation for more details.
1829
- Initial support for OpenID for Verifiable Credential Issuance
1930
(OpenID4VCI). Note that the implementation is experimental. You should not use
2031
it in production.
@@ -34,6 +45,8 @@ key roll-ower scenarios, etc.
3445
- `ModuleConfig::OPTION_TIMESTAMP_VALIDATION_LEEWAY` - optional, used for
3546
setting allowed time tolerance for timestamp validation in artifacts like JWSs.
3647
multiple Federation-related signing algorithms and key pairs.
48+
- `ModuleConfig::OPTION_API_OAUTH2_TOKEN_INTROSPECTION_ENDPOINT_ENABLED` -
49+
optional, enables the OAuth2 token introspection endpoint as per RFC7662.
3750
- Several new options regarding experimental support for OpenID4VCI.
3851

3952
Major impact changes:

docs/api.md renamed to docs/8-api.md

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ ModuleConfig::OPTION_API_TOKENS => [
3030
Scopes determine which endpoints are accessible by the API access token. The following scopes are available:
3131

3232
* `\SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::All`: Access to all endpoints.
33-
* `\SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::VciAll`: Access to all VCI-related endpoints
33+
* `\SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::VciAll`: Access to all VCI-related endpoints.
3434
* `\SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::VciCredentialOffer`: Access to credential offer endpoint.
35+
* `\SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::OAuth2All`: Access to all OAuth2-related endpoints.
36+
* `\SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::OAuth2TokenIntrospection`: Access to the OAuth2 token introspection endpoint.
3537

3638
## API Endpoints
3739

@@ -142,4 +144,117 @@ Response:
142144
{
143145
"credential_offer_uri": "openid-credential-offer://?credential_offer={\"credential_issuer\":\"https:\\/\\/idp.mivanci.incubator.hexaa.eu\",\"credential_configuration_ids\":[\"ResearchAndScholarshipCredentialDcSdJwt\"],\"grants\":{\"urn:ietf:params:oauth:grant-type:pre-authorized_code\":{\"pre-authorized_code\":\"_ffcdf6d86cd564c300346351dce0b4ccb2fde304e2\",\"tx_code\":{\"input_mode\":\"numeric\",\"length\":4,\"description\":\"Please provide the one-time code that was sent to e-mail testuser@example.com\"}}}}"
144146
}
145-
```
147+
```
148+
149+
### Token Introspection
150+
151+
Enables token introspection for OAuth2 access tokens and refresh tokens as per
152+
[RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662).
153+
154+
#### Path
155+
156+
`/api/oauth2/token-introspection`
157+
158+
#### Method
159+
160+
`POST`
161+
162+
#### Authorization
163+
164+
Access is granted if:
165+
* The client is authenticated using one of the supported OAuth2 client
166+
authentication methods (Basic, Post, Private Key JWT, Bearer).
167+
* Or, if the request is authorized using an API Bearer Token with
168+
the appropriate scope.
169+
170+
#### Request
171+
172+
The request is sent with `application/x-www-form-urlencoded` encoding with the
173+
following parameters:
174+
175+
* __token__ (string, mandatory): The string value of the token.
176+
* __token_type_hint__ (string, optional): A hint about the type of the
177+
token submitted for introspection. Allowed values:
178+
* `access_token`
179+
* `refresh_token`
180+
181+
#### Response
182+
183+
The response is a JSON object with the following fields:
184+
185+
* __active__ (boolean, mandatory): Indicator of whether or not the presented
186+
token is currently active.
187+
* __scope__ (string, optional): A JSON string containing a space-separated
188+
list of scopes associated with this token.
189+
* __client_id__ (string, optional): Client identifier for the OAuth 2.0 client
190+
that requested this token.
191+
* __token_type__ (string, optional): Type of the token as defined in OAuth 2.0.
192+
* __exp__ (integer, optional): Expiration time.
193+
* __iat__ (integer, optional): Issued at time.
194+
* __nbf__ (integer, optional): Not before time.
195+
* __sub__ (string, optional): Subject identifier for the user who
196+
authorized the token.
197+
* __aud__ (string/array, optional): Audience for the token.
198+
* __iss__ (string, optional): Issuer of the token.
199+
* __jti__ (string, optional): Identifier for the token.
200+
201+
If the token is not active, only the `active` field with a value of
202+
`false` is returned.
203+
204+
#### Sample 1
205+
206+
Introspect an active access token using an API Bearer Token.
207+
208+
Request:
209+
210+
```shell
211+
curl --location 'https://idp.mivanci.incubator.hexaa.eu/ssp/module.php/oidc/api/oauth2/token-introspection' \
212+
--header 'Content-Type: application/x-www-form-urlencoded' \
213+
--header 'Authorization: Bearer ***' \
214+
--data-urlencode 'token=access-token-string'
215+
```
216+
217+
Response:
218+
219+
```json
220+
{
221+
"active": true,
222+
"scope": "openid profile email",
223+
"client_id": "test-client",
224+
"token_type": "Bearer",
225+
"exp": 1712662800,
226+
"iat": 1712659200,
227+
"sub": "user-id",
228+
"aud": "test-client",
229+
"iss": "https://idp.mivanci.incubator.hexaa.eu",
230+
"jti": "token-id"
231+
}
232+
```
233+
234+
#### Sample 2
235+
236+
Introspect a refresh token using an API Bearer Token.
237+
238+
Request:
239+
240+
```shell
241+
curl --location 'https://idp.mivanci.incubator.hexaa.eu/ssp/module.php/oidc/api/oauth2/token-introspection' \
242+
--header 'Content-Type: application/x-www-form-urlencoded' \
243+
--header 'Authorization: Bearer ***' \
244+
--data-urlencode 'token=refresh-token-string' \
245+
--data-urlencode 'token_type_hint=refresh_token'
246+
```
247+
248+
Response:
249+
250+
```json
251+
{
252+
"active": true,
253+
"scope": "openid profile",
254+
"client_id": "test-client",
255+
"exp": 1715251200,
256+
"sub": "user-id",
257+
"aud": "test-client",
258+
"jti": "refresh-token-id"
259+
}
260+
```

docs/index.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

routing/routes/routes.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use SimpleSAML\Module\oidc\Controllers\Federation\EntityStatementController;
2020
use SimpleSAML\Module\oidc\Controllers\JwksController;
2121
use SimpleSAML\Module\oidc\Controllers\OAuth2\OAuth2ServerConfigurationController;
22+
use SimpleSAML\Module\oidc\Controllers\OAuth2\TokenIntrospectionController;
2223
use SimpleSAML\Module\oidc\Controllers\UserInfoController;
2324
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\CredentialIssuerConfigurationController;
2425
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\CredentialIssuerCredentialController;
@@ -142,4 +143,10 @@
142143
RoutesEnum::ApiVciCredentialOffer->value,
143144
)->controller([VciCredentialOfferApiController::class, 'credentialOffer'])
144145
->methods([HttpMethodsEnum::POST->value]);
146+
147+
$routes->add(
148+
RoutesEnum::ApiOAuth2TokenIntrospection->name,
149+
RoutesEnum::ApiOAuth2TokenIntrospection->value,
150+
)->controller(TokenIntrospectionController::class)
151+
->methods([HttpMethodsEnum::POST->value]);
145152
};

routing/services/services.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ services:
9696
SimpleSAML\Module\oidc\Utils\RequestParamsResolver: ~
9797
SimpleSAML\Module\oidc\Utils\ClassInstanceBuilder: ~
9898
SimpleSAML\Module\oidc\Utils\JwksResolver: ~
99+
SimpleSAML\Module\oidc\Utils\AuthenticatedOAuth2ClientResolver: ~
99100
SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor:
100101
factory: ['@SimpleSAML\Module\oidc\Factories\ClaimTranslatorExtractorFactory', 'build']
101102
SimpleSAML\Module\oidc\Utils\FederationCache:

src/Bridges/OAuth2Bridge.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Bridges;
6+
7+
use Defuse\Crypto\Crypto;
8+
use Defuse\Crypto\Key;
9+
use SimpleSAML\Module\oidc\Exceptions\OidcException;
10+
use SimpleSAML\Module\oidc\ModuleConfig;
11+
12+
class OAuth2Bridge
13+
{
14+
public function __construct(
15+
protected readonly ModuleConfig $moduleConfig,
16+
) {
17+
}
18+
19+
/**
20+
* Bridge `encrypt` function, which can be used instead of
21+
* \League\OAuth2\Server\CryptTrait::encrypt()
22+
*
23+
* @param string $unencryptedData
24+
* @param Key|string $encryptionKey
25+
* @return string
26+
* @throws OidcException
27+
*/
28+
public function encrypt(
29+
string $unencryptedData,
30+
null|Key|string $encryptionKey = null,
31+
): string {
32+
$encryptionKey ??= $this->moduleConfig->getEncryptionKey();
33+
34+
try {
35+
return $encryptionKey instanceof Key ?
36+
Crypto::encrypt($unencryptedData, $encryptionKey) :
37+
Crypto::encryptWithPassword($unencryptedData, $encryptionKey);
38+
} catch (\Exception $e) {
39+
throw new OidcException('Error encrypting data: ' . $e->getMessage(), (int)$e->getCode(), $e);
40+
}
41+
}
42+
43+
/**
44+
* Bridge `decrypt` function, which can be used instead of
45+
* \League\OAuth2\Server\CryptTrait::decrypt()
46+
*
47+
* @param string $encryptedData
48+
* @param Key|string $encryptionKey
49+
* @return string
50+
* @throws OidcException
51+
*/
52+
public function decrypt(
53+
string $encryptedData,
54+
null|Key|string $encryptionKey = null,
55+
): string {
56+
$encryptionKey ??= $this->moduleConfig->getEncryptionKey();
57+
58+
try {
59+
return $encryptionKey instanceof Key ?
60+
Crypto::decrypt($encryptedData, $encryptionKey) :
61+
Crypto::decryptWithPassword($encryptedData, $encryptionKey);
62+
} catch (\Exception $e) {
63+
throw new OidcException('Error decrypting data: ' . $e->getMessage(), (int)$e->getCode(), $e);
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)