Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit9681e6b

Browse files
committed
[Security] OAuth2 Introspection Endpoint (RFC7662)
In addition to the excellent work of@vincentchalamon#48272, this PR allows getting the data from the OAuth2 Introspection Endpoint. This endpoint is defined in the [RFC7662](https://datatracker.ietf.org/doc/html/rfc7662). It returns the following information that is used to retrieve the user:* If the access token is active* A set of claims that are similar to the OIDC one, including the `sub` or the `username`.
1 parent9319a0c commit9681e6b

File tree

12 files changed

+378
-0
lines changed

12 files changed

+378
-0
lines changed

‎src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Add encryption support to`OidcTokenHandler` (JWE)
99
* Add`expose_security_errors` config option to display`AccountStatusException`
1010
* Deprecate the`security.hide_user_not_found` config option in favor of`security.expose_security_errors`
11+
* Add`OAuth2TokenHandlerFactory` for`AccessTokenFactory`
1112

1213
7.2
1314
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespaceSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken;
13+
14+
useSymfony\Component\Config\Definition\Builder\NodeBuilder;
15+
useSymfony\Component\DependencyInjection\ChildDefinition;
16+
useSymfony\Component\DependencyInjection\ContainerBuilder;
17+
18+
/**
19+
* Configures a token handler for an OAuth2 Token Introspection endpoint.
20+
*
21+
* @internal
22+
*/
23+
class OAuth2TokenHandlerFactoryimplements TokenHandlerFactoryInterface
24+
{
25+
publicfunctioncreate(ContainerBuilder$container,string$id,array|string$config):void
26+
{
27+
$container->setDefinition($id,newChildDefinition('security.access_token_handler.oauth2'));
28+
}
29+
30+
publicfunctiongetKey():string
31+
{
32+
return'oauth2';
33+
}
34+
35+
publicfunctionaddConfiguration(NodeBuilder$node):void
36+
{
37+
$node->scalarNode($this->getKey())->end();
38+
}
39+
}

‎src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
useSymfony\Component\Security\Http\AccessToken\ChainAccessTokenExtractor;
3737
useSymfony\Component\Security\Http\AccessToken\FormEncodedBodyExtractor;
3838
useSymfony\Component\Security\Http\AccessToken\HeaderAccessTokenExtractor;
39+
useSymfony\Component\Security\Http\AccessToken\OAuth2\Oauth2TokenHandler;
3940
useSymfony\Component\Security\Http\AccessToken\Oidc\OidcTokenHandler;
4041
useSymfony\Component\Security\Http\AccessToken\Oidc\OidcUserInfoTokenHandler;
4142
useSymfony\Component\Security\Http\AccessToken\QueryAccessTokenExtractor;
@@ -186,5 +187,13 @@
186187

187188
->set('security.access_token_handler.oidc.encryption.A256GCM',A256GCM::class)
188189
->tag('security.access_token_handler.oidc.encryption_algorithm')
190+
191+
// OAuth2 Introspection (RFC 7662)
192+
->set('security.access_token_handler.oauth2', Oauth2TokenHandler::class)
193+
->abstract()
194+
->args([
195+
service('http_client'),
196+
service('logger')->nullOnInvalid(),
197+
])
189198
;
190199
};

‎src/Symfony/Bundle/SecurityBundle/SecurityBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
useSymfony\Bundle\SecurityBundle\DependencyInjection\Compiler\ReplaceDecoratedRememberMeHandlerPass;
2525
useSymfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass;
2626
useSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\CasTokenHandlerFactory;
27+
useSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OAuth2TokenHandlerFactory;
2728
useSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcTokenHandlerFactory;
2829
useSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcUserInfoTokenHandlerFactory;
2930
useSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\ServiceTokenHandlerFactory;
@@ -80,6 +81,7 @@ public function build(ContainerBuilder $container): void
8081
newOidcUserInfoTokenHandlerFactory(),
8182
newOidcTokenHandlerFactory(),
8283
newCasTokenHandlerFactory(),
84+
newOAuth2TokenHandlerFactory(),
8385
]));
8486

8587
$extension->addUserProviderFactory(newInMemoryFactory());

‎src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
usePHPUnit\Framework\TestCase;
1515
useSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\CasTokenHandlerFactory;
16+
useSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OAuth2TokenHandlerFactory;
1617
useSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcTokenHandlerFactory;
1718
useSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcUserInfoTokenHandlerFactory;
1819
useSymfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\ServiceTokenHandlerFactory;
@@ -423,6 +424,22 @@ public function testMultipleTokenHandlersSet()
423424
$this->processConfig($config,$factory);
424425
}
425426

427+
publicfunctiontestOAuth2TokenHandlerConfiguration()
428+
{
429+
$container =newContainerBuilder();
430+
$config = [
431+
'token_handler' => ['oauth2' =>true],
432+
];
433+
434+
$factory =newAccessTokenFactory($this->createTokenHandlerFactories());
435+
$finalizedConfig =$this->processConfig($config,$factory);
436+
437+
$factory->createAuthenticator($container,'firewall1',$finalizedConfig,'userprovider');
438+
439+
$this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
440+
$this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
441+
}
442+
426443
publicfunctiontestNoTokenHandlerSet()
427444
{
428445
$this->expectException(InvalidConfigurationException::class);
@@ -482,6 +499,7 @@ private function createTokenHandlerFactories(): array
482499
newOidcUserInfoTokenHandlerFactory(),
483500
newOidcTokenHandlerFactory(),
484501
newCasTokenHandlerFactory(),
502+
newOAuth2TokenHandlerFactory(),
485503
];
486504
}
487505
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
imports:
2+
-{ resource: ./../config/framework.yml }
3+
4+
framework:
5+
http_method_override:false
6+
serializer:~
7+
http_client:
8+
scoped_clients:
9+
oauth2.client:
10+
scope:'https://authorization-server\.example\.com'
11+
headers:
12+
Authorization:'Basic Y2xpZW50OnBhc3N3b3Jk'
13+
14+
security:
15+
password_hashers:
16+
Symfony\Component\Security\Core\User\InMemoryUser:plaintext
17+
18+
providers:
19+
in_memory:
20+
memory:
21+
users:
22+
dunglas:{ password: foo, roles: [ROLE_USER] }
23+
24+
firewalls:
25+
main:
26+
pattern:^/
27+
access_token:
28+
token_handler:
29+
oauth2:~
30+
token_extractors:'header'
31+
realm:'My API'
32+
33+
access_control:
34+
-{ path: ^/foo, roles: ROLE_USER }

‎src/Symfony/Component/Security/Core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Add`OfflineTokenInterface` to mark tokens that do not represent the currently logged-in user
1010
* Deprecate`UserInterface::eraseCredentials()` and`TokenInterface::eraseCredentials()`,
1111
erase credentials e.g. using`__serialize()` instead
12+
* Add`OAuth2User` with OAuth2 Access Token Introspection support for`OAuth2TokenHandler`
1213

1314
7.2
1415
---
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespaceSymfony\Component\Security\Core\Tests\User;
13+
14+
usePHPUnit\Framework\TestCase;
15+
useSymfony\Component\Security\Core\User\OAuth2User;
16+
17+
class OAuth2UserTestextends TestCase
18+
{
19+
publicfunctiontestCannotCreateUserWithoutSubProperty()
20+
{
21+
$this->expectException(\InvalidArgumentException::class);
22+
$this->expectExceptionMessage('The claim "sub" or "username" must be provided.');
23+
24+
newOAuth2User();
25+
}
26+
27+
publicfunctiontestCreateFullUserWithAdditionalClaimsUsingPositionalParameters()
28+
{
29+
$this->assertEquals(newOAuth2User(
30+
scope:'read write dolphin',
31+
username:'jdoe',
32+
exp:1419356238,
33+
iat:1419350238,
34+
sub:'Z5O3upPC88QrAjx00dis',
35+
aud:'https://protected.example.net/resource',
36+
iss:'https://server.example.com/',
37+
client_id:'l238j323ds-23ij4',
38+
extension_field:'twenty-seven'
39+
),newOAuth2User(...[
40+
'client_id' =>'l238j323ds-23ij4',
41+
'username' =>'jdoe',
42+
'scope' =>'read write dolphin',
43+
'sub' =>'Z5O3upPC88QrAjx00dis',
44+
'aud' =>'https://protected.example.net/resource',
45+
'iss' =>'https://server.example.com/',
46+
'exp' =>1419356238,
47+
'iat' =>1419350238,
48+
'extension_field' =>'twenty-seven',
49+
]));
50+
}
51+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespaceSymfony\Component\Security\Core\User;
13+
14+
/**
15+
* UserInterface implementation used by the access-token security workflow with an OIDC server.
16+
*/
17+
class OAuth2Userimplements UserInterface
18+
{
19+
publicreadonlyarray$additionalClaims;
20+
21+
publicfunction__construct(
22+
privatearray$roles = ['ROLE_USER'],
23+
// Standard Claims (https://datatracker.ietf.org/doc/html/rfc7662#section-2.2)
24+
publicreadonly ?string$scope =null,
25+
publicreadonly ?string$clientId =null,
26+
publicreadonly ?string$username =null,
27+
publicreadonly ?string$tokenType =null,
28+
publicreadonly ?int$exp =null,
29+
publicreadonly ?int$iat =null,
30+
publicreadonly ?int$nbf =null,
31+
publicreadonly ?string$sub =null,
32+
publicreadonly ?string$aud =null,
33+
publicreadonly ?string$iss =null,
34+
publicreadonly ?string$jti =null,
35+
36+
// Additional Claims ("
37+
// Specific implementations MAY extend this structure with
38+
// their own service-specific response names as top-level members
39+
// of this JSON object.
40+
// ")
41+
...$additionalClaims,
42+
) {
43+
if ((null ===$sub ||'' ===$sub) && (null ===$username ||'' ===$username)) {
44+
thrownew \InvalidArgumentException('The claim "sub" or "username" must be provided.');
45+
}
46+
47+
$this->additionalClaims =$additionalClaims['additionalClaims'] ??$additionalClaims;
48+
}
49+
50+
/**
51+
* OIDC or OAuth specs don't have any "role" notion.
52+
*
53+
* If you want to implement "roles" from your OIDC server,
54+
* send a "roles" constructor argument to this object
55+
* (e.g.: using a custom UserProvider).
56+
*/
57+
publicfunctiongetRoles():array
58+
{
59+
return$this->roles;
60+
}
61+
62+
publicfunctiongetUserIdentifier():string
63+
{
64+
return (string) ($this->sub ??$this->username);
65+
}
66+
67+
publicfunctioneraseCredentials():void
68+
{
69+
}
70+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespaceSymfony\Component\Security\Http\AccessToken\OAuth2;
13+
14+
usePsr\Log\LoggerInterface;
15+
useSymfony\Component\Security\Core\Exception\AuthenticationException;
16+
useSymfony\Component\Security\Core\Exception\BadCredentialsException;
17+
useSymfony\Component\Security\Core\User\OAuth2User;
18+
useSymfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
19+
useSymfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
20+
useSymfony\Contracts\HttpClient\HttpClientInterface;
21+
22+
usefunctionSymfony\Component\String\u;
23+
24+
/**
25+
* The token handler validates the token on the authorization server and the Introspection Endpoint.
26+
*
27+
* @see https://tools.ietf.org/html/rfc7662
28+
*
29+
* @internal
30+
*/
31+
finalclass Oauth2TokenHandlerimplements AccessTokenHandlerInterface
32+
{
33+
publicfunction__construct(
34+
privatereadonlyHttpClientInterface$client,
35+
privatereadonly ?LoggerInterface$logger =null,
36+
) {
37+
}
38+
39+
publicfunctiongetUserBadgeFrom(string$accessToken):UserBadge
40+
{
41+
try {
42+
// Call the Authorization server to retrieve the resource owner details
43+
// If the token is invalid or expired, the Authorization server will return an error
44+
$claims =$this->client->request('POST','', [
45+
'body' => [
46+
'token' =>$accessToken,
47+
'token_type_hint' =>'access_token',
48+
],
49+
])->toArray();
50+
51+
$sub =$claims['sub'] ??null;
52+
$username =$claims['username'] ??null;
53+
if (!$sub && !$username) {
54+
thrownewBadCredentialsException('"sub" and "username" claims not found on the authorization server response. At least one is required.');
55+
}
56+
$active =$claims['active'] ??false;
57+
if (!$active) {
58+
thrownewBadCredentialsException('The claim "active" was not found on the authorization server response or is set to false.');
59+
}
60+
61+
returnnewUserBadge($sub ??$username,fn () =>$this->createUser($claims),$claims);
62+
}catch (AuthenticationException$e) {
63+
$this->logger?->error('An error occurred on the authorization server.', [
64+
'error' =>$e->getMessage(),
65+
'trace' =>$e->getTraceAsString(),
66+
]);
67+
68+
thrownewBadCredentialsException('Invalid credentials.',$e->getCode(),$e);
69+
}
70+
}
71+
72+
privatefunctioncreateUser(array$claims):OAuth2User
73+
{
74+
if (!\function_exists(\Symfony\Component\String\u::class)) {
75+
thrownew \LogicException('You cannot use the "OAuth2TokenHandler" since the String component is not installed. Try running "composer require symfony/string".');
76+
}
77+
78+
foreach ($claimsas$claim =>$value) {
79+
unset($claims[$claim]);
80+
if ('' ===$value ||null ===$value) {
81+
continue;
82+
}
83+
$claims[u($claim)->camel()->toString()] =$value;
84+
}
85+
86+
if ('' !== ($claims['updatedAt'] ??'')) {
87+
$claims['updatedAt'] = (new \DateTimeImmutable())->setTimestamp($claims['updatedAt']);
88+
}
89+
90+
if ('' !== ($claims['emailVerified'] ??'')) {
91+
$claims['emailVerified'] = (bool)$claims['emailVerified'];
92+
}
93+
94+
if ('' !== ($claims['phoneNumberVerified'] ??'')) {
95+
$claims['phoneNumberVerified'] = (bool)$claims['phoneNumberVerified'];
96+
}
97+
98+
returnnewOAuth2User(...$claims);
99+
}
100+
}

‎src/Symfony/Component/Security/Http/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add encryption support to`OidcTokenHandler` (JWE)
88
* Replace`$hideAccountStatusExceptions` argument with`$exposeSecurityErrors` in`AuthenticatorManager` constructor
99
* Add argument`$identifierNormalizer` to`UserBadge::__construct()` to allow normalizing the identifier
10+
* Add`OAuth2TokenHandler` with OAuth2 Token Introspection support for`AccessTokenAuthenticator`
1011

1112
7.2
1213
---

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp