diff --git a/.version b/.version index 91ff572..26d99a2 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -5.2.0 +5.2.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 24082bb..cd94f73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [5.2.1](https://github.com/auth0/symfony/tree/5.2.1) (2023-12-16) +[Full Changelog](https://github.com/auth0/symfony/compare/5.2.0...5.2.1) + +**Fixed** +- Restore method signatures [\#174](https://github.com/auth0/symfony/pull/174) ([evansims](https://github.com/evansims)) + ## [5.2.0](https://github.com/auth0/symfony/tree/5.2.0) (2023-12-12) **Added** diff --git a/phpstan.neon.dist b/phpstan.neon.dist index dff6ccd..3beb255 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -19,6 +19,8 @@ parameters: - '#Cannot call method purge\(\) on Auth0\\SDK\\Contract\\StoreInterface\|null.#' - '#Casting to string something that(.*) already string.#' - '#\$object_or_class of function method_exists expects object\|string, (.*) given.#' + - '#Property (.*) is never read, only written.#' + - '#Call to function is_string\(\) with string will always evaluate to true.$#' - message: '#Parameter \#3 \$(.*) of function openssl_verify expects (.*), (.*) given.#' path: src\Token\Verifier.php @@ -48,3 +50,4 @@ parameters: path: src\Utility\HttpRequest.php reportUnmatchedIgnoredErrors: false + checkGenericClassInNonGenericObjectType: false diff --git a/psalm.xml.dist b/psalm.xml.dist index d36109b..6a528d4 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -10,10 +10,6 @@ > - - - - diff --git a/src/Auth0Bundle.php b/src/Auth0Bundle.php index ca603b5..59127b7 100644 --- a/src/Auth0Bundle.php +++ b/src/Auth0Bundle.php @@ -5,11 +5,17 @@ namespace Auth0\Symfony; use Auth0\SDK\Configuration\SdkConfiguration; +use Auth0\SDK\Contract\StoreInterface; use Auth0\SDK\Token; use Auth0\Symfony\Contracts\BundleInterface; use Auth0\Symfony\Controllers\AuthenticationController; use Auth0\Symfony\Security\{Authenticator, Authorizer, UserProvider}; use Auth0\Symfony\Stores\SessionStore; +use OpenSSLAsymmetricKey; +use Psr\Cache\CacheItemPoolInterface; +use Psr\EventDispatcher\ListenerProviderInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\{RequestFactoryInterface, ResponseFactoryInterface, StreamFactoryInterface}; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\{ContainerBuilder, Reference}; @@ -22,56 +28,84 @@ public function configure(DefinitionConfigurator $definition): void $definition->import('../config/definition.php'); } + /** + * @param array $config The configuration array. + * @param ContainerConfigurator $container The container configurator. + * @param ContainerBuilder $builder The container builder. + */ public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { - $tokenCache = $config['sdk']['token_cache'] ?? 'cache.app'; - $tokenCache = new Reference($tokenCache); + $sdkConfig = $config['sdk'] ?? []; - $managementTokenCache = $config['sdk']['management_token_cache'] ?? 'cache.app'; - $managementTokenCache = new Reference($managementTokenCache); + /** + * @var array{strategy: string, domain: ?string, custom_domain: ?string, client_id: ?string, redirect_uri: ?string, client_secret: ?string, audiences: null|array, organizations: null|array, use_pkce: bool, scopes: null|array, response_mode: string, response_type: string, token_algorithm: ?string, token_jwks_uri: ?string, token_max_age: ?int, token_leeway: ?int, token_cache: ?CacheItemPoolInterface, token_cache_ttl: int, http_client: null|ClientInterface|string, http_max_retries: int, http_request_factory: null|RequestFactoryInterface|string, http_response_factory: null|ResponseFactoryInterface|string, http_stream_factory: null|StreamFactoryInterface|string, http_telemetry: bool, session_storage: ?StoreInterface, session_storage_prefix: ?string, cookie_secret: ?string, cookie_domain: ?string, cookie_expires: int, cookie_path: string, cookie_secure: bool, cookie_same_site: ?string, persist_user: bool, persist_id_token: bool, persist_access_token: bool, persist_refresh_token: bool, transient_storage: ?StoreInterface, transient_storage_prefix: ?string, query_user_info: bool, management_token: ?string, management_token_cache: ?CacheItemPoolInterface, event_listener_provider: null|ListenerProviderInterface|string, client_assertion_signing_key: null|OpenSSLAsymmetricKey|string, client_assertion_signing_algorithm: string, pushed_authorization_request: bool, backchannel_logout_cache: ?CacheItemPoolInterface, backchannel_logout_expires: int} $sdkConfig + */ + $tokenCache = $sdkConfig['token_cache'] ?? 'cache.app'; - $backchannelLogoutCache = $config['sdk']['backchannel_logout_cache'] ?? 'cache.app'; - $backchannelLogoutCache = new Reference($backchannelLogoutCache); + if (! $tokenCache instanceof CacheItemPoolInterface) { + $tokenCache = new Reference($tokenCache); + } + + $managementTokenCache = $sdkConfig['management_token_cache'] ?? 'cache.app'; + + if (! $managementTokenCache instanceof CacheItemPoolInterface) { + $managementTokenCache = new Reference($managementTokenCache); + } + + $backchannelLogoutCache = $sdkConfig['backchannel_logout_cache'] ?? 'cache.app'; + + if (! $backchannelLogoutCache instanceof CacheItemPoolInterface) { + $backchannelLogoutCache = new Reference($backchannelLogoutCache); + } + + $transientStorage = $sdkConfig['transient_storage'] ?? 'auth0.store_transient'; + + if (! $transientStorage instanceof StoreInterface) { + $transientStorage = new Reference($transientStorage); + } + + $sessionStorage = $sdkConfig['session_storage'] ?? 'auth0.store_session'; - $transientStorage = new Reference($config['sdk']['transient_storage'] ?? 'auth0.store_transient'); - $sessionStorage = new Reference($config['sdk']['session_storage'] ?? 'auth0.store_session'); + if (! $sessionStorage instanceof StoreInterface) { + $sessionStorage = new Reference($sessionStorage); + } - $transientStoragePrefix = $config['sdk']['transient_storage_prefix'] ?? 'auth0_transient'; - $sessionStoragePrefix = $config['sdk']['session_storage_prefix'] ?? 'auth0_session'; + $transientStoragePrefix = $sdkConfig['transient_storage_prefix'] ?? 'auth0_transient'; + $sessionStoragePrefix = $sdkConfig['session_storage_prefix'] ?? 'auth0_session'; - $eventListenerProvider = $config['sdk']['event_listener_provider'] ?? null; + $eventListenerProvider = $sdkConfig['event_listener_provider'] ?? null; - if (null !== $eventListenerProvider && '' !== $eventListenerProvider) { + if (! $eventListenerProvider instanceof ListenerProviderInterface && '' !== $eventListenerProvider && null !== $eventListenerProvider) { $eventListenerProvider = new Reference($eventListenerProvider); } - $httpClient = $config['sdk']['http_client'] ?? null; + $httpClient = $sdkConfig['http_client'] ?? null; - if (null !== $httpClient && '' !== $httpClient) { + if (! $httpClient instanceof ClientInterface && '' !== $httpClient && null !== $httpClient) { $httpClient = new Reference($httpClient); } - $httpRequestFactory = $config['sdk']['http_request_factory'] ?? null; + $httpRequestFactory = $sdkConfig['http_request_factory'] ?? null; - if (null !== $httpRequestFactory && '' !== $httpRequestFactory) { + if (! $httpRequestFactory instanceof RequestFactoryInterface && '' !== $httpRequestFactory && null !== $httpRequestFactory) { $httpRequestFactory = new Reference($httpRequestFactory); } - $httpResponseFactory = $config['sdk']['http_response_factory'] ?? null; + $httpResponseFactory = $sdkConfig['http_response_factory'] ?? null; - if (null !== $httpResponseFactory && '' !== $httpResponseFactory) { + if (! $httpResponseFactory instanceof ResponseFactoryInterface && '' !== $httpResponseFactory && null !== $httpResponseFactory) { $httpResponseFactory = new Reference($httpResponseFactory); } - $httpStreamFactory = $config['sdk']['http_stream_factory'] ?? null; + $httpStreamFactory = $sdkConfig['http_stream_factory'] ?? null; - if (null !== $httpStreamFactory && '' !== $httpStreamFactory) { + if (! $httpStreamFactory instanceof StreamFactoryInterface && '' !== $httpStreamFactory && null !== $httpStreamFactory) { $httpStreamFactory = new Reference($httpStreamFactory); } - $audiences = $config['sdk']['audiences'] ?? []; - $organizations = $config['sdk']['organizations'] ?? []; - $scopes = $config['sdk']['scopes'] ?? []; + $audiences = $sdkConfig['audiences'] ?? []; + $organizations = $sdkConfig['organizations'] ?? []; + $scopes = $sdkConfig['scopes'] ?? []; if ([] === $audiences) { $audiences = null; @@ -88,38 +122,38 @@ public function loadExtension(array $config, ContainerConfigurator $container, C $container->services() ->set('auth0.configuration', SdkConfiguration::class) ->arg('$configuration', null) - ->arg('$strategy', $config['sdk']['strategy']) - ->arg('$domain', $config['sdk']['domain']) - ->arg('$customDomain', $config['sdk']['custom_domain']) - ->arg('$clientId', $config['sdk']['client_id']) - ->arg('$redirectUri', $config['sdk']['redirect_uri']) - ->arg('$clientSecret', $config['sdk']['client_secret']) + ->arg('$strategy', $sdkConfig['strategy']) + ->arg('$domain', $sdkConfig['domain']) + ->arg('$customDomain', $sdkConfig['custom_domain']) + ->arg('$clientId', $sdkConfig['client_id']) + ->arg('$redirectUri', $sdkConfig['redirect_uri']) + ->arg('$clientSecret', $sdkConfig['client_secret']) ->arg('$audience', $audiences) ->arg('$organization', $organizations) ->arg('$usePkce', true) ->arg('$scope', $scopes) ->arg('$responseMode', 'query') ->arg('$responseType', 'code') - ->arg('$tokenAlgorithm', $config['sdk']['token_algorithm'] ?? Token::ALGO_RS256) - ->arg('$tokenJwksUri', $config['sdk']['token_jwks_uri']) - ->arg('$tokenMaxAge', $config['sdk']['token_max_age']) - ->arg('$tokenLeeway', $config['sdk']['token_leeway'] ?? 60) + ->arg('$tokenAlgorithm', $sdkConfig['token_algorithm'] ?? Token::ALGO_RS256) + ->arg('$tokenJwksUri', $sdkConfig['token_jwks_uri']) + ->arg('$tokenMaxAge', $sdkConfig['token_max_age']) + ->arg('$tokenLeeway', $sdkConfig['token_leeway'] ?? 60) ->arg('$tokenCache', $tokenCache) - ->arg('$tokenCacheTtl', $config['sdk']['token_cache_ttl']) + ->arg('$tokenCacheTtl', $sdkConfig['token_cache_ttl']) ->arg('$httpClient', $httpClient) - ->arg('$httpMaxRetries', $config['sdk']['http_max_retries']) + ->arg('$httpMaxRetries', $sdkConfig['http_max_retries']) ->arg('$httpRequestFactory', $httpRequestFactory) ->arg('$httpResponseFactory', $httpResponseFactory) ->arg('$httpStreamFactory', $httpStreamFactory) - ->arg('$httpTelemetry', $config['sdk']['http_telemetry']) + ->arg('$httpTelemetry', $sdkConfig['http_telemetry']) ->arg('$sessionStorage', $sessionStorage) ->arg('$sessionStorageId', $sessionStoragePrefix) - ->arg('$cookieSecret', $config['sdk']['cookie_secret']) - ->arg('$cookieDomain', $config['sdk']['cookie_domain']) - ->arg('$cookieExpires', $config['sdk']['cookie_expires']) - ->arg('$cookiePath', $config['sdk']['cookie_path']) - ->arg('$cookieSameSite', $config['sdk']['cookie_same_site']) - ->arg('$cookieSecure', $config['sdk']['cookie_secure']) + ->arg('$cookieSecret', $sdkConfig['cookie_secret']) + ->arg('$cookieDomain', $sdkConfig['cookie_domain']) + ->arg('$cookieExpires', $sdkConfig['cookie_expires']) + ->arg('$cookiePath', $sdkConfig['cookie_path']) + ->arg('$cookieSameSite', $sdkConfig['cookie_same_site']) + ->arg('$cookieSecure', $sdkConfig['cookie_secure']) ->arg('$persistUser', true) ->arg('$persistIdToken', true) ->arg('$persistAccessToken', true) @@ -127,11 +161,11 @@ public function loadExtension(array $config, ContainerConfigurator $container, C ->arg('$transientStorage', $transientStorage) ->arg('$transientStorageId', $transientStoragePrefix) ->arg('$queryUserInfo', false) - ->arg('$managementToken', $config['sdk']['management_token']) + ->arg('$managementToken', $sdkConfig['management_token']) ->arg('$managementTokenCache', $managementTokenCache) ->arg('$eventListenerProvider', $eventListenerProvider) ->arg('$backchannelLogoutCache', $backchannelLogoutCache) - ->arg('$backchannelLogoutExpires', $config['sdk']['backchannel_logout_expires']); + ->arg('$backchannelLogoutExpires', $sdkConfig['backchannel_logout_expires']); $container->services() ->set('auth0', Service::class) diff --git a/src/Controllers/AuthenticationController.php b/src/Controllers/AuthenticationController.php index 5db8cc0..e8b85ce 100644 --- a/src/Controllers/AuthenticationController.php +++ b/src/Controllers/AuthenticationController.php @@ -7,19 +7,30 @@ use Auth0\SDK\Auth0; use Auth0\Symfony\Contracts\Controllers\AuthenticationControllerInterface; use Auth0\Symfony\Security\Authenticator; +use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Exception\{BadRequestException, ConflictingHeadersException, SuspiciousOperationException}; use Symfony\Component\HttpFoundation\{RedirectResponse, Request, Response}; use Symfony\Component\Routing\RouterInterface; use Throwable; +use function is_array; +use function is_string; + final class AuthenticationController extends AbstractController implements AuthenticationControllerInterface { public function __construct( private Authenticator $authenticator, private RouterInterface $router, + protected ContainerInterface $container, ) { } + /** + * @psalm-suppress InternalMethod + * + * @param Request $request + */ public function callback(Request $request): Response { $host = $request->getSchemeAndHttpHost(); @@ -31,7 +42,10 @@ public function callback(Request $request): Response $code = $request->get('code'); $state = $request->get('state'); - if (null !== $code && null !== $state) { + $code = is_string($code) ? trim($code) : ''; + $state = is_string($state) ? trim($state) : ''; + + if ('' !== $code && '' !== $state) { $route = $this->getRedirectUrl('success'); try { @@ -50,6 +64,10 @@ public function callback(Request $request): Response } } + /** + * @var string $redirect + */ + return new RedirectResponse($redirect); } @@ -87,9 +105,14 @@ public function logout(Request $request): Response private function getRedirectUrl(string $route): string { $routes = $this->authenticator->configuration['routes'] ?? []; + + if (! is_array($routes)) { + $routes = []; + } + $route = $routes[$route] ?? null; - if (null !== $route && '' !== $route) { + if (is_string($route) && '' !== $route) { try { return $this->router->generate($route); } catch (Throwable) { diff --git a/src/Controllers/BackchannelLogoutController.php b/src/Controllers/BackchannelLogoutController.php index 5b25faf..0d54fca 100644 --- a/src/Controllers/BackchannelLogoutController.php +++ b/src/Controllers/BackchannelLogoutController.php @@ -7,6 +7,7 @@ use Auth0\SDK\Auth0; use Auth0\Symfony\Contracts\Controllers\AuthenticationControllerInterface; use Auth0\Symfony\Security\Authenticator; +use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\{RedirectResponse, Request, Response}; use Throwable; @@ -17,9 +18,15 @@ final class BackchannelLogoutController extends AbstractController implements Au { public function __construct( private Authenticator $authenticator, + protected ContainerInterface $container, ) { } + /** + * @psalm-suppress InternalMethod + * + * @param Request $request + */ public function handle(Request $request): Response { if ('POST' !== $request->getMethod()) { diff --git a/src/Models/Stateful/User.php b/src/Models/Stateful/User.php index 30a1ca7..77439c8 100644 --- a/src/Models/Stateful/User.php +++ b/src/Models/Stateful/User.php @@ -8,5 +8,8 @@ class User extends \Auth0\Symfony\Models\User implements UserInterface { + /** + * @var array + */ protected array $roleAuthenticatedUsing = ['ROLE_USING_SESSION']; } diff --git a/src/Models/Stateless/User.php b/src/Models/Stateless/User.php index 8ffa829..ad29d1b 100644 --- a/src/Models/Stateless/User.php +++ b/src/Models/Stateless/User.php @@ -8,5 +8,8 @@ class User extends \Auth0\Symfony\Models\User implements UserInterface { + /** + * @var array + */ protected array $roleAuthenticatedUsing = ['ROLE_USING_TOKEN']; } diff --git a/src/Models/User.php b/src/Models/User.php index 6e7815c..920d631 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -10,14 +10,26 @@ use Symfony\Component\Security\Core\User\UserInterface as SymfonyUserInterface; use function array_key_exists; +use function is_array; +use function is_bool; +use function is_int; use function is_string; class User implements SymfonyUserInterface, UserInterface { + /** + * @var array + */ protected array $roleAuthenticatedUsing = []; + /** + * @var array + */ protected array $roles = ['IS_AUTHENTICATED_FULLY', 'ROLE_USER']; + /** + * @param array $data + */ public function __construct(protected array $data) { } @@ -26,9 +38,15 @@ public function eraseCredentials(): void { } + /** + * @param string $name + * @param mixed $default + * + * @return mixed + */ public function getAppMetadata(string $name, $default = null) { - if (! isset($this->data['app_metadata'])) { + if (! isset($this->data['app_metadata']) || ! is_array($this->data['app_metadata'])) { return $default; } @@ -41,12 +59,24 @@ public function getAppMetadata(string $name, $default = null) public function getClientId(): ?string { - return $this->data['clientID'] ?? null; + $clientId = $this->data['clientID'] ?? null; + + if (! is_string($clientId)) { + return null; + } + + return $clientId; } public function getCreatedAt(): ?DateTimeInterface { - return isset($this->data['created_at']) ? new DateTimeImmutable($this->data['created_at']) : null; + $createdAt = $this->data['created_at'] ?? null; + + if (! is_string($createdAt)) { + return null; + } + + return new DateTimeImmutable($createdAt); } public function getCustomData(string $key): mixed @@ -56,74 +86,166 @@ public function getCustomData(string $key): mixed public function getEmail(): ?string { - return $this->data['email'] ?? null; + $email = $this->data['email'] ?? null; + + if (! is_string($email)) { + return null; + } + + return $email; } public function getFamilyName(): ?string { - return $this->data['family_name'] ?? null; + $familyName = $this->data['family_name'] ?? null; + + if (! is_string($familyName)) { + return null; + } + + return $familyName; } public function getGivenName(): ?string { - return $this->data['given_name'] ?? null; + $givenName = $this->data['given_name'] ?? null; + + if (! is_string($givenName)) { + return null; + } + + return $givenName; } public function getId(): ?string { - return $this->data['user_id'] ?? null; + $id = $this->data['user_id'] ?? null; + + if (! is_string($id)) { + return null; + } + + return $id; } + /** + * @return array + */ public function getIdentities(): array { - return $this->data['identities'] ?? []; + $identities = $this->data['identities'] ?? []; + + if (! is_array($identities)) { + return []; + } + + return $identities; } public function getLastIp(): ?string { - return $this->data['last_ip'] ?? null; + $lastIp = $this->data['last_ip'] ?? null; + + if (! is_string($lastIp)) { + return null; + } + + return $lastIp; } public function getLastLoginAt(): ?DateTimeInterface { - return isset($this->data['last_login']) ? new DateTimeImmutable($this->data['last_login']) : null; + $lastLoginAt = $this->data['last_login'] ?? null; + + if (! is_string($lastLoginAt)) { + return null; + } + + return new DateTimeImmutable($lastLoginAt); } public function getLastPasswordResetAt(): ?DateTimeInterface { - return isset($this->data['last_password_reset']) ? new DateTimeImmutable($this->data['last_password_reset']) : null; + $lastPasswordResetAt = $this->data['last_password_reset']; + + if (! is_string($lastPasswordResetAt)) { + return null; + } + + return new DateTimeImmutable($lastPasswordResetAt); } public function getLoginsCount(): int { - return $this->data['logins_count'] ?? 0; + $loginsCount = $this->data['logins_count'] ?? 0; + + if (! is_int($loginsCount)) { + return 0; + } + + return $loginsCount; } public function getMultifactor(): ?string { - return $this->data['multifactor'] ?? null; + $multifactor = $this->data['multifactor'] ?? null; + + if (! is_string($multifactor)) { + return null; + } + + return $multifactor; } public function getName(): ?string { - return $this->data['name'] ?? $this->data['nickname'] ?? null; + $name = $this->data['name'] ?? $this->data['nickname'] ?? null; + + if (! is_string($name)) { + return null; + } + + return $name; } public function getNickname(): ?string { - return $this->data['nickname'] ?? null; + $nickname = $this->data['nickname'] ?? null; + + if (! is_string($nickname)) { + return null; + } + + return $nickname; } public function getPhoneNumber(): ?string { - return $this->data['phone_number'] ?? null; + $phoneNumber = $this->data['phone_number'] ?? null; + + if (! is_string($phoneNumber)) { + return null; + } + + return $phoneNumber; } public function getPicture(): ?string { - return $this->data['picture'] ?? null; + $picture = $this->data['picture'] ?? null; + + if (! is_string($picture)) { + return null; + } + + return $picture; } + /** + * @return array + * + * @psalm-suppress RedundantFunctionCall + */ public function getRoles(): array { $response = []; @@ -139,32 +261,60 @@ public function getRoles(): array $response[] = implode('_', explode(':', strtoupper($role))); } - foreach ($permissions as $permission) { - $response[] = 'ROLE_' . implode('_', explode(':', strtoupper($permission))); + if (is_array($permissions)) { + foreach ($permissions as $permission) { + if (is_string($permission)) { + $response[] = 'ROLE_' . implode('_', explode(':', strtoupper($permission))); + } + } } - foreach ($scopes as $scope) { - $response[] = 'ROLE_' . implode('_', explode(':', strtoupper($scope))); + if (is_array($scopes)) { + foreach ($scopes as $scope) { + if (is_string($scope)) { + $response[] = 'ROLE_' . implode('_', explode(':', strtoupper($scope))); + } + } } - $response[] = $this->roleAuthenticatedUsing; + foreach ($this->roleAuthenticatedUsing as $using) { + $response[] = $using; + } return array_unique(array_values($response)); } public function getUpdatedAt(): ?DateTimeImmutable { - return isset($this->data['updated_at']) ? new DateTimeImmutable($this->data['updated_at']) : null; + $updatedAt = $this->data['updated_at'] ?? null; + + if (! is_string($updatedAt)) { + return null; + } + + return new DateTimeImmutable($updatedAt); } public function getUserIdentifier(): string { - return $this->getId() ?? $this->data['sub']; + $userIdentifier = $this->getId() ?? $this->data['sub']; + + if (! is_string($userIdentifier)) { + return ''; + } + + return $userIdentifier; } + /** + * @param string $name + * @param mixed $default + * + * @return mixed + */ public function getUserMetadata(string $name, $default = null) { - if (! isset($this->data['user_metadata'])) { + if (! isset($this->data['user_metadata']) || ! is_array($this->data['user_metadata'])) { return $default; } @@ -177,16 +327,28 @@ public function getUserMetadata(string $name, $default = null) public function getUsername(): ?string { - return $this->data['username'] ?? null; + $username = $this->data['username'] ?? null; + + if (! is_string($username)) { + return null; + } + + return $username; } public function isBlocked(): ?bool { - return $this->data['blocked'] ?? null; + $blocked = $this->data['blocked'] ?? null; + + if (! is_bool($blocked)) { + return null; + } + + return $blocked; } public function isEmailVerified(): bool { - return filter_var($this->data['email_verified'], FILTER_VALIDATE_BOOLEAN) ?? false; + return filter_var($this->data['email_verified'], FILTER_VALIDATE_BOOLEAN); } } diff --git a/src/Security/Authenticator.php b/src/Security/Authenticator.php index f2c65c5..8768660 100644 --- a/src/Security/Authenticator.php +++ b/src/Security/Authenticator.php @@ -16,8 +16,17 @@ use Symfony\Component\Security\Http\Authenticator\Passport\{Passport, SelfValidatingPassport}; use Throwable; +use function is_array; +use function is_string; + final class Authenticator extends AbstractAuthenticator implements AuthenticatorInterface { + /** + * @param array $configuration + * @param Service $service + * @param RouterInterface $router + * @param LoggerInterface $logger + */ public function __construct( public array $configuration, public Service $service, @@ -34,7 +43,7 @@ public function authenticate(Request $request): Passport throw new CustomUserMessageAuthenticationException('No Auth0 session was found.'); } - $user = json_encode(['type' => 'stateful', 'data' => $session]); + $user = json_encode(['type' => 'stateful', 'data' => $session], JSON_THROW_ON_ERROR); return new SelfValidatingPassport(new UserBadge($user)); } @@ -45,9 +54,15 @@ public function onAuthenticationFailure(Request $request, SymfonyAuthenticationE $request->getSession()->set('auth0:callback_redirect', $request->getUri()); } - $route = $this->configuration['routes']['login'] ?? null; + $routes = $this->configuration['routes'] ?? []; + + if (! is_array($routes)) { + $routes = []; + } + + $route = $routes['login'] ?? null; - if (null !== $route && '' !== $route) { + if (is_string($route) && '' !== $route) { try { return new RedirectResponse($this->router->generate($route)); } catch (Throwable) { diff --git a/src/Security/Authorizer.php b/src/Security/Authorizer.php index 704487c..e276cd5 100644 --- a/src/Security/Authorizer.php +++ b/src/Security/Authorizer.php @@ -14,8 +14,15 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\{Passport, SelfValidatingPassport}; +use function is_string; + final class Authorizer extends AbstractAuthenticator implements AuthorizerInterface { + /** + * @param array $configuration + * @param Service $service + * @param LoggerInterface $logger + */ public function __construct( private array $configuration, private Service $service, @@ -23,16 +30,21 @@ public function __construct( ) { } + /** + * @psalm-suppress InternalMethod + * + * @param Request $request + */ public function authenticate(Request $request): Passport { // Extract any available value from the authorization header $param = $request->get('token', null); - $header = trim($request->headers->get('Authorization', '')); + $header = trim($request->headers->get('Authorization', '') ?? ''); $token = $param ?? $header; $usingHeader = null === $param; // Ensure the 'authorization' header is present in the request - if ('' === $token) { + if (! is_string($token) || '' === $token) { throw new AuthenticationException('`Authorization` header not present.'); } @@ -47,14 +59,17 @@ public function authenticate(Request $request): Passport // Decode, validate and verify token. $token = $this->getService()->getSdk()->decode( token: $token, - tokenType: \Auth0\SDK\Token::TYPE_TOKEN, + tokenType: \Auth0\SDK\Token::TYPE_ACCESS_TOKEN, ); - $user = json_encode(['type' => 'stateless', 'data' => ['user' => $token->toArray()]]); + $user = json_encode(['type' => 'stateless', 'data' => ['user' => $token->toArray()]], JSON_THROW_ON_ERROR); return new SelfValidatingPassport(new UserBadge($user)); } + /** + * @return array + */ public function getConfiguration(): array { return $this->configuration; @@ -85,6 +100,11 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token, return null; } + /** + * @psalm-suppress InternalMethod + * + * @param Request $request + */ public function supports(Request $request): ?bool { if (null !== $request->get('token')) { diff --git a/src/Security/UserProvider.php b/src/Security/UserProvider.php index 5897413..d4bf4c5 100644 --- a/src/Security/UserProvider.php +++ b/src/Security/UserProvider.php @@ -15,6 +15,11 @@ use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\User\{UserInterface as SymfonyUserInterface, UserProviderInterface as SymfonyUserProviderInterface}; +use function is_array; + +/** + * @template-implements SymfonyUserProviderInterface + */ final class UserProvider implements SymfonyUserProviderInterface, UserProviderInterface { public function __construct( @@ -29,13 +34,21 @@ public function loadByUserModel(User $user): SymfonyUserInterface public function loadUserByIdentifier(string $identifier): SymfonyUserInterface { - $identifier = json_decode($identifier, true); + $identifier = json_decode($identifier, true, 512, JSON_THROW_ON_ERROR); + + if (! is_array($identifier)) { + throw new UnsupportedUserException(); + } + + $type = $identifier['type'] ?? null; + $data = $identifier['data'] ?? []; + $user = $data['user'] ?? null; - if ('stateful' === $identifier['type']) { - return new StatefulUser($identifier['data']['user']); + if ('stateful' === $type) { + return new StatefulUser($user); } - return new StatelessUser($identifier['data']['user']); + return new StatelessUser($user); } public function refreshUser(SymfonyUserInterface $user): SymfonyUserInterface @@ -47,6 +60,9 @@ public function refreshUser(SymfonyUserInterface $user): SymfonyUserInterface return $user; } + /** + * @param string|UserInterface $class + */ public function supportsClass($class): bool { return $class instanceof UserInterface || is_subclass_of($class, UserInterface::class); diff --git a/src/Service.php b/src/Service.php index 76c70d4..f274309 100644 --- a/src/Service.php +++ b/src/Service.php @@ -14,7 +14,7 @@ final class Service implements ServiceInterface { - public const VERSION = '5.2.0'; + public const VERSION = '5.2.1'; private ?Auth0 $sdk = null; @@ -25,7 +25,7 @@ public function __construct( ) { } - public function getSdk(): ?Auth0 + public function getSdk(): Auth0 { if (! $this->sdk instanceof \Auth0\SDK\Auth0) { $this->warmUp(); diff --git a/src/Stores/SessionStore.php b/src/Stores/SessionStore.php index 734ee39..506cbb4 100644 --- a/src/Stores/SessionStore.php +++ b/src/Stores/SessionStore.php @@ -5,18 +5,37 @@ namespace Auth0\Symfony\Stores; use Auth0\SDK\Contract\StoreInterface; +use InvalidArgumentException; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\{Request, RequestStack}; use Throwable; +use function gettype; +use function is_array; +use function is_string; + final class SessionStore implements StoreInterface { + /** + * @param string $namespace + * @param RequestStack $requestStack + * @param LoggerInterface $logger + * + * @psalm-suppress DocblockTypeContradiction + */ public function __construct( private $namespace, private RequestStack $requestStack, private LoggerInterface $logger, ) { + if (! is_string($namespace)) { + $this->logger->error('SessionStore: $namespace must be a string, {type} given.', [ + 'type' => gettype($namespace), + ]); + + throw new InvalidArgumentException('$namespace must be a string.'); + } } /** @@ -41,7 +60,7 @@ public function delete( ): void { $manifest = $this->session()?->get($this->namespace, []); - if ([] === $manifest) { + if (! is_array($manifest) || [] === $manifest) { return; } @@ -73,7 +92,7 @@ public function get( ) { $manifest = $this->session()?->get($this->namespace, []); - if ([] === $manifest || ! isset($manifest[$key])) { + if (! is_array($manifest) || [] === $manifest || ! isset($manifest[$key])) { return $default; } @@ -100,6 +119,10 @@ public function set( ): void { $manifest = $this->session()?->get($this->namespace, []); + if (! is_array($manifest)) { + $manifest = []; + } + $manifest[$key] = $value; $this->session()?->set($this->namespace, $manifest); @@ -115,13 +138,17 @@ private function session( $request ??= $this->requestStack->getCurrentRequest(); $session = null; + if (! $request instanceof Request) { + return null; + } + try { $session = $request->getSession(); } catch (Throwable) { } if ($session instanceof \Symfony\Component\HttpFoundation\Session\SessionInterface) { - if ($session instanceof \Symfony\Component\HttpFoundation\Session\SessionInterface && ! $session->isStarted()) { + if (! $session->isStarted()) { $session->start(); }