The start of something beautiful

This commit is contained in:
2024-09-11 22:48:07 -06:00
parent 45acea47f3
commit f5997ee5ec
5614 changed files with 630696 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
<?php
$header = <<<EOF
This file is part of the HWIOAuthBundle package.
(c) Hardware Info <opensource@hardware.info>
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
EOF;
return (new PhpCsFixer\Config())
->setRules(array(
'@Symfony' => true,
'@Symfony:risky' => true,
'@PHP71Migration' => true,
'@PHPUnit60Migration:risky' => true,
'combine_consecutive_issets' => true,
'combine_consecutive_unsets' => true,
'heredoc_to_nowdoc' => false,
'header_comment' => ['header' => $header],
'no_unreachable_default_argument_value' => false,
'ordered_class_elements' => true,
'ordered_imports' => true,
'php_unit_method_casing' => ['case' => 'camel_case'],
'php_unit_set_up_tear_down_visibility' => true,
'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'namespaced'],
'array_syntax' => ['syntax' => 'short'],
'no_superfluous_phpdoc_tags' => [
'allow_mixed' => true,
'allow_unused_params' => false,
],
'phpdoc_types_order' => false,
))
->setRiskyAllowed(true)
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__.'/src')
->in(__DIR__.'/tests')
)
;
+531
View File
@@ -0,0 +1,531 @@
Changelog
=========
## 2.2.0 (2024-02-28)
* BC Break: Dropped support for PHP 7.4 & 8.0,
* Added: Telegram resource owner,
* Bugfix: Allow `use_authorization_to_get_token` to be configured to `false` for generic OAuth2,
* Bugfix: Update API version for Facebook to latest available
* Bugfix: Replace custom authenticator passport with custom badge usage,
* Bugfix: Fix registration of failure handler,
* Bugfix: Don't miss refresh token in registration controller,
* Bugfix: Allow `null` as `$registrationForm` in `RegisterController`,
* Bugfix: Fix connect functionality with authentication managers,
## 2.1.0 (2023-11-30)
* BC Break: Dropped support for Symfony: `>6.0, <6.3`,
* Added: New Passage resource owner,
* Bugfix: Remove deprecations reported by Symfony 6.4,
* Chore: Added support for Symfony 7,
## 2.0.0 (2023-10-01)
* Bugfix: Prevent refreshing non-expired tokens
* Bugfix: Remove deprecations reported by Symfony 6.x
* Bugfix: Prevent fatal error when token doesn't have resource owner name set
## 2.0.0-BETA3 (2023-08-20)
* BC Break: Dropped support for Symfony: 6.0.*,
* BC Break: Class `Templating\Helper\OAuthHelper` was merged into `Twig\Extension\OAuthRuntime`,
* BC Break: When resource owner class doesn't define `TYPE` constant or is `null`, then key will be calculated by converting its class name without `ResourceOwner` suffix to `snake_case`, if neither is felt, then `\LogicException` will be thrown,
* Deprecated: method `UserResponseInterface::getUsername()` was deprecated in favour of `UserResponseInterface::getUserIdentifier()` to match changes in Symfony Security component,
* Enhancement: `@internal` resourceOwner oauth types in Configuration are calculated automatically by scandir. All classes extended from `GenericOAuth[X]ResourceOwner` get `oauth[X]` type. If class only implements ResourceOwnerInterface then its oauth type is `unknown`. ResourceOwner key (parameter `type` in configs) should have defined ResourceOwner::TYPE constant. Each user defined custom ResourceOwner class that implemented `ResourceOwnerInterface` will be registered automatically. If `autoconfigure` option is disabled user have to add the tag `hwi_oauth.resource_owner` to the service definition,
* Enhancement: Class `ConnectController` was split into two smaller ones, `Connect\ConnectController` & `Connect\RegisterController`,
* Bugfix: Added `OAuth1ResourceOwner` & `OAuth2ResourceOwner` to cover case of implementing custom oauth resource owners,
* Bugfix: Fixed Authorization Header in `CleverResourceOwner::doGetRequest`,
* Bugfix: Catch also the `TransportExceptionInterface` in `AbstractResourceOwner::getResponseContent()` method,
* Bugfix: Current matched Firewall is respected during generation of resource owner check path links,
* Bugfix: Prevent fatal error in `OAuthUserProvider::loadUserByOAuthUserResponse()` when `nickname` is not available in OAuth response,
* Bugfix: Use newer version of `firebase/php-jwt` library,
* Chore: Removed not used Symfony Templating component
## 2.0.0-BETA2 (2022-01-16)
* Deprecated: configuration parameter `firewall_names`, firewalls are now computed automatically - all firewalls that have defined `oauth` authenticator/provider will be collected,
* Added: Ability to automatically refresh expired access tokens (only for derived from `GenericOAuth2ResourceOwner` resource owners), if option `refresh_on_expire` set to `true`,
* Enhancement: Refresh token listener is disabled by default and will only be enabled if at least one resource owner has option `refresh_on_expure` set to `true`,
* Enhancement: (`@internal`) Removed/replaced redundant argument `$firewallNames` from controllers. If controller class was copied and replaced, adapt list of arguments: In controller use `$resourceOwnerMapLocator->getFirewallNames()`,
* Bugfix: `RefreshTokenListener` cannot be lazy. If current firewall is lazy (or anonymous: lazy) then current auth token is often initializing on `kernel.response`. In this case new access token will not be stored in session. Therefore, the expired token will be refreshed on each request,
* Bugfix: `InteractiveLoginEvent` will be triggered also for `OAuthAuthenticator`,
* Maintain: Changed config files from `*.xml` to `*.php` (services and routes). Xml routing configs `connect.xml`, `login.xml` and `redirect.xml` are steel present but deprecated. Please use `*.php` variants in your includes instead.
## 2.0.0-BETA1 (2021-12-10)
* BC Break: Dropped PHP 7.3 support,
* BC Break: Dropped support for Symfony: >=5.1 & <5.4,
* BC Break: `OAuthExtension` is now a lazy Twig extension using a Runtime,
* BC Break: removed support for `FOSUserBundle`,
* BC Break: changed `process()` argument for `Form/RegistrationFormHandlerInterface`, from `Form $form` to `FormInterface $form`,
* BC Break: changed form class name in template `Resources/views/Connect/connect_confirm.html.twig` from `fos_user_registration_register` to `registration_register`,
* BC Break: removed configuration option `fosub` from `oauth_user_provider`,
* BC Break: removed configuration options `hwi_oauth.fosub`, & all related DI parameters,
* BC Break: removed DI parameter `hwi_oauth.registration.form.factory` in favour of declaring form class name as DI parameter: `hwi_oauth.connect.registration_form`,
* BC Break: changed `ResourceOwnerMapInterface::hasResourceOwnerByName` signature, update if you use a custom resource owner,
* BC Break: changed `ResourceOwnerMapInterface::getResourceOwnerByName` signature, update if you use a custom resource owner,
* BC Break: changed `ResourceOwnerMapInterface::getResourceOwnerByRequest` signature, update if you use a custom resource owner,
* BC Break: changed `ResourceOwnerMapInterface::getResourceOwnerCheckPath` signature, update if you use a custom resource owner,
* BC Break: `ResourceOwnerMap` uses service locator instead of DI container,
* BC Break: Removed abstract services: `hwi_oauth.abstract_resource_owner.generic`, `hwi_oauth.abstract_resource_owner.oauth1` & `hwi_oauth.abstract_resource_owner.oauth2`,
* BC Break: Removed `setName()` method from `OAuth/ResourceOwnerInterface`,
* BC Break: changed `__construct()` argument for `OAuth/ResourceOwner/AbstractResourceOwner`, from `HttpMethodsClient $httpClient` to `HttpClientInterface $httpClient`,
* BC Break: replaced `php-http/httplug-bundle` with `symfony/http-client`
* BC Break: removed `hwi_oauth.http` configuration,
* BC Break: reworked bundles structure to match Symfony best practices:
- bundle code moved to: `src/`,
- tests moved to: `tests/`,
- docs moved from `Resources/doc` into: `docs/`,
* BC Break: routes provided by bundle now have `methods` requirements:
- `hwi_oauth_connect_service`: `GET` & `POST`,
- `hwi_oauth_connect_registration`: `GET` & `POST`,
- `hwi_oauth_connect`: `GET`,
- `hwi_oauth_service_redirect`: `GET`,
* Added support for PHP 8.1,
* Added support for Symfony 5.6,
## 1.4.5 (2021-12-08)
* Bugfix: Fixed: BC break by restoring wrongly moved `AbstractOAuthToken::getCredentials()` method,
## 1.4.3 (2021-12-07)
* Bugfix: Fixed support for PHP 8.1,
* Bugfix: Fixed support for Symfony 5.4,
* Bugfix: Fixed `VkontakteResourceOwner` option: `api_version` to not point to deprecated one,
* Bugfix: `RequestStack::getMasterRequest()` is deprecated since Symfony 5.3, use `RequestStack::getMainRequest()` if exists,
* Maintain: Added `GenericOAuth1ResourceOwnerTestCase`, `GenericOAuth2ResourceOwnerTestCase` & `ResourceOwnerTestCase` test case classes for easier unit testing custom resource owners
## 1.4.2 (2021-08-09)
* Bugfix: remove `@final` declaration from `OAuthFactory` & `FOSUBUserProvider`,
* Maintain: added `.gitattributes` to reduce amount of code in archives,
## 1.4.1 (2021-07-28)
* Bugfix: Define missing `hwi_oauth.connect.confirmation` parameter,
* Bugfix: Added missing success/failure handlers,
## 1.4.0 (2021-07-26)
* BC Break: dropped Symfony 5.0 support as it is EOL,
* BC Break: dropped PHP 7.2 support as it is EOL,
* BC Break: changed `__construct()` argument for `OAuth/RequestDataStorage/SessionStorage`, from `SessionInterface $session` to `RequestStack $requestStack`,
* BC Break: all internal classes are "softly" marked as `final`,
* Added: Symfony 5.1 Security system support,
* Added: Forward compatibility layer for session service deprecation,
* Added: state support for service authentication URL's,
* Added: ability to change the response after `HWIOAuthEvents::CONNECT_COMPLETED` is fired,
* Added: PHPStan static analyse into CI,
* Fixed: `OAuthProvide` to properly refresh data inside tokens,
* Fixed: PHP notice in `AppleResourceOwner`,
* Fixed: use new GitHub API in `GitHubResourceOwner`,
* Fixed: functional tests with & without FOSUserBundle,
* Fixed: controller don't depend on service container if possible,
* Maintain: removed `Wunderlist` resource owner,
* Maintain: removed several Symfony BC layers,
* Maintain: removed Prophecy in favour of PHPUnit mocking,
## 1.3.0 (2021-01-03)
* BC Break: dropped support for Symfony `<4.4`,
* BC Break: dropped support for Doctrine Bundle `<2.0`,
* Added PHP 8 support,
* Upgraded Facebook API to v8.0,
* Upgraded Twitch resource owner to incorporate latest Twitch API,
* Fixed: undefined `id_token` exception in Azure resource owner,
* Docs: changed firewall name to match flex receipt,
* Maintain: moved from Travis CI to Github Actions,
## 1.2.0 (2020-10-19)
* BC Break: dropped Symfony 4.3 support,
* Added `first_name` & `last_name` in AzureResourceOwner,
* Added: support for multiple OAuth2 state parameters,
* Added: Apple resource owner,
* Fixed: updated Azure `authorization` & `access_token` urls,
* Fixed: Doctrine persistence deprecation errors,
* Allow modification of the response in `FilterUserResponseEvent`,
## 1.1.0 (2020-04-06)
* Added Symfony 5 support,
* Added domain whitelist service to avoid open redirect on `target_path`,
* Fixed: session service was not injected in `LoginController`,
* Fixed: missing `setContainer` call to service configuration for `LoginController`,
* Fixed: client id and client secret must be set in `Auth0ResourceOwner::doGetTokenRequest`,
* Fixed: missing client id and client secret in `Auth0ResourceOwner`,
* Twig dependency on `LoginController` is now optional,
## 1.0.0 (2020-01-17)
* Dropped support for PHP 5.6, 7.0 and 7.1,
* Dropped support for FOSUserBundle 1.3,
* Dropped support for Symfony 2.8,
* Minimum Symfony 3 requirement is 3.4,
* Minimum Symfony 4 requirement is 4.3,
* Fixed: WindowsLive Resource Owner token request,
* Fixed: Update Facebook API to v3.1,
* Fixed: Update Linkedin API to v2,
* Fixed: YahooResourceOwner::doGetUserInformationRequest uses wrong arguments,
* Fixed: Symfony deprecation warning in `symfony/config`,
* Fixed: SensioConnect now uses new API URLs,
* Fixed: Do not add Authorization header if no client_secret is present,
* Fixed: `LoginController::connectAction` should not fail if no token is available,
* Added: Genius.com resource owner,
* Added: HTTPlug 2.0 support,
* Added: Keycloak resource owner,
* Added: The controller is now available as a service,
* Added: Allow to use HTTP Basic auth for token request,
* [BC break] Class `Configuration` has been marked final,
* [BC break] Class `ConnectController` has been marked final,
* [BC break] Class `HWIOAuthExtension` has been marked final,
* [BC break] Class `OAuthExtension` has been marked final,
* [BC break] Class `SetResourceOwnerServiceNameCompilerPass` has been marked final,
* [BC break] Class `ConnectController` extends `AbstractController` instead of `Controller`,
* [BC break] Service `hwi_oauth.http_client` has been marked private,
* [BC break] Service `hwi_oauth.security.oauth_utils` has been marked private,
* [BC break] Several service class parameters have been removed,
## 0.6.3 (2018-07-31)
* Fixed: Vkontakte profile picture & nickname path,
* Fixed: `Content-Length` header must be a string,
* Fixed: Upgraded GitLab end point to v4,
* Fixed: Resource owner map parameters must be public,
* Fixed: Azure resource owner `infos_url` should not be empty,
* Fixed: Don't start sessions twice & don't start sessions if already started,
* Fixed: Updated BitBucket docs,
* Added: Further compatibility changes for Symfony 4.1,
* Added: LinkedIn `first-` & `last-` names,
* Added: Facebook profile picture
## 0.6.2 (2018-03-28)
* Fixed: VK requires API version now,
* Fixed: Updated Slack resource owner to use new Slack API methods,
* Fixed: Changing authorization and access token to v2 for LinkedIn,
* Fixed: Fix double call of `getUserInformation()` in `ConnectController`,
* Fixed: Fix serialization of `AccountNotLinkedException`,
* Fixed: Check for grant_rule value `IS_AUTHENTICATED_FULLY` in DI configuration,
* Fixed: Don't execute `OAuthProvider::refreshAccessToken()` when there is no refresh token
## 0.6.1 (2018-01-23)
* BC BREAK: Replaced `PHPUnit_Framework_TestCase` with `PHPUnit\Framework\TestCase` in tests,
* Added: Implemented `getUserInformation()` for Dropbox v2,
* Fixed: Headers passed to `httpRequest()` method in various resource owners,
* Fixed: Marked some services as `public` to make code compatible with Symfony 4
## 0.6.0 (2017-12-01)
* BC BREAK: Fully replaced Buzz library with usage of HTTPlug & Guzzle 6,
* BC BREAK: `hwi.http_client` config options are remove. HTTP configuration must rely on the HTTPlug client,
* BC BREAK: Template engine other than Twig are no longer supported,
* BC BREAK: Option `hwi_oauth.templating_engine` was removed,
* Added: Symfony 4 support,
* Added: `php-http/httplug-bundle` support, to auto-provide needed HTTPlug services and get full Symfony integration,
* Added: `hwi.http.client` and `hwi.http.message_factory` config keys to provide your own HTTPlug services,
* Added: `HWIOAuthEvents`,
* Added: `ResourceOwnerInterface::addPaths()` method for easier managing paths in resource owners,
* Fixed: Update Facebook API to v2.8,
## 0.5.3 (2017-01-08)
* Fixed: Bitbucket2 resource owner,
* Fixed: GitHub resource owner documentation,
* Fixed: Don't require any form for the connect feature,
* Fixed: Uncaught exception with custom error page,
* Fixed: `php-cs-fixer` updated to latest version & run on base code
## 0.5.2 (2016-12-12)
* Fixed: Prevent uncaught exception when redirecting to invalid route,
* Fixed: Add more details too exception when account was not linked,
* Fixed: Odnoklassinki resource owner,
* Fixed: Office365 resource owner,
* Fixed: StackExchange resource owner,
* Fixed: WeChat resource owner,
* Fixed: WindowsLive resource owner
## 0.5.1 (2016-10-03)
* Fixed error that could occur with message "302 Header already sent",
* Exclude tests from Composer autoloader
## 0.5.0 (2016-09-11)
* Fixed: `OAuthHelper` should fallback to new `Request` in case of receiving `null`,
* Fixed: Better `FOSUserBundle` integration,
* Fixed: Serialization issue in `WechatResourceOwner`,
* Fixed: Incorrect refresh token in `WechatResourceOwner`,
* Fixed: Broken `TrelloResourceOwner`,
* Fixed: Removed dead code in `OAuthProvider`,
* Fixed: Update Facebook API to v2.7,
* Added: Symfony 3 support,
* Added: Redirect to `target_path` after successful registration/connection,
* Added: Asana resource owner,
* Added: Bitbucket resource owner,
* Added: Clever resource owner,
* Added: Itembase resource owner,
* Added: Jawbon resource owner,
* Added: Office365 resource owner,
* Added: Wunderlist resource owner,
* Added: Hungarian translation
## 0.4.3 (2016-09-11)
* Fixed: Request parameters are not copied into new Request on forward,
* Fixed: Fixed scope deprecating message,
* Fixed: Resolved deprecated message in ConnectController,
* Fixed: Removed usage of deprecated code in tests
## 0.4.2 (2016-07-27)
* Fixed: Change Discogs URL from http to https,
* Fixed: Update Facebook API URLs to not use outdated ones
## 0.4.1 (2016-03-08)
* Fixed: Remove usage of deprecated Twig function `form_enctype` & replace with usage of `form_start`/`form_end`,
* Fixed: Mark as not fully compatible with Symfony `~3.0`,
* Fixed: Multiple firewalls can now have different resource owners,
* Fixed: Wrong URL generated for Safesforce resource owner,
* Added: `include_email` option into Twitter resource owner,
* Added: Hungarian translation,
* Added: Documentation about FOSUser integration
## 0.4.0 (2015-12-04)
* [BC break] Added `UserResponseInterface#getFirstName()` method, also a new default path `firstname`
was added, this path holds the first name of user,
* [BC break] Added `UserResponseInterface#getLastName()` method, also a new default path `lastname`
was added, this path holds the last name of user,
* [BC break] Added `UserResponseInterface::getOAuthToken()` & basic implementation in `AbstractUserResponse`,
* [BC break] `GenericOAuth1ResourceOwner::getRequestToken()` is now public method (was protected),
* Added: configuration parameter `firewall_name` (will be removed in next major version)
renamed to `firewall_names` to support multiple firewalls,
* Added: configuration parameter: `failed_auth_path` which contains route name, on which user
will be redirected after failure when connecting accounts (i.e. user denies connection),
* Added: `appsecret_proof` functionality support to the Facebook resource owner,
* Added: `sandbox` functionality support to the Salesforce resource owner,
* Added Auth0 resource owner,
* Added Azure resource owner,
* Added BufferApp resource owner,
* Added Deezer resource owner,
* Added Discogs resource owner,
* Added EveOnline resource owner,
* Added Fiware resource owner,
* Added Hubic resource owner,
* Added Paypal resource owner,
* Added Reddit resource owner,
* Added Runkeeper resource owner,
* Added Slack resource owner,
* Added Spotify resource owner,
* Added Soundcloud resource owner,
* Added Strava resource owner,
* Added Toshl resource owner,
* Added Trakt resource owner,
* Added Wechat resource owner,
* Added Wordpress resource owner,
* Added Xing resource owner,
* Added Youtube resource owner,
* Fixed: Revoking tokens for Facebook & Google resource owners,
* Fixed: Instagram allows only GET calls to fetch user details,
* Fixed: `ResourceOwnerMap` no longer depends on deprecated `ContainerAware` class,
* Fixed: Wrong usage of `json_decode` in Mail.ru resource owner,
* Fixed: Transform storage exceptions in OAuth1 resource owners into `AuthenticationException`
* Fixed: Default scopes & fields for VKontakte resource owner
## 0.3.9 (2015-08-28)
* Fix: Remove deprecated Twig features
* Fix: Undefined variable in `FOSUBUserProvider::refreshUser`
* Fix: Restore property accessor for Symfony 2.3
## 0.3.8 (2015-05-04)
* Fix: Remove BC break for Symfony < 2.5,
* Fix: Compatibility issues with Symfony 2.6+,
* Fix: Deprecated graph URLs for `FacebookResourceOwner`
## 0.3.7 (2014-11-15)
* Fix: `SessionStorage::save()` could throw php error,
* Fix: `OAuthToken::isExpired()` always returned `false`,
* Fix: `FoursquareResourceOwner`, `TwitchResourceOwner`, `SensioConnectResourceOwner`
not working with bearer header,
* Fix: Don't use deprecated fields in `FacebookResourceOwner`,
* Fix: `FOSUBUserProvider::refreshUser()` always returning old user,
## 0.3.6 (2014-06-02)
* Fix: `InstagramResourceOwner` regression while getting user details,
* Fix: Add smooth migration for session (de)serialization
## 0.3.5 (2014-05-30)
* Fix: `LinkedinResourceOwner` regression while getting user details,
* Fix: OAuth `revoke` functionality to be available wider,
* Fix: Removed undocumented functionality from `SinaWeiboResourceOwner`,
* Fix: Always remove default ports from URLs to match OAuth 1.0a, Spec: 9.1.2
## 0.3.4 (2014-05-12)
* Fix: Instagram OAuth redirect to one url,
* Fix: `FOSUBUserProvider` should also implement `UserProviderInterface`,
* Fix: `YahooResourceOwner` `infos_url` to use new format,
* Fix: Send authorization via headers instead of URL parameter,
* Fix: `GithubResourceOwner` revoke method,
* Fix: Add login routing documentation note
## 0.3.3 (2014-02-17)
* Fix: Incorrect redirect URL when no parameters are set,
* Fix: Add missing parameter `prompt` for `GoogleResourceOwner`,
* Fix: `WordpressResourceOwner` user details API call,
* Fix: PHP Notice when `oauth_callback_confirmed` was set too `false`,
* Fix: PHP Fatal when session returns boolean instead of object,
* Fix: Add missing query parameters for `FacebookResourceOwner`
## 0.3.2 (2014-02-07)
* Fix: Prevent `SessionUnavailableException` when returns back from service,
* Fix: `EntityUserProvider` should implement `UserProviderInterface`,
* Fix: `createdAt` property was missing when serializing the `OAuthToken`,
* Added Italian translations
## 0.3.1 (2014-01-17)
* Fix: Change Twitter API call to use SSL URL,
* Fix: Problems with options in `VkontakteResourceOwner`,
* Fix: Problems with OAuth 1.0a token & `YahooResourceOwner`,
* Fix: Throw exception in `FOSUBUserProvider` when username is missing
* Added SalesForce resource owner
## 0.3.0 (2013-09-28)
* [BC break] `AccountConnectorInterface::connect()` method now requires the first
parameter to be instance of `Symfony\Component\Security\Core\User\UserInterface`
* [BC break] `ConnectController::authenticateUser()` method now requires the first
parameter to be instance of `Symfony\Component\HttpFoundation\Request`
* [BC break] Removed `AbstractResourceOwner::addOptions()` method
* [BC break] `OAuthUtils::getAuthorizationUrl()` & `OAuthUtils::getLoginUrl()` methods
now expect first parameter to be instance of `Symfony\Component\HttpFoundation\Request`
* [BC break] LinkedIn resource owner now uses OAuth2 approach, visit official
web page for details how to migrate: https://developer.linkedin.com/documents/authentication#migration
* [BC break] Dropbox resource owner now uses OAuth2 approach
* Added ability to merge response parts into single path
* Added Bitly resource owner
* Added Box resource owner
* Added Dailymotion resource owner
* Added DeviantArt resource owner
* Added Eventbrite resource owner
* Added Mail.ru resource owner
* Added Sina Weibo resource owner
* Added QQ.com resource owner
* Added Trello resource owner
* Added Wordpress resource owner
## 0.3.0-alpha2 (2013-07-29)
* [BC break] Added `ResourceOwnerInterface::isCsrfTokenValid()` method
* [BC break] Removed `OAuth1RequestTokenStorageInterface` along with the implementations
* [BC break] `AbstractResourceOwner::__construct()` now requires `RequestDataStorageInterface`
instance as last argument
* Fix: Yandex resource owner using invalid parameter when requesting user data
* Fix: To prevent unusual content headers response from resource owners should
be first threaten as json and only in case of failure threaten as query text
* Fix: Instagram resource owner is not able to receive user data more than once
* Added ability to disable confirmation page when connecting accounts
* Added CSRF protection for OAuth2 providers (turned off by default)
* Added `RequestDataStorageInterface` along with implementation
* Added Stereomood resource owner
## 0.3.0-alpha1 (2013-07-03)
* [BC break] `GenericOAuth2ResourceOwner::getAccessToken()` now returns an array
instead of a string. This array contains the access token and its 'expires_in'
value, along with any other parameters returned from the authentication provider
* [BC break] Added `OAuthAwareExceptionInterface#setToken()`, `OAuthAwareExceptionInterface#getRefreshToken()`,
`OAuthAwareExceptionInterface#getRawToken()`, `OAuthAwareExceptionInterface#getExpiresIn()`
methods
* [BC break] Renamed `AbstractResourceOwner::doGetAccessTokenRequest` to `doGetTokenRequest`
* [BC break] Removed `AdvancedPathUserResponse` & `AdvancedUserResponseInterface`
* [BC break] Added `UserResponseInterface#getEmail()`, `UserResponseInterface#getProfilePicture()`,
`UserResponseInterface#getRefreshToken()`, `UserResponseInterface#getExpiresIn()`,
`UserResponseInterface#setOAuthToken()` methods
* [BC break] Removed `UserResponseInterface::setAccessToken()` method
* [BC break] Removed `AbstractUserResponse::getOAuthToken()` method because it was ambiguous
* [BC break] `PathUserResponse#setPaths()` method no longer overwrite default paths
* [BC break] `PathUserResponse#getPath()` method no longer throws an exception if path
not exists
* [BC break] `PathUserResponse#getValueForPath()` removed second argument from this method,
it will not throw exception anymore if response or value is missing, but now will return
`null` instead
* [BC break] Added `ResourceOwnerInterface#getOption($name)` method
* [BC break] `ResourceOwnerInterface#getUserInformation()` now must receive array (`$accessToken`)
as first parameter, also added second parameter (`$extraParameters`) to be consistent
along all implementations
* Added `OAuthToken::getRefreshToken()`, `OAuthToken::setRefreshToken()`, `OAuthToken::getExpiresIn()`,
`OAuthToken::setExpiresIn()`, `OAuthToken::getRawToken()`, `OAuthToken::setRawToken()`
* Added `AbstractResourceOwner#addOptions()` & `ResourceOwnerInterface#setOption($name, $value)`
methods which allows easy overwriting resource specific options
* Added support for options: `access_type`, `request_visible_actions`, `approval_prompt` & `hd`
in Google resource owner
* Added 37signals resource owner
* Added Amazon resource owner
* Added Bitbucket resource owner
* Added Disqus resource owner
* Added Dropbox resource owner
* Added Flickr resource owner
* Added Instagram resource owner
* Added Odnoklassniki resource owner
* Added Yandex resource owner
## 0.2.10 (2013-12-09)
* Fix: use `Symfony\Component\Security\Core\User\UserInterface` in `EntityUserProvider::refreshUser`
* Fix: made `SessionStorage` compatible with Symfony 2.0
## 0.2.9 (2013-09-25)
* Fix: Regression done in version `0.2.8` blocking usage without `FOSUserBundle`
* Fix: `OAuthUtils::getAuthorizationUrl()` ignoring given redirect URL
## 0.2.8 (2013-09-19)
* Fix: Added missing parts in user providers like: `loadUserByUsername()`
or `refreshUser()` methods
* Fix: Registering of user provider services
* Fix: Make `OAuthUtils::signRequest()` compatible with OAuth1.0a specification
## 0.2.7 (2013-08-03)
* Fix: Polish oauth error detection to cover cases from i.e. Facebook resource owner
* Fix: Changed authorization url for Vkontakte resource owner
## 0.2.6 (2013-06-24)
* Fix: Use same check for FOSUserBundle compatibility to prevent strange errors
with calls of undefined services
* Fix: User-land aliased (resource owner) services have the appropriate name
## 0.2.5 (2013-05-29)
* Fix: Use user identifier represented as string for Twitter to prevent issues with
losing accuracy for large numbers (i.e. Javascript) or type comparison (i.e. MongoDB)
* Fix: Don't depend on `arg_separator.output` data for URL generation to prevent issues
## 0.2.4 (2013-05-15)
* Fix: Throw `Symfony\Component\Security\Core\Exception\AccessDeniedException`
& `Symfony\Component\HttpKernel\Exception\NotFoundHttpException` instead of `\Exception`
to make cases more clear
* Fix: Detect `oauth_problem` as authorization error and inform user instead logging error
in background
* Fix: Request extra parameters should have higher priority than default
* Fix: How urls are build in resource owners
* Fix: Missing parameter in `YahooResourceOwner`
## 0.2.3 (2013-05-06)
* Added `AbstractUserResponse::getOAuthToken()` method to allow fetching only OAuth token details
* Added french translation
* Fix: FB incompatibility with 'error' field in response
## 0.2.2 (2013-04-15)
* Fix: FOSUB registration form handler
* Fix: Use API 1.1 for Twitter, not the deprecated 1.0
## 0.2.1 (2013-03-27)
* Fixed issue with FOSUserBundle 2.x integration
## 0.2.0 (2013-03-26)
* Added support for a `target_path_parameter` in order to control the redirect path after login
* Added `hwi_oauth_authorization_url()` twig helper function
* Added Jira resource owner
* Added Yahoo resource owner
* Added setting `realm` in configuration
* Added support for FOSUserBundle 2.x integration
* Added Stack Exchange resource owner
* Fix: configuration parameter `firewall_name` is required
* Fix: prevent throwing `AlreadyBoundException` when using FOSUserBundle 1.x integration
* Fix: check for availability of `profilePicture` in views before calling it
* Fix: `InMemoryProvider` now shows user nickname as name instead of unique identifier
* Fix: don't set `realm` option if is empty in request headers
* Fix: for infinity loop blockade and error token response handling
## 0.1-alpha (2012-08-27)
* [BC break] Renamed path `username` to `identifier` to make it more clear that this path should
hold the unique user identifier (previously `username`)
* [BC break] Method `UserResponseInterface#getUsername()` now always returns a real
unique user identifier, and uses path `identifier`
* [BC break] `OAuth1RequestTokenStorageInterface#save()` second param `$token` must
now be an array
* [BC break] Configuration type 'generic' is renamed to 'oauth2'
* [BC break] `redirect.xml` routing has to be imported. See the setup docs
* Added `UserResponseInterface#getRealName()` method, also a new default path `realname`
was added, this path holds the real name of user
* Added `UserResponseInterface#getNickName()` method, also a new default path `nickname`
was added, this path holds the nickname of user
* Added `UserResponseInterface#getAccessToken()` and `UserResponseInterface#setAccessToken`
* Added `OAuthToken#getCredentials()` returns an empty string to be consistent with
the security component. The access token can still be retrieved from the
`getAccessToken()` method
* Added change that forces all authentication requests are now redirected to the login path
* Added change that makes `firewall_name` option required setting
* Added OAuth 1.0a support (linkedin/twitter/generic)
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2012-2019 Hardware Info - https://hardware.info
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+90
View File
@@ -0,0 +1,90 @@
HWIOAuthBundle
==============
[![Build Status](https://github.com/hwi/HWIOAuthBundle/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/hwi/HWIOAuthBundle/actions/workflows/ci.yaml) [![Latest Stable Version](https://poser.pugx.org/hwi/oauth-bundle/v/stable.svg)](https://packagist.org/packages/hwi/oauth-bundle) [![Total Downloads](https://poser.pugx.org/hwi/oauth-bundle/downloads.svg)](https://packagist.org/packages/hwi/oauth-bundle) [![License](https://poser.pugx.org/hwi/oauth-bundle/license.svg)](https://packagist.org/packages/hwi/oauth-bundle)
The HWIOAuthBundle adds support for authenticating users via OAuth1.0a or OAuth2 in Symfony.
> __Note__: this bundle adds easy way to implement any of OAuth1.0a or OAuth2 provider!
Installation
------------
All the installation instructions are located in the documentation, check it for a specific version:
* [__2.x__](https://github.com/hwi/HWIOAuthBundle/blob/master/docs/1-setting_up_the_bundle.md) (current) - with support for Symfony: `^5.4`, `^6.3` & `^7.0` (PHP: `^8.1`),
Documentation
-------------
The bulk of the documentation is stored in the `Resources/doc/index.md`
file in this bundle. Read the documentation for version:
* [__2.x__](https://github.com/hwi/HWIOAuthBundle/blob/master/docs/index.md)
This bundle contains support for 58 different providers:
* 37signals,
* Amazon,
* Apple,
* Asana,
* Auth0,
* Azure,
* Bitbucket,
* Bitly,
* Box,
* BufferApp,
* Clever,
* Dailymotion,
* Deezer,
* DeviantArt,
* Discogs,
* Disqus,
* Dropbox,
* EVE Online,
* Facebook,
* FI-WARE,
* Flickr,
* Foursquare,
* Genius,
* GitHub,
* Google,
* Hubic,
* Instagram,
* Itembase,
* Jawbone,
* JIRA,
* Keycloak,
* LinkedIn,
* Mail.ru
* Odnoklassniki,
* Office365,
* Passage,
* PayPal,
* QQ,
* RunKeeper,
* Salesforce,
* Sensio Connect,
* Sina Weibo,
* Slack,
* Soundcloud,
* Spotify,
* Stack Exchange,
* Stereomood,
* Strava,
* Toshl,
* Trakt,
* Trello,
* Twitch,
* Twitter,
* VKontakte,
* Windows Live,
* Wordpress,
* XING,
* Yahoo,
* Yandex,
* Youtube
License
-------
This bundle is under the MIT license. See the complete [license in the bundle](https://github.com/hwi/HWIOAuthBundle/blob/master/LICENSE).
+12
View File
@@ -0,0 +1,12 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 2.1 | :white_check_mark: |
| <2.1 | :x: |
## Reporting a Vulnerability
If you discover a security vulnerability, please send an email to: stloyd@gmail.com
+174
View File
@@ -0,0 +1,174 @@
{
"name": "hwi/oauth-bundle",
"type": "symfony-bundle",
"homepage": "https://github.com/hwi/HWIOAuthBundle",
"license": "MIT",
"description": "Support for authenticating users using both OAuth1.0a and OAuth2 in Symfony.",
"keywords": [
"authentication",
"firewall",
"oauth",
"oauth1",
"oauth2",
"security",
"amazon",
"apple",
"asana",
"auth0",
"azure",
"bitbucket",
"bitly",
"box",
"bufferapp",
"clever",
"dailymotion",
"deezer",
"deviantart",
"discogs",
"disqus",
"dropbox",
"eventbrite",
"eve online",
"facebook",
"fiware",
"flickr",
"foursquare",
"genius",
"github",
"gitlab",
"google",
"hubic",
"instagram",
"jawbone",
"jira",
"linkedin",
"mail.ru",
"odnoklassniki",
"paypal",
"qq",
"reddit",
"runkeeper",
"salesforce",
"sensio connect",
"sina weibo",
"slack",
"sound cloud",
"spotify",
"stack exchange",
"stereomood",
"strava",
"toshl",
"trakt",
"trello",
"twitch",
"twitter",
"vkontakte",
"windows live",
"wordpress",
"xing",
"yahoo",
"yandex",
"youtube",
"37signals"
],
"authors": [
{
"name": "Alexander",
"email": "iam.asm89@gmail.com"
},
{
"name": "Joseph Bielawski",
"email": "stloyd@gmail.com"
},
{
"name": "Geoffrey Bachelet",
"email": "geoffrey.bachelet@gmail.com"
},
{
"name": "Contributors",
"homepage": "https://github.com/hwi/HWIOAuthBundle/contributors"
}
],
"require": {
"php": "^8.1",
"symfony/deprecation-contracts": "^3.0",
"symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0",
"symfony/security-bundle": "^5.4 || ^6.3 || ^7.0",
"symfony/options-resolver": "^5.4 || ^6.3 || ^7.0",
"symfony/form": "^5.4 || ^6.3 || ^7.0",
"symfony/http-client": "^5.4 || ^6.3 || ^7.0",
"symfony/routing": "^5.4 || ^6.3 || ^7.0",
"symfony/twig-bundle": "^5.4 || ^6.3 || ^7.0"
},
"require-dev": {
"doctrine/doctrine-bundle": "^2.4",
"doctrine/orm": "^2.9",
"symfony/browser-kit": "^5.4 || ^6.3 || ^7.0",
"symfony/css-selector": "^5.4 || ^6.3 || ^7.0",
"symfony/phpunit-bridge": "^5.4 || ^6.3 || ^7.0",
"symfony/property-access": "^5.4 || ^6.3 || ^7.0",
"symfony/validator": "^5.4 || ^6.3 || ^7.0",
"symfony/stopwatch": "^5.4 || ^6.3 || ^7.0",
"symfony/translation": "^5.4 || ^6.3 || ^7.0",
"symfony/yaml": "^5.4 || ^6.3 || ^7.0",
"phpunit/phpunit": "^9.6.11",
"friendsofphp/php-cs-fixer": "^3.23",
"symfony/monolog-bundle": "^3.4",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-symfony": "^1.3",
"phpstan/extension-installer": "^1.3",
"firebase/php-jwt": "^6.8"
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"phpstan/extension-installer": true
}
},
"conflict": {
"symfony/security-bundle": ">=6.0,<6.3.4",
"twig/twig": "<1.43|>=2.0,<2.13"
},
"scripts": {
"csfixer": "vendor/bin/php-cs-fixer fix --verbose --dry-run",
"csfixer-fix": "vendor/bin/php-cs-fixer fix --verbose",
"phpunit": "vendor/bin/phpunit",
"phpstan": "vendor/bin/phpstan"
},
"suggest": {
"doctrine/doctrine-bundle": "to use Doctrine user provider",
"firebase/php-jwt": "to use JWT utility functions",
"symfony/property-access": "to use FOSUB integration with this bundle",
"symfony/twig-bundle": "to use the Twig hwi_oauth_* functions"
},
"autoload": {
"psr-4": {
"HWI\\Bundle\\OAuthBundle\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HWI\\Bundle\\OAuthBundle\\Test\\": "src/Test/",
"HWI\\Bundle\\OAuthBundle\\Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Connect;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Account connector objects are responsible for connecting an OAuth response
* to the appropriate fields of the user object.
*
* @author Alexander <iam.asm89@gmail.com>
*/
interface AccountConnectorInterface
{
/**
* Connects the response to the user object.
*
* @param UserInterface $user The user object
* @param UserResponseInterface $response The oauth response
*/
public function connect(UserInterface $user, UserResponseInterface $response);
}
@@ -0,0 +1,204 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Controller\Connect;
use HWI\Bundle\OAuthBundle\Connect\AccountConnectorInterface;
use HWI\Bundle\OAuthBundle\Event\FilterUserResponseEvent;
use HWI\Bundle\OAuthBundle\Event\GetResponseUserEvent;
use HWI\Bundle\OAuthBundle\HWIOAuthEvents;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwnerInterface;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use HWI\Bundle\OAuthBundle\Security\Http\ResourceOwnerMapLocator;
use Symfony\Component\EventDispatcher\Event as DeprecatedEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Twig\Environment;
/**
* @author Alexander <iam.asm89@gmail.com>
*
* @internal
*/
abstract class AbstractController
{
protected ResourceOwnerMapLocator $resourceOwnerMapLocator;
protected RequestStack $requestStack;
protected EventDispatcherInterface $dispatcher;
protected TokenStorageInterface $tokenStorage;
protected UserCheckerInterface $userChecker;
protected Environment $twig;
protected ?AccountConnectorInterface $accountConnector;
public function __construct(
ResourceOwnerMapLocator $resourceOwnerMapLocator,
RequestStack $requestStack,
EventDispatcherInterface $dispatcher,
TokenStorageInterface $tokenStorage,
UserCheckerInterface $userChecker,
Environment $twig,
?AccountConnectorInterface $accountConnector
) {
$this->resourceOwnerMapLocator = $resourceOwnerMapLocator;
$this->requestStack = $requestStack;
$this->dispatcher = $dispatcher;
$this->tokenStorage = $tokenStorage;
$this->userChecker = $userChecker;
$this->twig = $twig;
$this->accountConnector = $accountConnector;
}
/**
* Get a resource owner by name.
*
* @throws NotFoundHttpException if there is no resource owner with the given name
*/
protected function getResourceOwnerByName(string $name): ResourceOwnerInterface
{
foreach ($this->resourceOwnerMapLocator->getResourceOwnerMaps() as $ownerMap) {
if ($resourceOwner = $ownerMap->getResourceOwnerByName($name)) {
return $resourceOwner;
}
}
throw new NotFoundHttpException(sprintf("No resource owner with name '%s'.", $name));
}
/**
* Authenticate a user with Symfony Security.
*
* @param string|array $accessToken
*/
protected function authenticateUser(Request $request, UserInterface $user, string $resourceOwnerName, $accessToken, bool $fakeLogin = true): void
{
try {
$this->userChecker->checkPreAuth($user);
$this->userChecker->checkPostAuth($user);
} catch (AccountStatusException $e) {
// Don't authenticate locked, disabled or expired users
return;
}
$token = new OAuthToken($accessToken, $user->getRoles());
$token->setResourceOwnerName($resourceOwnerName);
$token->setUser($user);
// required for compatibility with Symfony 5.4
if (method_exists($token, 'setAuthenticated')) {
$token->setAuthenticated(true, false);
}
$this->tokenStorage->setToken($token);
if ($fakeLogin) {
// Since we're "faking" normal login, we need to throw our INTERACTIVE_LOGIN event manually
$this->dispatch(
new InteractiveLoginEvent($request, $token),
SecurityEvents::INTERACTIVE_LOGIN
);
}
}
/**
* @param string $service name of the resource owner to connect to
*
* @throws NotFoundHttpException if there is no resource owner with the given name
*/
protected function getConfirmationResponse(Request $request, array $accessToken, string $service): Response
{
/** @var OAuthToken $currentToken */
$currentToken = $this->tokenStorage->getToken();
/** @var UserInterface $currentUser */
$currentUser = $currentToken->getUser();
$resourceOwner = $this->getResourceOwnerByName($service);
$userInformation = $resourceOwner->getUserInformation($accessToken);
$event = new GetResponseUserEvent($currentUser, $request);
$this->dispatch($event, HWIOAuthEvents::CONNECT_CONFIRMED);
$this->accountConnector->connect($currentUser, $userInformation);
if ($currentToken instanceof OAuthToken) {
// Update user token with new details
$newToken =
(isset($accessToken['access_token']) || isset($accessToken['oauth_token'])) ?
$accessToken : $currentToken->getRawToken();
$this->authenticateUser($request, $currentUser, $service, $newToken, false);
}
if (null === $response = $event->getResponse()) {
if ($targetPath = $this->getTargetPath($request->getSession())) {
$response = new RedirectResponse($targetPath);
} else {
$response = new Response($this->twig->render('@HWIOAuth/Connect/connect_success.html.twig', [
'userInformation' => $userInformation,
'service' => $service,
]));
}
}
$event = new FilterUserResponseEvent($currentUser, $request, $response);
$this->dispatch($event, HWIOAuthEvents::CONNECT_COMPLETED);
return $event->getResponse();
}
/**
* @param Event|DeprecatedEvent $event
*/
protected function dispatch($event, ?string $eventName = null): void
{
$this->dispatcher->dispatch($event, $eventName);
}
protected function getSession(): ?SessionInterface
{
if (method_exists($this->requestStack, 'getSession')) {
return $this->requestStack->getSession();
}
if ((null !== $request = $this->requestStack->getCurrentRequest()) && $request->hasSession()) {
return $request->getSession();
}
return null;
}
protected function getTargetPath(?SessionInterface $session): ?string
{
if (!$session) {
return null;
}
foreach ($this->resourceOwnerMapLocator->getFirewallNames() as $firewallName) {
$sessionKey = '_security.'.$firewallName.'.target_path';
if ($session->has($sessionKey)) {
return $session->get($sessionKey);
}
}
return null;
}
}
@@ -0,0 +1,171 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Controller\Connect;
use HWI\Bundle\OAuthBundle\Connect\AccountConnectorInterface;
use HWI\Bundle\OAuthBundle\Event\GetResponseUserEvent;
use HWI\Bundle\OAuthBundle\HWIOAuthEvents;
use HWI\Bundle\OAuthBundle\Security\Http\ResourceOwnerMapLocator;
use HWI\Bundle\OAuthBundle\Security\OAuthUtils;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Twig\Environment;
/**
* @author Alexander <iam.asm89@gmail.com>
*
* @internal
*/
final class ConnectController extends AbstractController
{
private OAuthUtils $oauthUtils;
private AuthorizationCheckerInterface $authorizationChecker;
private FormFactoryInterface $formFactory;
private RouterInterface $router;
private string $grantRule;
private bool $failedUseReferer;
private string $failedAuthPath;
private bool $enableConnectConfirmation;
public function __construct(
OAuthUtils $oauthUtils,
ResourceOwnerMapLocator $resourceOwnerMapLocator,
RequestStack $requestStack,
EventDispatcherInterface $dispatcher,
TokenStorageInterface $tokenStorage,
UserCheckerInterface $userChecker,
AuthorizationCheckerInterface $authorizationChecker,
FormFactoryInterface $formFactory,
Environment $twig,
RouterInterface $router,
string $grantRule,
bool $failedUseReferer,
string $failedAuthPath,
bool $enableConnectConfirmation,
?AccountConnectorInterface $accountConnector
) {
parent::__construct(
$resourceOwnerMapLocator,
$requestStack,
$dispatcher,
$tokenStorage,
$userChecker,
$twig,
$accountConnector
);
$this->oauthUtils = $oauthUtils;
$this->grantRule = $grantRule;
$this->failedUseReferer = $failedUseReferer;
$this->failedAuthPath = $failedAuthPath;
$this->enableConnectConfirmation = $enableConnectConfirmation;
$this->authorizationChecker = $authorizationChecker;
$this->formFactory = $formFactory;
$this->router = $router;
}
/**
* Connects a user to a given account if the user is logged in and connect is enabled.
*
* @param string $service name of the resource owner to connect to
*
* @throws \Exception
* @throws NotFoundHttpException if `connect` functionality was not enabled
* @throws AccessDeniedException if no user is authenticated
*/
public function connectServiceAction(Request $request, string $service): Response
{
if (!$this->accountConnector) {
throw new NotFoundHttpException();
}
$hasUser = $this->authorizationChecker->isGranted($this->grantRule);
if (!$hasUser) {
throw new AccessDeniedException('Cannot connect an account.');
}
// Get the data from the resource owner
$resourceOwner = $this->getResourceOwnerByName($service);
$session = $request->hasSession() ? $request->getSession() : $this->getSession();
if ($session && !$session->isStarted()) {
$session->start();
}
$key = $request->query->get('key', (string) time());
$accessToken = null;
if ($resourceOwner->handles($request)) {
$accessToken = $resourceOwner->getAccessToken(
$request,
$this->oauthUtils->getServiceAuthUrl($request, $resourceOwner)
);
if ($session) {
// save in session
$session->set('_hwi_oauth.connect_confirmation.'.$key, $accessToken);
}
} elseif ($session) {
$accessToken = $session->get('_hwi_oauth.connect_confirmation.'.$key);
}
// Redirect to the login path if the token is empty (Eg. User cancelled auth)
if (null === $accessToken) {
if ($this->failedUseReferer && $targetPath = $this->getTargetPath($session)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->router->generate($this->failedAuthPath));
}
// Show confirmation page?
if (!$this->enableConnectConfirmation) {
return $this->getConfirmationResponse($request, $accessToken, $service);
}
$form = $this->formFactory->create();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return $this->getConfirmationResponse($request, $accessToken, $service);
}
/** @var TokenInterface $token */
$token = $this->tokenStorage->getToken();
$event = new GetResponseUserEvent($token->getUser(), $request);
$this->dispatch($event, HWIOAuthEvents::CONNECT_INITIALIZE);
if ($response = $event->getResponse()) {
return $response;
}
return new Response($this->twig->render('@HWIOAuth/Connect/connect_confirm.html.twig', [
'key' => $key,
'service' => $service,
'form' => $form->createView(),
'userInformation' => $resourceOwner->getUserInformation($accessToken),
]));
}
}
@@ -0,0 +1,175 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Controller\Connect;
use HWI\Bundle\OAuthBundle\Connect\AccountConnectorInterface;
use HWI\Bundle\OAuthBundle\Event\FilterUserResponseEvent;
use HWI\Bundle\OAuthBundle\Event\FormEvent;
use HWI\Bundle\OAuthBundle\Event\GetResponseUserEvent;
use HWI\Bundle\OAuthBundle\Form\RegistrationFormHandlerInterface;
use HWI\Bundle\OAuthBundle\HWIOAuthEvents;
use HWI\Bundle\OAuthBundle\Security\Core\Exception\AccountNotLinkedException;
use HWI\Bundle\OAuthBundle\Security\Http\ResourceOwnerMapLocator;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Twig\Environment;
/**
* @author Alexander <iam.asm89@gmail.com>
*
* @internal
*/
final class RegisterController extends AbstractController
{
private AuthorizationCheckerInterface $authorizationChecker;
private FormFactoryInterface $formFactory;
private ?RegistrationFormHandlerInterface $formHandler;
private string $grantRule;
private ?string $registrationForm;
public function __construct(
ResourceOwnerMapLocator $resourceOwnerMapLocator,
RequestStack $requestStack,
EventDispatcherInterface $dispatcher,
TokenStorageInterface $tokenStorage,
UserCheckerInterface $userChecker,
AuthorizationCheckerInterface $authorizationChecker,
FormFactoryInterface $formFactory,
Environment $twig,
string $grantRule,
?string $registrationForm,
?AccountConnectorInterface $accountConnector,
?RegistrationFormHandlerInterface $formHandler
) {
parent::__construct(
$resourceOwnerMapLocator,
$requestStack,
$dispatcher,
$tokenStorage,
$userChecker,
$twig,
$accountConnector
);
$this->grantRule = $grantRule;
$this->registrationForm = $registrationForm;
$this->formHandler = $formHandler;
$this->authorizationChecker = $authorizationChecker;
$this->formFactory = $formFactory;
}
/**
* Shows a registration form if there is no user logged in and connecting
* is enabled.
*
* @param string $key key used for retrieving the right information for the registration form
*
* @throws NotFoundHttpException if `connect` functionality was not enabled
* @throws AccessDeniedException if any user is authenticated
* @throws \RuntimeException
*/
public function registrationAction(Request $request, string $key): Response
{
if (!$this->accountConnector || !$this->formHandler) {
throw new NotFoundHttpException();
}
$hasUser = $this->authorizationChecker->isGranted($this->grantRule);
if ($hasUser) {
throw new AccessDeniedException('Cannot connect already registered account.');
}
$error = null;
$session = $request->hasSession() ? $request->getSession() : $this->getSession();
if ($session) {
if (!$session->isStarted()) {
$session->start();
}
$error = $session->get('_hwi_oauth.registration_error.'.$key);
$session->remove('_hwi_oauth.registration_error.'.$key);
}
if (!$error instanceof AccountNotLinkedException) {
throw new \RuntimeException('Cannot register an account.', 0, $error instanceof \Exception ? $error : null);
}
if (!$this->registrationForm) {
throw new \InvalidArgumentException('Registration form class must be set.');
}
$userInformation = $this
->getResourceOwnerByName($error->getResourceOwnerName())
->getUserInformation($error->getRawToken())
;
$form = $this->formFactory->create($this->registrationForm);
if ($this->formHandler->process($request, $form, $userInformation)) {
$event = new FormEvent($form, $request);
$this->dispatch($event, HWIOAuthEvents::REGISTRATION_SUCCESS);
/** @var UserInterface $user */
$user = $form->getData();
$this->accountConnector->connect($user, $userInformation);
// Authenticate the user
$this->authenticateUser($request, $user, $error->getResourceOwnerName(), $error->getRawToken());
if (null === $response = $event->getResponse()) {
if ($targetPath = $this->getTargetPath($session)) {
$response = new RedirectResponse($targetPath);
} else {
$response = new Response($this->twig->render('@HWIOAuth/Connect/registration_success.html.twig', [
'userInformation' => $userInformation,
]));
}
}
$event = new FilterUserResponseEvent($user, $request, $response);
$this->dispatch($event, HWIOAuthEvents::REGISTRATION_COMPLETED);
return $event->getResponse();
}
if ($session) {
// reset the error in the session
$session->set('_hwi_oauth.registration_error.'.$key, $error);
}
/** @var UserInterface $user */
$user = $form->getData();
$event = new GetResponseUserEvent($user, $request);
$this->dispatch($event, HWIOAuthEvents::REGISTRATION_INITIALIZE);
if ($response = $event->getResponse()) {
return $response;
}
return new Response($this->twig->render('@HWIOAuth/Connect/registration.html.twig', [
'key' => $key,
'form' => $form->createView(),
'userInformation' => $userInformation,
]));
}
}
@@ -0,0 +1,112 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Controller;
use HWI\Bundle\OAuthBundle\Security\Core\Exception\AccountNotLinkedException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Twig\Environment;
/**
* @author Alexander <iam.asm89@gmail.com>
*
* @internal
*/
final class LoginController
{
private bool $connect;
private string $grantRule;
private AuthenticationUtils $authenticationUtils;
private RouterInterface $router;
private AuthorizationCheckerInterface $authorizationChecker;
private RequestStack $requestStack;
private Environment $twig;
public function __construct(
AuthenticationUtils $authenticationUtils,
RouterInterface $router,
AuthorizationCheckerInterface $authorizationChecker,
RequestStack $requestStack,
Environment $twig,
bool $connect,
string $grantRule
) {
$this->authenticationUtils = $authenticationUtils;
$this->router = $router;
$this->authorizationChecker = $authorizationChecker;
$this->requestStack = $requestStack;
$this->twig = $twig;
$this->connect = $connect;
$this->grantRule = $grantRule;
}
/**
* Action that handles the login 'form'. If connecting is enabled the
* user will be redirected to the appropriate login urls or registration forms.
*
* @throws \LogicException
*/
public function connectAction(Request $request): Response
{
try {
$hasUser = $this->authorizationChecker->isGranted($this->grantRule);
} catch (AuthenticationCredentialsNotFoundException $exception) {
$hasUser = false;
}
$error = $this->authenticationUtils->getLastAuthenticationError();
// if connecting is enabled and there is no user, redirect to the registration form
if ($this->connect && !$hasUser && $error instanceof AccountNotLinkedException) {
$key = time();
$session = $request->hasSession() ? $request->getSession() : $this->getSession();
if ($session) {
if (!$session->isStarted()) {
$session->start();
}
$session->set('_hwi_oauth.registration_error.'.$key, $error);
}
return new RedirectResponse($this->router->generate('hwi_oauth_connect_registration', ['key' => $key], UrlGeneratorInterface::ABSOLUTE_PATH));
}
if (null !== $error) {
$error = $error->getMessageKey();
}
return new Response(
$this->twig->render('@HWIOAuth/Connect/login.html.twig', ['error' => $error])
);
}
private function getSession(): ?SessionInterface
{
if (method_exists($this->requestStack, 'getSession')) {
return $this->requestStack->getSession();
}
if ((null !== $request = $this->requestStack->getCurrentRequest()) && $request->hasSession()) {
return $request->getSession();
}
return null;
}
}
@@ -0,0 +1,87 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Controller;
use HWI\Bundle\OAuthBundle\Security\Http\ResourceOwnerMapLocator;
use HWI\Bundle\OAuthBundle\Security\OAuthUtils;
use HWI\Bundle\OAuthBundle\Util\DomainWhitelist;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* @author Alexander <iam.asm89@gmail.com>
*
* @internal
*/
final class RedirectToServiceController
{
public function __construct(
private readonly OAuthUtils $oauthUtils,
private readonly DomainWhitelist $domainWhitelist,
private readonly ResourceOwnerMapLocator $resourceOwnerMapLocator,
private readonly ?string $targetPathParameter,
private readonly bool $failedUseReferer,
private readonly bool $useReferer
) {
}
/**
* @throws NotFoundHttpException
*/
public function redirectToServiceAction(Request $request, string $service): RedirectResponse
{
try {
$authorizationUrl = $this->oauthUtils->getAuthorizationUrl($request, $service);
} catch (\RuntimeException $e) {
throw new NotFoundHttpException($e->getMessage(), $e);
}
$this->storeReturnPath($request, $authorizationUrl);
return new RedirectResponse($authorizationUrl);
}
private function storeReturnPath(Request $request, string $authorizationUrl): void
{
try {
$session = $request->getSession();
} catch (SessionNotFoundException $e) {
return;
}
$param = $this->targetPathParameter;
foreach ($this->resourceOwnerMapLocator->getFirewallNames() as $firewallName) {
$sessionKey = '_security.'.$firewallName.'.target_path';
$sessionKeyFailure = '_security.'.$firewallName.'.failed_target_path';
if (!empty($param) && $targetUrl = $request->get($param)) {
if (!$this->domainWhitelist->isValidTargetUrl($targetUrl)) {
throw new AccessDeniedHttpException('Not allowed to redirect to '.$targetUrl);
}
$session->set($sessionKey, $targetUrl);
}
if ($this->failedUseReferer && !$session->has($sessionKeyFailure) && ($targetUrl = $request->headers->get('Referer')) && $targetUrl !== $authorizationUrl) {
$session->set($sessionKeyFailure, $targetUrl);
}
if ($this->useReferer && !$session->has($sessionKey) && ($targetUrl = $request->headers->get('Referer')) && $targetUrl !== $authorizationUrl) {
$session->set($sessionKey, $targetUrl);
}
}
}
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\DependencyInjection\CompilerPass;
use HWI\Bundle\OAuthBundle\DependencyInjection\HWIOAuthExtension;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class EnableRefreshOAuthTokenListenerCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
/** @var HWIOAuthExtension $extension */
$extension = $container->getExtension('hwi_oauth');
if (!$extension->isRefreshTokenListenerEnabled()) {
return;
}
foreach ($extension->getFirewallNames() as $firewallName => $_) {
$container->findDefinition('hwi_oauth.context_listener.token_refresher.'.$firewallName)
->addMethodCall('enable');
}
}
}
@@ -0,0 +1,105 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\DependencyInjection\CompilerPass;
use HWI\Bundle\OAuthBundle\DependencyInjection\Configuration;
use HWI\Bundle\OAuthBundle\DependencyInjection\HWIOAuthExtension;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
/**
* Registers "hwi_oauth.resource_owner.$type.class" Parameters and checks resource owner configurations, whether given
* type exists (Apps can add own ResourceOwners).
*
* Adds resource owner maps to the locator and utils.
*/
final class ResourceOwnerCompilerPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container): void
{
$this->registerResourceOwnerTypeClassParameters($container);
$this->addResourceOwnerMapToLocatorAndUtils($container);
}
private function registerResourceOwnerTypeClassParameters(ContainerBuilder $container): void
{
foreach ($container->findTaggedServiceIds('hwi_oauth.resource_owner') as $serviceId => $_) {
$definition = $container->findDefinition($serviceId);
Configuration::registerResourceOwner($definition->getClass());
}
foreach (Configuration::getResourceOwnerTypesClassMap() as $type => $resourceOwnerClass) {
$parameterName = "hwi_oauth.resource_owner.$type.class";
if (!$container->hasParameter($parameterName)) {
$container->setParameter($parameterName, $resourceOwnerClass);
}
}
// Check whether resource owner set with parameter '%hwi_oauth.resource_owner.[type].class%' type exists
/** @var ServiceLocator $locator */
$locator = $container->get('hwi_oauth.resource_owners.locator');
foreach ($locator->getProvidedServices() as $resourceOwnerName => $_) {
try {
$definition = $container->findDefinition('hwi_oauth.resource_owner.'.$resourceOwnerName);
} catch (ServiceNotFoundException $e) {
// Resource owner defined with "options.service"
continue;
}
$resourceOwnerClass = $definition->getClass();
// Check whether a ResourceOwner class exists only if resource owner was set by its "options.type"
if (false === preg_match('~^%(?P<parameter>hwi_oauth.resource_owner.(?P<type>.+).class)%$~', $resourceOwnerClass, $match)) {
return;
}
if (!($match['type'] ?? null)) {
continue;
}
if (!Configuration::isResourceOwnerSupported($match['type'])) {
$e = new \InvalidArgumentException(sprintf('Unknown resource owner type "%s"', $match['type']));
throw new InvalidConfigurationException(sprintf('Invalid configuration for path "hwi_oauth.resource_owners.%s.type": %s', $resourceOwnerName, $e->getMessage()), $e->getCode(), $e);
}
}
}
private function addResourceOwnerMapToLocatorAndUtils(ContainerBuilder $container): void
{
/** @var HWIOAuthExtension $extension */
$extension = $container->getExtension('hwi_oauth');
$locatorDef = $container->findDefinition('hwi_oauth.resource_ownermap_locator');
$oauthUtilsDef = $container->findDefinition('hwi_oauth.security.oauth_utils');
foreach ($extension->getFirewallNames() as $firewallName => $_) {
$resourceOwnerMapId = 'hwi_oauth.resource_ownermap.'.$firewallName;
$container->findDefinition($resourceOwnerMapId)
->setArgument('$locator', new Reference('hwi_oauth.resource_owners.locator'));
$resourceOwnerMapRef = new Reference($resourceOwnerMapId);
$locatorDef->addMethodCall('set', [$firewallName, $resourceOwnerMapRef]);
$oauthUtilsDef->addMethodCall('addResourceOwnerMap', [$firewallName, $resourceOwnerMapRef]);
}
}
}
@@ -0,0 +1,441 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\DependencyInjection;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\GenericOAuth1ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\GenericOAuth2ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwnerInterface;
use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Finder\Finder;
/**
* Configuration for the extension.
*
* @author Alexander <iam.asm89@gmail.com>
*/
final class Configuration implements ConfigurationInterface
{
/**
* type => ResourceOwner mapping for hwi_oauth.resource_owner.*.class parameters.
*
* @var array<string, class-string<GenericOAuth1ResourceOwner|GenericOAuth2ResourceOwner|ResourceOwnerInterface>>
*/
private static array $resourceOwnerTypesClassMap = [];
/**
* Array of supported resource owners.
*
* @var array<string, string>
*/
private static array $resourceOwnerTypes = [];
public function __construct()
{
if ([] === self::$resourceOwnerTypes) {
self::loadResourceOwners();
}
}
public static function getResourceOwnerTypesClassMap(): array
{
return self::$resourceOwnerTypesClassMap;
}
/**
* Return the type (oauth1 or oauth2) of given resource owner.
*/
public static function getResourceOwnerType(string $resourceOwner): ?string
{
$resourceOwner = strtolower($resourceOwner);
return self::$resourceOwnerTypes[$resourceOwner] ?? null;
}
/**
* Checks that given resource owner is supported by this bundle.
*/
public static function isResourceOwnerSupported(string $resourceOwner): bool
{
return isset(self::$resourceOwnerTypes[strtolower($resourceOwner)]);
}
public static function registerResourceOwner(string $resourceOwnerClass): void
{
$reflection = new \ReflectionClass($resourceOwnerClass);
if (!$reflection->implementsInterface(ResourceOwnerInterface::class)) {
throw new \LogicException('Resource owner class should implement "ResourceOwnerInterface", or extended class "GenericOAuth1ResourceOwner"/"GenericOAuth2ResourceOwner".');
}
$type = \defined("$resourceOwnerClass::TYPE") ? $resourceOwnerClass::TYPE : null;
if (null === $type) {
if (preg_match('~(?P<resource_owner>[^\\\\]+)ResourceOwner$~', $resourceOwnerClass, $match)) {
$type = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $match['resource_owner']));
} else {
throw new \LogicException(sprintf('Resource owner class either should have "TYPE" const defined or end with "ResourceOwner" so that type can be calculated by converting its class name without suffix to "snake_case". Given class name is "%s"', $resourceOwnerClass));
}
}
$oAuth = 'unknown';
if ($reflection->isSubclassOf(GenericOAuth2ResourceOwner::class)) {
$oAuth = 'oauth2';
} elseif ($reflection->isSubclassOf(GenericOAuth1ResourceOwner::class)) {
$oAuth = 'oauth1';
}
self::$resourceOwnerTypes[$type] = $oAuth;
self::$resourceOwnerTypesClassMap[$type] = $resourceOwnerClass;
}
/**
* Generates the configuration tree builder.
*/
public function getConfigTreeBuilder(): TreeBuilder
{
$builder = new TreeBuilder('hwi_oauth');
/** @var ArrayNodeDefinition $rootNode */
$rootNode = $builder->getRootNode();
$rootNode
->fixXmlConfig('firewall_name')
->children()
->arrayNode('firewall_names')
->setDeprecated(...$this->getDeprecationParams())
->defaultValue([])
->prototype('scalar')->end()
->end()
->scalarNode('target_path_parameter')->defaultNull()->end()
->arrayNode('target_path_domains_whitelist')
->defaultValue([])
->prototype('scalar')->end()
->end()
->booleanNode('use_referer')->defaultFalse()->end()
->booleanNode('failed_use_referer')->defaultFalse()->end()
->scalarNode('failed_auth_path')->defaultValue('hwi_oauth_connect')->end()
->scalarNode('grant_rule')
->defaultValue('IS_AUTHENTICATED_REMEMBERED')
->validate()
->ifTrue(function ($role) {
return !('IS_AUTHENTICATED_REMEMBERED' === $role || 'IS_AUTHENTICATED_FULLY' === $role);
})
->thenInvalid('Unknown grant role set "%s".')
->end()
->end()
->end()
;
$this->addConnectConfiguration($rootNode);
$this->addResourceOwnersConfiguration($rootNode);
return $builder;
}
private function addResourceOwnersConfiguration(ArrayNodeDefinition $node): void
{
/* @phpstan-ignore-next-line */
$node
->fixXmlConfig('resource_owner')
->children()
->arrayNode('resource_owners')
->isRequired()
->useAttributeAsKey('name')
->prototype('array')
->ignoreExtraKeys()
->children()
->scalarNode('base_url')->end()
->scalarNode('access_token_url')
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('authorization_url')
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('request_token_url')
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('revoke_token_url')
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('infos_url')
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('client_id')->cannotBeEmpty()->end()
->scalarNode('client_secret')->cannotBeEmpty()->end()
->scalarNode('realm')
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('scope')
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('user_response_class')
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('service')
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('class')
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('type')
// will be validated in ResourceOwnerCompilerPass, other apps can register own resource
// owner maps later with tag hwi_oauth.resource_owner
->validate()
->ifEmpty()
->thenUnset()
->end()
->end()
->scalarNode('use_authorization_to_get_token')
->validate()
->ifTrue(function ($v) {
if (false === $v) {
return false;
}
return empty($v);
})
->thenUnset()
->end()
->end()
->arrayNode('paths')
->useAttributeAsKey('name')
->prototype('variable')
->validate()
->ifTrue(function ($v) {
if (null === $v) {
return true;
}
if (\is_array($v)) {
return 0 === \count($v);
}
if (\is_string($v)) {
return empty($v);
}
return !is_numeric($v);
})
->thenInvalid('Path can be only string or array type.')
->end()
->end()
->end()
->arrayNode('options')
->useAttributeAsKey('name')
->prototype('scalar')->end()
->end()
->end()
->validate()
->ifTrue(function ($c) {
// skip if this contains a service
if (isset($c['service'])) {
return false;
}
// for each type at least these have to be set
foreach (['client_id', 'client_secret'] as $child) {
if (!isset($c[$child])) {
return true;
}
}
if (!isset($c['type']) && !isset($c['class'])) {
return true;
}
return false;
})
->thenInvalid("You should set at least the 'type' or 'class' with 'client_id' and the 'client_secret' of a resource owner.")
->end()
->validate()
->ifTrue(function ($c) {
return isset($c['type'], $c['class']);
})
->then(function ($c) {
trigger_deprecation('hwi/oauth-bundle', '2.0', 'No need to set both "type" and "class" for resource owner.');
return $c;
})
->end()
->validate()
->ifTrue(function ($c) {
// Skip if this contains a service or a class
if (isset($c['service']) || isset($c['class'])) {
return false;
}
// Only validate the 'oauth2' and 'oauth1' type
if ('oauth2' !== $c['type'] && 'oauth1' !== $c['type']) {
return false;
}
$children = ['authorization_url', 'access_token_url', 'request_token_url', 'infos_url'];
foreach ($children as $child) {
// This option exists only for OAuth1.0a
if ('request_token_url' === $child && 'oauth2' === $c['type']) {
continue;
}
if (!isset($c[$child])) {
return true;
}
}
return false;
})
->thenInvalid("All parameters are mandatory for types 'oauth2' and 'oauth1'. Check if you're missing one of: 'access_token_url', 'authorization_url', 'infos_url' and 'request_token_url' for 'oauth1'.")
->end()
->validate()
->ifTrue(function ($c) {
// skip if this contains a service
if (isset($c['service']) || isset($c['class'])) {
return false;
}
// Only validate the 'oauth2' and 'oauth1' type
if ('oauth2' !== $c['type'] && 'oauth1' !== $c['type']) {
return false;
}
// one of this two options must be set
if (0 === \count($c['paths'])) {
return !isset($c['user_response_class']);
}
foreach (['identifier', 'nickname', 'realname'] as $child) {
if (!isset($c['paths'][$child])) {
return true;
}
}
return false;
})
->thenInvalid("At least the 'identifier', 'nickname' and 'realname' paths should be configured for 'oauth2' and 'oauth1' types.")
->end()
->validate()
->ifTrue(function ($c) {
if (isset($c['service'])) {
// ignore paths & options if none were set
return 0 !== \count($c['paths']) || 0 !== \count($c['options']) || 3 < \count($c);
}
return false;
})
->thenInvalid("If you're setting a 'service', no other arguments should be set.")
->end()
->validate()
->ifTrue(function ($c) {
return isset($c['class']);
})
->then(function ($c) {
self::registerResourceOwner($c['class']);
return $c;
})
->end()
->end()
->end()
->end()
;
}
private function addConnectConfiguration(ArrayNodeDefinition $node): void
{
$node
->children()
->arrayNode('connect')
->children()
->booleanNode('confirmation')->defaultTrue()->end()
->scalarNode('account_connector')->cannotBeEmpty()->end()
->scalarNode('registration_form_handler')->cannotBeEmpty()->end()
->scalarNode('registration_form')->cannotBeEmpty()->end()
->end()
->end()
->end()
;
}
private static function loadResourceOwners(): void
{
$files = (new Finder())
->in(__DIR__.'/../OAuth/ResourceOwner')
->name('~^(.+)ResourceOwner\.php$~')
->files();
foreach ($files as $f) {
if (!str_contains($f->getFilename(), 'ResourceOwner')) {
continue;
}
// Skip known abstract classes
if (\in_array($f->getFilename(), ['AbstractResourceOwner.php', 'GenericOAuth1ResourceOwner.php', 'GenericOAuth2ResourceOwner.php'], true)) {
continue;
}
self::registerResourceOwner('HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\\'.str_replace('.php', '', $f->getFilename()));
}
}
/**
* Returns the correct deprecation params as an array for setDeprecated().
*
* symfony/config v5.1 introduces a deprecation notice when calling
* setDeprecated() with less than 3 args and the getDeprecation() method was
* introduced at the same time. By checking if getDeprecation() exists,
* we can determine the correct param count to use when calling setDeprecated().
*
* @return string[]
*/
private function getDeprecationParams(): array
{
if (method_exists(BaseNode::class, 'getDeprecation')) {
return [
'hwi/oauth-bundle',
'2.0',
'option "%path%.%node%" is deprecated. Firewall names are collected automatically.',
];
}
return ['Since hwi/oauth-bundle 2.0: option "hwi_oauth.firewall_names" is deprecated. Firewall names are collected automatically.'];
}
}
@@ -0,0 +1,199 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\DependencyInjection;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwnerInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\BadMethodCallException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
* @author Geoffrey Bachelet <geoffrey.bachelet@gmail.com>
* @author Alexander <iam.asm89@gmail.com>
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class HWIOAuthExtension extends Extension
{
/**
* @var \ArrayIterator<string, true>
*/
private \ArrayIterator $firewallNames;
private bool $refreshTokenListenerEnabled = false;
public function __construct()
{
$this->firewallNames = new \ArrayIterator();
}
public function getConfiguration(array $config, ContainerBuilder $container): Configuration
{
return new Configuration();
}
/**
* {@inheritdoc}
*
* @throws \Exception
* @throws \RuntimeException
* @throws InvalidConfigurationException
* @throws BadMethodCallException
* @throws InvalidArgumentException
* @throws OutOfBoundsException
* @throws ServiceNotFoundException
*/
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config/'));
$loader->load('controller.php');
$loader->load('oauth.php');
$loader->load('resource_owners.php');
$loader->load('twig.php');
$loader->load('util.php');
$container->registerForAutoconfiguration(ResourceOwnerInterface::class)
->addTag('hwi_oauth.resource_owner');
$processor = new Processor();
$configuration = $this->getConfiguration($configs, $container);
$config = $processor->processConfiguration($configuration, $configs);
// set target path parameter
$container->setParameter('hwi_oauth.target_path_parameter', $config['target_path_parameter']);
// set target path domains whitelist parameter
$container->setParameter('hwi_oauth.target_path_domains_whitelist', $config['target_path_domains_whitelist']);
// set use referer parameter
$container->setParameter('hwi_oauth.use_referer', $config['use_referer']);
// set failed use referer parameter
$container->setParameter('hwi_oauth.failed_use_referer', $config['failed_use_referer']);
// set failed auth path
$container->setParameter('hwi_oauth.failed_auth_path', $config['failed_auth_path']);
// set grant rule
$container->setParameter('hwi_oauth.grant_rule', $config['grant_rule']);
// setup services for all configured resource owners
$resourceOwners = [];
$resourceOwnerReferenceMap = [];
foreach ($config['resource_owners'] as $name => $options) {
$resourceOwners[$name] = $name;
$resourceOwnerReferenceMap[$name] = $this->createResourceOwnerService($container, $name, $options);
if (!$this->refreshTokenListenerEnabled) {
$this->refreshTokenListenerEnabled = $options['options']['refresh_on_expire'] ?? false;
}
}
$container->setParameter('hwi_oauth.resource_owners', $resourceOwners);
$container->setAlias(
'hwi_oauth.resource_owners.locator',
(string) ServiceLocatorTagPass::register($container, $resourceOwnerReferenceMap)
);
$this->createConnectIntegration($container, $config);
}
/**
* Creates a resource owner service.
*
* @param ContainerBuilder $container The container builder
* @param string $name The name of the service
* @param array $options Additional options of the service
*
* @throws InvalidConfigurationException
* @throws BadMethodCallException
* @throws InvalidArgumentException
*/
public function createResourceOwnerService(ContainerBuilder $container, string $name, array $options): Reference
{
// alias services
if (isset($options['service'])) {
return new Reference($options['service']);
}
// handle external resource owners with given class
if (isset($options['class'])) {
if (!is_subclass_of($options['class'], ResourceOwnerInterface::class, true)) {
throw new InvalidConfigurationException(sprintf('Class "%s" must implement interface "HWI\Bundle\OAuthBundle\OAuth\ResourceOwnerInterface".', $options['class']));
}
$definition = new Definition($options['class']);
} else {
$definition = new Definition("%hwi_oauth.resource_owner.{$options['type']}.class%");
}
unset($options['class'], $options['type']);
$definition->setArgument('$httpClient', new Reference('hwi_oauth.http_client'));
$definition->setArgument('$httpUtils', new Reference('security.http_utils'));
$definition->setArgument('$options', $options);
$definition->setArgument('$name', $name);
$definition->setArgument('$storage', new Reference('hwi_oauth.storage.session'));
$container->setDefinition('hwi_oauth.resource_owner.'.$name, $definition);
return new Reference('hwi_oauth.resource_owner.'.$name);
}
/**
* {@inheritdoc}
*/
public function getAlias(): string
{
return 'hwi_oauth';
}
public function getFirewallNames(): \ArrayIterator
{
return $this->firewallNames;
}
public function isRefreshTokenListenerEnabled(): bool
{
return $this->refreshTokenListenerEnabled;
}
/**
* Check of the connect controllers etc should be enabled.
*
* @throws BadMethodCallException
* @throws InvalidArgumentException
*/
private function createConnectIntegration(ContainerBuilder $container, array $config): void
{
$container->setParameter('hwi_oauth.connect', isset($config['connect']));
$container->setParameter('hwi_oauth.connect.confirmation', $config['connect']['confirmation'] ?? false);
$container->setParameter('hwi_oauth.connect.registration_form', $config['connect']['registration_form'] ?? null);
if (isset($config['connect']['account_connector'])) {
$container->setAlias('hwi_oauth.account.connector', new Alias($config['connect']['account_connector'], true));
}
if (isset($config['connect']['registration_form_handler'])) {
$container->setAlias('hwi_oauth.registration.form.handler', new Alias($config['connect']['registration_form_handler'], true));
}
}
}
@@ -0,0 +1,328 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\DependencyInjection\Security\Factory;
use HWI\Bundle\OAuthBundle\Security\Http\Authenticator\OAuthAuthenticator;
use HWI\Bundle\OAuthBundle\Security\Http\Firewall\RefreshAccessTokenListener;
use HWI\Bundle\OAuthBundle\Security\Http\Firewall\RefreshAccessTokenListenerOld;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Geoffrey Bachelet <geoffrey.bachelet@gmail.com>
* @author Alexander <iam.asm89@gmail.com>
* @author Vadim Borodavko <vadim.borodavko@gmail.com>
*/
final class OAuthAuthenticatorFactory extends AbstractFactory implements AuthenticatorFactoryInterface, FirewallListenerFactoryInterface
{
public function __construct(private \ArrayIterator $firewallNames)
{
}
/**
* @param ArrayNodeDefinition $node
*/
public function addConfiguration(NodeDefinition $node): void
{
parent::addConfiguration($node);
$builder = $node->children();
$builder
->scalarNode('login_path')->cannotBeEmpty()->isRequired()->end()
;
$this->addOAuthProviderConfiguration($node);
$this->addResourceOwnersConfiguration($node);
}
/**
* {@inheritdoc}
*/
public function createAuthenticator(
ContainerBuilder $container,
string $firewallName,
array $config,
string $userProviderId
): string {
$authenticatorId = 'security.authenticator.oauth.'.$firewallName;
$this->createResourceOwnerMap($container, $firewallName, $config);
$container
->register($authenticatorId, OAuthAuthenticator::class)
->addArgument(new Reference('security.http_utils'))
->addArgument(
$this->createOAuthAwareUserProvider($container, $firewallName, $config['oauth_user_provider'])
)
->addArgument($this->createResourceOwnerMapReference($firewallName))
->addArgument($config['resource_owners'])
->addArgument(new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config)))
->addArgument(new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)))
->addArgument(new Reference('http_kernel'))
->addArgument(array_intersect_key($config, $this->options))
;
$this->firewallNames[$firewallName] = true;
return $authenticatorId;
}
public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array
{
$authenticatorId = 'security.authenticator.oauth.'.$firewallName;
$providerId = 'hwi_oauth.authentication.provider.oauth.'.$firewallName;
$listenerId = 'hwi_oauth.context_listener.token_refresher.'.$firewallName;
$listenerDef = $container->setDefinition($listenerId, new ChildDefinition('hwi_oauth.context_listener.abstract_token_refresher'));
$listenerDef->addMethodCall('setResourceOwnerMap', [$this->createResourceOwnerMapReference($firewallName)]);
if ($container->hasDefinition($authenticatorId)) {
// new auth manager
$listenerDef
->setClass(RefreshAccessTokenListener::class)
->replaceArgument(0, new Reference($authenticatorId));
} else {
// old auth manager
$listenerDef
->setClass(RefreshAccessTokenListenerOld::class)
->replaceArgument(0, new Reference($providerId));
}
return [$listenerId];
}
public function getPriority(): int
{
return 0;
}
/**
* {@inheritdoc}
*/
public function getKey(): string
{
return 'oauth';
}
/**
* {@inheritdoc}
*/
public function getPosition(): string
{
return 'http';
}
public function getFirewallNames(): \ArrayIterator
{
return $this->firewallNames;
}
protected function createAuthenticationFailureHandler(ContainerBuilder $container, string $id, array $config): string
{
$id = $this->getFailureHandlerId($id);
$options = array_intersect_key($config, $this->defaultFailureHandlerOptions);
$failureHandler = $container->setDefinition($id, new ChildDefinition('security.authentication.custom_failure_handler'));
$failureHandler->replaceArgument(0, new ChildDefinition('hwi_oauth.authentication.failure_handler'));
$failureHandler->replaceArgument(1, $options);
return $id;
}
/**
* {@inheritdoc}
*/
protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId): string
{
$providerId = 'hwi_oauth.authentication.provider.oauth.'.$id;
$this->createResourceOwnerMap($container, $id, $config);
$container
->setDefinition($providerId, new ChildDefinition('hwi_oauth.authentication.provider.oauth'))
->addArgument($this->createOAuthAwareUserProvider($container, $id, $config['oauth_user_provider']))
->addArgument($this->createResourceOwnerMapReference($id))
->addArgument(new Reference('hwi_oauth.user_checker'))
->addArgument(new Reference('security.token_storage'))
;
$this->firewallNames[$id] = true;
return $providerId;
}
/**
* {@inheritdoc}
*/
protected function createEntryPoint($container, $id, $config, ?string $defaultEntryPointId): ?string
{
$entryPointId = 'hwi_oauth.authentication.entry_point.oauth.'.$id;
$container
->setDefinition($entryPointId, new ChildDefinition('hwi_oauth.authentication.entry_point.oauth'))
->addArgument($config['login_path'])
->addArgument($config['use_forward'])
;
return $entryPointId;
}
protected function createListener(ContainerBuilder $container, string $id, array $config, string $userProvider): string
{
// @phpstan-ignore-next-line Symfony <5.4 BC layer
$listenerId = parent::createListener($container, $id, $config, $userProvider);
$checkPaths = $config['resource_owners'];
$container
->getDefinition($listenerId)
->addMethodCall('setResourceOwnerMap', [$this->createResourceOwnerMapReference($id)])
->addMethodCall('setCheckPaths', [$checkPaths])
;
return $listenerId;
}
/**
* {@inheritdoc}
*/
protected function getListenerId(): string
{
return 'hwi_oauth.authentication.listener.oauth';
}
/**
* Gets a reference to the resource owner map.
*/
private function createResourceOwnerMapReference(string $firewallName): Reference
{
return new Reference('hwi_oauth.resource_ownermap.'.$firewallName);
}
/**
* Creates a resource owner map for the given configuration.
*/
private function createResourceOwnerMap(ContainerBuilder $container, string $firewallName, array $config): void
{
$resourceOwnersMap = [];
foreach ($config['resource_owners'] as $name => $checkPath) {
$resourceOwnersMap[$name] = $checkPath;
}
$container->setParameter('hwi_oauth.resource_ownermap.configured.'.$firewallName, $resourceOwnersMap);
$container
->setDefinition(
$this->createResourceOwnerMapReference($firewallName),
new ChildDefinition('hwi_oauth.abstract_resource_ownermap')
)
->replaceArgument('$resourceOwners', new Parameter('hwi_oauth.resource_ownermap.configured.'.$firewallName))
->setPublic(true)
;
}
private function createOAuthAwareUserProvider(ContainerBuilder $container, $id, $config): Reference
{
$serviceId = 'hwi_oauth.user.provider.entity.'.$id;
// todo: move this to factories?
switch (key($config)) {
case 'oauth':
$container
->setDefinition($serviceId, new ChildDefinition('hwi_oauth.user.provider'))
;
break;
case 'orm':
$container
->setDefinition($serviceId, new ChildDefinition('hwi_oauth.user.provider.entity'))
->addArgument($config['orm']['class'])
->addArgument($config['orm']['properties'])
->addArgument($config['orm']['manager_name'])
;
break;
case 'service':
$container
->setAlias($serviceId, $config['service']);
break;
}
return new Reference($serviceId);
}
private function addOAuthProviderConfiguration(ArrayNodeDefinition $node): void
{
$builder = $node->children();
$builder
->arrayNode('oauth_user_provider')
->isRequired()
->children()
->arrayNode('orm')
->children()
->scalarNode('class')->isRequired()->cannotBeEmpty()->end()
->scalarNode('manager_name')->defaultNull()->end()
->arrayNode('properties')
->isRequired()
->useAttributeAsKey('name')
->prototype('scalar')
->end()
->end()
->end()
->end()
->scalarNode('service')->cannotBeEmpty()->end()
->scalarNode('oauth')->end()
->end()
->validate()
->ifTrue(function ($c) {
return 1 !== \count($c) || !\in_array(key($c), ['oauth', 'orm', 'service'], true);
})
->thenInvalid("You should configure (only) one of: 'oauth', 'orm', 'service'.")
->end()
->end()
;
}
private function addResourceOwnersConfiguration(ArrayNodeDefinition $node): void
{
$builder = $node->children();
$builder
->arrayNode('resource_owners')
->isRequired()
->useAttributeAsKey('name')
->prototype('scalar')
->end()
->validate()
->ifTrue(function ($c) {
$checkPaths = [];
foreach ($c as $checkPath) {
if (\in_array($checkPath, $checkPaths, true)) {
return true;
}
$checkPaths[] = $checkPath;
}
return false;
})
->thenInvalid('Each resource owner should have a unique "check_path".')
->end()
->end()
;
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Event;
use Symfony\Contracts\EventDispatcher\Event;
abstract class AbstractEvent extends Event
{
}
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @author Marek Štípek
*/
final class FilterUserResponseEvent extends UserEvent
{
private Response $response;
public function __construct(UserInterface $user, Request $request, Response $response)
{
parent::__construct($user, $request);
$this->response = $response;
}
public function getResponse(): Response
{
return $this->response;
}
public function setResponse(Response $response): void
{
$this->response = $response;
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Event;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @author Marek Štípek
*/
final class FormEvent extends AbstractEvent
{
private FormInterface $form;
private Request $request;
private ?Response $response = null;
public function __construct(FormInterface $form, Request $request)
{
$this->form = $form;
$this->request = $request;
}
public function getForm(): FormInterface
{
return $this->form;
}
public function getRequest(): Request
{
return $this->request;
}
public function setResponse(Response $response): void
{
$this->response = $response;
}
public function getResponse(): ?Response
{
return $this->response;
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Event;
use Symfony\Component\HttpFoundation\Response;
/**
* @author Marek Štípek
*/
final class GetResponseUserEvent extends UserEvent
{
private ?Response $response = null;
public function setResponse(Response $response): void
{
$this->response = $response;
}
public function getResponse(): ?Response
{
return $this->response;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @author Marek Štípek
*/
class UserEvent extends AbstractEvent
{
/**
* @var UserInterface
*/
private $user;
/**
* @var Request
*/
private $request;
public function __construct(UserInterface $user, Request $request)
{
$this->user = $user;
$this->request = $request;
}
/**
* @return UserInterface
*/
public function getUser()
{
return $this->user;
}
/**
* @return Request
*/
public function getRequest()
{
return $this->request;
}
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\Form;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* RegistrationFormHandlerInterface.
*
* Interface for objects that are able to handle a form.
*
* @author Alexander <iam.asm89@gmail.com>
*/
interface RegistrationFormHandlerInterface
{
/**
* Processes the form for a given request.
*
* @return bool True if the processing was successful
*/
public function process(Request $request, FormInterface $form, UserResponseInterface $userInformation);
}
+62
View File
@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle;
use HWI\Bundle\OAuthBundle\DependencyInjection\CompilerPass\EnableRefreshOAuthTokenListenerCompilerPass;
use HWI\Bundle\OAuthBundle\DependencyInjection\CompilerPass\ResourceOwnerCompilerPass;
use HWI\Bundle\OAuthBundle\DependencyInjection\Security\Factory\OAuthAuthenticatorFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* @author Geoffrey Bachelet <geoffrey.bachelet@gmail.com>
* @author Alexander <geoffrey.bachelet@gmail.com>
*/
class HWIOAuthBundle extends Bundle
{
/**
* {@inheritdoc}
*/
public function build(ContainerBuilder $container): void
{
parent::build($container);
/** @var SecurityExtension $extension */
$extension = $container->getExtension('security');
$firewallNames = $this->extension->getFirewallNames();
if (method_exists($extension, 'addAuthenticatorFactory')) {
$extension->addAuthenticatorFactory(new OAuthAuthenticatorFactory($firewallNames));
} elseif (method_exists($extension, 'addSecurityListenerFactory')) {
// Symfony < 5.4 BC layer
$extension->addSecurityListenerFactory(new OAuthAuthenticatorFactory($firewallNames));
} else {
throw new \RuntimeException('Unsupported Symfony Security component version');
}
$container->addCompilerPass(new ResourceOwnerCompilerPass());
$container->addCompilerPass(new EnableRefreshOAuthTokenListenerCompilerPass());
}
/**
* {@inheritdoc}
*/
public function getContainerExtension(): ?ExtensionInterface
{
// return the right extension instead of "auto-registering" it. Now the
// alias can be hwi_oauth instead of hwi_o_auth.
return $this->extension ?: $this->extension = $this->createContainerExtension();
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle;
/**
* @author Marek Štípek
*/
final class HWIOAuthEvents
{
/**
* @Event("HWI\Bundle\OAuthBundle\Event\GetResponseUserEvent")
*/
public const REGISTRATION_INITIALIZE = 'hwi_oauth.registration.initialize';
/**
* @Event("HWI\Bundle\OAuthBundle\Event\FormEvent")
*/
public const REGISTRATION_SUCCESS = 'hwi_oauth.registration.success';
/**
* @Event("HWI\Bundle\OAuthBundle\Event\GetResponseUserEvent")
*/
public const REGISTRATION_COMPLETED = 'hwi_oauth.registration.completed';
/**
* @Event("HWI\Bundle\OAuthBundle\Event\GetResponseUserEvent")
*/
public const CONNECT_INITIALIZE = 'hwi_oauth.connect.initialize';
/**
* @Event("HWI\Bundle\OAuthBundle\Event\GetResponseUserEvent")
*/
public const CONNECT_CONFIRMED = 'hwi_oauth.connect.confirmed';
/**
* @Event("HWI\Bundle\OAuthBundle\Event\FilterUserResponseEvent")
*/
public const CONNECT_COMPLETED = 'hwi_oauth.connect.completed';
}
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
final class HttpTransportException extends AuthenticationException
{
private string $ownerName;
public function __construct(string $message, string $ownerName, int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->ownerName = $ownerName;
}
public function getOwnerName(): string
{
return $this->ownerName;
}
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\Exception;
final class StateRetrievalException extends \InvalidArgumentException
{
/**
* @param string $key The provided string key
*/
public static function forKey(string $key): self
{
return new static(sprintf('No value found in state for key [%s]', $key));
}
}
@@ -0,0 +1,121 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\RequestDataStorage;
use HWI\Bundle\OAuthBundle\OAuth\RequestDataStorageInterface;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwnerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Request token storage implementation using the Symfony session.
*
* @author Alexander <iam.asm89@gmail.com>
* @author Francisco Facioni <fran6co@gmail.com>
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class SessionStorage implements RequestDataStorageInterface
{
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
/**
* {@inheritdoc}
*/
public function fetch(ResourceOwnerInterface $resourceOwner, $key, $type = 'token')
{
$key = $this->generateKey($resourceOwner, $key, $type);
if (null === $data = $this->getSession()->get($key)) {
throw new \InvalidArgumentException('No data available in storage.');
}
// Request tokens are one time use only
if (\in_array($type, ['token', 'csrf_state'], true)) {
$this->getSession()->remove($key);
}
return $data;
}
/**
* {@inheritdoc}
*/
public function save(ResourceOwnerInterface $resourceOwner, $value, $type = 'token')
{
if ('token' === $type) {
if (!\is_array($value) || !isset($value['oauth_token'])) {
throw new \InvalidArgumentException('Invalid request token.');
}
$key = $this->generateKey($resourceOwner, $value['oauth_token'], 'token');
} else {
$key = $this->generateKey($resourceOwner, $this->getStorageKey($value), $type);
}
$this->getSession()->set($key, $this->getStorageValue($value));
}
/**
* Key to for fetching or saving a token.
*/
private function generateKey(ResourceOwnerInterface $resourceOwner, string $key, string $type): string
{
return sprintf('_hwi_oauth.%s.%s.%s.%s', $resourceOwner->getName(), $resourceOwner->getOption('client_id'), $type, $key);
}
/**
* @param array|string|object $value
*
* @return array|string
*/
private function getStorageValue($value)
{
if (\is_object($value)) {
$value = serialize($value);
}
return $value;
}
/**
* @param array|string|object $value
*/
private function getStorageKey($value): string
{
if (\is_array($value)) {
$storageKey = reset($value);
} elseif (\is_object($value)) {
$storageKey = $value::class;
} else {
$storageKey = $value;
}
return (string) $storageKey;
}
private function getSession(): SessionInterface
{
if (method_exists($this->requestStack, 'getSession')) {
return $this->requestStack->getSession();
}
if ((null !== $request = $this->requestStack->getCurrentRequest()) && $request->hasSession()) {
return $request->getSession();
}
throw new \LogicException('There is currently no session available.');
}
}
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth;
/**
* Interface for classes providing a request tokens storage.
*
* The storage is needed because the OAuth1.0a authentication flow requires
* requests to be signed with the same values in consecutive requests.
*
* Additionally we require this to provide CSRF protection for all resource
* owners.
*
* @author Alexander <iam.asm89@gmail.com>
* @author Francisco Facioni <fran6co@gmail.com>
* @author Joseph Bielawski <stloyd@gmail.com>
*/
interface RequestDataStorageInterface
{
/**
* Fetch a request data from the storage.
*
* @param string $key
* @param string $type
*/
public function fetch(ResourceOwnerInterface $resourceOwner, $key, $type = 'token');
/**
* Save a request data to the storage.
*
* @param array|string|object $value
* @param string $type
*/
public function save(ResourceOwnerInterface $resourceOwner, $value, $type = 'token');
}
@@ -0,0 +1,347 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\OAuth\RequestDataStorageInterface;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwnerInterface;
use HWI\Bundle\OAuthBundle\OAuth\Response\PathUserResponse;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\OAuth\State\State;
use HWI\Bundle\OAuthBundle\OAuth\StateInterface;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\OptionsResolver\Exception\AccessException;
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Geoffrey Bachelet <geoffrey.bachelet@gmail.com>
* @author Alexander <iam.asm89@gmail.com>
* @author Francisco Facioni <fran6co@gmail.com>
* @author Joseph Bielawski <stloyd@gmail.com>
*/
abstract class AbstractResourceOwner implements ResourceOwnerInterface
{
protected array $options = [];
/**
* @var array<string, array<int, string>|string|null>
*/
protected array $paths = [];
protected HttpClientInterface $httpClient;
protected HttpUtils $httpUtils;
protected string $name;
protected StateInterface $state;
protected RequestDataStorageInterface $storage;
private bool $stateLoaded = false;
/**
* @param array $options Options for the resource owner
* @param string $name Name for the resource owner
*/
public function __construct(
HttpClientInterface $httpClient,
HttpUtils $httpUtils,
array $options,
string $name,
RequestDataStorageInterface $storage
) {
$this->httpClient = $httpClient;
$this->httpUtils = $httpUtils;
$this->name = $name;
$this->storage = $storage;
if (!empty($options['paths'])) {
$this->addPaths($options['paths']);
}
unset($options['paths']);
if (!empty($options['options'])) {
$options += $options['options'];
unset($options['options']);
}
unset($options['options']);
// Resolve merged options
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$this->options = $resolver->resolve($options);
$this->state = new State($this->options['state'] ?: null);
$this->configure();
}
/**
* Gives a chance for extending providers to customize stuff.
*/
public function configure()
{
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getOption($name)
{
if (!\array_key_exists($name, $this->options)) {
throw new \InvalidArgumentException(sprintf('Unknown option "%s"', $name));
}
return $this->options[$name];
}
/**
* {@inheritdoc}
*/
public function addPaths(array $paths)
{
$this->paths = array_merge($this->paths, $paths);
}
/**
* {@inheritdoc}
*/
public function getState(): StateInterface
{
if ($this->stateLoaded) {
return $this->state;
}
// lazy-loading for stored states
try {
$storedData = $this->storage->fetch($this, State::class, 'state');
} catch (\Throwable $e) {
$storedData = null;
}
if (null !== $storedData && false !== $storedState = unserialize($storedData)) {
foreach ($storedState->getAll() as $key => $value) {
$this->addStateParameter($key, $value);
}
}
$this->stateLoaded = true;
return $this->state;
}
/**
* {@inheritdoc}
*/
public function addStateParameter(string $key, string $value): void
{
if (!$this->state->has($key)) {
$this->state->add($key, $value);
}
}
/**
* {@inheritdoc}
*/
public function storeState(?StateInterface $state = null): void
{
if (null === $state || 0 === \count($state->getAll())) {
return;
}
$this->storage->save($this, $state, 'state');
}
/**
* Retrieve an access token for a given code.
*
* @param HttpRequest $request The request object from where the code is going to extracted
* @param mixed $redirectUri The uri to redirect the client back to
* @param array $extraParameters An array of parameters to add to the url
*
* @return array array containing the access token and it's 'expires_in' value,
* along with any other parameters returned from the authentication
* provider
*
* @throws AuthenticationException If an OAuth error occurred or no access token is found
* @throws HttpTransportException
*/
abstract public function getAccessToken(HttpRequest $request, $redirectUri, array $extraParameters = []);
/**
* Refresh an access token using a refresh token.
*
* @param string $refreshToken Refresh token
* @param array $extraParameters An array of parameters to add to the url
*
* @return array array containing the access token and it's 'expires_in' value,
* along with any other parameters returned from the authentication
* provider
*
* @throws AuthenticationException If an OAuth error occurred or no access token is found
* @throws HttpTransportException
*/
public function refreshAccessToken($refreshToken, array $extraParameters = [])
{
throw new AuthenticationException('OAuth error: "Method unsupported."');
}
/**
* Revoke an OAuth access token or refresh token.
*
* @param string $token the token (access token or a refresh token) that should be revoked
*
* @return bool returns True if the revocation was successful, otherwise False
*
* @throws AuthenticationException If an OAuth error occurred
* @throws HttpTransportException
*/
public function revokeToken($token)
{
throw new AuthenticationException('OAuth error: "Method unsupported."');
}
/**
* Get the response object to return.
*
* @return UserResponseInterface
*/
protected function getUserResponse()
{
$response = new $this->options['user_response_class']();
if ($response instanceof PathUserResponse) {
$response->setPaths($this->paths);
}
return $response;
}
/**
* @param string $url
*
* @return string
*/
protected function normalizeUrl($url, array $parameters = [])
{
$normalizedUrl = $url;
if (!empty($parameters)) {
$normalizedUrl .= (str_contains($url, '?') ? '&' : '?').http_build_query($parameters, '', '&');
}
return $normalizedUrl;
}
/**
* Performs an HTTP request.
*
* @param string $url The url to fetch
* @param string|array $content The content of the request
* @param array $headers The headers of the request
* @param string $method The HTTP method to use
*
* @return ResponseInterface The response content
*
* @throws HttpTransportException
*/
protected function httpRequest($url, $content = null, array $headers = [], $method = null)
{
if (null === $method) {
$method = null === $content || '' === $content ? 'GET' : 'POST';
}
$options = ['headers' => $headers];
$options['headers'] += ['User-Agent' => 'HWIOAuthBundle (https://github.com/hwi/HWIOAuthBundle)'];
if (\is_string($content)) {
if (!isset($options['headers']['Content-Length'])) {
$options['headers'] += ['Content-Length' => (string) \strlen($content)];
}
}
$options['body'] = $content;
try {
return $this->httpClient->request(
$method,
$url,
$options
);
} catch (TransportExceptionInterface $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
protected function getResponseContent(ResponseInterface $rawResponse): array
{
try {
return $rawResponse->toArray(false);
} catch (JsonException $e) {
parse_str($rawResponse->getContent(false), $response);
return $response;
} catch (TransportExceptionInterface $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* @param string $url
*
* @return ResponseInterface
*
* @throws HttpTransportException
*/
abstract protected function doGetTokenRequest($url, array $parameters = []);
/**
* @param string $url
*
* @return ResponseInterface
*
* @throws HttpTransportException
*/
abstract protected function doGetUserInformationRequest($url, array $parameters = []);
/**
* Configure the option resolver.
*
* @throws AccessException
* @throws UndefinedOptionsException
*/
protected function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired([
'client_id',
'client_secret',
'authorization_url',
'access_token_url',
'infos_url',
]);
$resolver->setDefaults([
'scope' => null,
'state' => null,
'csrf' => false,
'user_response_class' => PathUserResponse::class,
'auth_with_one_url' => false,
]);
$resolver->setAllowedValues('csrf', [true, false]);
}
}
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Fabian Kiss <fabian.kiss@ymc.ch>
*/
final class AmazonResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'amazon';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'user_id',
'nickname' => 'name',
'realname' => 'name',
'email' => 'email',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://www.amazon.com/ap/oa',
'access_token_url' => 'https://api.amazon.com/auth/o2/token',
'infos_url' => 'https://api.amazon.com/user/profile',
'scope' => 'profile',
]);
}
}
@@ -0,0 +1,196 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Firebase\JWT\JWT;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use HWI\Bundle\OAuthBundle\Security\OAuthErrorHandler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Geoffrey Bachelet <geoffrey.bachelet@gmail.com>
* @author Josip Letica <leticajosip.09@gmail.com>
* @author Sébastien Alfaiate <seb33300@hotmail.com>
*/
final class AppleResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'apple';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'sub',
'firstname' => 'firstName',
'lastname' => 'lastName',
'email' => 'email',
];
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
return parent::getAuthorizationUrl($redirectUri, array_merge([
'response_mode' => $this->options['response_mode'],
], $extraParameters));
}
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
if (!isset($accessToken['id_token'])) {
throw new \InvalidArgumentException('Undefined index id_token');
}
$jwt = self::jwtDecode($accessToken['id_token']);
$data = $jwt ? json_decode(base64_decode($jwt), true) : [];
if (isset($accessToken['firstName'], $accessToken['lastName'])) {
$data['firstName'] = $accessToken['firstName'];
$data['lastName'] = $accessToken['lastName'];
}
$response = $this->getUserResponse();
$response->setData(json_encode($data));
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
}
/**
* {@inheritdoc}
*/
public function getAccessToken(Request $request, $redirectUri, array $extraParameters = [])
{
OAuthErrorHandler::handleOAuthError($request);
$parameters = array_merge([
'code' => $request->request->get('code'),
'grant_type' => 'authorization_code',
'client_id' => $this->options['client_id'],
'client_secret' => $this->getClientSecret(),
'redirect_uri' => $redirectUri,
], $extraParameters);
$response = $this->doGetTokenRequest($this->options['access_token_url'], $parameters);
$response = $this->getResponseContent($response);
$this->validateResponseContent($response);
$userInfo = $request->request->get('user');
if (null !== $userInfo) {
$userInfo = json_decode($userInfo, true, 512, \JSON_THROW_ON_ERROR);
if (isset($userInfo['name'])) {
$response['firstName'] = $userInfo['name']['firstName'];
$response['lastName'] = $userInfo['name']['lastName'];
}
}
return $response;
}
/**
* {@inheritdoc}
*/
public function refreshAccessToken($refreshToken, array $extraParameters = [])
{
$parameters = [
'client_id' => $this->options['client_id'],
'client_secret' => $this->options['client_secret'],
];
return parent::refreshAccessToken($refreshToken, array_merge($parameters, $extraParameters));
}
/**
* {@inheritdoc}
*/
public function handles(Request $request)
{
return $request->request->has('code');
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://appleid.apple.com/auth/authorize',
'access_token_url' => 'https://appleid.apple.com/auth/token',
'infos_url' => '',
'use_commas_in_scope' => false,
'display' => null,
'scope' => 'name email',
'appsecret_proof' => false,
'response_mode' => 'form_post',
'auth_key' => null,
'key_id' => null,
'team_id' => null,
]);
}
private static function jwtDecode(string $idToken)
{
// from http://stackoverflow.com/a/28748285/624544
[, $jwt] = explode('.', $idToken, 3);
// if the token was urlencoded, do some fixes to ensure that it is valid base64 encoded
$jwt = str_replace(['-', '_'], ['+', '/'], $jwt);
// complete token if needed
switch (\strlen($jwt) % 4) {
case 0:
break;
case 2:
case 3:
$jwt .= '=';
break;
default:
throw new \InvalidArgumentException('Invalid base64 format sent back');
}
return $jwt;
}
private function getClientSecret(): string
{
if ('auto' !== $this->options['client_secret']) {
return $this->options['client_secret'];
}
if (!isset($this->options['auth_key'], $this->options['key_id'], $this->options['team_id'])) {
throw new \InvalidArgumentException('Options "auth_key", "key_id" and "team_id" must be defined to use automatic "client_secret" generation.');
}
if (!class_exists(JWT::class)) {
throw new \RuntimeException('PHP-JWT library is required to use automatic "client_secret" generation. Please try "composer require firebase/php-jwt".');
}
$payload = [
'iss' => $this->options['team_id'],
'iat' => time(),
'exp' => time() + 600,
'aud' => 'https://appleid.apple.com',
'sub' => $this->options['client_id'],
];
return JWT::encode($payload, $this->options['auth_key'], 'ES256', $this->options['key_id']);
}
}
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Guillaume Potier <guillaume@wisembly.com>
*/
final class AsanaResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'asana';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'data.id',
'nickname' => 'data.name',
'realname' => 'data.name',
'email' => 'data.email',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://app.asana.com/-/oauth_authorize',
'access_token_url' => 'https://app.asana.com/-/oauth_token',
'infos_url' => 'https://app.asana.com/api/1.0/users/me',
]);
}
}
@@ -0,0 +1,82 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Hernan Rajchert <hrajchert@gmail.com>
*/
final class Auth0ResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'auth0';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'sub',
'nickname' => 'nickname',
'realname' => 'name',
'email' => 'email',
'profilepicture' => 'picture',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$auth0Client = base64_encode(json_encode([
'name' => 'HWIOAuthBundle',
'version' => 'unknown',
'environment' => [
'name' => 'PHP',
'version' => \PHP_VERSION,
],
]));
$resolver->setDefaults([
'authorization_url' => '{base_url}/authorize?auth0Client='.$auth0Client,
'access_token_url' => '{base_url}/oauth/token',
'infos_url' => '{base_url}/userinfo',
'auth0_client' => $auth0Client,
]);
$resolver->setRequired([
'base_url',
]);
$normalizer = function (Options $options, $value) {
return str_replace('{base_url}', $options['base_url'], $value);
};
$resolver->setNormalizer('authorization_url', $normalizer);
$resolver->setNormalizer('access_token_url', $normalizer);
$resolver->setNormalizer('infos_url', $normalizer);
}
/**
* {@inheritdoc}
*/
protected function httpRequest($url, $content = null, array $headers = [], $method = null)
{
if (isset($this->options['auth0_client'])) {
$headers['Auth0-Client'] = $this->options['auth0_client'];
}
return parent::httpRequest($url, $content, $headers, $method);
}
}
@@ -0,0 +1,94 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Baptiste Clavié <clavie.b@gmail.com>
*/
final class AzureResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'azure';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'sub',
'nickname' => 'unique_name',
'lastname' => 'family_name',
'firstname' => 'given_name',
'realname' => ['given_name', 'family_name'],
'email' => ['upn', 'email'],
'profilepicture' => null,
];
/**
* {@inheritdoc}
*/
public function configure()
{
$this->options['access_token_url'] = sprintf($this->options['access_token_url'], $this->options['application']);
$this->options['authorization_url'] = sprintf($this->options['authorization_url'], $this->options['application']);
}
/**
* {@inheritdoc}
*
* @throws \InvalidArgumentException
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
// from http://stackoverflow.com/a/28748285/624544
[, $jwt] = explode('.', \array_key_exists('id_token', $accessToken) ? $accessToken['id_token'] : $accessToken['access_token'], 3);
// if the token was urlencoded, do some fixes to ensure that it is valid base64 encoded
$jwt = str_replace(['-', '_'], ['+', '/'], $jwt);
// complete token if needed
switch (\strlen($jwt) % 4) {
case 0:
break;
case 2:
case 3:
$jwt .= '=';
break;
default:
throw new \InvalidArgumentException('Invalid base64 format sent back');
}
$response = parent::getUserInformation($accessToken, $extraParameters);
$response->setData(base64_decode($jwt));
return $response;
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'infos_url' => 'https://graph.microsoft.com/v1.0/me',
'authorization_url' => 'https://login.microsoftonline.com/%s/oauth2/v2.0/authorize',
'access_token_url' => 'https://login.microsoftonline.com/%s/oauth2/v2.0/token',
'application' => 'common',
'api_version' => 'v1.0',
'csrf' => true,
]);
}
}
@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author David Sanchez <david38sanchez@gmail.com>
*/
final class Bitbucket2ResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'bitbucket2';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'uuid',
'nickname' => 'username',
'email' => 'email',
'realname' => 'display_name',
'profilepicture' => 'links.avatar.href',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$response = parent::getUserInformation($accessToken, $extraParameters);
$responseData = $response->getData();
// fetch the email addresses linked to the account
if (empty($responseData['email'])) {
$content = $this->httpRequest($this->normalizeUrl($this->options['emails_url']), null, ['Authorization' => 'Bearer '.$accessToken['access_token']]);
foreach ($this->getResponseContent($content)['values'] as $email) {
// we only need the primary email address
if (true === $email['is_primary']) {
$responseData['email'] = $email['email'];
}
}
$response->setData($responseData);
}
return $response;
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://bitbucket.org/site/oauth2/authorize',
'access_token_url' => 'https://bitbucket.org/site/oauth2/access_token',
'infos_url' => 'https://api.bitbucket.org/2.0/user',
'emails_url' => 'https://api.bitbucket.org/2.0/user/emails',
]);
}
}
@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class BitbucketResourceOwner extends GenericOAuth1ResourceOwner
{
public const TYPE = 'bitbucket';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'user.username',
'nickname' => 'user.username',
'realname' => 'user.display_name',
'profilepicture' => 'user.avatar',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://bitbucket.org/api/1.0/oauth/authenticate',
'request_token_url' => 'https://bitbucket.org/api/1.0/oauth/request_token',
'access_token_url' => 'https://bitbucket.org/api/1.0/oauth/access_token',
'infos_url' => 'https://bitbucket.org/api/1.0/user',
]);
}
}
@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class BitlyResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'bitly';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'data.login',
'nickname' => 'data.display_name',
'realname' => 'data.full_name',
'profilepicture' => 'data.profile_image',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'use_bearer_authorization' => false,
'authorization_url' => 'https://bitly.com/oauth/authorize',
'access_token_url' => 'https://api-ssl.bitly.com/oauth/access_token',
'infos_url' => 'https://api-ssl.bitly.com/v3/user/info?format=json',
]);
}
}
@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class BoxResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'box';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'name',
'realname' => 'name',
'email' => 'login',
'profilepicture' => 'avatar_url',
];
/**
* {@inheritdoc}
*/
public function revokeToken($token)
{
$parameters = [
'client_id' => $this->options['client_id'],
'client_secret' => $this->options['client_secret'],
'token' => $token,
];
$response = $this->httpRequest($this->normalizeUrl($this->options['revoke_token_url']), $parameters, [], 'POST');
return 200 === $response->getStatusCode();
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://www.box.com/api/oauth2/authorize',
'access_token_url' => 'https://www.box.com/api/oauth2/token',
'revoke_token_url' => 'https://www.box.com/api/oauth2/revoke',
'infos_url' => 'https://api.box.com/2.0/users/me',
]);
}
}
@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author João Paulo Cercal <sistemas@cekurte.com>
*/
final class BufferAppResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'bufferapp';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'id',
'realname' => 'id',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://bufferapp.com/oauth2/authorize',
'access_token_url' => 'https://api.bufferapp.com/1/oauth2/token.json',
'infos_url' => 'https://api.bufferapp.com/1/user.json',
]);
}
}
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Matt Farmer <work@mattfarmer.net>
*/
final class CleverResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'clever';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'data.id',
'email' => 'data.email',
'firstname' => 'data.name.first',
'lastname' => 'data.name.last',
'realname' => [
'data.name.first',
'data.name.middle',
'data.name.last',
],
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://clever.com/oauth/authorize',
'access_token_url' => 'https://clever.com/oauth/tokens',
'infos_url' => 'https://api.clever.com/me',
]);
}
/**
* {@inheritdoc}
*/
protected function doGetTokenRequest($url, array $parameters = [])
{
$authPreHash = $this->options['client_id'].':'.$this->options['client_secret'];
return $this->httpRequest(
$url,
http_build_query($parameters, '', '&'),
[
'Authorization' => 'Basic '.base64_encode($authPreHash),
]
);
}
}
@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class DailymotionResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'dailymotion';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'screenname',
'realname' => 'fullname', // requires 'userinfo' scope
'email' => 'email', // requires 'email' scope
'profilepicture' => 'avatar_medium_url',
];
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
return parent::getAuthorizationUrl($redirectUri, array_merge(['display' => $this->options['display']], $extraParameters));
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://api.dailymotion.com/oauth/authorize',
'access_token_url' => 'https://api.dailymotion.com/oauth/token',
'infos_url' => 'https://api.dailymotion.com/me',
'display' => null,
]);
// @link http://www.dailymotion.com/doc/api/authentication.html#dialog-form-factors
$resolver->setAllowedValues('display', ['page', 'popup', 'mobile', null]);
}
}
@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Kieu Anh Tuan <passkey1510@gmail.com>
*/
final class DeezerResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'deezer';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'name',
'realname' => 'firstname',
'email' => 'email',
'firstname' => 'firstname',
'lastname' => 'lastname',
'profilepicture' => 'picture',
'gender' => 'gender',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://connect.deezer.com/oauth/auth.php',
'access_token_url' => 'https://connect.deezer.com/oauth/access_token.php',
'infos_url' => 'https://api.deezer.com/user/me',
'use_bearer_authorization' => false,
]);
}
}
@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class DeviantartResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'deviantart';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'username',
'nickname' => 'username',
'profilepicture' => 'usericonurl',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://www.deviantart.com/oauth2/draft15/authorize',
'access_token_url' => 'https://www.deviantart.com/oauth2/draft15/token',
'infos_url' => 'https://www.deviantart.com/api/draft15/user/whoami',
]);
}
}
@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class DiscogsResourceOwner extends GenericOAuth1ResourceOwner
{
public const TYPE = 'discogs';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'username',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://www.discogs.com/oauth/authorize',
'request_token_url' => 'https://api.discogs.com/oauth/request_token',
'access_token_url' => 'https://api.discogs.com/oauth/access_token',
'infos_url' => 'https://api.discogs.com/oauth/identity',
]);
}
}
@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Alexander Müller <amr@kapthon.com>
*/
final class DisqusResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'disqus';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'response.id',
'nickname' => 'response.username',
'realname' => 'response.name',
];
/**
* {@inheritdoc}
*/
protected function doGetUserInformationRequest($url, array $parameters = [])
{
// Disqus requires api key and secret for user information requests
$url = $this->normalizeUrl($url, [
'api_key' => $this->options['client_id'],
'api_secret' => $this->options['client_secret'],
]);
return parent::doGetUserInformationRequest($url, $parameters);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://disqus.com/api/oauth/2.0/authorize/',
'access_token_url' => 'https://disqus.com/api/oauth/2.0/access_token/',
'infos_url' => 'https://disqus.com/api/3.0/users/details.json',
'scope' => 'read',
'use_commas_in_scope' => true,
]);
}
}
@@ -0,0 +1,89 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Jamie Sutherland<me@jamiesutherland.com>
*/
final class DropboxResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'dropbox';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'account_id',
'nickname' => 'email',
'realname' => 'email',
'email' => 'email',
];
/**
* Dropbox API v2 requires a POST request to simply get user info!
*
* @return UserResponseInterface
*/
public function getUserInformation(array $accessToken,
array $extraParameters = []
) {
if ($this->options['use_bearer_authorization']) {
$content = $this->httpRequest(
$this->normalizeUrl($this->options['infos_url'], $extraParameters),
'null',
[
'Authorization' => 'Bearer '.$accessToken['access_token'],
'Accept' => 'application/json',
'Content-Type' => 'application/json; charset=utf-8',
], 'POST');
} else {
$content = $this->doGetUserInformationRequest(
$this->normalizeUrl(
$this->options['infos_url'],
array_merge([$this->options['attr_name'] => $accessToken['access_token']], $extraParameters)
)
);
}
try {
$response = $this->getUserResponse();
$response->setData($content->toArray(false));
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
} catch (TransportExceptionInterface|JsonException $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://www.dropbox.com/oauth2/authorize',
'access_token_url' => 'https://api.dropbox.com/oauth2/token',
'infos_url' => 'https://api.dropboxapi.com/2/users/get_current_account',
]);
}
}
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Ivan Stankovic <ivan.stankovic@webstorm.rs>
*/
final class EveOnlineResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'eveonline';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'CharacterID',
'nickname' => 'CharacterName',
'realname' => 'CharacterName',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://login.eveonline.com/oauth/authorize',
'access_token_url' => 'https://login.eveonline.com/oauth/token',
'infos_url' => 'https://login.eveonline.com/oauth/verify',
'use_commas_in_scope' => true,
]);
}
}
@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class EventbriteResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'eventbrite';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'user.user_id',
'nickname' => 'user.first_name',
'firstname' => 'user.first_name',
'lastname' => 'user.last_name',
'realname' => ['user.first_name', 'user.last_name'],
'email' => 'email',
];
/**
* {@inheritdoc}
*/
protected function doGetTokenRequest($url, array $parameters = [])
{
return $this->httpRequest($url, http_build_query($parameters, '', '&'), [], 'POST');
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://www.eventbrite.com/oauth/authorize',
'access_token_url' => 'https://www.eventbrite.com/oauth/token',
'infos_url' => 'https://www.eventbrite.com/json/user_get',
'use_bearer_authorization' => true,
]);
}
}
@@ -0,0 +1,122 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Geoffrey Bachelet <geoffrey.bachelet@gmail.com>
*/
final class FacebookResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'facebook';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'name',
'firstname' => 'first_name',
'lastname' => 'last_name',
'realname' => 'name',
'email' => 'email',
'profilepicture' => 'picture.data.url',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
if ($this->options['appsecret_proof']) {
$extraParameters['appsecret_proof'] = hash_hmac('sha256', $accessToken['access_token'], $this->options['client_secret']);
}
return parent::getUserInformation($accessToken, $extraParameters);
}
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
$extraOptions = [];
if (isset($this->options['display'])) {
$extraOptions['display'] = $this->options['display'];
}
if (isset($this->options['auth_type'])) {
$extraOptions['auth_type'] = $this->options['auth_type'];
}
return parent::getAuthorizationUrl($redirectUri, array_merge($extraOptions, $extraParameters));
}
/**
* {@inheritdoc}
*/
public function getAccessToken(Request $request, $redirectUri, array $extraParameters = [])
{
$parameters = [];
if ($request->query->has('fb_source')) {
$parameters['fb_source'] = $request->query->get('fb_source');
}
if ($request->query->has('fb_appcenter')) {
$parameters['fb_appcenter'] = $request->query->get('fb_appcenter');
}
return parent::getAccessToken($request, $this->normalizeUrl($redirectUri, $parameters), $extraParameters);
}
/**
* {@inheritdoc}
*/
public function revokeToken($token)
{
$parameters = [
'client_id' => $this->options['client_id'],
'client_secret' => $this->options['client_secret'],
];
$response = $this->httpRequest($this->normalizeUrl($this->options['revoke_token_url'], ['access_token' => $token]), $parameters, [], 'DELETE');
return 200 === $response->getStatusCode();
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://www.facebook.com/v19.0/dialog/oauth',
'access_token_url' => 'https://graph.facebook.com/v19.0/oauth/access_token',
'revoke_token_url' => 'https://graph.facebook.com/v19.0/me/permissions',
'infos_url' => 'https://graph.facebook.com/v19.0/me?fields=id,first_name,last_name,name,email,picture.type(large)',
'use_commas_in_scope' => true,
'display' => null,
'auth_type' => null,
'appsecret_proof' => false,
]);
$resolver
->setAllowedValues('display', ['page', 'popup', 'touch', null]) // @link https://developers.facebook.com/docs/reference/dialogs/#display
->setAllowedValues('auth_type', ['rerequest', null]) // @link https://developers.facebook.com/docs/reference/javascript/FB.login/
->setAllowedTypes('appsecret_proof', 'bool') // @link https://developers.facebook.com/docs/graph-api/securing-requests
;
}
}
@@ -0,0 +1,122 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Resource owner for the fiware keyrock idm oauth 2.0 service
* more infos at https://github.com/ging/fi-ware-idm/wiki/Using-the-FIWARE-LAB-instance.
*
* @author Christian Kaspar <christian@sponsoo.de>
*/
final class FiwareResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'fiware';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'nickName',
'realname' => 'displayName',
'email' => 'email',
];
/**
* {@inheritdoc}
*/
public function getAccessToken(Request $request, $redirectUri, array $extraParameters = [])
{
$parameters = array_merge([
'code' => $request->query->get('code'),
'grant_type' => 'authorization_code',
'redirect_uri' => $redirectUri,
], $extraParameters);
$headers = [
'Authorization' => 'Basic '.base64_encode($this->options['client_id'].':'.$this->options['client_secret']),
];
$response = $this->httpRequest($this->options['access_token_url'], http_build_query($parameters, '', '&'), $headers, 'POST');
$responseContent = $this->getResponseContent($response);
$this->validateResponseContent($responseContent);
return $responseContent;
}
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
if ($this->options['use_bearer_authorization']) {
$content = $this->httpRequest(
$this->normalizeUrl(
$this->options['infos_url'],
['access_token' => $accessToken['access_token']]
),
null,
['Authorization' => 'Bearer']
);
} else {
$content = $this->doGetUserInformationRequest(
$this->normalizeUrl(
$this->options['infos_url'],
[$this->options['attr_name'] => $accessToken['access_token']]
)
);
}
$response = $this->getUserResponse();
$response->setData($content->getContent());
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => '{base_url}/oauth2/authorize',
'access_token_url' => '{base_url}/oauth2/token',
'revoke_token_url' => '{base_url}/oauth2/revoke',
'infos_url' => '{base_url}/user',
]);
$resolver->setRequired([
'base_url',
]);
$normalizer = function (Options $options, $value) {
return str_replace('{base_url}', $options['base_url'], $value);
};
$resolver
->setNormalizer('authorization_url', $normalizer)
->setNormalizer('access_token_url', $normalizer)
->setNormalizer('revoke_token_url', $normalizer)
->setNormalizer('infos_url', $normalizer)
;
}
}
@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Dmitri Lakachauskis <lakiboy83@gmail.com>
*/
final class FlickrResourceOwner extends GenericOAuth1ResourceOwner
{
public const TYPE = 'flickr';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'user_nsid',
'nickname' => 'username',
'realname' => 'fullname',
];
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
$token = $this->getRequestToken($redirectUri, $extraParameters);
return $this->normalizeUrl($this->options['authorization_url'], [
'oauth_token' => $token['oauth_token'],
'perms' => $this->options['perms'],
'nojsoncallback' => 1,
]);
}
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$response = $this->getUserResponse();
$response->setData($accessToken);
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'http://www.flickr.com/services/oauth/authorize',
'request_token_url' => 'http://www.flickr.com/services/oauth/request_token',
'access_token_url' => 'http://www.flickr.com/services/oauth/access_token',
// Flickr don't use `infos_url`
'infos_url' => null,
'perms' => 'read',
]);
}
}
@@ -0,0 +1,97 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class FoursquareResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'foursquare';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'response.user.id',
'firstname' => 'response.user.firstName',
'lastname' => 'response.user.lastName',
'nickname' => 'response.user.firstName',
'realname' => ['response.user.firstName', 'response.user.lastName'],
'email' => 'response.user.contact.email',
'profilepicture' => 'response.user.photo',
];
/**
* {@inheritdoc}
*/
protected function getResponseContent(ResponseInterface $rawResponse): array
{
$response = parent::getResponseContent($rawResponse);
// Foursquare use quite custom response structure in case of error
if (isset($response['meta']['errorType'])) {
// Prevent to mark deprecated calls as errors
if (200 === (int) $response['meta']['code']) {
$response['error'] = $response['meta']['errorType'];
// Try to add some details of error if available
if (isset($response['meta']['errorMessage'])) {
$response['error'] .= ' '.$response['meta']['errorMessage'];
} elseif (isset($response['meta']['errorDetail'])) {
$response['error'] .= ' '.$response['meta']['errorDetail'];
}
}
unset($response['meta']);
}
return $response;
}
/**
* {@inheritdoc}
*/
protected function doGetUserInformationRequest($url, array $parameters = [])
{
// Foursquare require to pass the 'v' ('version' = date in format 'YYYYMMDD') parameter when requesting API
$url = $this->normalizeUrl($url, [
'v' => $this->options['version'],
]);
// Foursquare require to pass the OAuth token as 'oauth_token' instead of 'access_token'
$url = str_replace('access_token', 'oauth_token', $url);
return $this->httpRequest($url);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://foursquare.com/oauth2/authenticate',
'access_token_url' => 'https://foursquare.com/oauth2/access_token',
'infos_url' => 'https://api.foursquare.com/v2/users/self',
// @link https://developer.foursquare.com/overview/versioning
'version' => '20121206',
'use_bearer_authorization' => false,
]);
}
}
@@ -0,0 +1,240 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use HWI\Bundle\OAuthBundle\Security\Helper\NonceGenerator;
use HWI\Bundle\OAuthBundle\Security\OAuthErrorHandler;
use HWI\Bundle\OAuthBundle\Security\OAuthUtils;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* @author Francisco Facioni <fran6co@gmail.com>
*/
abstract class GenericOAuth1ResourceOwner extends AbstractResourceOwner
{
public const TYPE = null; // it must be null
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$parameters = array_merge([
'oauth_consumer_key' => $this->options['client_id'],
'oauth_timestamp' => time(),
'oauth_nonce' => NonceGenerator::generate(),
'oauth_version' => '1.0',
'oauth_signature_method' => $this->options['signature_method'],
'oauth_token' => $accessToken['oauth_token'],
], $extraParameters);
$url = $this->options['infos_url'];
$parameters['oauth_signature'] = OAuthUtils::signRequest(
'GET',
$url,
$parameters,
$this->options['client_secret'],
$accessToken['oauth_token_secret'],
$this->options['signature_method']
);
$content = $this->doGetUserInformationRequest($url, $parameters);
$response = $this->getUserResponse();
$response->setData($content->getContent());
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
}
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
$token = $this->getRequestToken($redirectUri, $extraParameters);
return $this->normalizeUrl($this->options['authorization_url'], ['oauth_token' => $token['oauth_token']]);
}
/**
* {@inheritdoc}
*/
public function getAccessToken(HttpRequest $request, $redirectUri, array $extraParameters = [])
{
OAuthErrorHandler::handleOAuthError($request);
try {
if (null === $requestToken = $this->storage->fetch($this, $request->query->get('oauth_token'))) {
throw new \RuntimeException('No request token found in the storage.');
}
} catch (\InvalidArgumentException $e) {
throw new AuthenticationException('Given token is not valid.');
}
$parameters = array_merge([
'oauth_consumer_key' => $this->options['client_id'],
'oauth_timestamp' => time(),
'oauth_nonce' => NonceGenerator::generate(),
'oauth_version' => '1.0',
'oauth_signature_method' => $this->options['signature_method'],
'oauth_token' => $requestToken['oauth_token'],
'oauth_verifier' => $request->query->get('oauth_verifier'),
], $extraParameters);
$url = $this->options['access_token_url'];
$parameters['oauth_signature'] = OAuthUtils::signRequest(
'POST',
$url,
$parameters,
$this->options['client_secret'],
$requestToken['oauth_token_secret'],
$this->options['signature_method']
);
$response = $this->doGetTokenRequest($url, $parameters);
$response = $this->getResponseContent($response);
if (isset($response['oauth_problem'])) {
throw new AuthenticationException(sprintf('OAuth error: "%s"', $response['oauth_problem']));
}
if (!isset($response['oauth_token'], $response['oauth_token_secret'])) {
throw new AuthenticationException('Not a valid request token.');
}
return $response;
}
/**
* {@inheritdoc}
*/
public function handles(HttpRequest $request)
{
return $request->query->has('oauth_token');
}
/**
* {@inheritdoc}
*/
public function isCsrfTokenValid($csrfToken)
{
// OAuth1.0a passes token with every call
return true;
}
/**
* {@inheritdoc}
*/
public function getRequestToken($redirectUri, array $extraParameters = [])
{
$timestamp = time();
$parameters = array_merge([
'oauth_consumer_key' => $this->options['client_id'],
'oauth_timestamp' => $timestamp,
'oauth_nonce' => NonceGenerator::generate(),
'oauth_version' => '1.0',
'oauth_callback' => $redirectUri,
'oauth_signature_method' => $this->options['signature_method'],
], $extraParameters);
$url = $this->options['request_token_url'];
$parameters['oauth_signature'] = OAuthUtils::signRequest(
'POST',
$url,
$parameters,
$this->options['client_secret'],
'',
$this->options['signature_method']
);
$apiResponse = $this->httpRequest($url, null, [], 'POST', $parameters);
$response = $this->getResponseContent($apiResponse);
if (isset($response['oauth_problem'])) {
throw new AuthenticationException(sprintf('OAuth error: "%s"', $response['oauth_problem']));
}
if (isset($response['oauth_callback_confirmed']) && 'true' !== $response['oauth_callback_confirmed']) {
throw new AuthenticationException('Defined OAuth callback was not confirmed.');
}
if (!isset($response['oauth_token'], $response['oauth_token_secret'])) {
throw new AuthenticationException('Not a valid request token.');
}
$response['timestamp'] = $timestamp;
$this->storage->save($this, $response);
return $response;
}
/**
* {@inheritdoc}
*/
protected function doGetTokenRequest($url, array $parameters = [])
{
return $this->httpRequest($url, null, [], 'POST', $parameters);
}
/**
* {@inheritdoc}
*/
protected function doGetUserInformationRequest($url, array $parameters = [])
{
return $this->httpRequest($url, null, [], null, $parameters);
}
/**
* {@inheritdoc}
*/
protected function httpRequest($url, $content = null, array $headers = [], $method = null, array $parameters = [])
{
foreach ($parameters as $key => $value) {
$parameters[$key] = $key.'="'.rawurlencode($value ?: '').'"';
}
if (!$this->options['realm']) {
array_unshift($parameters, 'realm="'.rawurlencode($this->options['realm'] ?? '').'"');
}
$headers['Authorization'] = 'OAuth '.implode(', ', $parameters);
return parent::httpRequest($url, $content, $headers, $method);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setRequired([
'request_token_url',
]);
$resolver->setDefaults([
'realm' => null,
'signature_method' => 'HMAC-SHA1',
]);
$resolver->setAllowedValues('signature_method', ['HMAC-SHA1', 'RSA-SHA1', 'PLAINTEXT']);
}
}
@@ -0,0 +1,279 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use HWI\Bundle\OAuthBundle\Security\Helper\NonceGenerator;
use HWI\Bundle\OAuthBundle\Security\OAuthErrorHandler;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Geoffrey Bachelet <geoffrey.bachelet@gmail.com>
* @author Alexander <iam.asm89@gmail.com>
*/
abstract class GenericOAuth2ResourceOwner extends AbstractResourceOwner
{
public const TYPE = null; // it must be null
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
if ($this->options['use_bearer_authorization']) {
$content = $this->httpRequest(
$this->normalizeUrl($this->options['infos_url'], $extraParameters),
null,
['Authorization' => 'Bearer '.$accessToken['access_token']]
);
} else {
$content = $this->doGetUserInformationRequest(
$this->normalizeUrl(
$this->options['infos_url'],
array_merge([$this->options['attr_name'] => $accessToken['access_token']], $extraParameters)
)
);
}
try {
$response = $this->getUserResponse();
$response->setData($content->toArray(false));
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
} catch (TransportExceptionInterface|JsonException $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
if ($this->options['csrf']) {
$this->handleCsrfToken();
}
$parameters = array_merge([
'response_type' => 'code',
'client_id' => $this->options['client_id'],
'scope' => $this->options['scope'],
'state' => $this->state->encode(),
'redirect_uri' => $redirectUri,
], $extraParameters);
return $this->normalizeUrl($this->options['authorization_url'], $parameters);
}
/**
* {@inheritdoc}
*/
public function getAccessToken(HttpRequest $request, $redirectUri, array $extraParameters = [])
{
OAuthErrorHandler::handleOAuthError($request);
$parameters = array_merge([
'code' => $request->query->get('code'),
'grant_type' => 'authorization_code',
'redirect_uri' => $redirectUri,
], $extraParameters);
$response = $this->doGetTokenRequest($this->options['access_token_url'], $parameters);
$response = $this->getResponseContent($response);
$this->validateResponseContent($response);
return $response;
}
/**
* {@inheritdoc}
*/
public function refreshAccessToken($refreshToken, array $extraParameters = [])
{
$parameters = array_merge([
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token',
], $extraParameters);
$response = $this->doGetTokenRequest($this->options['access_token_url'], $parameters);
$response = $this->getResponseContent($response);
$this->validateResponseContent($response);
return $response;
}
/**
* {@inheritdoc}
*/
public function revokeToken($token)
{
if (!isset($this->options['revoke_token_url'])) {
throw new AuthenticationException('OAuth error: "Method unsupported."');
}
$parameters = [
'client_id' => $this->options['client_id'],
'client_secret' => $this->options['client_secret'],
];
$response = $this->httpRequest($this->normalizeUrl($this->options['revoke_token_url'], ['token' => $token]), $parameters, [], 'DELETE');
return 200 === $response->getStatusCode();
}
/**
* {@inheritdoc}
*/
public function handles(HttpRequest $request)
{
return $request->query->has('code');
}
/**
* {@inheritdoc}
*/
public function isCsrfTokenValid($csrfToken)
{
// Mark token valid when validation is disabled
if (!$this->options['csrf']) {
return true;
}
if (null === $csrfToken) {
throw new AuthenticationException('Given CSRF token is not valid.');
}
try {
return null !== $this->storage->fetch($this, urldecode($csrfToken), 'csrf_state');
} catch (\InvalidArgumentException $e) {
throw new AuthenticationException('Given CSRF token is not valid.');
}
}
/**
* {@inheritdoc}
*/
public function shouldRefreshOnExpire()
{
return $this->options['refresh_on_expire'] ?? false;
}
/**
* {@inheritdoc}
*/
protected function doGetTokenRequest($url, array $parameters = [])
{
$headers = [];
if ($this->options['use_authorization_to_get_token']) {
if ($this->options['client_secret']) {
$headers['Authorization'] = 'Basic '.base64_encode($this->options['client_id'].':'.$this->options['client_secret']);
}
} else {
$parameters['client_id'] = $this->options['client_id'];
$parameters['client_secret'] = $this->options['client_secret'];
}
return $this->httpRequest($url, http_build_query($parameters, '', '&'), $headers);
}
/**
* {@inheritdoc}
*/
protected function doGetUserInformationRequest($url, array $parameters = [])
{
return $this->httpRequest($url, http_build_query($parameters, '', '&'));
}
/**
* @param mixed $response the 'parsed' content based on the response headers
*
* @throws AuthenticationException If an OAuth error occurred or no access token is found
*/
protected function validateResponseContent($response)
{
if (isset($response['error_description'])) {
throw new AuthenticationException(sprintf('OAuth error: "%s"', $response['error_description']));
}
if (isset($response['error'])) {
throw new AuthenticationException(sprintf('OAuth error: "%s"', $response['error']['message'] ?? $response['error']));
}
if (!isset($response['access_token'])) {
throw new AuthenticationException('Not a valid access token.');
}
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'attr_name' => 'access_token',
'use_commas_in_scope' => false,
'use_bearer_authorization' => true,
'use_authorization_to_get_token' => true,
'refresh_on_expire' => false,
]);
$resolver->setDefined('revoke_token_url');
$resolver->setAllowedValues('refresh_on_expire', [true, false]);
// Unfortunately some resource owners break the spec by using commas instead
// of spaces to separate scopes (Disqus, Facebook, Github, Vkontante)
$scopeNormalizer = function (Options $options, $value) {
if (!$value) {
return null;
}
if (!$options['use_commas_in_scope']) {
return $value;
}
return str_replace(',', ' ', $value);
};
$resolver->setNormalizer('scope', $scopeNormalizer);
}
/**
* {@inheritdoc}
*/
protected function httpRequest($url, $content = null, array $headers = [], $method = null)
{
$headers += ['Content-Type' => 'application/x-www-form-urlencoded'];
return parent::httpRequest($url, $content, $headers, $method);
}
private function handleCsrfToken(): void
{
if (null === $this->state->getCsrfToken()) {
$this->state->setCsrfToken(NonceGenerator::generate());
}
$this->storage->save($this, $this->state->getCsrfToken(), 'csrf_state');
}
}
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Krystian Marcisz <simivar@gmail.com>
*/
final class GeniusResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'genius';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'response.user.id',
'nickname' => 'response.user.name',
'realname' => 'response.user.name',
'email' => 'response.user.email',
'profilepicture' => 'response.user.avatar.medium.url',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://api.genius.com/oauth/authorize',
'access_token_url' => 'https://api.genius.com/oauth/token',
'infos_url' => 'https://api.genius.com/account',
'use_bearer_authorization' => true,
'use_commas_in_scope' => true,
'scope' => 'me',
]);
}
}
@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Geoffrey Bachelet <geoffrey.bachelet@gmail.com>
* @author Alexander <iam.asm89@gmail.com>
*/
final class GitHubResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'github';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'login',
'realname' => 'name',
'email' => 'email',
'profilepicture' => 'avatar_url',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$response = parent::getUserInformation($accessToken, $extraParameters);
$responseData = $response->getData();
if (empty($responseData['email'])) {
// fetch the email addresses linked to the account
$content = $this->httpRequest(
$this->normalizeUrl($this->options['emails_url']), null, ['Authorization' => 'Bearer '.$accessToken['access_token']]
);
foreach ($this->getResponseContent($content) as $email) {
if (!empty($email['primary'])) {
// we only need the primary email address
$responseData['email'] = $email['email'];
break;
}
}
$response->setData($responseData);
}
return $response;
}
/**
* {@inheritdoc}
*/
public function revokeToken($token)
{
$response = $this->httpRequest(
sprintf($this->options['revoke_token_url'], $this->options['client_id']),
json_encode(['access_token' => $token]),
[
'Authorization' => 'Basic '.base64_encode($this->options['client_id'].':'.$this->options['client_secret']),
'Content-Type' => 'application/json',
],
'DELETE'
);
return 204 === $response->getStatusCode();
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://github.com/login/oauth/authorize',
'access_token_url' => 'https://github.com/login/oauth/access_token',
'revoke_token_url' => 'https://api.github.com/applications/%s/token',
'infos_url' => 'https://api.github.com/user',
'emails_url' => 'https://api.github.com/user/emails',
'use_commas_in_scope' => true,
]);
}
}
@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Indra Gunawan <hello@indra.my.id>
*/
final class GitLabResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'gitlab';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'username',
'realname' => 'name',
'email' => 'email',
'profilepicture' => 'avatar_url',
];
/**
* {@inheritdoc}
*/
public function revokeToken($token)
{
$parameters = [
'token' => $token,
'client_id' => $this->options['client_id'],
'client_secret' => $this->options['client_secret'],
];
$response = $this->httpRequest(
$this->options['revoke_token_url'],
$parameters,
[],
'POST'
);
return 200 === $response->getStatusCode();
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://gitlab.com/oauth/authorize',
'access_token_url' => 'https://gitlab.com/oauth/token',
'revoke_token_url' => 'https://gitlab.com/oauth/revoke',
'infos_url' => 'https://gitlab.com/api/v4/user',
'scope' => 'read_user',
'use_commas_in_scope' => false,
'use_bearer_authorization' => true,
]);
}
}
@@ -0,0 +1,103 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Geoffrey Bachelet <geoffrey.bachelet@gmail.com>
* @author Alexander <iam.asm89@gmail.com>
*/
final class GoogleResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'google';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'name',
'realname' => 'name',
'firstname' => 'given_name',
'lastname' => 'family_name',
'email' => 'email',
'profilepicture' => 'picture',
];
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
$url = parent::getAuthorizationUrl($redirectUri, array_merge([
'access_type' => $this->options['access_type'],
'approval_prompt' => $this->options['approval_prompt'],
'request_visible_actions' => $this->options['request_visible_actions'],
'prompt' => $this->options['prompt'],
], $extraParameters));
// This parameter have specific value (uses "&" as a separator of domains)
if (null !== $this->options['hd']) {
$url .= '&hd='.implode('&', array_map('trim', explode(',', $this->options['hd'])));
}
return $url;
}
/**
* {@inheritdoc}
*/
public function revokeToken($token)
{
$response = $this->httpRequest($this->normalizeUrl($this->options['revoke_token_url'], ['token' => $token]));
return 200 === $response->getStatusCode();
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://accounts.google.com/o/oauth2/auth',
'access_token_url' => 'https://accounts.google.com/o/oauth2/token',
'revoke_token_url' => 'https://accounts.google.com/o/oauth2/revoke',
'infos_url' => 'https://www.googleapis.com/oauth2/v1/userinfo',
'scope' => 'https://www.googleapis.com/auth/userinfo.profile',
'access_type' => null,
'approval_prompt' => null,
'display' => null,
// Identifying a particular hosted domain account to be accessed (for example, 'mycollege.edu')
'hd' => null,
'login_hint' => null,
'prompt' => null,
'request_visible_actions' => null,
]);
$resolver
// @link https://developers.google.com/accounts/docs/OAuth2WebServer#offline
->setAllowedValues('access_type', ['online', 'offline', null])
// sometimes we need to force for approval prompt (e.g. when we lost refresh token)
->setAllowedValues('approval_prompt', ['force', 'auto', null])
// @link https://developers.google.com/accounts/docs/OAuth2Login#authenticationuriparameters
->setAllowedValues('display', ['page', 'popup', 'touch', 'wap', null])
->setAllowedValues('login_hint', ['email address', 'sub', null])
->setAllowedValues('prompt', ['consent', 'select_account', null])
;
}
}
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Vincent Cassé <vincent@casse.me>
*/
final class HubicResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'hubic';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'email',
'nickname' => 'email',
'firstname' => 'firstname',
'lastname' => 'lastname',
'realname' => 'firstname',
'email' => 'email',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://api.hubic.com/oauth/auth/',
'access_token_url' => 'https://api.hubic.com/oauth/token/',
'infos_url' => 'https://api.hubic.com/1.0/account',
]);
}
}
@@ -0,0 +1,82 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\Security\OAuthErrorHandler;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Jean-Christophe Cuvelier <jcc@atomseeds.com>
* @author Fabiano Roberto <fabiano.roberto@ped.technology>
*/
final class InstagramResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'instagram';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'username',
'realname' => 'username',
'email' => 'id',
'accounttype' => 'account_type',
];
/**
* {@inheritdoc}
*/
public function getAccessToken(HttpRequest $request, $redirectUri, array $extraParameters = [])
{
OAuthErrorHandler::handleOAuthError($request);
$parameters = array_merge([
'code' => $request->query->get('code'),
'grant_type' => 'authorization_code',
'client_id' => $this->options['client_id'],
'client_secret' => $this->options['client_secret'],
'redirect_uri' => $redirectUri,
], $extraParameters);
$response = $this->doGetTokenRequest($this->options['access_token_url'], $parameters);
$response = $this->getResponseContent($response);
$this->validateResponseContent($response);
return $response;
}
/**
* {@inheritdoc}
*/
protected function doGetUserInformationRequest($url, array $parameters = [])
{
return $this->httpRequest($this->normalizeUrl($url, $parameters), null, [], 'GET');
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://api.instagram.com/oauth/authorize',
'access_token_url' => 'https://api.instagram.com/oauth/access_token',
'infos_url' => 'https://api.instagram.com/v1/users/self',
'use_bearer_authorization' => false,
]);
}
}
@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Thomas Bretzke <tb@itembase.biz>
*/
final class ItembaseResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'itembase';
public const ITEMBASE_AUTH_URL = 'https://accounts.itembase.com/oauth/v2/auth';
public const ITEMBASE_TOKEN_URL = 'https://accounts.itembase.com/oauth/v2/token';
public const ITEMBASE_INFOS_URL = 'https://users.itembase.com/v1/me';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'uuid',
'nickname' => 'username',
'firstname' => 'first_name',
'lastname' => 'last_name',
'email' => 'email',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => self::ITEMBASE_AUTH_URL,
'access_token_url' => self::ITEMBASE_TOKEN_URL,
'infos_url' => self::ITEMBASE_INFOS_URL,
]);
}
}
@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Dmitry Matora <dmitry.matora@gmail.com>
*/
final class JawboneResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'jawbone';
/**
* {@inheritdoc}
*/
protected array $paths = [
'xid' => 'data.id',
'firstname' => 'data.first',
'lastname' => 'data.last',
'profilepicture' => 'data.image',
];
/**
* {@inheritdoc}
*/
public function revokeToken($accessToken)
{
$response = $this->getInformation($accessToken, 'PartnerAppMembership');
return 200 === $response->getStatusCode();
}
/**
* {@inheritdoc}
*/
public function getInformation($accessToken, $type, array $extraParameters = [])
{
$url = $this->normalizeUrl($this->options['infos_url'].'/'.$type, $extraParameters);
$headers = [
'Authorization' => 'Bearer '.$accessToken['access_token'],
'Accept' => 'application/json',
];
return $this->httpRequest($url, null, $headers);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://jawbone.com/auth/oauth2/auth',
'access_token_url' => 'https://jawbone.com/auth/oauth2/token',
'infos_url' => 'https://jawbone.com/nudge/api/v.1.0/users/@me',
'use_commas_in_scope' => true,
]);
}
}
@@ -0,0 +1,126 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use HWI\Bundle\OAuthBundle\Security\Helper\NonceGenerator;
use HWI\Bundle\OAuthBundle\Security\OAuthUtils;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
final class JiraResourceOwner extends GenericOAuth1ResourceOwner
{
public const TYPE = 'jira';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'name',
'nickname' => 'name',
'realname' => 'displayName',
'email' => 'emailAddress',
'profilepicture' => 'avatarUrls.48x48',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$parameters = array_merge([
'oauth_consumer_key' => $this->options['client_id'],
'oauth_timestamp' => time(),
'oauth_nonce' => NonceGenerator::generate(),
'oauth_version' => '1.0',
'oauth_signature_method' => $this->options['signature_method'],
'oauth_token' => $accessToken['oauth_token'],
], $extraParameters);
$parameters['oauth_signature'] = OAuthUtils::signRequest(
'GET',
$this->options['infos_session_url'],
$parameters,
$this->options['client_secret'],
$accessToken['oauth_token_secret'],
$this->options['signature_method']
);
$content = $this->getResponseContent($this->doGetUserInformationRequest($this->options['infos_session_url'], $parameters));
$url = $this->normalizeUrl($this->options['infos_url'], ['username' => $content['name']]);
// Regenerate nonce & signature as URL was changed
$parameters['oauth_nonce'] = NonceGenerator::generate();
$parameters['oauth_signature'] = OAuthUtils::signRequest(
'GET',
$url,
$parameters,
$this->options['client_secret'],
$accessToken['oauth_token_secret'],
$this->options['signature_method']
);
try {
$content = $this->doGetUserInformationRequest($url, $parameters);
$response = $this->getUserResponse();
$response->setData($content->getContent(false));
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
} catch (TransportExceptionInterface $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => '{base_url}/plugins/servlet/oauth/authorize',
'request_token_url' => '{base_url}/plugins/servlet/oauth/request-token',
'access_token_url' => '{base_url}/plugins/servlet/oauth/access-token',
// JIRA API requires to first know the username to be able to ask for user details
'infos_session_url' => '{base_url}/rest/auth/1/session',
'infos_url' => '{base_url}/rest/api/2/user',
'signature_method' => 'RSA-SHA1',
]);
$resolver->setRequired([
'base_url',
]);
$normalizer = function (Options $options, $value) {
return str_replace('{base_url}', $options['base_url'], $value);
};
$resolver
->setNormalizer('authorization_url', $normalizer)
->setNormalizer('request_token_url', $normalizer)
->setNormalizer('access_token_url', $normalizer)
->setNormalizer('infos_session_url', $normalizer)
->setNormalizer('infos_url', $normalizer)
;
}
}
@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Andrea Quintino <andreaquin1990@gmail.com>
*/
final class KeycloakResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'keycloak';
protected array $paths = [
'identifier' => 'sub',
'nickname' => 'preferred_username',
'firstname' => 'given_name',
'lastname' => 'family_name',
'realname' => 'name',
'email' => 'email',
'profilepicture' => 'picture',
];
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
return parent::getAuthorizationUrl($redirectUri, array_merge([
'approval_prompt' => $this->getOption('approval_prompt'),
'kc_idp_hint' => $this->getOption('idp_hint'),
], $extraParameters));
}
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'protocol' => 'openid-connect',
'scope' => 'openid email',
'response_type' => 'code',
'approval_prompt' => 'auto',
'authorization_url' => '{keycloak_url}/auth',
'access_token_url' => '{keycloak_url}/token',
'infos_url' => '{keycloak_url}/userinfo',
'idp_hint' => null,
]);
$resolver->setRequired([
'realm',
'base_url',
]);
$normalizer = function (Options $options, $value) {
return str_replace(
'{keycloak_url}',
$options['base_url'].'/realms/'.$options['realm'].'/protocol/'.$options['protocol'],
$value
);
};
$resolver->setNormalizer('authorization_url', $normalizer);
$resolver->setNormalizer('access_token_url', $normalizer);
$resolver->setNormalizer('infos_url', $normalizer);
}
}
@@ -0,0 +1,113 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Response\LinkedinUserResponse;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Francisco Facioni <fran6co@gmail.com>
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class LinkedinResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'linkedin';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'emailAddress',
'firstname' => 'firstName',
'lastname' => 'lastName',
'email' => 'emailAddress',
'profilepicture' => 'profilePicture',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$response = parent::getUserInformation($accessToken, $extraParameters);
$responseData = $response->getData();
// The user info returned by /me doesn't contain the email so we make an extra request to fetch it
$content = $this->httpRequest(
$this->normalizeUrl($this->options['email_url'], $extraParameters),
null,
['Authorization' => 'Bearer '.$accessToken['access_token']]
);
$emailResponse = $this->getResponseContent($content);
if (isset($emailResponse['elements']) && \count($emailResponse['elements']) > 0) {
$responseData['emailAddress'] = $emailResponse['elements'][0]['handle~']['emailAddress'];
}
// errors not handled because I don't see any relevant thing to do with them
$response->setData($responseData);
return $response;
}
/**
* {@inheritdoc}
*/
protected function doGetTokenRequest($url, array $parameters = [])
{
$parameters['client_id'] = $this->options['client_id'];
$parameters['client_secret'] = $this->options['client_secret'];
return $this->httpRequest($this->normalizeUrl($url, $parameters), null, [], 'POST');
}
/**
* {@inheritdoc}
*/
protected function httpRequest($url, $content = null, array $headers = [], $method = null)
{
// LinkedIn v2 API is supposed to require Content-Type: application/json but it works without
// and request to get the access token doesn't seems to work with Content-Type: application/json
// so we don't put any Content-Type header.
// Skip the Content-Type header in GenericOAuth2ResourceOwner::httpRequest
//
// LinkedIn API requires to always set Content-Length in POST requests
if ('POST' === $method) {
$headers['Content-Length'] = \is_string($content) ? (string) \strlen($content) : '0';
}
return AbstractResourceOwner::httpRequest($url, $content, $headers, $method);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'scope' => 'r_liteprofile r_emailaddress',
'authorization_url' => 'https://www.linkedin.com/oauth/v2/authorization',
'access_token_url' => 'https://www.linkedin.com/oauth/v2/accessToken',
'infos_url' => 'https://api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))',
'email_url' => 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))',
'user_response_class' => LinkedinUserResponse::class,
'csrf' => true,
'use_bearer_authorization' => true,
]);
}
}
@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Gaponov Igor <jiminy96@gmail.com>
*/
final class MailRuResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'mailru';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'uid',
'nickname' => 'nick',
'realname' => 'nick',
'email' => 'email',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$params = [
'app_id' => $this->options['client_id'],
'method' => 'users.getInfo',
'secure' => '1',
'session_key' => $accessToken['access_token'],
];
$params['sig'] = md5(vsprintf('app_id=%smethod=%ssecure=%ssession_key=%s', $params).$this->options['client_secret']);
$url = $this->normalizeUrl($this->options['infos_url'], $params);
try {
$content = $this->doGetUserInformationRequest($url)->toArray(false);
if (isset($content[0])) {
$content = (array) $content[0];
}
$response = $this->getUserResponse();
$response->setData($content);
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
} catch (TransportExceptionInterface|JsonException $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://connect.mail.ru/oauth/authorize',
'access_token_url' => 'https://connect.mail.ru/oauth/token',
'infos_url' => 'http://www.appsmail.ru/platform/api',
]);
}
}
@@ -0,0 +1,17 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
final class OAuth1ResourceOwner extends GenericOAuth1ResourceOwner
{
public const TYPE = 'oauth1';
}
@@ -0,0 +1,17 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
final class OAuth2ResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'oauth2';
}
@@ -0,0 +1,108 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Sergey Polischook <spolischook@gmail.com>
*/
final class OdnoklassnikiResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'odnoklassniki';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'uid',
'nickname' => 'username',
'realname' => 'name',
'email' => 'email',
'firstname' => 'first_name',
'lastname' => 'last_name',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$parameters = [
'access_token' => $accessToken['access_token'],
'application_key' => $this->options['application_key'],
];
if ($this->options['fields']) {
$parameters['fields'] = $this->options['fields'];
$parameters['sig'] = md5(sprintf(
'application_key=%sfields=%smethod=users.getCurrentUser%s',
$this->options['application_key'],
$this->options['fields'],
md5($accessToken['access_token'].$this->options['client_secret'])
));
} else {
$parameters['sig'] = md5(sprintf(
'application_key=%smethod=users.getCurrentUser%s',
$this->options['application_key'],
md5($accessToken['access_token'].$this->options['client_secret'])
));
}
$url = $this->normalizeUrl($this->options['infos_url'], $parameters);
try {
$content = $this->doGetUserInformationRequest($url)->toArray(false);
$response = $this->getUserResponse();
$response->setData($content);
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
} catch (TransportExceptionInterface|JsonException $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://connect.ok.ru/oauth/authorize',
'access_token_url' => 'https://api.ok.ru/oauth/token.do',
'infos_url' => 'https://api.ok.ru/fb.do?method=users.getCurrentUser',
'application_key' => null,
'fields' => null,
]);
$fieldsNormalizer = function (Options $options, $value) {
if (!$value) {
return null;
}
return \is_array($value) ? implode(',', $value) : $value;
};
$resolver->setNormalizer('fields', $fieldsNormalizer);
}
}
@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class Office365ResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'office365';
protected array $paths = [
'identifier' => 'id',
'email' => 'mail',
'realname' => 'displayName',
'firstname' => 'givenName',
'lastname' => 'surname',
];
/**
* {@inheritdoc}
*/
public function getAccessToken(Request $request, $redirectUri, array $extraParameters = [])
{
$extraParameters = array_merge([
'resource' => 'https://graph.microsoft.com',
], $extraParameters);
return parent::getAccessToken($request, $redirectUri, $extraParameters);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://login.microsoftonline.com/common/oauth2/authorize',
'access_token_url' => 'https://login.microsoftonline.com/common/oauth2/token',
'infos_url' => 'https://graph.microsoft.com/v1.0/me',
]);
}
}
@@ -0,0 +1,85 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
final class PassageResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'passage';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'sub',
'email' => 'email',
'phone_number' => 'phone_number',
'email_verified' => 'email_verified',
'phone_number_verified' => 'phone_number_verified',
];
/**
* {@inheritdoc}
*/
public function revokeToken($token)
{
if (!isset($this->options['revoke_token_url'])) {
throw new AuthenticationException('OAuth error: "Method unsupported."');
}
$parameters = [
'client_id' => $this->options['client_id'],
'client_secret' => $this->options['client_secret'],
'token' => $token,
];
$response = $this->httpRequest($this->normalizeUrl($this->options['revoke_token_url']), $parameters, [], 'POST');
return 200 === $response->getStatusCode();
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://{sub_domain}.withpassage.com/authorize',
'access_token_url' => 'https://{sub_domain}.withpassage.com/token',
'revoke_token_url' => 'https://{sub_domain}.withpassage.com/revoke',
'infos_url' => 'https://{sub_domain}.withpassage.com/userinfo',
'use_commas_in_scope' => false,
'scope' => 'openid email',
]);
$resolver->setRequired([
'sub_domain',
]);
$normalizer = function (Options $options, $value) {
return str_replace('{sub_domain}', $options['sub_domain'], $value);
};
$resolver
->setNormalizer('authorization_url', $normalizer)
->setNormalizer('access_token_url', $normalizer)
->setNormalizer('revoke_token_url', $normalizer)
->setNormalizer('infos_url', $normalizer)
;
}
}
@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Berny Cantos <be@rny.cc>
*/
final class PaypalResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'paypal';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'user_id',
'nickname' => 'email',
'email' => 'email',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'sandbox' => false,
'scope' => 'openid email',
'authorization_url' => 'https://www.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize',
'access_token_url' => 'https://api.paypal.com/v1/identity/openidconnect/tokenservice',
'infos_url' => 'https://api.paypal.com/v1/identity/openidconnect/userinfo/?schema=openid',
]);
$resolver->addAllowedTypes('sandbox', 'bool');
$sandboxTransformation = function (Options $options, $value) {
if (!$options['sandbox']) {
return $value;
}
return preg_replace('~\.paypal\.~', '.sandbox.paypal.', $value, 1);
};
$resolver
->setNormalizer('authorization_url', $sandboxTransformation)
->setNormalizer('access_token_url', $sandboxTransformation)
->setNormalizer('infos_url', $sandboxTransformation)
;
}
}
@@ -0,0 +1,108 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Contracts\HttpClient\ResponseInterface;
final class QQResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'qq';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'openid',
'nickname' => 'nickname',
'realname' => 'nickname',
'profilepicture' => 'figureurl_qq_1',
];
/**
* {@inheritdoc}
*/
public function getResponseContent(ResponseInterface $rawResponse): array
{
$content = $rawResponse->getContent(false);
if (preg_match('/^callback\((.+)\);$/', $content, $matches)) {
return json_decode(trim($matches[1]), true) ?: [];
}
return parent::getResponseContent($rawResponse);
}
/**
* {@inheritdoc}
*/
public function getUserInformation(?array $accessToken = null, array $extraParameters = [])
{
$openid = $extraParameters['openid'] ?? $this->requestUserIdentifier($accessToken);
$url = $this->normalizeUrl($this->options['infos_url'], [
'oauth_consumer_key' => $this->options['client_id'],
'access_token' => $accessToken['access_token'],
'openid' => $openid,
'format' => 'json',
]);
$response = $this->doGetUserInformationRequest($url);
$content = $this->getResponseContent($response);
// Custom errors:
if (isset($content['ret']) && 0 === $content['ret']) {
$content['openid'] = $openid;
} else {
throw new AuthenticationException(sprintf('OAuth error: %s', isset($content['ret']) ? $content['msg'] : 'invalid response'));
}
$response = $this->getUserResponse();
$response->setData($content);
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://graph.qq.com/oauth2.0/authorize?format=json',
'access_token_url' => 'https://graph.qq.com/oauth2.0/token',
'infos_url' => 'https://graph.qq.com/user/get_user_info',
'me_url' => 'https://graph.qq.com/oauth2.0/me',
]);
}
private function requestUserIdentifier(?array $accessToken = null)
{
$url = $this->normalizeUrl($this->options['me_url'], [
'access_token' => $accessToken['access_token'],
]);
$response = $this->httpRequest($url);
$content = $this->getResponseContent($response);
if (!isset($content['openid'])) {
throw new AuthenticationException();
}
return $content['openid'];
}
}
@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Martin Aarhof <martin.aarhof@gmail.com>
*/
final class RedditResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'reddit';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'name',
'realname' => null,
'email' => null,
];
/**
* {@inheritdoc}
*/
protected function doGetTokenRequest($url, array $parameters = [])
{
return $this->httpRequest(
$url,
http_build_query($parameters, '', '&'),
[
'Authorization' => 'Basic '.base64_encode(sprintf('%s:%s', $this->options['client_id'], $this->options['client_secret'])),
],
'POST'
);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://ssl.reddit.com/api/v1/authorize',
'access_token_url' => 'https://ssl.reddit.com/api/v1/access_token',
'infos_url' => 'https://oauth.reddit.com/api/v1/me.json',
'use_bearer_authorization' => true,
'use_commas_in_scope' => true,
'csrf' => true,
'scope' => 'identity',
'duration' => 'permanent',
]);
}
}
@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Artem Genvald <genvaldartem@gmail.com>
*/
final class RunKeeperResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'runkeeper';
/**
* {@inheritdoc}
*/
protected array $paths = [
'realname' => 'name',
'profilepicture' => 'medium_picture',
];
/**
* {@inheritdoc}
*/
public function getUserResource($accessToken)
{
$response = $this->httpRequest(
$this->normalizeUrl($this->options['user_resource_url']),
null,
['Authorization' => 'Bearer '.$accessToken]
);
return $this->getResponseContent($response);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://runkeeper.com/apps/authorize',
'access_token_url' => 'https://runkeeper.com/apps/token',
'infos_url' => 'https://api.runkeeper.com/profile',
'user_resource_url' => 'https://api.runkeeper.com/user',
]);
}
}
@@ -0,0 +1,96 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Tyler Pugh <tylerism@gmail.com>
*/
final class SalesforceResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'salesforce';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'user_id',
'nickname' => 'nick_name',
'realname' => 'nick_name',
'email' => 'email',
'profilepicture' => 'photos.picture',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
// SalesForce returns the infos_url in the OAuth Response Token
$this->options['infos_url'] = $accessToken['id'];
return parent::getUserInformation($accessToken, $extraParameters);
}
/**
* {@inheritdoc}
*/
protected function doGetUserInformationRequest($url, array $parameters = [])
{
// Salesforce requires format parameter in order for API to return json response
$url = $this->normalizeUrl($url, [
'format' => $this->options['format'],
]);
// Salesforce require to pass the OAuth token as 'oauth_token' instead of 'access_token'
$url = str_replace('access_token', 'oauth_token', $url);
return $this->httpRequest($url);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'sandbox' => false,
'authorization_url' => 'https://login.salesforce.com/services/oauth2/authorize',
'access_token_url' => 'https://login.salesforce.com/services/oauth2/token',
// @see SalesforceResourceOwner::getUserInformation()
'infos_url' => null,
// @see SalesforceResourceOwner::doGetUserInformationRequest()
'format' => 'json',
]);
$sandboxTransformation = function (Options $options, $value) {
if (!$options['sandbox']) {
return $value;
}
return preg_replace('~login\.~', 'test.', $value, 1);
};
$resolver
->setNormalizer('authorization_url', $sandboxTransformation)
->setNormalizer('access_token_url', $sandboxTransformation)
;
$resolver->addAllowedTypes('sandbox', 'bool');
}
}
@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\OAuth\Response\SensioConnectUserResponse;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class SensioConnectResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'sensio_connect';
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$content = $this->doGetUserInformationRequest(
$this->normalizeUrl(
$this->options['infos_url'],
array_merge([$this->options['attr_name'] => $accessToken['access_token']], $extraParameters)
)
);
try {
$response = $this->getUserResponse();
$response->setData($content->getContent(false));
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
} catch (TransportExceptionInterface|JsonException $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function doGetUserInformationRequest($url, array $parameters = [])
{
return $this->httpRequest($url, null, ['Accept' => 'application/vnd.com.sensiolabs.connect+xml']);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://connect.symfony.com/oauth/authorize',
'access_token_url' => 'https://connect.symfony.com/oauth/access_token',
'infos_url' => 'https://connect.symfony.com/api',
'user_response_class' => SensioConnectUserResponse::class,
'response_type' => 'code',
'use_bearer_authorization' => false,
'csrf' => true,
]);
}
}
@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
final class SinaWeiboResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'sina_weibo';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'screen_name',
'realname' => 'screen_name',
'profilepicture' => 'profile_image_url',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(?array $accessToken = null, array $extraParameters = [])
{
$url = $this->normalizeUrl($this->options['infos_url'], [
'access_token' => $accessToken['access_token'],
'uid' => $accessToken['uid'],
]);
try {
$content = $this->doGetUserInformationRequest($url);
$response = $this->getUserResponse();
$response->setData($content->toArray(false));
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
} catch (TransportExceptionInterface|JsonException $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://api.weibo.com/oauth2/authorize',
'access_token_url' => 'https://api.weibo.com/oauth2/access_token',
'infos_url' => 'https://api.weibo.com/2/users/show.json',
]);
}
}
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Baptiste Clavié <clavie.b@gmail.com>
*/
final class SlackResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'slack';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'user.id',
'nickname' => 'user.name',
'email' => 'user.email',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://slack.com/oauth/authorize',
'access_token_url' => 'https://slack.com/api/oauth.access',
'infos_url' => 'https://slack.com/api/users.identity',
'scope' => 'identify',
'use_bearer_authorization' => false,
'attr_name' => 'token',
]);
}
}
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Anthony AHMED <antho.ahmed@gmail.com>
*/
final class SoundcloudResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'soundcloud';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'username',
'realname' => 'full_name',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'access_token_url' => 'https://api.soundcloud.com/oauth2/token',
'attr_name' => 'oauth_token',
'authorization_url' => 'https://soundcloud.com/connect',
'infos_url' => 'https://api.soundcloud.com/me.json',
'scope' => 'non-expiring',
'use_bearer_authorization' => false,
]);
}
}
@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Janne Savolainen <janne.savolainen@sempre.fi>
*/
final class SpotifyResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'spotify';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'id',
'realname' => 'display_name',
'email' => 'email',
'profilepicture' => 'images.0.url',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(?array $accessToken = null, array $extraParameters = [])
{
$url = $this->normalizeUrl($this->options['infos_url'], [
'access_token' => $accessToken['access_token'],
]);
try {
$content = $this->doGetUserInformationRequest($url);
$response = $this->getUserResponse();
$response->setData($content->toArray(false));
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
} catch (TransportExceptionInterface|JsonException $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://accounts.spotify.com/authorize',
'access_token_url' => 'https://accounts.spotify.com/api/token',
'infos_url' => 'https://api.spotify.com/v1/me',
]);
}
}
@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class StackExchangeResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'stack_exchange';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'items.0.user_id',
'nickname' => 'items.0.display_name',
'realname' => 'items.0.display_name',
'profilepicture' => 'items.0.profile_image',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$parameters = array_merge(
[$this->options['attr_name'] => $accessToken['access_token']],
['site' => $this->options['site'], 'key' => $this->options['key']],
$extraParameters
);
try {
$content = $this->doGetUserInformationRequest($this->normalizeUrl($this->options['infos_url'], $parameters));
$response = $this->getUserResponse();
$response->setData($content->toArray(false));
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
} catch (TransportExceptionInterface|JsonException $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setRequired([
'key',
]);
$resolver->setDefaults([
'authorization_url' => 'https://stackexchange.com/oauth',
'access_token_url' => 'https://stackexchange.com/oauth/access_token',
'infos_url' => 'https://api.stackexchange.com/2.0/me',
'scope' => 'no_expiry',
'site' => 'stackoverflow',
'use_bearer_authorization' => false,
]);
}
}
@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Vincenzo Di Biaggio <aniceweb@gmail.com>
*/
final class StereomoodResourceOwner extends GenericOAuth1ResourceOwner
{
public const TYPE = 'stereomood';
protected array $paths = [
'identifier' => 'oauth_token',
'nickname' => 'oauth_token',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$response = $this->getUserResponse();
$response->setData($accessToken);
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'http://www.stereomood.com/api/oauth/authenticate',
'request_token_url' => 'http://www.stereomood.com/api/oauth/request_token',
'access_token_url' => 'http://www.stereomood.com/api/oauth/access_token',
// Stereomood don't use `infos_url`
'infos_url' => null,
]);
}
}
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Artem Genvald <genvaldartem@gmail.com>
*/
final class StravaResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'strava';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'realname' => ['firstname', 'lastname'],
'profilepicture' => 'profile_medium',
'email' => 'email',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://www.strava.com/oauth/authorize',
'access_token_url' => 'https://www.strava.com/oauth/token',
'infos_url' => 'https://www.strava.com/api/v3/athlete',
]);
}
}
@@ -0,0 +1,116 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Response\PathUserResponse;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\LazyResponseException;
/**
* @author zorn-v
*/
final class TelegramResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'telegram';
protected array $paths = [
'identifier' => 'id',
'nickname' => 'username',
'realname' => 'first_name',
'firstname' => 'first_name',
'lastname' => 'last_name',
'profilepicture' => 'photo_url',
];
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
[$botId] = explode(':', $this->options['client_secret']);
$parameters = array_merge([
'bot_id' => $botId,
'origin' => $redirectUri,
'return_to' => $redirectUri,
], $extraParameters);
return $this->normalizeUrl($this->options['authorization_url'], $parameters);
}
public function handles(Request $request)
{
if (!$request->query->has('code')) {
$js = '<script>location.href = "?code=" + new URLSearchParams(location.hash.substring(1)).get("tgAuthResult")</script>';
throw new LazyResponseException(new Response($js));
}
return true;
}
public function getAccessToken(Request $request, $redirectUri, array $extraParameters = [])
{
$token = $request->query->get('code', '');
$token = str_pad(strtr($token, '-_', '+/'), \strlen($token) % 4, '=', \STR_PAD_RIGHT);
$authData = json_decode(base64_decode($token), true);
if (empty($authData['hash'])) {
throw new AuthenticationException('Invalid Telegram auth data');
}
if (empty($authData['auth_date']) || (time() - $authData['auth_date']) > 300) {
throw new AuthenticationException('Telegram auth data expired');
}
$botToken = $this->options['client_secret'];
$checkHash = $authData['hash'];
unset($authData['hash']);
ksort($authData);
$dataCheckStr = '';
foreach ($authData as $k => $v) {
$dataCheckStr .= sprintf("\n%s=%s", $k, $v);
}
$dataCheckStr = substr($dataCheckStr, 1);
$secretKey = hash('sha256', $botToken, true);
$hash = hash_hmac('sha256', $dataCheckStr, $secretKey);
if ($hash !== $checkHash) {
throw new AuthenticationException('Telegram auth data check failed');
}
return ['access_token' => $token];
}
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$data = base64_decode($accessToken['access_token']);
$response = $this->getUserResponse();
$response->setData($data);
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
}
protected function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired([
'client_id',
'client_secret',
'authorization_url',
]);
$resolver->setDefaults([
'authorization_url' => 'https://oauth.telegram.org/auth',
'auth_with_one_url' => true,
'state' => null,
'csrf' => false,
'user_response_class' => PathUserResponse::class,
]);
}
}
@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Richard van den Brand <richard@vandenbrand.org>
*/
final class ThirtySevenSignalsResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = '37signals';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'identity.id',
'nickname' => 'identity.email_address',
'firstname' => 'identity.first_name',
'lastname' => 'identity.last_name',
'realname' => ['identity.last_name', 'identity.first_name'],
'email' => 'identity.email_address',
];
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
return parent::getAuthorizationUrl($redirectUri, array_merge(['type' => 'web_server'], $extraParameters));
}
/**
* {@inheritdoc}
*/
public function getAccessToken(Request $request, $redirectUri, array $extraParameters = [])
{
return parent::getAccessToken($request, $redirectUri, array_merge(['type' => 'web_server'], $extraParameters));
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://launchpad.37signals.com/authorization/new',
'access_token_url' => 'https://launchpad.37signals.com/authorization/token',
'infos_url' => 'https://launchpad.37signals.com/authorization.json',
]);
}
}
@@ -0,0 +1,66 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Davide Bellettini <davide@bellettini.me>
*/
final class ToshlResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'toshl';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'email',
'firstname' => 'first_name',
'lastname' => 'last_name',
'realname' => ['first_name', 'last_name'],
'email' => 'email',
];
/**
* {@inheritdoc}
*/
public function revokeToken($token)
{
$response = $this->httpRequest(
$this->options['revoke_token_url'],
null,
['Authorization' => 'Basic '.base64_encode($this->options['client_id'].':'.$this->options['client_secret'])],
'DELETE'
);
return 204 === $response->getStatusCode();
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://toshl.com/oauth2/authorize',
'access_token_url' => 'https://toshl.com/oauth2/token',
'revoke_token_url' => 'https://toshl.com/oauth2/revoke',
'infos_url' => 'https://api.toshl.com/me',
'csrf' => true,
'use_commas_in_scope' => true,
]);
}
}
@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Julien DIDIER <julien@didier.io>
*/
final class TraktResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'trakt';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'username',
'nickname' => 'username',
'realname' => 'name',
'profilepicture' => 'images.avatar.full',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$content = $this->httpRequest($this->normalizeUrl($this->options['infos_url']), null, [
'Authorization' => 'Bearer '.$accessToken['access_token'],
'Content-Type' => 'application/json',
'trakt-api-key' => $this->options['client_id'],
'trakt-api-version' => 2,
]);
try {
$response = $this->getUserResponse();
$response->setData($content->toArray(false));
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
return $response;
} catch (TransportExceptionInterface|JsonException $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://api-v2launch.trakt.tv/oauth/authorize',
'access_token_url' => 'https://api-v2launch.trakt.tv/oauth/token',
'infos_url' => 'https://api-v2launch.trakt.tv/users/me?extended=images',
]);
}
}
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class TrelloResourceOwner extends GenericOAuth1ResourceOwner
{
public const TYPE = 'trello';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'username',
'realname' => 'fullName',
'email' => 'email',
'profilepicture' => 'avatarSource',
];
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
$token = $this->getRequestToken($redirectUri, $extraParameters);
return $this->normalizeUrl($this->options['authorization_url'], [
'scope' => $this->options['scopes'],
'name' => $this->options['application'],
'expiration' => $this->options['expiration'],
'oauth_token' => $token['oauth_token'],
]);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://trello.com/1/OAuthAuthorizeToken',
'request_token_url' => 'https://trello.com/1/OAuthGetRequestToken',
'access_token_url' => 'https://trello.com/1/OAuthGetAccessToken',
'infos_url' => 'https://api.trello.com/1/members/me?fields=username,fullName,avatarSource,email',
'realm' => 'trello.com',
'application' => null,
'scopes' => 'read',
'expiration' => null,
]);
}
}
@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Simon Bräuer <redshark1802>
*/
final class TwitchResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'twitch';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'data.0.id',
'nickname' => 'data.0.login',
'realname' => 'data.0.display_name',
'email' => 'data.0.email', // Require scope "user:read:email"
'profilepicture' => 'data.0.profile_image_url',
];
/**
* {@inheritdoc}
*/
protected function httpRequest($url, $content = null, array $headers = [], $method = null)
{
// Twitch also require that you provide the client id as a header
$headers += ['Client-ID' => $this->options['client_id']];
return parent::httpRequest($url, $content, $headers, $method);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://id.twitch.tv/oauth2/authorize',
'access_token_url' => 'https://id.twitch.tv/oauth2/token',
'infos_url' => 'https://api.twitch.tv/helix/users',
'use_bearer_authorization' => true,
'use_authorization_to_get_token' => false,
]);
}
}
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Alexander <iam.asm89@gmail.com>
*/
final class TwitterResourceOwner extends GenericOAuth1ResourceOwner
{
public const TYPE = 'twitter';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id_str',
'nickname' => 'screen_name',
'realname' => 'name',
'profilepicture' => 'profile_image_url_https',
'email' => 'email',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
if ($this->options['include_email']) {
$this->options['infos_url'] = $this->normalizeUrl($this->options['infos_url'], ['include_email' => 'true']);
}
return parent::getUserInformation($accessToken, $extraParameters);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://api.twitter.com/oauth/authenticate',
'request_token_url' => 'https://api.twitter.com/oauth/request_token',
'access_token_url' => 'https://api.twitter.com/oauth/access_token',
'infos_url' => 'https://api.twitter.com/1.1/account/verify_credentials.json',
'include_email' => false,
]);
$resolver->setDefined('x_auth_access_type');
// @link https://dev.twitter.com/oauth/reference/post/oauth/request_token
$resolver->setAllowedValues('x_auth_access_type', ['read', 'write']);
// @link https://dev.twitter.com/rest/reference/get/account/verify_credentials
$resolver->setAllowedTypes('include_email', 'bool');
}
}
@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Adrov Igor <nucleartux@gmail.com>
* @author Vladislav Vlastovskiy <me@vlastv.ru>
* @author Alexander Latushkin <alex@skazo4neg.ru>
*/
final class VkontakteResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'vkontakte';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'response.0.id',
'nickname' => 'response.0.nickname',
'firstname' => 'response.0.first_name',
'lastname' => 'response.0.last_name',
'realname' => ['response.0.last_name', 'response.0.first_name'],
'profilepicture' => 'response.0.photo_medium',
'email' => 'email',
];
/**
* {@inheritdoc}
*/
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
$url = $this->normalizeUrl($this->options['infos_url'], [
'access_token' => $accessToken['access_token'],
'fields' => $this->options['fields'],
'name_case' => $this->options['name_case'],
'v' => $this->options['api_version'],
]);
try {
$response = $this->getUserResponse();
$response->setResourceOwner($this);
$response->setOAuthToken(new OAuthToken($accessToken));
$content = $this->doGetUserInformationRequest($url)->toArray(false);
$content['email'] = $accessToken['email'] ?? null;
if (isset($content['response'][0]['screen_name'])) {
$content['response'][0]['nickname'] = $content['response'][0]['screen_name'];
}
$response->setData($content);
return $response;
} catch (TransportExceptionInterface|JsonException $e) {
throw new HttpTransportException('Error while sending HTTP request', $this->getName(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://oauth.vk.com/authorize',
'access_token_url' => 'https://oauth.vk.com/access_token',
'infos_url' => 'https://api.vk.com/method/users.get',
'use_authorization_to_get_token' => false,
// Based on: https://vk.com/dev/constant_version_updates
'api_version' => '5.131',
'scope' => 'email',
'use_commas_in_scope' => true,
'fields' => 'nickname,photo_medium,screen_name,email',
'name_case' => null,
]);
$fieldsNormalizer = function (Options $options, $value) {
if (!$value) {
return null;
}
return \is_array($value) ? implode(',', $value) : $value;
};
$resolver->setNormalizer('fields', $fieldsNormalizer);
}
}
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Alexander <iam.asm89@gmail.com>
*/
final class WindowsLiveResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'windows_live';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'name',
'realname' => 'name',
'firstname' => 'first_name',
'lastname' => 'last_name',
'email' => 'emails.account', // requires 'wl.emails' scope
];
/**
* {@inheritdoc}
*/
protected function doGetTokenRequest($url, array $parameters = [])
{
return parent::httpRequest($url, http_build_query($parameters, '', '&'));
}
/**
* {@inheritdoc}
*/
protected function httpRequest($url, $content = null, array $headers = [], $method = null)
{
// Skip the Content-Type header in GenericOAuth2ResourceOwner::httpRequest
return AbstractResourceOwner::httpRequest($url, $content, $headers, $method);
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://login.live.com/oauth20_authorize.srf',
'access_token_url' => 'https://login.live.com/oauth20_token.srf',
'infos_url' => 'https://apis.live.net/v5.0/me',
'scope' => 'wl.signin',
]);
}
}
@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Joseph Bielawski <stloyd@gmail.com>
*/
final class WordpressResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'wordpress';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'ID',
'nickname' => 'username',
'realname' => 'display_name',
'email' => 'email',
'profilepicture' => 'avatar_URL',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://public-api.wordpress.com/oauth2/authorize',
'access_token_url' => 'https://public-api.wordpress.com/oauth2/token',
'infos_url' => 'https://public-api.wordpress.com/rest/v1/me',
]);
}
}
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author othillo <othillo@othillo.nl>
*/
final class XingResourceOwner extends GenericOAuth1ResourceOwner
{
public const TYPE = 'xing';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'users.0.id',
'nickname' => 'users.0.display_name',
'firstname' => 'users.0.first_name',
'lastname' => 'users.0.last_name',
'realname' => ['users.0.first_name', 'users.0.last_name'],
'profilepicture' => 'users.0.photo_urls.large',
'email' => 'users.0.active_email',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://api.xing.com/v1/authorize',
'request_token_url' => 'https://api.xing.com/v1/request_token',
'access_token_url' => 'https://api.xing.com/v1/access_token',
'infos_url' => 'https://api.xing.com/v1/users/me',
]);
}
}
@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Tom <tomilett@gmail.com>
* @author Alexander <iam.asm89@gmail.com>
*/
final class YahooResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'yahoo';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'sub',
'nickname' => 'given_name',
'realname' => 'name',
'email' => 'email',
'firstname' => 'given_name',
'lastname' => 'family_name',
];
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://api.login.yahoo.com/oauth2/request_auth',
'request_token_url' => 'https://api.login.yahoo.com/oauth2/get_token',
'access_token_url' => 'https://api.login.yahoo.com/oauth2/get_token',
'infos_url' => 'https://api.login.yahoo.com/openid/v1/userinfo',
'realm' => 'yahooapis.com',
]);
}
}
@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Anton Kamenschikov <wiistriker [at] gmail.com>
*/
final class YandexResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'yandex';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'id',
'nickname' => 'display_name',
'realname' => 'real_name',
'email' => 'default_email',
];
/**
* {@inheritdoc}
*/
protected function doGetUserInformationRequest($url, array $parameters = [])
{
// Yandex require to pass the OAuth token as 'oauth_token' instead of 'access_token'
return $this->httpRequest(str_replace('access_token', 'oauth_token', $url));
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://oauth.yandex.ru/authorize',
'access_token_url' => 'https://oauth.yandex.ru/token',
'infos_url' => 'https://login.yandex.ru/info?format=json',
]);
}
}
@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Gennady Telegin <gtelegin@gmail.com>
*/
final class YoutubeResourceOwner extends GenericOAuth2ResourceOwner
{
public const TYPE = 'youtube';
/**
* {@inheritdoc}
*/
protected array $paths = [
'identifier' => 'items.0.id',
'nickname' => 'items.0.snippet.title',
'realname' => 'items.0.snippet.title',
'email' => 'email',
'profilepicture' => 'items.0.snippet.thumbnails.high.url',
];
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
return parent::getAuthorizationUrl($redirectUri, array_merge([
'access_type' => $this->options['access_type'],
'approval_prompt' => $this->options['approval_prompt'],
'request_visible_actions' => $this->options['request_visible_actions'],
'hd' => $this->options['hd'],
'prompt' => $this->options['prompt'],
], $extraParameters));
}
/**
* {@inheritdoc}
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://accounts.google.com/o/oauth2/auth',
'access_token_url' => 'https://accounts.google.com/o/oauth2/token',
'revoke_token_url' => 'https://accounts.google.com/o/oauth2/revoke',
'infos_url' => 'https://www.googleapis.com/youtube/v3/channels?part=id,snippet&mine=true',
'scope' => 'https://www.googleapis.com/auth/youtube.readonly',
'access_type' => null,
'approval_prompt' => null,
'display' => null,
// Identifying a particular hosted domain account to be accessed (for example, 'mycollege.edu')
'hd' => null,
'login_hint' => null,
'prompt' => null,
'request_visible_actions' => null,
]);
$resolver
// @link https://developers.google.com/accounts/docs/OAuth2WebServer#offline
->setAllowedValues('access_type', ['online', 'offline', null])
// sometimes we need to force for approval prompt (e.g. when we lost refresh token)
->setAllowedValues('approval_prompt', ['force', 'auto', null])
// @link https://developers.google.com/accounts/docs/OAuth2Login#authenticationuriparameters
->setAllowedValues('display', ['page', 'popup', 'touch', 'wap', null])
->setAllowedValues('login_hint', ['email address', 'sub', null])
->setAllowedValues('prompt', [null, 'consent', 'select_account', null])
;
}
}
@@ -0,0 +1,114 @@
<?php
/*
* This file is part of the HWIOAuthBundle package.
*
* (c) Hardware Info <opensource@hardware.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HWI\Bundle\OAuthBundle\OAuth;
use HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* ResourceOwnerInterface.
*
* @author Geoffrey Bachelet <geoffrey.bachelet@gmail.com>
* @author Alexander <iam.asm89@gmail.com>
*/
interface ResourceOwnerInterface
{
/**
* Retrieves the user's information from an access_token.
*
* @param array $accessToken The access token
* @param array $extraParameters An array of parameters to add to the url
*
* @return UserResponseInterface the wrapped response interface
*
* @throws HttpTransportException
*/
public function getUserInformation(array $accessToken, array $extraParameters = []);
/**
* Returns the provider's authorization url.
*
* @param string $redirectUri The uri to redirect the client back to
* @param array $extraParameters An array of parameters to add to the url
*
* @return string The authorization url
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = []);
/**
* Retrieve an access token for a given code.
*
* @param HttpRequest $request The request object where is going to extract the code from
* @param string $redirectUri The uri to redirect the client back to
* @param array $extraParameters An array of parameters to add to the url
*
* @return array The access token
*
* @throws HttpTransportException
*/
public function getAccessToken(HttpRequest $request, $redirectUri, array $extraParameters = []);
/**
* Check whatever CSRF token from request is valid or not.
*
* @param string|null $csrfToken
*
* @return bool True if CSRF token is valid
*
* @throws AuthenticationException When token is not valid
*/
public function isCsrfTokenValid($csrfToken);
/**
* Return a name for the resource owner.
*
* @return string
*/
public function getName();
/**
* Retrieve an option by name.
*
* @param string $name The option name
*
* @return mixed The option value
*
* @throws \InvalidArgumentException When the option does not exist
*/
public function getOption($name);
/**
* Checks whether the class can handle the request.
*
* @return bool
*/
public function handles(HttpRequest $request);
/**
* Add extra paths to the configuration.
*/
public function addPaths(array $paths);
/**
* @param string $refreshToken Refresh token
* @param array $extraParameters An array of parameters to add to the url
*/
public function refreshAccessToken($refreshToken, array $extraParameters = []);
public function getState(): StateInterface;
public function storeState(?StateInterface $state = null);
public function addStateParameter(string $key, string $value): void;
}

Some files were not shown because too many files have changed in this diff Show More