Skip to content

Commit 461c0bd

Browse files
committed
Initial access token support
1 parent 77dd5c5 commit 461c0bd

18 files changed

Lines changed: 384 additions & 393 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
];

routing/routes/routes.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +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\TokenIntrospectionController;
22+
use SimpleSAML\Module\oidc\Controllers\OAuth2\TokenIntrospectionController;
2323
use SimpleSAML\Module\oidc\Controllers\UserInfoController;
2424
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\CredentialIssuerConfigurationController;
2525
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\CredentialIssuerCredentialController;
@@ -105,8 +105,6 @@
105105

106106
$routes->add(RoutesEnum::OAuth2Configuration->name, RoutesEnum::OAuth2Configuration->value)
107107
->controller(OAuth2ServerConfigurationController::class);
108-
$routes->add(RoutesEnum::TokenIntrospection->name, RoutesEnum::TokenIntrospection->value)
109-
->controller(TokenIntrospectionController::class);
110108

111109
/*****************************************************************************************************************
112110
* OpenID Federation
@@ -145,4 +143,10 @@
145143
RoutesEnum::ApiVciCredentialOffer->value,
146144
)->controller([VciCredentialOfferApiController::class, 'credentialOffer'])
147145
->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]);
148152
};

routing/services/services.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ services:
9797
SimpleSAML\Module\oidc\Utils\ClassInstanceBuilder: ~
9898
SimpleSAML\Module\oidc\Utils\JwksResolver: ~
9999
SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor:
100+
SimpleSAML\Module\oidc\Utils\AuthenticatedOAuth2ClientResolver:
100101
factory: ['@SimpleSAML\Module\oidc\Factories\ClaimTranslatorExtractorFactory', 'build']
101102
SimpleSAML\Module\oidc\Utils\FederationCache:
102103
factory: ['@SimpleSAML\Module\oidc\Factories\CacheFactory', 'forFederation'] # Can return null

src/Codebooks/ApiScopesEnum.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ enum ApiScopesEnum: string
1111
// Verifiable Credential Issuance related scopes.
1212
case VciAll = 'vci_all'; // Gives access to all VCI-related endpoints.
1313
case VciCredentialOffer = 'vci_credential_offer'; // Gives access to the credential offer endpoint.
14+
15+
// OAuth2 related scopes.
16+
case OAuth2All = 'oauth2_all'; // Gives access to all OAuth2-related endpoints.
17+
case OAuth2TokenIntrospection = 'oauth2_token_introspection'; // Gives access to the token introspection endpoint.
1418
}

src/Codebooks/RoutesEnum.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ enum RoutesEnum: string
4242
case Jwks = 'jwks';
4343
case EndSession = 'end-session';
4444

45-
case TokenIntrospection = 'introspect';
46-
4745
/*****************************************************************************************************************
4846
* OAuth 2.0 Authorization Server
4947
****************************************************************************************************************/
@@ -77,4 +75,5 @@ enum RoutesEnum: string
7775
****************************************************************************************************************/
7876

7977
case ApiVciCredentialOffer = 'api/vci/credential-offer';
78+
case ApiOAuth2TokenIntrospection = 'api/oauth2/token-introspection';
8079
}

src/Controllers/Api/VciCredentialOfferApiController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,15 @@ public function __construct(
4040
}
4141

4242
/**
43+
* @throws OidcServerException
4344
*/
4445
public function credentialOffer(Request $request): Response
4546
{
47+
if (!$this->moduleConfig->getApiVciCredentialOfferEndpointEnabled()) {
48+
$this->loggerService->warning('Credential Offer API endpoint not enabled.');
49+
throw OidcServerException::forbidden('Credential Offer API endpoint not enabled.');
50+
}
51+
4652
$this->loggerService->debug('VciCredentialOfferApiController::credentialOffer');
4753

4854
$this->loggerService->debug(
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Controllers\OAuth2;
6+
7+
use SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum;
8+
use SimpleSAML\Module\oidc\Exceptions\AuthorizationException;
9+
use SimpleSAML\Module\oidc\ModuleConfig;
10+
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
11+
use SimpleSAML\Module\oidc\Server\Validators\BearerTokenValidator;
12+
use SimpleSAML\Module\oidc\Services\Api\Authorization;
13+
use SimpleSAML\Module\oidc\Services\LoggerService;
14+
use SimpleSAML\Module\oidc\Utils\AuthenticatedOAuth2ClientResolver;
15+
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
16+
use SimpleSAML\Module\oidc\Utils\Routes;
17+
use SimpleSAML\Module\oidc\ValueAbstracts\ResolvedClientAuthenticationMethod;
18+
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
19+
use SimpleSAML\OpenID\Codebooks\HttpMethodsEnum;
20+
use SimpleSAML\OpenID\Codebooks\ParamsEnum;
21+
use Symfony\Component\HttpFoundation\Request;
22+
use Symfony\Component\HttpFoundation\Response;
23+
24+
class TokenIntrospectionController
25+
{
26+
/**
27+
* @throws OidcServerException
28+
*/
29+
public function __construct(
30+
protected readonly ModuleConfig $moduleConfig,
31+
protected readonly AuthenticatedOAuth2ClientResolver $authenticatedOAuth2ClientResolver,
32+
protected readonly Routes $routes,
33+
protected readonly LoggerService $loggerService,
34+
protected readonly Authorization $apiAuthorization,
35+
protected readonly RequestParamsResolver $requestParamsResolver,
36+
protected readonly BearerTokenValidator $bearerTokenValidator,
37+
) {
38+
if (!$this->moduleConfig->getApiEnabled()) {
39+
$this->loggerService->warning('API capabilities not enabled.');
40+
throw OidcServerException::forbidden('API capabilities not enabled.');
41+
}
42+
43+
if (!$this->moduleConfig->getApiOAuth2TokenIntrospectionEndpointEnabled()) {
44+
$this->loggerService->warning('OAuth2 Token Introspection API endpoint not enabled.');
45+
throw OidcServerException::forbidden('OAuth2 Token Introspection API endpoint not enabled.');
46+
}
47+
}
48+
49+
public function __invoke(Request $request): Response
50+
{
51+
// TODO mivanci Add support for Refresh Tokens.
52+
// TODO mivanci Add endpoint to OAuth2 discovery document.
53+
54+
try {
55+
$this->ensureAuthenticatedClient($request);
56+
} catch (AuthorizationException $e) {
57+
$this->loggerService->error(
58+
'TokenIntrospectionController::invoke: AuthorizationException: ' . $e->getMessage(),
59+
);
60+
return $this->routes->newJsonErrorResponse(
61+
error: 'unauthorized',
62+
description: $e->getMessage(),
63+
httpCode: Response::HTTP_UNAUTHORIZED,
64+
);
65+
}
66+
67+
$allowedMethods = [HttpMethodsEnum::POST];
68+
69+
$tokenParam = $this->requestParamsResolver->getFromRequestBasedOnAllowedMethods(
70+
ParamsEnum::Token->value,
71+
$request,
72+
$allowedMethods,
73+
);
74+
75+
if (!$tokenParam) {
76+
return $this->routes->newJsonErrorResponse(
77+
error: 'invalid_request',
78+
description: 'Missing token parameter.',
79+
httpCode: Response::HTTP_BAD_REQUEST,
80+
);
81+
}
82+
83+
// For now, we will only support Access Tokens.
84+
// $tokenTypeHintParam = $this->requestParamsResolver->getFromRequestBasedOnAllowedMethods(
85+
// ParamsEnum::TokenTypeHint->value,
86+
// $request,
87+
// $allowedMethods,
88+
// );
89+
90+
try {
91+
$accessToken = $this->bearerTokenValidator->ensureValidAccessToken($tokenParam);
92+
} catch (\Throwable $e) {
93+
$this->loggerService->error('Token validation failed: ' . $e->getMessage());
94+
return $this->routes->newJsonResponse(['active' => false]);
95+
}
96+
$scopeClaim = null;
97+
/** @psalm-suppress MixedAssignment */
98+
$accessTokenScopes = $accessToken->getPayloadClaim('scopes');
99+
if (is_array($accessTokenScopes)) {
100+
$accessTokenScopes = array_filter(
101+
$accessTokenScopes,
102+
static fn($scope) => is_string($scope) && !empty($scope),
103+
);
104+
$scopeClaim = implode(' ', $accessTokenScopes);
105+
}
106+
107+
$audience = is_array($audience = $accessToken->getAudience()) ? $audience[0] ?? null : null;
108+
109+
$payload = array_filter([
110+
'active' => true,
111+
'scope' => $scopeClaim,
112+
'token_type' => 'Bearer',
113+
ClaimsEnum::Exp->value => $accessToken->getExpirationTime(),
114+
ClaimsEnum::Iat->value => $accessToken->getIssuedAt(),
115+
ClaimsEnum::Nbf->value => $accessToken->getNotBefore(),
116+
ClaimsEnum::Sub->value => $accessToken->getSubject(),
117+
ClaimsEnum::Aud->value => $audience,
118+
ClaimsEnum::Iss->value => $accessToken->getIssuer(),
119+
ClaimsEnum::Jti->value => $accessToken->getJwtId(),
120+
]);
121+
122+
return $this->routes->newJsonResponse($payload);
123+
}
124+
125+
/**
126+
* @throws AuthorizationException
127+
*/
128+
protected function ensureAuthenticatedClient(Request $request): void
129+
{
130+
$this->loggerService->debug('TokenIntrospectionController::ensureAuthenticatedClient - start');
131+
$this->loggerService->debug('Trying supported OAuth2 client authentication methods.');
132+
133+
// First, try regular OAuth2 client authentication methods.
134+
$resolvedClientAuthenticationMethod = $this->authenticatedOAuth2ClientResolver->forAnySupportedMethod($request);
135+
136+
if (
137+
$resolvedClientAuthenticationMethod instanceof ResolvedClientAuthenticationMethod &&
138+
$resolvedClientAuthenticationMethod->getClientAuthenticationMethod()->isNotNone()
139+
) {
140+
$this->loggerService->debug(
141+
'Client authenticated using supported OAuth2 client authentication method: ' .
142+
$resolvedClientAuthenticationMethod->getClientAuthenticationMethod()->value,
143+
);
144+
return;
145+
}
146+
147+
$this->loggerService->debug('No regular OAuth2 client authentication method found.');
148+
$this->loggerService->debug('Trying API client authentication method.');
149+
150+
151+
$this->apiAuthorization->requireTokenForAnyOfScope(
152+
$request,
153+
[ApiScopesEnum::OAuth2TokenIntrospection, ApiScopesEnum::OAuth2All, ApiScopesEnum::All],
154+
);
155+
}
156+
}

0 commit comments

Comments
 (0)