-
-
Notifications
You must be signed in to change notification settings - Fork 177
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: use OIDC server with fine-grained authorization
- Loading branch information
1 parent
8122ec6
commit d4d0104
Showing
19 changed files
with
526 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?php | ||
|
||
namespace App\Security\Voter; | ||
|
||
use ApiPlatform\Metadata\IriConverterInterface; | ||
use Symfony\Component\DependencyInjection\Attribute\Autowire; | ||
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||
use Symfony\Component\Security\Core\Authorization\Voter\Voter; | ||
use Symfony\Component\Security\Http\AccessToken\AccessTokenExtractorInterface; | ||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; | ||
use Symfony\Contracts\HttpClient\HttpClientInterface; | ||
|
||
/** | ||
* Check user permissions. | ||
* | ||
* @see https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_obtaining_permissions | ||
*/ | ||
final class OIDCPermissionVoter extends Voter | ||
{ | ||
use OIDCVoterTrait; | ||
|
||
public function __construct( | ||
#[Autowire('%env(OIDC_API_CLIENT_ID)%')] | ||
private readonly string $oidcClientId, | ||
private readonly HttpClientInterface $securityAuthorizationClient, | ||
private readonly IriConverterInterface $iriConverter, | ||
private readonly RequestStack $requestStack, | ||
#[Autowire('@security.access_token_extractor.header')] | ||
private readonly AccessTokenExtractorInterface $accessTokenExtractor, | ||
) {} | ||
|
||
protected function supports(string $attribute, mixed $subject): bool | ||
{ | ||
return !empty($subject); | ||
} | ||
|
||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool | ||
{ | ||
$accessToken = $this->getToken($token); | ||
if (!$accessToken) { | ||
return false; | ||
} | ||
|
||
if (is_object($subject)) { | ||
$subject = $this->iriConverter->getIriFromResource($subject); | ||
} | ||
|
||
if (!is_string($subject)) { | ||
throw new \InvalidArgumentException(sprintf('Invalid subject type, expected "string" or "object", got "%s".', get_debug_type($subject))); | ||
} | ||
|
||
try { | ||
$response = $this->securityAuthorizationClient->request('POST', 'protocol/openid-connect/token', [ | ||
'auth_bearer' => $accessToken, | ||
'body' => [ | ||
'grant_type' => 'urn:ietf:params:oauth:grant-type:uma-ticket', | ||
'audience' => $this->oidcClientId, | ||
'response_mode' => 'decision', | ||
'permission_resource_format' => 'uri', | ||
'permission_resource_matching_uri' => true, | ||
'permission' => sprintf('%s', $subject), | ||
], | ||
]); | ||
|
||
return $response->toArray()['result'] ?? false; | ||
} catch (ExceptionInterface) { | ||
return false; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<?php | ||
|
||
namespace App\Security\Voter; | ||
|
||
use Symfony\Component\DependencyInjection\Attribute\Autowire; | ||
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||
use Symfony\Component\Security\Core\Authorization\Voter\Voter; | ||
use Symfony\Component\Security\Http\AccessToken\AccessTokenExtractorInterface; | ||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; | ||
use Symfony\Contracts\HttpClient\HttpClientInterface; | ||
|
||
/** | ||
* Check user roles. | ||
* | ||
* @see https://www.keycloak.org/docs/latest/authorization_services/index.html#obtaining-information-about-an-rpt | ||
*/ | ||
final class OIDCRoleVoter extends Voter | ||
{ | ||
use OIDCVoterTrait; | ||
|
||
public function __construct( | ||
#[Autowire('%env(OIDC_API_CLIENT_ID)%')] | ||
private readonly string $oidcClientId, | ||
#[Autowire('%env(OIDC_API_CLIENT_SECRET)%')] | ||
private readonly string $oidcClientSecret, | ||
private readonly HttpClientInterface $securityAuthorizationClient, | ||
private readonly RequestStack $requestStack, | ||
#[Autowire('@security.access_token_extractor.header')] | ||
private readonly AccessTokenExtractorInterface $accessTokenExtractor, | ||
) {} | ||
|
||
protected function supports(string $attribute, mixed $subject): bool | ||
{ | ||
return empty($subject); | ||
} | ||
|
||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool | ||
{ | ||
$accessToken = $this->getToken($token); | ||
if (!$accessToken) { | ||
return false; | ||
} | ||
|
||
if (!empty($subject)) { | ||
throw new \InvalidArgumentException(sprintf('Invalid subject type, expected empty string or "null", got "%s".', get_debug_type($subject))); | ||
} | ||
|
||
try { | ||
$response = $this->securityAuthorizationClient->request('POST', 'protocol/openid-connect/token/introspect', [ | ||
'body' => [ | ||
'client_id' => $this->oidcClientId, | ||
'client_secret' => $this->oidcClientSecret, | ||
'token' => $accessToken, | ||
], | ||
]); | ||
|
||
$roles = array_map(static fn (string $role): string => strtolower($role), $response->toArray()['realm_access']['roles'] ?? []); | ||
|
||
return in_array(strtolower($attribute), $roles, true); | ||
} catch (ExceptionInterface) { | ||
return false; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
namespace App\Security\Voter; | ||
|
||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||
use Symfony\Component\Security\Core\Exception\BadCredentialsException; | ||
use Symfony\Component\Security\Core\User\UserInterface; | ||
|
||
trait OIDCVoterTrait | ||
{ | ||
/** | ||
* @throws BadCredentialsException | ||
*/ | ||
private function getToken(TokenInterface $token): bool|string | ||
{ | ||
// ensure user is authenticated | ||
if (!$token->getUser() instanceof UserInterface) { | ||
return false; | ||
} | ||
|
||
$request = $this->requestStack->getCurrentRequest(); | ||
|
||
// user is authenticated, its token should be valid (validated through AccessTokenAuthenticator) | ||
// todo is there a better way to retrieve the access-token? | ||
$accessToken = $this->accessTokenExtractor->extractAccessToken($request); | ||
if (!$accessToken) { | ||
return false; | ||
} | ||
|
||
return $accessToken; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.