Skip to content

Commit

Permalink
Add passport credentials and authenticator
Browse files Browse the repository at this point in the history
  • Loading branch information
iisisrael committed Oct 27, 2021
1 parent 1b6c861 commit 7c5a57c
Show file tree
Hide file tree
Showing 7 changed files with 694 additions and 13 deletions.
10 changes: 5 additions & 5 deletions DependencyInjection/Security/Factory/OAuthFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ class OAuthFactory implements AuthenticatorFactoryInterface, SecurityFactoryInte
*/
public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId)
{
$providerId = 'fos_oauth_server.security.authentication.provider.'.$id;
$providerId = 'fos_oauth_server.security.authentication.authenticator.'.$id;
$container
->setDefinition($providerId, new ChildDefinition('fos_oauth_server.security.authentication.provider'))
->replaceArgument(0, new Reference($userProviderId))
->replaceArgument(1, new Reference('security.user_checker.'.$id))
->replaceArgument(2, $id)
->setDefinition($providerId, new ChildDefinition('fos_oauth_server.security.authentication.authenticator'))
->replaceArgument(0, new Reference('fos_oauth_server.server'))
->replaceArgument(1, new Reference('security.token_storage'))
->replaceArgument(2, new Reference('security.user_checker.'.$id))
;

return $providerId;
Expand Down
8 changes: 8 additions & 0 deletions Resources/config/security.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<parameters>
<parameter key="fos_oauth_server.security.authentication.authenticator.class">FOS\OAuthServerBundle\Security\Authentication\Authenticator\OAuthAuthenticator</parameter>
<parameter key="fos_oauth_server.security.authentication.provider.class">FOS\OAuthServerBundle\Security\Authentication\Provider\OAuthProvider</parameter>
<parameter key="fos_oauth_server.security.authentication.listener.class">FOS\OAuthServerBundle\Security\Firewall\OAuthListener</parameter>
<parameter key="fos_oauth_server.security.entry_point.class">FOS\OAuthServerBundle\Security\EntryPoint\OAuthEntryPoint</parameter>
</parameters>

<services>
<service id="fos_oauth_server.security.authentication.authenticator" class="%fos_oauth_server.security.authentication.authenticator.class%" public="false">
<argument type="service" id="fos_oauth_server.server" />
<argument type="service" id="security.token_storage"/>
<argument type="service" id="security.user_checker" />
<argument /> <!-- user provider -->
</service>

<service id="fos_oauth_server.security.authentication.provider" class="%fos_oauth_server.security.authentication.provider.class%" public="false">
<argument /> <!-- user provider -->
<argument type="service" id="fos_oauth_server.server" />
Expand Down
199 changes: 199 additions & 0 deletions Security/Authentication/Authenticator/OAuthAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<?php

declare(strict_types=1);

/*
* This file is part of the FOSOAuthServerBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\OAuthServerBundle\Security\Authentication\Authenticator;

use FOS\OAuthServerBundle\Security\Authentication\Passport\OAuthCredentials;
use FOS\OAuthServerBundle\Security\Authentication\Token\OAuthToken;
use OAuth2\OAuth2;
use OAuth2\OAuth2AuthenticateException;
use OAuth2\OAuth2ServerException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;

/**
* OAuthAuthenticator class.
*
* @author Israel J. Carberry <[email protected]>
*/
class OAuthAuthenticator implements AuthenticatorInterface
{
/**
* @var OAuth2
*/
protected $serverService;

/**
* @var TokenStorageInterface
*/
private $tokenStorage;

/**
* @var UserCheckerInterface
*/
protected $userChecker;

/**
* @var UserProviderInterface
*/
protected $userProvider;

public function __construct(
OAuth2 $serverService,
TokenStorageInterface $tokenStorage,
UserCheckerInterface $userChecker,
UserProviderInterface $userProvider
) {
$this->serverService = $serverService;
$this->tokenStorage = $tokenStorage;
$this->userChecker = $userChecker;
$this->userProvider = $userProvider;
}

/**
* {@inheritdoc}
*/
public function authenticate(Request $request): UserPassportInterface
{
try {
$token = $this->tokenStorage->getToken();
$tokenString = $token->getToken();

$accessToken = $this->serverService->verifyAccessToken($tokenString);

/** @var \Symfony\Component\Security\Core\User\UserInterface **/
$user = $accessToken->getUser();

if (null === $user) {
throw new AuthenticationException('OAuth2 authentication failed');
}

// check the user
try {
$this->userChecker->checkPreAuth($user);
} catch (AccountStatusException $e) {
throw new OAuth2AuthenticateException(
Response::HTTP_UNAUTHORIZED,
OAuth2::TOKEN_TYPE_BEARER,
$this->serverService->getVariable(OAuth2::CONFIG_WWW_REALM),
'access_denied',
$e->getMessage()
);
}

return new Passport(
new UserBadge($user->getUsername()),
new OAuthCredentials($tokenString, $accessToken->getScope())
);
} catch (OAuth2ServerException $e) {
throw new AuthenticationException('OAuth2 authentication failed', 0, $e);
}

// this should never be reached
throw new AuthenticationException('OAuth2 authentication failed');
}

/**
* {@inheritdoc}
*/
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
try {
// expect the badges in the passport from authenticate method above
if (!$passport->hasBadge(OAuthCredentials::class)
|| !$passport->hasBadge(UserBadge::class)
) {
throw new OAuth2AuthenticateException(
Response::HTTP_UNAUTHORIZED,
OAuth2::TOKEN_TYPE_BEARER,
$this->serverService->getVariable(OAuth2::CONFIG_WWW_REALM),
'access_denied',
'Unexpected credentials type.'
);
}

// get the passport badges
$credentials = $passport->getBadge(OAuthCredentials::class);
$user = $this->userProvider->loadUserByUsername(
$passport->getBadge(UserBadge::class)->getUserIdentifier()
);

// check the user
try {
$this->userChecker->checkPostAuth($user);
} catch (AccountStatusException $e) {
throw new OAuth2AuthenticateException(
Response::HTTP_UNAUTHORIZED,
OAuth2::TOKEN_TYPE_BEARER,
$this->serverService->getVariable(OAuth2::CONFIG_WWW_REALM),
'access_denied',
$e->getMessage()
);
}
} catch (OAuth2ServerException $e) {
throw new AuthenticationException('OAuth2 authentication failed', 0, $e);
}

$token = new OAuthToken($credentials->getRoles($user));
$token->setAuthenticated(true);
$token->setToken($credentials->getTokenString());
$token->setUser($user);

$credentials->markResolved();

return $token;
}

/**
* {@inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}

/**
* {@inheritdoc}
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$responseException = new OAuth2AuthenticateException(
Response::HTTP_UNAUTHORIZED,
OAuth2::TOKEN_TYPE_BEARER,
$this->serverService->getVariable(OAuth2::CONFIG_WWW_REALM),
'access_denied',
$exception->getMessage()
);

return $responseException->getHttpResponse();
}

/**
* {@inheritdoc}
*/
public function supports(Request $request): ?bool
{
return $this->tokenStorage->getToken() instanceof OAuthToken;
}
}
80 changes: 80 additions & 0 deletions Security/Authentication/Passport/OAuthCredentials.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types=1);

/*
* This file is part of the FOSOAuthServerBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\OAuthServerBundle\Security\Authentication\Passport;

use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CredentialsInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
* Implements credentials checking for an OAuth token.
*
* @author Israel J. Carberry <[email protected]>
*
* @final
*/
class OAuthCredentials implements CredentialsInterface
{
/**
* @var bool
*/
private $resolved = false;

/**
* @var string
*/
private $scope;

/**
* @var string
*/
private $tokenString;

public function __construct(string $tokenString, string $scope)
{
$this->tokenString = $tokenString;
$this->scope = $scope;
}

public function getRoles(UserInterface $user): array
{
$roles = $user->getRoles();

if (empty($this->scope)) {
return $roles;
}

foreach (explode(' ', $this->scope) as $role) {
$roles[] = 'ROLE_'.mb_strtoupper($role);
}

return array_unique($roles, SORT_REGULAR);
}

public function getTokenString(): ?string
{
return $this->tokenString;
}

public function markResolved(): void
{
$this->resolved = true;
$this->scope = null;
$this->tokenString = null;
}

public function isResolved(): bool
{
return $this->resolved;
}
}
15 changes: 7 additions & 8 deletions Tests/DependencyInjection/Security/Factory/OAuthFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ public function testCreateAuthenticator(): void
;
$id = '12';
$config = [];
$userProvider = 'mock.user.provider.service';

$definition = $this->getMockBuilder(Definition::class)
->disableOriginalConstructor()
Expand All @@ -135,8 +134,8 @@ public function testCreateAuthenticator(): void
->expects($this->once())
->method('setDefinition')
->with(
'fos_oauth_server.security.authentication.provider.'.$id,
new ChildDefinition('fos_oauth_server.security.authentication.provider')
'fos_oauth_server.security.authentication.authenticator.'.$id,
new ChildDefinition('fos_oauth_server.security.authentication.authenticator')
)
->will($this->returnValue($definition))
;
Expand All @@ -147,15 +146,15 @@ public function testCreateAuthenticator(): void
->withConsecutive(
[
0,
new Reference($userProvider),
new Reference('fos_oauth_server.server'),
],
[
1,
new Reference('security.user_checker.'.$id),
new Reference('security.token_storage'),
],
[
2,
$id,
new Reference('security.user_checker.'.$id),
]
)
->willReturnOnConsecutiveCalls(
Expand All @@ -166,8 +165,8 @@ public function testCreateAuthenticator(): void
;

$this->assertSame(
'fos_oauth_server.security.authentication.provider.'.$id,
$this->instance->createAuthenticator($container, $id, $config, $userProvider)
'fos_oauth_server.security.authentication.authenticator.'.$id,
$this->instance->createAuthenticator($container, $id, $config, 'ignored.user.provider.service')
);
}

Expand Down
Loading

0 comments on commit 7c5a57c

Please sign in to comment.