├── lang ├── _manifest_exclude ├── en_GB.yml └── en.yml ├── code-of-conduct.md ├── code ├── OpauthMemberExtension.php ├── OpauthValidationException.php ├── OpauthValidator.php ├── OpauthMemberLoginFormExtension.php ├── OpauthAuthenticator.php ├── OpauthResponseHelper.php ├── OpauthLoginForm.php ├── OpauthRegisterForm.php ├── OpauthIdentity.php └── OpauthController.php ├── _config.php ├── _config ├── _routes.yml └── _config.yml ├── .editorconfig ├── composer.json ├── license.md └── README.md /lang/_manifest_exclude: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | When having discussions about this module in issues or pull request please adhere to the [SilverStripe Community Code of Conduct](https://docs.silverstripe.org/en/contributing/code_of_conduct). 2 | -------------------------------------------------------------------------------- /code/OpauthMemberExtension.php: -------------------------------------------------------------------------------- 1 | "OpauthIdentity" 6 | ); 7 | 8 | public function onBeforeDelete() { 9 | $this->owner->OpauthIdentities()->removeAll(); 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | 5 | * @author Dan Hensby <@dhensby> 6 | * @copyright Copyright (c) 2013, Better Brief LLP 7 | */ 8 | // Set base constant so devs can put this module wherever 9 | define('OPAUTH_BASE', basename(dirname(__FILE__))); 10 | 11 | Authenticator::register_authenticator('OpauthAuthenticator'); 12 | -------------------------------------------------------------------------------- /_config/_routes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: opauthroutes 3 | After: framework/routes#rootroutes 4 | --- 5 | # Config weirdness - confusing. 6 | # Remember to replicate this path with no slashes in the Director rules. 7 | OpauthController: 8 | opauth_path: '/opauth/' 9 | Director: 10 | rules: 11 | 'opauth/strategy/$Strategy/$StrategyMethod': 'OpauthController' 12 | 'opauth/strategy/$Strategy': 'OpauthController' 13 | 'opauth': 'OpauthController' 14 | -------------------------------------------------------------------------------- /lang/en_GB.yml: -------------------------------------------------------------------------------- 1 | en_GB: 2 | OpauthAuthenticator: 3 | TITLE: 'Social Login' 4 | OpauthLoginForm: 5 | OAUTHFAILURE: 'There was a problem getting a response from {provider}.' 6 | OpauthRegisterForm: 7 | ERROREMAILTAKEN: 'It looks like this email has already been used' 8 | OpauthController: 9 | TITLE: 'Social login' 10 | PROFILECOMPLETIONTITLE: 'Complete your profile to register' 11 | OpauthRegisterForm: 12 | COMPLETE: 'Complete' -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /_config/_config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: silverstripe-opauth 3 | After: 'framework/*','cms/*' 4 | --- 5 | # YAML configuration for SilverStripe 6 | # See http://doc.silverstripe.org/framework/en/topics/configuration 7 | # Caution: Indentation through two spaces, not tabs 8 | OpauthAuthenticator: 9 | opauth_settings: 10 | security_iteration: 500 11 | security_timeout: '2 minutes' 12 | callback_transport: 'session' 13 | Member: 14 | extensions: 15 | - OpauthMemberExtension 16 | MemberLoginForm: 17 | extensions: 18 | - OpauthMemberLoginFormExtension -------------------------------------------------------------------------------- /lang/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | OpauthAuthenticator: 3 | TITLE: 'Social Login' 4 | OpauthLoginForm: 5 | OAUTHFAILURE: 'There was a problem getting a response from {provider}.' 6 | OpauthRegisterForm: 7 | ERROREMAILTAKEN: 'It looks like this email has already been used' 8 | OpauthController: 9 | TITLE: 'Social login' 10 | PROFILECOMPLETIONTITLE: 'Complete your profile to register' 11 | OpauthMemberLoginFormExtension: 12 | NoResetPassword: 'Can''t reset password for accounts registered through {provider}' 13 | OpauthRegisterForm: 14 | COMPLETE: 'Complete' -------------------------------------------------------------------------------- /code/OpauthValidationException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2013, Better Brief LLP 8 | */ 9 | class OpauthValidationException extends Exception { 10 | 11 | protected $data; 12 | 13 | public function __construct($message, $code, $data = null) { 14 | parent::__construct($message, $code); 15 | $this->setData($data); 16 | } 17 | 18 | public function setData($data) { 19 | $this->data = $data; 20 | } 21 | 22 | public function getData() { 23 | return $this->data; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /code/OpauthValidator.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class OpauthValidator extends RequiredFields { 9 | 10 | public function php($data) { 11 | $customValid = true; 12 | $requiredValid = parent::php($data); 13 | // If there's a custom validator set, validate with that too 14 | if($validatorClass = self::config()->custom_validator) { 15 | $custom = new $validatorClass(); 16 | $custom->setForm($this->form); 17 | $customValid = $custom->php($data); 18 | if(!$customValid) { 19 | if($requiredValid) { 20 | $this->errors = array(); 21 | } 22 | $this->errors = array_merge($this->errors, $custom->errors); 23 | } 24 | } 25 | return $customValid && $requiredValid; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "betterbrief/silverstripe-opauth", 3 | "type": "silverstripe-module", 4 | "description": "SilverStripe 3.1 OpAuth module. See: http://opauth.org/", 5 | "keywords": ["silverstripe", "social", "opauth", "oauth", "login"], 6 | "homepage": "https://github.com/BetterBrief/silverstripe-opauth", 7 | "license": "BSD", 8 | "authors": [ 9 | { 10 | "name": "Will Morgan", 11 | "homepage": "http://twitter.com/willmorgan", 12 | "role": "Lead Developer" 13 | }, 14 | { 15 | "name": "Dan Hensby", 16 | "homepage": "https://github.com/dhensby", 17 | "role": "Code Scrutinizer" 18 | } 19 | ], 20 | "require": { 21 | "silverstripe/framework": "~3.1", 22 | "opauth/opauth": "~0.4.5" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "~3.7" 26 | }, 27 | "repositories": [ 28 | { 29 | "type": "vcs", 30 | "url": "https://github.com/dhensby/opauth" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /code/OpauthMemberLoginFormExtension.php: -------------------------------------------------------------------------------- 1 | get('OpauthMemberLoginFormExtension', 'allow_password_reset')) { 18 | return null; 19 | } 20 | 21 | $identity = OpauthIdentity::get()->find('MemberID', $member->ID); 22 | if(!$member->Password && $identity) { 23 | $this->owner->sessionMessage( 24 | _t( 25 | 'OpauthMemberLoginFormExtension.NoResetPassword', 26 | 'Can\'t reset password for accounts registered through {provider}', 27 | array('provider' => $identity->Provider) 28 | ), 29 | 'bad' 30 | ); 31 | return false; 32 | } else { 33 | return null; 34 | } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Better Brief LLP 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /code/OpauthAuthenticator.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Dan Hensby <@dhensby> 7 | * @copyright Copyright (c) 2013, Better Brief LLP 8 | */ 9 | class OpauthAuthenticator extends MemberAuthenticator { 10 | 11 | private static 12 | /** 13 | * @var Opauth Persistent Opauth instance. 14 | */ 15 | $opauth; 16 | 17 | /** 18 | * get_enabled_strategies 19 | * @return array Enabled strategies set in _config 20 | */ 21 | public static function get_enabled_strategies() { 22 | $strategyConfig = self::config()->opauth_settings['Strategy']; 23 | return array_keys($strategyConfig); 24 | } 25 | 26 | /** 27 | * get_opauth_config 28 | * @param array Any extra overrides 29 | * @return array Config for use with Opauth 30 | */ 31 | public static function get_opauth_config($mergeConfig = array()) { 32 | $config = self::config(); 33 | return array_merge( 34 | array( 35 | 'path' => OpauthController::get_path(), 36 | 'callback_url' => OpauthController::get_callback_path(), 37 | ), 38 | $config->opauth_settings, 39 | $mergeConfig 40 | ); 41 | } 42 | 43 | /** 44 | * opauth 45 | * @param boolean $autoRun Should Opauth auto run? Default: false 46 | * @return Opauth The Opauth instance. Isn't it easy to typo this as Opeth? 47 | */ 48 | public static function opauth($autoRun = false, $config = array()) { 49 | if(!isset(self::$opauth)) { 50 | self::$opauth = new Opauth(self::get_opauth_config($config), $autoRun); 51 | } 52 | return self::$opauth; 53 | } 54 | 55 | /** 56 | * get_strategy_segment 57 | * Works around Opauth's weird URL scheme - GoogleStrategy => /google/ 58 | * @return string 59 | */ 60 | public static function get_strategy_segment($strategy) { 61 | return preg_replace('/(strategy)$/', '', strtolower($strategy)); 62 | } 63 | 64 | /** 65 | * @return OpauthLoginForm 66 | */ 67 | public static function get_login_form(Controller $controller) { 68 | return Injector::inst()->create('OpauthLoginForm', $controller, 'LoginForm'); 69 | } 70 | 71 | /** 72 | * Get the name of the authentication method 73 | * 74 | * @return string Returns the name of the authentication method. 75 | */ 76 | public static function get_name() { 77 | return _t('OpauthAuthenticator.TITLE', 'Social Login'); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /code/OpauthResponseHelper.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2013, Better Brief LLP 10 | */ 11 | class OpauthResponseHelper { 12 | 13 | /** 14 | * Take the first part of the name 15 | * @return string 16 | */ 17 | public static function get_first_name($source) { 18 | $name = explode(' ', self::parse_source_path('info.name', $source)); 19 | return array_shift($name); 20 | } 21 | 22 | /** 23 | * Take all but the first part of the name 24 | * @return string 25 | */ 26 | public static function get_last_name($source) { 27 | $name = explode(' ', self::parse_source_path('info.name', $source)); 28 | array_shift($name); 29 | return join(' ', $name); 30 | } 31 | 32 | /** 33 | * Twitter responds with just a language (also a TZ, but unused for now) 34 | * If the PECL Locale extension is used it may be possible to combine both 35 | * the TZ and the language to fine tune a user's location, but a bit OTT. 36 | * @return string 37 | */ 38 | public static function get_twitter_locale($source) { 39 | $language = self::parse_source_path('raw.lang', $source); 40 | return self::get_smart_locale($language); 41 | } 42 | 43 | /** 44 | * Google responds near perfectly for locales, if populated. 45 | * Fallback otherwise. 46 | * @return string 47 | */ 48 | public static function get_google_locale($source) { 49 | $locale = self::parse_source_path('raw.locale', $source); 50 | if(!$locale) { 51 | return self::get_smart_locale(); 52 | } 53 | return str_replace('-', '_', $locale); 54 | } 55 | 56 | /** 57 | * Try very hard to get a locale for this user. Helps for i18n etc. 58 | * @return string 59 | */ 60 | public static function get_smart_locale($language = null) { 61 | 62 | require_once FRAMEWORK_PATH . '/thirdparty/Zend/Locale.php'; 63 | $locale = Zend_Locale::getBrowser(); 64 | 65 | if(!$locale) { 66 | if($language) { 67 | return i18n::get_locale_from_lang($language); 68 | } 69 | else { 70 | return i18n::get_locale(); 71 | } 72 | } 73 | 74 | $locale = array_keys($locale); 75 | $firstPref = array_shift($locale); 76 | 77 | if(strpos($firstPref, '_') === false) { 78 | return i18n::get_locale_from_lang($language); 79 | } 80 | 81 | return $firstPref; 82 | } 83 | 84 | /** 85 | * Dot notation parser. Looks for an index or fails gracefully if not found. 86 | * @param string $path The path, dot notated. 87 | * @param array $source The source in which to search. 88 | * @return string|null 89 | */ 90 | public static function parse_source_path($path, $source) { 91 | $fragments = explode('.', $path); 92 | $currentFrame = $source; 93 | foreach($fragments as $fragment) { 94 | if(!isset($currentFrame[$fragment])) { 95 | return null; 96 | } 97 | $currentFrame = $currentFrame[$fragment]; 98 | } 99 | return $currentFrame; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /code/OpauthLoginForm.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Dan Hensby <@dhensby> 10 | * @copyright Copyright (c) 2013, Better Brief LLP 11 | */ 12 | class OpauthLoginForm extends LoginForm { 13 | 14 | private 15 | /* 16 | * @var boolean 17 | */ 18 | $_strategiesDefined = false; 19 | 20 | protected 21 | /** 22 | * @var array config 23 | */ 24 | $authenticator_class = 'OpauthAuthenticator'; 25 | 26 | private static 27 | $allowed_actions = array( 28 | 'httpSubmission', 29 | ); 30 | 31 | public function __construct($controller, $name) { 32 | parent::__construct($controller, $name, $this->getFields(), $this->getActions()); 33 | $this->configureBackURL(); 34 | } 35 | 36 | /** 37 | * Handle any backURL. Uses sessions as state gets lost through OAuth flow. 38 | * Use the same session key as MemberLoginForm for x-compat 39 | */ 40 | public function configureBackURL() { 41 | if($backURL = $this->controller->request->requestVar('BackURL')) { 42 | Session::set('BackURL', $backURL); 43 | } 44 | } 45 | 46 | /** 47 | * Override httpSubmission so we definitely have strategy handlers. 48 | * This is because Form::httpSubmission is directly called. 49 | */ 50 | public function httpSubmission($request) { 51 | $this->defineStrategyHandlers(); 52 | return parent::httpSubmission($request); 53 | } 54 | 55 | /** 56 | * Channel several unknown strategies in to one handler 57 | */ 58 | protected function defineStrategyHandlers() { 59 | if(!$this->_strategiesDefined) { 60 | foreach($this->getStrategies() as $strategyClass) { 61 | $strategyMethod = 'handleStrategy' . $strategyClass; 62 | $this->addWrapperMethod($strategyMethod, 'handleStrategy'); 63 | } 64 | $this->_strategiesDefined = true; 65 | } 66 | } 67 | 68 | /** 69 | * Ensure AuthenticationMethod is set to tell Security which form to process 70 | * Very important for multi authenticator form setups. 71 | * @return FieldList 72 | */ 73 | protected function getFields() { 74 | return new FieldList( 75 | new HiddenField('AuthenticationMethod', null, $this->authenticator_class) 76 | ); 77 | } 78 | 79 | /** 80 | * Provide an action button to be clicked per strategy 81 | * @return FieldList 82 | */ 83 | protected function getActions() { 84 | $actions = new FieldList(); 85 | foreach($this->getStrategies() as $strategyClass) { 86 | $strategyMethod = 'handleStrategy' . $strategyClass; 87 | $fa = new FormAction($strategyMethod, $strategyClass); 88 | $fa->setUseButtonTag(true); 89 | $actions->push($fa); 90 | } 91 | return $actions; 92 | } 93 | 94 | /** 95 | * @return array All enabled strategies from config 96 | */ 97 | public function getStrategies() { 98 | return OpauthAuthenticator::get_enabled_strategies(); 99 | } 100 | 101 | /** 102 | * Global endpoint for handleStrategy - all strategy actions point here. 103 | * @throws LogicException This should not be directly called. 104 | * @throws InvalidArgumentException The strategy must be valid and existent 105 | * @param string $funcName The bound function name from addWrapperMethod 106 | * @param array $data Standard data param as part of form submission 107 | * @param OpauthLoginForm $form 108 | * @param SS_HTTPRequest $request 109 | * @return ViewableData 110 | */ 111 | public function handleStrategy($funcName, $data, $form, $request) { 112 | if(func_num_args() < 4) { 113 | throw new LogicException('Must be called with a strategy handler'); 114 | } 115 | // Trim handleStrategy from the function name: 116 | $strategy = substr($funcName, strlen('handleStrategy')) . 'Strategy'; 117 | 118 | // Check the strategy is good 119 | if(!class_exists($strategy) || $strategy instanceof OpauthStrategy) { 120 | throw new InvalidArgumentException('Opauth strategy ' . $strategy . ' was not found or is not a valid strategy'); 121 | } 122 | 123 | return $this->controller->redirect( 124 | Controller::join_links( 125 | OpauthController::get_path(), 126 | OpauthAuthenticator::get_strategy_segment($strategy) 127 | ) 128 | ); 129 | } 130 | 131 | /** 132 | * The authenticator name, used in templates 133 | * @return string 134 | */ 135 | public function getAuthenticatorName() { 136 | return OpauthAuthenticator::get_name(); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /code/OpauthRegisterForm.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2013, Better Brief LLP 10 | */ 11 | class OpauthRegisterForm extends Form { 12 | 13 | protected 14 | $fields, 15 | $requiredFields; 16 | 17 | protected static 18 | $field_source; 19 | 20 | /** 21 | * @param Controller $controller 22 | * @param string $name 23 | * @param array $requiredFields 24 | */ 25 | public function __construct($controller, $name, array $requiredFields = null) { 26 | if(isset($requiredFields)) { 27 | $this->requiredFields = $requiredFields; 28 | } 29 | parent::__construct($controller, $name, $this->getFields(), $this->getActions(), $this->getValidator()); 30 | // Manually call extensions here as Object must first construct extensions 31 | $this->extend('updateFields', $this->fields); 32 | $this->extend('updateActions', $this->actions); 33 | } 34 | 35 | /** 36 | * setRequiredFields 37 | * Resets everything if the fields change 38 | */ 39 | public function setRequiredFields($fields) { 40 | $this->requiredFields = $fields; 41 | $this->setValidator($this->getValidator()); 42 | return $this; 43 | } 44 | 45 | /** 46 | * getFields 47 | * Picks only the required fields from the field source 48 | * and then presents them in a field set. 49 | * @return FieldList 50 | */ 51 | public function getFields() { 52 | $fields = $this->getFieldSource(); 53 | $this->extend('updateFields', $fields); 54 | return $fields; 55 | } 56 | 57 | /** 58 | * Uses the field_source defined, or falls back to the Member's getCMSFields 59 | * @return FieldList 60 | */ 61 | public function getFieldSource() { 62 | if(is_callable(self::$field_source)) { 63 | $fields = call_user_func(self::$field_source, $this); 64 | if(!$fields instanceof FieldList) { 65 | throw new InvalidArgumentException('Field source must be callable and return a FieldList'); 66 | } 67 | return $fields; 68 | } 69 | return new FieldList(singleton('Member')->getCMSFields()->dataFields()); 70 | } 71 | 72 | /** 73 | * Set a callable as a data provider for the field source. Field names must 74 | * match those found on @see Member so they can be filtered accordingly. 75 | * 76 | * Callable docs: http://php.net/manual/en/language.types.callable.php 77 | * @param callable $sourceFn Source closure to use, accepts $this as param 78 | */ 79 | public static function set_field_source($sourceFn) { 80 | if(!is_callable($sourceFn)) { 81 | throw new InvalidArgumentException('$sourceFn must be callable and return a FieldList'); 82 | } 83 | self::$field_source = $sourceFn; 84 | } 85 | 86 | /** 87 | * Get actions 88 | * Points to a controller action 89 | * @return FieldList 90 | */ 91 | public function getActions() { 92 | $actions = new FieldList(array( 93 | new FormAction('doCompleteRegister', _t('OpauthRegisterForm.COMPLETE', 'Complete')), 94 | )); 95 | $this->extend('updateActions', $actions); 96 | return $actions; 97 | } 98 | 99 | /** 100 | * @return RequiredFields 101 | */ 102 | public function getValidator() { 103 | return Injector::inst()->create('OpauthValidator', $this->requiredFields); 104 | } 105 | 106 | /** 107 | * Populates the form somewhat intelligently 108 | * @param SS_HTTPRequest $request Any request 109 | * @param Member $member Any member 110 | * @param array $required Any validation messages 111 | * @return $this 112 | */ 113 | public function populateFromSources(SS_HTTPRequest $request = null, Member $member = null, array $required = null) { 114 | $dataPath = "FormInfo.{$this->FormName()}.data"; 115 | if(isset($member)) { 116 | $this->loadDataFrom($member); 117 | } 118 | else if(isset($request)) { 119 | $this->loadDataFrom($request->postVars()); 120 | } 121 | // Hacky again :( 122 | else if(Session::get($dataPath)) { 123 | $this->loadDataFrom(Session::get($dataPath)); 124 | } 125 | else if($failover = $this->getSessionData()) { 126 | $this->loadDataFrom($failover); 127 | } 128 | if(!empty($required)) { 129 | $this->setRequiredFields($required); 130 | } 131 | return $this; 132 | } 133 | 134 | /** 135 | * Set failover data, so a user can refresh without losing his or her data 136 | * @param mixed $data Any type useable with $this->loadDataFrom 137 | */ 138 | public function setSessionData($data) { 139 | Session::set($this->class.'.data', $data); 140 | return $this; 141 | } 142 | 143 | public function getSessionData() { 144 | return Session::get($this->class.'.data'); 145 | } 146 | 147 | public function clearSessionData() { 148 | Session::clear($this->class.'.data'); 149 | return $this; 150 | } 151 | 152 | /** 153 | * mockErrors 154 | * Uses a very nasty trick to dynamically create some required field errors 155 | */ 156 | public function mockErrors() { 157 | $this->validate(); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /code/OpauthIdentity.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Dan Hensby <@dhensby> 8 | * @copyright Copyright (c) 2013, Better Brief LLP 9 | */ 10 | class OpauthIdentity extends DataObject { 11 | 12 | private static 13 | $db = array( 14 | 'UID' => 'Varchar(255)', 15 | 'Provider' => 'Varchar(45)', 16 | ), 17 | $has_one = array( 18 | 'Member' => 'Member', 19 | ), 20 | $summary_fields = array( 21 | 'Member.Email' => 'MemberEmail', 22 | 'Provider' => 'Provider', 23 | 'UID' => 'UID', 24 | ); 25 | 26 | protected 27 | /** 28 | * @var array source from Opauth 29 | */ 30 | $authSource, 31 | /** 32 | * @var array The parsed member record, if any 33 | */ 34 | $parsedRecord; 35 | 36 | private 37 | /** 38 | * @var boolean shim for onBeforeCreate 39 | */ 40 | $_isCreating = false; 41 | 42 | /** 43 | * factory 44 | * Returns or creates a fresh OpauthIdentity. 45 | * @param array $oaResponse The response object from Opauth. 46 | * @return OpauthIdentity instance based on $oaResponse. 47 | */ 48 | public static function factory(array $oaResponse) { 49 | 50 | if(empty($oaResponse['auth'])) { 51 | throw new InvalidArgumentException('The auth key is required to continue.'); 52 | } 53 | if(empty($oaResponse['auth']['provider'])) { 54 | throw new InvalidArgumentException('Unable to determine provider.'); 55 | } 56 | 57 | $auth = $oaResponse['auth']; 58 | 59 | $do = OpauthIdentity::get()->filter( 60 | array( 61 | 'Provider' => $auth['provider'], 62 | 'UID' => $auth['uid'], 63 | ) 64 | )->first(); 65 | 66 | if(!$do || !$do->exists()) { 67 | $do = new OpauthIdentity(); 68 | $do->Provider = $auth['provider']; 69 | $do->UID = $auth['uid']; 70 | } 71 | 72 | $do->setAuthSource($auth); 73 | return $do; 74 | } 75 | 76 | /** 77 | * Add an extension point for creation and member linking 78 | */ 79 | public function onBeforeWrite() { 80 | parent::onBeforeWrite(); 81 | if(!$this->isInDb()) { 82 | $this->_isCreating = true; 83 | $this->extend('onBeforeCreate'); 84 | } 85 | if($this->isChanged('MemberID')) { 86 | $this->extend('onMemberLinked'); 87 | } 88 | } 89 | 90 | /** 91 | * Add an extension point for afterCreate 92 | */ 93 | public function onAfterWrite() { 94 | parent::onAfterWrite(); 95 | if($this->_isCreating === true) { 96 | $this->_isCreating = false; 97 | $this->extend('onAfterCreate'); 98 | } 99 | } 100 | 101 | /** 102 | * Finds a member based on this identity. Searches existing records before 103 | * creating a new Member object. 104 | * Note that this method does not write anything, merely sets everything up. 105 | * @param array $usrSettings A map of settings because there are so many. 106 | * @return Member 107 | */ 108 | public function findOrCreateMember($usrSettings = array()) { 109 | 110 | $defaults = array( 111 | /** 112 | * Link this identity to any newly discovered member. 113 | */ 114 | 'linkOnMatch' => true, 115 | /** 116 | * True, false, or an array of fields to overwrite if we merge data. 117 | * Exception to this rule is overwriteEmail, which takes precedence. 118 | */ 119 | 'overwriteExistingFields' => false, 120 | /** 121 | * Overwrite the email field if it's different. Effectively changes 122 | * the Member login details, so it's set to false for now. 123 | */ 124 | 'overwriteEmail' => false, 125 | ); 126 | 127 | $settings = array_merge($defaults, $usrSettings); 128 | 129 | if($this->isInDB()) { 130 | $member = $this->Member(); 131 | if($member->exists()) { 132 | return $member; 133 | } 134 | } 135 | 136 | $record = $this->getMemberRecordFromAuth(); 137 | 138 | if(empty($record['Email'])) { 139 | $member = new Member(); 140 | } 141 | else { 142 | $member = Member::get()->filter('Email', $record['Email'])->first(); 143 | 144 | if(!$member) { 145 | $member = new Member(); 146 | } 147 | } 148 | 149 | if($settings['linkOnMatch'] && $member->isInDB()) { 150 | $this->MemberID = $member->ID; 151 | } 152 | 153 | // If this is a new member, give it everything we have. 154 | if(!$member->isInDB()) { 155 | $member->update($record); 156 | } 157 | // If not, we update it carefully using the settings described above. 158 | else { 159 | $overwrite = $settings['overwriteExistingFields']; 160 | $overwriteEmail = $settings['overwriteEmail']; 161 | $fieldsToWrite = array(); 162 | 163 | // If overwrite is true, take everything (subtract Email later) 164 | if($overwrite === true) { 165 | $fieldsToWrite = $record; 166 | } 167 | else if(is_array($overwrite)) { 168 | $fieldsToWrite = array_intersect_key($record, ArrayLib::valuekey($overwrite)); 169 | } 170 | // If false then fieldsToWrite remains empty, let's coast it out. 171 | 172 | // Subtract email if setting is not precisely true: 173 | if($overwriteEmail !== true && isset($fieldsToWrite['Email'])) { 174 | unset($fieldsToWrite['Email']); 175 | } 176 | 177 | // Boom, we're so done. 178 | $member->update($fieldsToWrite); 179 | } 180 | 181 | return $member; 182 | } 183 | 184 | /** 185 | * @param array $auth 186 | */ 187 | public function setAuthSource($auth) { 188 | $this->authSource = $auth; 189 | unset($this->parsedRecord); 190 | return $this; 191 | } 192 | 193 | /** 194 | * @return array 195 | */ 196 | public function getAuthSource() { 197 | return $this->authSource; 198 | } 199 | 200 | /** 201 | * @return array The mapping arrangement from auth response to Member. 202 | */ 203 | public function getMemberMapper() { 204 | $mapper = Config::inst()->get(__CLASS__, 'member_mapper'); 205 | if(!isset($mapper[$this->Provider])) { 206 | return array(); 207 | } 208 | return $mapper[$this->Provider]; 209 | } 210 | 211 | /** 212 | * Use dot notation and/or a parser to retrieve information from a provider. 213 | * Examples of simple dot notation: 214 | * - 'FirstName' => 'info.first_name' 215 | * - 'Surname' => 'info.surname' 216 | * Examples of a parser, for example when only a "name" param is present: 217 | * - 'FirstName' => array('OpauthResponseHelper', 'get_first_name') 218 | * - 'Surname' => array('OpauthResponseHelper', 'get_last_name') 219 | * @see OpauthResponseHelper 220 | * @return array The data record to add to a member 221 | */ 222 | public function getMemberRecordFromAuth() { 223 | if(empty($this->parsedRecord)) { 224 | $record = array(); 225 | foreach($this->getMemberMapper() as $memberField => $sourcePath) { 226 | if(is_array($sourcePath)) { 227 | $record[$memberField] = call_user_func($sourcePath, $this->authSource); 228 | } 229 | else if(is_string($sourcePath)) { 230 | $record[$memberField] = OpauthResponseHelper::parse_source_path($sourcePath, $this->authSource); 231 | } 232 | } 233 | $this->parsedRecord = $record; 234 | } 235 | return $this->parsedRecord; 236 | } 237 | 238 | } 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This module depends on Opauth which is no longer maintained - we don't recommend continuing to use this module as we aren't actively developing it any longer.** 2 | 3 | # SilverStripe Opauth Module 4 | 5 | [![Build Status](https://secure.travis-ci.org/BetterBrief/silverstripe-opauth.png?branch=master)](http://travis-ci.org/BetterBrief/silverstripe-opauth) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/BetterBrief/silverstripe-opauth/badges/quality-score.png?s=257116d0420a86115addee48affc91a8abb41939)](https://scrutinizer-ci.com/g/BetterBrief/silverstripe-opauth/) 7 | [![Code Coverage](https://scrutinizer-ci.com/g/BetterBrief/silverstripe-opauth/badges/coverage.png?s=3ca06d73fa9aeb2dc7513c4ca6f6cf703a684911)](https://scrutinizer-ci.com/g/BetterBrief/silverstripe-opauth/) 8 | 9 | ## Introduction 10 | Uses the [Opauth library](http://opauth.org) for easy drop-in strategies for social login. See their [documentation](https://github.com/opauth/opauth/wiki/) 11 | 12 | ## Current Status 13 | 1.1 - stable. No known major issues. Report issues using the [bug tracker](https://github.com/BetterBrief/silverstripe-opauth/issues). 14 | 15 | ## How does it work? 16 | The module provides an additional login form which the developer has control over, that allows users to instantly sign in to your website with an identity provided by any Oauth provider. The providers are each handled by using an `OpauthStrategy`, many of which are freely available. There are strategies for Facebook, Twitter, Google, and many more. 17 | 18 | Based on the identity data from the Oauth provider, the module will find or create a new `Member` object based on the provided email address in the identity. This also means a Member can have many Oauth identities linked to a single account; these are saved in to the `OpauthIdentity` object. 19 | 20 | If the resultant `Member` generated from the provider's response doesn't have an email address, or any other piece of data you require, there is functionality built in to handle this. You can enforce required fields, or any other kind of validation, by setting the `OpauthValidator`'s `custom_validator` property to the class name of the validator you require. 21 | 22 | Other than that, the user flow is quite simple. Provided all required data is there, the member is logged in with `Member::login` and then redirected to the page they were looking at or the default destination, settable in your config - just like the default `MemberAuthenticator`. 23 | 24 | ## Requirements 25 | 26 | * SilverStripe 3.1 (maybe 3.0, but untested so far) 27 | * At least one Opauth strategy 28 | * Preferably, allow_url_fopen enabled in php.ini. We've written a custom cURL workaround that works with Twitter, Google and Facebook strategies, but it's proprietary. 29 | * For extended cURL support we rely on an Opauth fork 30 | 31 | ## FAQ 32 | 33 | ### What does this module include? 34 | It includes: 35 | * the Opauth core (see below); 36 | * `OpauthAuthenticator`: intended to be comparable with `MemberAuthenticator`; 37 | * `OpauthLoginForm`: which offers different ways you can authenticate; 38 | * `OpauthRegisterForm`: which, if configured, provides an intermediate step so incomplete `OpauthIdentity`-authenticating members can fill in extra information as required; 39 | * `OpauthController`: which acts as a negotiator for the communication that strategies undertake; 40 | * `OpauthIdentity`: which acts as a service-agnostic interface with which to save Oauth identities in to the `Member` object. These are associated with a `Member` upon successful login so that the auth provider's UID and signed response act as a key. 41 | 42 | *NB: Opauth's maintainers recommend you include strategies as required, rather than bundling them together.* 43 | 44 | ### Where can I get strategies? 45 | 46 | You can find a list under the "Available Strategies" heading on the [Opauth homepage](http://opauth.org) 47 | 48 | Packagist provides a [list of strategies](https://packagist.org/search/?q=opauth/) you can use to install via Composer. 49 | 50 | ### Where should I put strategies? 51 | Use composer to require your strategies 52 | 53 | ### How do I map the API responses to a `Member`? 54 | You define the `OpauthIdentity` `member_mapper` block in your `_config.yml`. Simply provide a hash map of member fields to dot notated paths of the Opauth response array for simple fields, or if you need to perform some parsing to retrieve the value you want, an array of class name and function, like `['OpauthResponseHelper', 'get_first_name']`. It takes the auth response array as an argument. See the example config YAML below for more details. 55 | 56 | ### How do I configure the module and its strategies? 57 | All Opauth-specific configuration variables can be put under `opauth_settings` and are passed directly to `Opauth`. 58 | 59 | You can put these settings in your `_config.yml` file. Additionally, as your strategy API details will likely change per domain and thus per environment, you are able to update these using the `Config` API. Please see the [Opauth config documentation](https://github.com/opauth/opauth/wiki/Opauth-configuration#configuration-array). Here's some examples to help you: 60 | 61 | ###### `_config.yml` example: 62 | ```yml 63 | --- 64 | Name: silverstripe-opauth 65 | After: 'framework/*','cms/*' 66 | --- 67 | # see the Opauth docs for the config settings - https://github.com/opauth/opauth/wiki/Opauth-configuration#configuration-array 68 | OpauthAuthenticator: 69 | opauth_settings: 70 | #Register your strategies here 71 | #Including any extra config 72 | Strategy: 73 | Facebook: 74 | app_id: '' 75 | app_secret: '' 76 | scope: email 77 | Google: 78 | client_id: '' 79 | client_secret: '' 80 | Twitter: 81 | key: '' 82 | secret: '' 83 | security_salt: 'correct horse battery staple' 84 | security_iteration: 500 85 | security_timeout: '2 minutes' 86 | callback_transport: 'session' 87 | #Configuration for the Identity-Member mapping 88 | OpauthIdentity: 89 | member_mapper: 90 | Facebook: 91 | FirstName: 'info.first_name' 92 | Surname: 'info.last_name' 93 | Locale: 'raw.locale' 94 | Email: 'info.email' 95 | Twitter: 96 | FirstName: ['OpauthResponseHelper', 'get_first_name'] 97 | Surname: ['OpauthResponseHelper', 'get_last_name'] 98 | Locale: ['OpauthResponseHelper', 'get_twitter_locale'] 99 | Google: 100 | FirstName: 'info.first_name' 101 | Surname: 'info.last_name' 102 | Email: 'info.email' 103 | Locale: ['OpauthResponseHelper', 'get_google_locale'] 104 | ``` 105 | 106 | ##### `_config.php` example: 107 | ```php 108 | //Register and configure strategies 109 | Config::inst()->update('OpauthAuthenticator', 'opauth_settings', array( 110 | 'Strategy' => array( 111 | 'Facebook' => array( 112 | 'app_id' => '', 113 | 'app_secret' => '', 114 | 'scope' => 'email', 115 | ), 116 | 'Google' => array( 117 | 'client_id' => '', 118 | 'client_secret' => '' 119 | ), 120 | 'Twitter' => array( 121 | 'key' => '', 122 | 'secret' => '' 123 | ), 124 | ), 125 | )); 126 | 127 | //Identity to member mapping settings per strategy 128 | Config::inst()->update('OpauthIdentity', 'member_mapper', array( 129 | 'Facebook' => array( 130 | 'FirstName' => 'info.first_name', 131 | 'Surname' => 'info.last_name', 132 | 'Locale' => 'raw.locale', 133 | 'Email' => 'info.email', 134 | ), 135 | 'Twitter' => array( 136 | 'FirstName' => array('OpauthResponseHelper', 'get_first_name'), 137 | 'Surname' => array('OpauthResponseHelper', 'get_last_name'), 138 | 'Locale' => array('OpauthResponseHelper', 'get_twitter_locale'), 139 | ), 140 | 'Google' => array( 141 | 'FirstName' => 'info.first_name', 142 | 'Surname' => 'info.last_name', 143 | 'Email' => 'info.email', 144 | 'Locale' => array('OpauthResponseHelper', 'get_google_locale'), 145 | ), 146 | )); 147 | ``` 148 | 149 | *NB: As you can see, sometimes the Strategy configuration settings may have inconsistent namings - we can't help with that, sorry!* 150 | 151 | ## Documentation 152 | Please read the [Opauth documentation](https://github.com/opauth/opauth/wiki/) and [our own documentation](docs/en/) 153 | 154 | ## Raising bugs and suggesting enhancements 155 | If you find a bug or have a Really Good Idea™, please [raise an issue](https://github.com/BetterBrief/silverstripe-opauth/issues). Better still, if you can fix the bug, then feel free to send in a pull request with the remedial code that ideally respects the coding conventions used thus far. 156 | 157 | ## Attribution 158 | * Opauth available under MIT licence by U-Zyn Chua (http://uzyn.com) Copyright © 2012-2013 159 | -------------------------------------------------------------------------------- /code/OpauthController.php: -------------------------------------------------------------------------------- 1 | 8 | * @author Dan Hensby <@dhensby> 9 | * @copyright Copyright (c) 2013, Better Brief LLP 10 | */ 11 | class OpauthController extends ContentController { 12 | 13 | private static 14 | $allowed_actions = array( 15 | 'index', 16 | 'finished', 17 | 'profilecompletion', 18 | 'RegisterForm', 19 | ), 20 | $url_handlers = array( 21 | 'finished' => 'finished', 22 | ); 23 | 24 | /** 25 | * Bitwise indicators to extensions what sort of action is happening 26 | */ 27 | const 28 | /** 29 | * LOGIN = already a user with an OAuth ID 30 | */ 31 | AUTH_FLAG_LOGIN = 2, 32 | /** 33 | * LINK = already a user, linking a new OAuth ID 34 | */ 35 | AUTH_FLAG_LINK = 4, 36 | /** 37 | * REGISTER = new user, linking OAuth ID 38 | */ 39 | AUTH_FLAG_REGISTER = 8; 40 | 41 | protected 42 | $registerForm; 43 | 44 | /** 45 | * Fake a Page_Controller by using that class as a failover 46 | */ 47 | public function __construct($dataRecord = null) { 48 | if(class_exists('Page_Controller')) { 49 | $dataRecord = new Page_Controller($dataRecord); 50 | } 51 | parent::__construct($dataRecord); 52 | } 53 | 54 | /** 55 | * This function only catches the request to pass it straight on. 56 | * Opauth uses the last segment of the URL to identify the auth method. 57 | * In _routes.yml we enforce a $Strategy request parameter to enforce this. 58 | * Equivalent to "index.php" in the Opauth package. 59 | * @todo: Validate the strategy works before delegating to Opauth. 60 | */ 61 | public function index(SS_HTTPRequest $request) { 62 | 63 | $strategy = $request->param('Strategy'); 64 | $method = $request->param('StrategyMethod'); 65 | 66 | if(!isset($strategy)) { 67 | return Security::permissionFailure($this); 68 | } 69 | 70 | // If there is no method then we redirect (not a callback) 71 | if(!isset($method)) { 72 | // Redirects: 73 | OpauthAuthenticator::opauth(true); 74 | } 75 | else { 76 | return $this->oauthCallback($request); 77 | } 78 | } 79 | 80 | /** 81 | * This is executed when the Oauth provider redirects back to us 82 | * Opauth handles everything sent back in this request. 83 | */ 84 | protected function oauthCallback(SS_HTTPRequest $request) { 85 | 86 | // Set up and run opauth with the correct params from the strategy: 87 | OpauthAuthenticator::opauth(true, array( 88 | 'strategy' => $request->param('Strategy'), 89 | 'action' => $request->param('StrategyMethod'), 90 | )); 91 | 92 | } 93 | 94 | /** 95 | * Equivalent to "callback.php" in the Opauth package. 96 | * If there is a problem with the response, we throw an HTTP error. 97 | * When done validating, we return back to the Authenticator continue auth. 98 | * @throws SS_HTTPResponse_Exception if any validation errors 99 | */ 100 | public function finished(SS_HTTPRequest $request) { 101 | 102 | $opauth = OpauthAuthenticator::opauth(false); 103 | 104 | $response = $this->getOpauthResponse(); 105 | 106 | if (!$response) { 107 | $response = array(); 108 | } 109 | // Clear the response as it is only to be read once (if Session) 110 | Session::clear('opauth'); 111 | 112 | // Handle all Opauth validation in this handy function 113 | try { 114 | $this->validateOpauthResponse($opauth, $response); 115 | } 116 | catch(OpauthValidationException $e) { 117 | return $this->handleOpauthException($e); 118 | } 119 | 120 | $identity = OpauthIdentity::factory($response); 121 | 122 | $member = $identity->findOrCreateMember(); 123 | 124 | // If the member exists, associate it with the identity and log in 125 | if($member->isInDB() && $member->validate()->valid()) { 126 | if(!$identity->exists()) { 127 | $identity->write(); 128 | $flag = self::AUTH_FLAG_LINK; 129 | } 130 | else { 131 | $flag = self::AUTH_FLAG_LOGIN; 132 | } 133 | 134 | Session::set('OpauthIdentityID', $identity->ID); 135 | } 136 | else { 137 | 138 | $flag = self::AUTH_FLAG_REGISTER; 139 | 140 | // Write the identity 141 | $identity->write(); 142 | 143 | // Keep a note of the identity ID 144 | Session::set('OpauthIdentityID', $identity->ID); 145 | 146 | // Even if written, check validation - we might not have full fields 147 | $validationResult = $member->validate(); 148 | if(!$validationResult->valid()) { 149 | // Set up the register form before it's output 150 | $regForm = $this->RegisterForm(); 151 | $regForm->loadDataFrom($member); 152 | $regForm->setSessionData($member); 153 | $regForm->validate(); 154 | return $this->redirect($this->Link('profilecompletion')); 155 | } 156 | else { 157 | $member->extend('onBeforeOpauthRegister'); 158 | $member->write(); 159 | $identity->MemberID = $member->ID; 160 | $identity->write(); 161 | } 162 | } 163 | return $this->loginAndRedirect($member, $identity, $flag); 164 | } 165 | 166 | /** 167 | * @param Member 168 | * @param OpauthIdentity 169 | * @param int $mode One or more AUTH_FLAGs. 170 | */ 171 | protected function loginAndRedirect(Member $member, OpauthIdentity $identity, $mode) { 172 | // Back up the BackURL as Member::logIn regenerates the session 173 | $backURL = Session::get('BackURL'); 174 | 175 | // Check if we can log in: 176 | $canLogIn = $member->canLogIn(); 177 | 178 | if(!$canLogIn->valid()) { 179 | $extendedURLs = $this->extend('getCantLoginBackURL', $member, $identity, $canLogIn, $mode); 180 | if(count($extendedURLs)) { 181 | $redirectURL = array_pop($extendedURLs); 182 | $this->redirect($redirectURL, 302); 183 | return; 184 | } 185 | Security::permissionFailure($this, $canLogIn->message()); 186 | return; 187 | } 188 | 189 | // Decide where to go afterwards... 190 | if(!empty($backURL)) { 191 | $redirectURL = $backURL; 192 | } 193 | else { 194 | $redirectURL = Security::config()->default_login_dest; 195 | } 196 | 197 | $extendedURLs = $this->extend('getSuccessBackURL', $member, $identity, $redirectURL, $mode); 198 | 199 | if(count($extendedURLs)) { 200 | $redirectURL = array_pop($extendedURLs); 201 | } 202 | 203 | $member->logIn(); 204 | 205 | // Clear any identity ID 206 | Session::clear('OpauthIdentityID'); 207 | 208 | // Clear the BackURL 209 | Session::clear('BackURL'); 210 | 211 | return $this->redirect($redirectURL); 212 | } 213 | 214 | public function profilecompletion(SS_HTTPRequest $request = null) { 215 | if(!Session::get('OpauthIdentityID')) { 216 | Security::permissionFailure($this); 217 | } 218 | // Redirect to complete register step by adding in extra info 219 | return $this->renderWith(array( 220 | 'OpauthController_profilecompletion', 221 | 'Security_profilecompletion', 222 | 'Page', 223 | ) 224 | ); 225 | } 226 | 227 | public function RegisterForm(SS_HTTPRequest $request = null, Member $member = null, $result = null) { 228 | if(!isset($this->registerForm)) { 229 | $form = Injector::inst()->create('OpauthRegisterForm', $this, 'RegisterForm', $result); 230 | $form->populateFromSources($request, $member, $result); 231 | // Set manually the form action due to how routing works 232 | $form->setFormAction(Controller::join_links( 233 | self::config()->opauth_path, 234 | 'RegisterForm' 235 | )); 236 | $this->registerForm = $form; 237 | } 238 | else { 239 | $this->registerForm->populateFromSources($request, $member, $result); 240 | } 241 | return $this->registerForm; 242 | } 243 | 244 | public function doCompleteRegister($data, $form, $request) { 245 | $member = new Member(); 246 | $form->saveInto($member); 247 | $identityID = Session::get('OpauthIdentityID'); 248 | $identity = DataObject::get_by_id('OpauthIdentity', $identityID); 249 | $validationResult = $member->validate(); 250 | $existing = Member::get()->filter('Email', $member->Email)->first(); 251 | $emailCollision = $existing && $existing->exists(); 252 | // If not valid then we have to manually transpose errors to the form 253 | if(!$validationResult->valid() || $emailCollision) { 254 | $errors = $validationResult->messageList(); 255 | $form->setRequiredFields($errors); 256 | // Mandatory check on the email address 257 | if($emailCollision) { 258 | $form->addErrorMessage('Email', _t( 259 | 'OpauthRegisterForm.ERROREMAILTAKEN', 260 | 'It looks like this email has already been used' 261 | ), 'required'); 262 | } 263 | return $this->redirect('profilecompletion'); 264 | } 265 | // If valid then write and redirect 266 | else { 267 | $member->extend('onBeforeOpauthRegister'); 268 | $member->write(); 269 | $identity->MemberID = $member->ID; 270 | $identity->write(); 271 | return $this->loginAndRedirect($member, $identity, self::AUTH_FLAG_REGISTER); 272 | } 273 | } 274 | 275 | /** 276 | * Returns the response from the Oauth callback. 277 | * @throws InvalidArugmentException 278 | * @return array The response 279 | */ 280 | protected function getOpauthResponse() { 281 | $config = OpauthAuthenticator::get_opauth_config(); 282 | $transportMethod = $config['callback_transport']; 283 | switch($transportMethod) { 284 | case 'session': 285 | return $this->getResponseFromSession(); 286 | case 'get': 287 | case 'post': 288 | return $this->getResponseFromRequest($transportMethod); 289 | default: 290 | throw new InvalidArgumentException('Invalid transport method: ' . $transportMethod); 291 | } 292 | } 293 | 294 | /** 295 | * Validates the Oauth response for Opauth. 296 | * @throws InvalidArgumentException 297 | */ 298 | protected function validateOpauthResponse($opauth, $response) { 299 | if(!empty($response['error'])) { 300 | throw new OpauthValidationException('Oauth provider error', 1, $response['error']); 301 | } 302 | 303 | // Required components within the response 304 | $this->requireResponseComponents( 305 | array('auth', 'timestamp', 'signature'), 306 | $response 307 | ); 308 | 309 | // More required components within the auth section... 310 | $this->requireResponseComponents( 311 | array('provider', 'uid'), 312 | $response['auth'] 313 | ); 314 | 315 | $invalidReason = ''; 316 | 317 | /** 318 | * @todo: improve this signature check. it's a bit weak. 319 | */ 320 | if(!$opauth->validate( 321 | sha1(print_r($response['auth'], true)), 322 | $response['timestamp'], 323 | $response['signature'], 324 | $invalidReason 325 | )) { 326 | throw new OpauthValidationException('Invalid auth response', 3, $invalidReason); 327 | } 328 | } 329 | 330 | /** 331 | * Shorthand for quickly finding missing components and complaining about it 332 | * @throws InvalidArgumentException 333 | */ 334 | protected function requireResponseComponents(array $components, $response) { 335 | foreach($components as $component) { 336 | if(empty($response[$component])) { 337 | throw new OpauthValidationException('Required component missing', 2, $component); 338 | } 339 | } 340 | } 341 | 342 | /** 343 | * @return array Opauth response from session 344 | */ 345 | protected function getResponseFromSession() { 346 | return Session::get('opauth'); 347 | } 348 | 349 | /** 350 | * @param OpauthValidationException $e 351 | */ 352 | protected function handleOpauthException(OpauthValidationException $e) { 353 | $data = $e->getData(); 354 | $loginFormName = 'OpauthLoginForm_LoginForm'; 355 | $message = ''; 356 | switch($e->getCode()) { 357 | case 1: // provider error 358 | $message = _t( 359 | 'OpauthLoginForm.OAUTHFAILURE', 360 | 'There was a problem logging in with {provider}.', 361 | array( 362 | 'provider' => $data['provider'], 363 | ) 364 | ); 365 | break; 366 | case 2: // validation error 367 | case 3: // invalid auth response 368 | $message = _t( 369 | 'OpauthLoginForm.RESPONSEVALIDATIONFAILURE', 370 | 'There was a problem logging in - {message}', 371 | array( 372 | 'message' => $e->getMessage(), 373 | ) 374 | ); 375 | break; 376 | } 377 | // Set form message, redirect to login with permission failure 378 | Form::messageForForm($loginFormName, $message, 'bad'); 379 | // always redirect to login 380 | Security::permissionFailure($this, $message); 381 | } 382 | 383 | /** 384 | * Looks at $method (GET, POST, PUT etc) for the response. 385 | * @return array Opauth response 386 | */ 387 | protected function getResponseFromRequest($method) { 388 | return unserialize(base64_decode($this->request->{$method.'Var'}('opauth'))); 389 | } 390 | 391 | public function Link($action = null) { 392 | return Controller::join_links( 393 | self::config()->opauth_path, 394 | $action 395 | ); 396 | } 397 | 398 | /** 399 | * 'path' param for use in Opauth's config 400 | * MUST have trailling slash for Opauth needs 401 | * @return string 402 | */ 403 | public static function get_path() { 404 | return Controller::join_links( 405 | self::config()->opauth_path, 406 | 'strategy/' 407 | ); 408 | } 409 | 410 | /** 411 | * 'callback_url' param for use in Opauth's config 412 | * MUST have trailling slash for Opauth needs 413 | * @return string 414 | */ 415 | public static function get_callback_path() { 416 | return Controller::join_links( 417 | self::config()->opauth_path, 418 | 'finished/' 419 | ); 420 | } 421 | 422 | ////**** Template variables ****//// 423 | function Title() { 424 | if($this->action == 'profilecompletion') { 425 | return _t('OpauthController.PROFILECOMPLETIONTITLE', 'Complete your profile'); 426 | } 427 | return _t('OpauthController.TITLE', 'Social Login'); 428 | } 429 | 430 | public function Form() { 431 | return $this->RegisterForm(); 432 | } 433 | ////**** END Template variables ****//// 434 | 435 | } 436 | --------------------------------------------------------------------------------