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

Commit4ee96eb

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 parent36a920e commit4ee96eb

File tree

13 files changed

+382
-1
lines changed

13 files changed

+382
-1
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add`Security::isGrantedForUser()` to test user authorization without relying on the session. For example, users not currently logged in, or while processing a message from a message queue
8+
* Add`OAuth2TokenHandlerFactory` for`AccessTokenFactory`
89

910
7.2
1011
---
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
@@ -27,6 +27,7 @@
2727
useSymfony\Component\Security\Http\AccessToken\ChainAccessTokenExtractor;
2828
useSymfony\Component\Security\Http\AccessToken\FormEncodedBodyExtractor;
2929
useSymfony\Component\Security\Http\AccessToken\HeaderAccessTokenExtractor;
30+
useSymfony\Component\Security\Http\AccessToken\OAuth2\Oauth2TokenHandler;
3031
useSymfony\Component\Security\Http\AccessToken\Oidc\OidcTokenHandler;
3132
useSymfony\Component\Security\Http\AccessToken\Oidc\OidcUserInfoTokenHandler;
3233
useSymfony\Component\Security\Http\AccessToken\QueryAccessTokenExtractor;
@@ -135,5 +136,13 @@
135136

136137
->set('security.access_token_handler.oidc.signature.PS512',PS512::class)
137138
->tag('security.access_token_handler.oidc.signature_algorithm')
139+
140+
// OAuth2 Introspection (RFC 7662)
141+
->set('security.access_token_handler.oauth2', Oauth2TokenHandler::class)
142+
->abstract()
143+
->args([
144+
service('http_client'),
145+
service('logger')->nullOnInvalid(),
146+
])
138147
;
139148
};

‎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;
@@ -341,6 +342,22 @@ public function testMultipleTokenHandlersSet()
341342
$this->processConfig($config,$factory);
342343
}
343344

345+
publicfunctiontestOAuth2TokenHandlerConfiguration()
346+
{
347+
$container =newContainerBuilder();
348+
$config = [
349+
'token_handler' => ['oauth2' =>true],
350+
];
351+
352+
$factory =newAccessTokenFactory($this->createTokenHandlerFactories());
353+
$finalizedConfig =$this->processConfig($config,$factory);
354+
355+
$factory->createAuthenticator($container,'firewall1',$finalizedConfig,'userprovider');
356+
357+
$this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
358+
$this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
359+
}
360+
344361
publicfunctiontestNoTokenHandlerSet()
345362
{
346363
$this->expectException(InvalidConfigurationException::class);
@@ -400,6 +417,7 @@ private function createTokenHandlerFactories(): array
400417
newOidcUserInfoTokenHandlerFactory(),
401418
newOidcTokenHandlerFactory(),
402419
newCasTokenHandlerFactory(),
420+
newOAuth2TokenHandlerFactory(),
403421
];
404422
}
405423
}
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
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add`UserAuthorizationChecker::isGrantedForUser()` to test user authorization without relying on the session.
88
For example, users not currently logged in, or while processing a message from a message queue.
99
* Add`OfflineTokenInterface` to mark tokens that do not represent the currently logged-in user
10+
* Add`OAuth2User` with OAuth2 Access Token Introspection support for`OAuth2TokenHandler`
1011

1112
7.2
1213
---
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(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+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp