├── providers ├── index.html ├── sso_login │ ├── modules │ │ ├── index.html │ │ ├── sso_tos.php │ │ ├── sso_email_two_factor.php │ │ └── sso_google_authenticator.php │ └── sso_login.js ├── sso_ldap │ └── sso_ldap.js └── sso_facebook │ ├── facebook.php │ └── facebook-sdk-src │ └── facebook.php ├── docs ├── install.md ├── images │ ├── sso_server_login.png │ ├── sso_client_architecture.png │ ├── sso_server_architecture.png │ ├── sso_provider_field_mapping.png │ ├── sso_server_field_flowchart.png │ ├── sso_server_frontend_workflow.png │ ├── sso_server_tutorial_series.png │ ├── sso_how_api_key_namespaces_work.png │ ├── sso_client_logged_in_user_verification.png │ └── sso_per_api_key_field_transformation.png ├── user-impersonation.md ├── porting-the-sso-client.md ├── using-custom-api-keys.md ├── upgrade.md ├── reserved-global-variables.md ├── all-features.md ├── creating-providers.md ├── endpoint-api.md ├── import-existing-user-accounts.md └── integrating-with-third-party-software.md ├── examples ├── ok.png ├── error.png ├── wait.png ├── warn.png ├── sso_ldap.png ├── sso_google.png ├── sso_login.png ├── sso_login.psd ├── sso_remote.png ├── sso_twitter.png ├── sso_facebook.png ├── sso_linkedin.png └── main.css ├── .gitignore ├── support ├── jquery_ui_themes │ └── smoothness │ │ └── images │ │ ├── animated-overlay.gif │ │ ├── ui-icons_222222_256x240.png │ │ ├── ui-icons_2e83ff_256x240.png │ │ ├── ui-icons_454545_256x240.png │ │ ├── ui-icons_888888_256x240.png │ │ ├── ui-icons_cd0a0a_256x240.png │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ ├── ui-bg_flat_75_ffffff_40x100.png │ │ ├── ui-bg_glass_55_fbf9ee_1x400.png │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ ├── ui-bg_glass_75_dadada_1x400.png │ │ ├── ui-bg_glass_75_e6e6e6_1x400.png │ │ ├── ui-bg_glass_95_fef1ec_1x400.png │ │ └── ui-bg_highlight-soft_75_cccccc_1x100.png ├── geoip │ └── Reader │ │ ├── InvalidDatabaseException.php │ │ ├── Util.php │ │ ├── Metadata.php │ │ └── Decoder.php ├── debug.php ├── admin.js ├── phpseclib │ ├── license.txt │ └── AES.php ├── admin_print.css ├── Net │ └── license.txt ├── install.css ├── csdb │ ├── db_mysql_lite.php │ ├── db_sqlite_lite.php │ ├── db_pgsql_lite.php │ └── db_oci_lite.php ├── aes.php ├── request.php ├── crc32_stream.php ├── str_basics.php ├── blowfish.php ├── sdk_twilio.php ├── ipaddr.php └── jquery.tablednd-20140418.min.js ├── cron.php ├── README.md └── upgrade.php /providers/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /providers/sso_login/modules/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/install.md -------------------------------------------------------------------------------- /examples/ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/ok.png -------------------------------------------------------------------------------- /examples/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/error.png -------------------------------------------------------------------------------- /examples/wait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/wait.png -------------------------------------------------------------------------------- /examples/warn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/warn.png -------------------------------------------------------------------------------- /examples/sso_ldap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/sso_ldap.png -------------------------------------------------------------------------------- /examples/sso_google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/sso_google.png -------------------------------------------------------------------------------- /examples/sso_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/sso_login.png -------------------------------------------------------------------------------- /examples/sso_login.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/sso_login.psd -------------------------------------------------------------------------------- /examples/sso_remote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/sso_remote.png -------------------------------------------------------------------------------- /examples/sso_twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/sso_twitter.png -------------------------------------------------------------------------------- /examples/sso_facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/sso_facebook.png -------------------------------------------------------------------------------- /examples/sso_linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/examples/sso_linkedin.png -------------------------------------------------------------------------------- /docs/images/sso_server_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/images/sso_server_login.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /admin_hook.php 2 | /config.php 3 | /header.php 4 | /footer.php 5 | /index_*.php 6 | /settings.php 7 | /support/*.mmdb 8 | -------------------------------------------------------------------------------- /docs/images/sso_client_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/images/sso_client_architecture.png -------------------------------------------------------------------------------- /docs/images/sso_server_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/images/sso_server_architecture.png -------------------------------------------------------------------------------- /docs/images/sso_provider_field_mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/images/sso_provider_field_mapping.png -------------------------------------------------------------------------------- /docs/images/sso_server_field_flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/images/sso_server_field_flowchart.png -------------------------------------------------------------------------------- /docs/images/sso_server_frontend_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/images/sso_server_frontend_workflow.png -------------------------------------------------------------------------------- /docs/images/sso_server_tutorial_series.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/images/sso_server_tutorial_series.png -------------------------------------------------------------------------------- /docs/images/sso_how_api_key_namespaces_work.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/images/sso_how_api_key_namespaces_work.png -------------------------------------------------------------------------------- /docs/images/sso_client_logged_in_user_verification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/images/sso_client_logged_in_user_verification.png -------------------------------------------------------------------------------- /docs/images/sso_per_api_key_field_transformation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/docs/images/sso_per_api_key_field_transformation.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/animated-overlay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/animated-overlay.gif -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-icons_888888_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-icons_888888_256x240.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-bg_glass_75_dadada_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-bg_glass_75_dadada_1x400.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png -------------------------------------------------------------------------------- /support/jquery_ui_themes/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/sso-server/master/support/jquery_ui_themes/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png -------------------------------------------------------------------------------- /support/geoip/Reader/InvalidDatabaseException.php: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /support/geoip/Reader/Util.php: -------------------------------------------------------------------------------- 1 | explicit_key_length) { 44 | $length = strlen($key); 45 | switch (true) { 46 | case $length <= 16: 47 | $this->key_length = 16; 48 | break; 49 | case $length <= 24: 50 | $this->key_length = 24; 51 | break; 52 | default: 53 | $this->key_length = 32; 54 | } 55 | $this->_setEngine(); 56 | } 57 | } 58 | }} -------------------------------------------------------------------------------- /providers/sso_ldap/sso_ldap.js: -------------------------------------------------------------------------------- 1 | function SSO_ChangePasswordField(node, newtype) 2 | { 3 | var srcnode = node.parent().parent().find('.sso_main_formdata input').get(0); 4 | if (srcnode) 5 | { 6 | var destnode = jQuery(''); 7 | destnode.attr('type', newtype); 8 | for (var key in srcnode.attributes) 9 | { 10 | if (!isNaN(key) && srcnode.attributes[key].name.toLowerCase() != 'type') destnode.attr(srcnode.attributes[key].name, srcnode.attributes[key].value); 11 | } 12 | destnode.val(jQuery(srcnode).val()); 13 | jQuery(srcnode).replaceWith(destnode); 14 | } 15 | } 16 | 17 | jQuery(function() { 18 | jQuery('.sso_main_wrap input[type=password]').each(function() { 19 | if (this.name) jQuery(this).parent().parent().append('
'); 20 | }); 21 | jQuery('.sso_main_formshowhide input').click(function() { 22 | SSO_ChangePasswordField(jQuery(this), this.checked ? 'text' : 'password'); 23 | }); 24 | jQuery('form.sso_main_form').submit(function() { 25 | jQuery('.sso_main_formshowhide input:checked').each(function() { 26 | SSO_ChangePasswordField(jQuery(this), 'password'); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /support/geoip/Reader/Metadata.php: -------------------------------------------------------------------------------- 1 | binaryFormatMajorVersion = 26 | $metadata['binary_format_major_version']; 27 | $this->binaryFormatMinorVersion = 28 | $metadata['binary_format_minor_version']; 29 | $this->buildEpoch = $metadata['build_epoch']; 30 | $this->databaseType = $metadata['database_type']; 31 | $this->languages = $metadata['languages']; 32 | $this->description = $metadata['description']; 33 | $this->ipVersion = $metadata['ip_version']; 34 | $this->nodeCount = $metadata['node_count']; 35 | $this->recordSize = $metadata['record_size']; 36 | $this->nodeByteSize = $this->recordSize / 4; 37 | $this->searchTreeSize = $this->nodeCount * $this->nodeByteSize; 38 | } 39 | 40 | public function __get($var) 41 | { 42 | return $this->$var; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /support/admin_print.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: auto; 3 | } 4 | 5 | body { 6 | height: auto; 7 | min-width: 0; 8 | overflow: visible; 9 | } 10 | 11 | div.contentwrap div.colmask { 12 | position: static; 13 | float: none; 14 | width: 100%; 15 | overflow: visible; 16 | background-color: #FFFFFF; /* Left-column background color. */ 17 | } 18 | 19 | div.contentwrap div.colmask div.colright { 20 | float: none; 21 | width: 100%; 22 | position: static; 23 | left: auto; /* Left-column width. */ 24 | background-color: #FFFFFF; /* Right-column background color. */ 25 | border-left: 0 none; 26 | } 27 | 28 | div.contentwrap div.colmask div.colright div.col1wrap { 29 | float: none; 30 | width: 100%; 31 | position: static; 32 | right: auto; 33 | } 34 | 35 | div.contentwrap div.colmask div.colright div.col1wrap div.col1 { 36 | position: static; 37 | width: 100%; 38 | overflow: visible; 39 | } 40 | 41 | div.contentwrap div.colmask div.colright div.col1wrap div.col1inner { 42 | position: static; 43 | margin-right: 0; /* Left-column width. */ 44 | } 45 | 46 | div.contentwrap div.colmask div.colright div.col2 { 47 | display: none; 48 | } 49 | 50 | div.stickycol { 51 | display: none; 52 | } 53 | 54 | div.maincontent { 55 | position: static; 56 | margin: 5px 10px 15px 10px; 57 | padding: 0; 58 | background-color: #FFFFFF; 59 | border: 0 none; 60 | } 61 | 62 | div.maincontent div.proptitle div.navbutton { 63 | display: none; 64 | } 65 | 66 | div.maincontent div.propmain div.formfields { 67 | border: 0 none; 68 | background-color: #FFFFFF; 69 | padding-left: 0; 70 | padding-right: 0; 71 | } 72 | 73 | div.maincontent div.propmain div.alt { 74 | background-color: #FFFFFF; 75 | } 76 | -------------------------------------------------------------------------------- /support/Net/license.txt: -------------------------------------------------------------------------------- 1 | Net_DNS2 - DNS Library for handling lookups and updates. 2 | 3 | Copyright (c) 2010-2020, Mike Pultz . 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | 18 | * Neither the name of Mike Pultz nor the names of his contributors 19 | may be used to endorse or promote products derived from this 20 | software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | POSSIBILITY OF SUCH DAMAGE. 34 | -------------------------------------------------------------------------------- /providers/sso_login/sso_login.js: -------------------------------------------------------------------------------- 1 | function SSO_SendField() 2 | { 3 | if (this.name) 4 | { 5 | var data = {}; 6 | data[this.name] = jQuery(this).val(); 7 | jQuery('form.sso_main_form input[type=hidden]').each(function() { 8 | data[this.name] = jQuery(this).val(); 9 | }); 10 | jQuery(this).parent().parent().find('.sso_main_formresult').html('
' + SSO_Vars['checking'] + '
'); 11 | jQuery(this).parent().parent().find('.sso_main_formresult').load(SSO_Vars['ajaxurl'], data); 12 | } 13 | } 14 | 15 | function SSO_ChangePasswordField(node, newtype) 16 | { 17 | var srcnode = node.parent().parent().find('.sso_main_formdata input').get(0); 18 | if (srcnode) 19 | { 20 | var destnode = jQuery(''); 21 | if (jQuery(srcnode).hasClass('sso_login_changehook')) destnode.change(SSO_SendField); 22 | destnode.attr('type', newtype); 23 | for (var key in srcnode.attributes) 24 | { 25 | if (!isNaN(key) && srcnode.attributes[key].name.toLowerCase() != 'type') destnode.attr(srcnode.attributes[key].name, srcnode.attributes[key].value); 26 | } 27 | destnode.val(jQuery(srcnode).val()); 28 | jQuery(srcnode).replaceWith(destnode); 29 | } 30 | } 31 | 32 | jQuery(function() { 33 | jQuery('.sso_main_wrap input[type=password]').each(function() { 34 | if (this.name) jQuery(this).parent().parent().append('
'); 35 | }); 36 | jQuery('.sso_main_formshowhide input').click(function() { 37 | SSO_ChangePasswordField(jQuery(this), this.checked ? 'text' : 'password'); 38 | }); 39 | jQuery('form.sso_main_form').submit(function() { 40 | jQuery('.sso_main_formshowhide input:checked').each(function() { 41 | SSO_ChangePasswordField(jQuery(this), 'password'); 42 | }); 43 | }); 44 | jQuery('input.sso_login_changehook').change(SSO_SendField).parent().parent().append('
'); 45 | }); 46 | -------------------------------------------------------------------------------- /docs/user-impersonation.md: -------------------------------------------------------------------------------- 1 | User Impersonation 2 | ================== 3 | 4 | Sometimes it becomes necessary to impersonate users. There are two common scenarios where user impersonation becomes useful: Seeing an issue that a specific user is seeing with an application and users who find new ways to forget their sign in password. Both scenarios are equally frustrating for the user and system administrators. System administrators will generally have site admin privileges and not be able to see what ordinary user accounts see unless they have a separate account for that purpose. User impersonation becomes a tool for making life simpler. However, user impersonation support is a security risk. 5 | 6 | The SSO server/client, by default, does not enable user impersonation for the aforementioned security reasons. Enabling user impersonation requires enabling it for each user account that needs it, the application's API key, and the application itself. The first step is to enable the API key and the application for passing 'sso_impersonate' with an account impersonation token. Once that is done, the API key will accept impersonation tokens from that point on (unless it is disabled again later). The next step is to locate a user account and enable impersonation by selecting 'Yes' from the appropriate drop down and clicking 'Save'. The server will generate a token and a new drop down for automation appears that is optional. Take the impersonation token and construct a URL by using the token with 'sso_impersonate'. If you are signed in, you'll likely need to sign out first. An impersonation URL will look like: 7 | 8 | https://yourdomain.com/?sso_impersonate=[impersonation_token] 9 | 10 | User impersonation URLs can be bookmarked in a browser for a one-click sign in. This can be useful for users who frequently forget their password. Such users are likely a security risk anyway by being part of the group of people who select weak passwords, so the one-click sign in token (64 completely random letters and numbers) is theoretically more secure as long as it is only transmitted over HTTPS. 11 | -------------------------------------------------------------------------------- /docs/porting-the-sso-client.md: -------------------------------------------------------------------------------- 1 | Porting The SSO Client 2 | ====================== 3 | 4 | The SSO server is written in PHP and likely won't be ported to another language. It is big, complex, and has a lot of PHP-specific stuff in it and is intended to be installed standalone. The SSO client, however, is intended to be lightweight and relatively easy to port to other languages. This section aims to be a guide to porting the SSO client. 5 | 6 | The SSO client has four primary responsibilities: Staying out of the way of someone integrating with it (i.e. avoiding naming conflicts), exposing classes and functions to the user that make sense, communicating with the SSO server endpoint and correctly processing replies including dealing with server outages, and managing everything in a secure fashion while being bandwidth-friendly (e.g. encrypting certain cookies while minimizing cookie length). 7 | 8 | In theory, the SSO client can be ported by following a similar naming convention to the existing PHP code and doing a one-to-one port. However, each language is different and nice language features like "optional parameters" or "passing arrays/maps as a parameter" may not be possible or behave differently. Try to maintain similarities when naming variables and functions as much as possible. 9 | 10 | The most difficult part of porting the SSO client will be communication with the SSO server. The endpoint expects communication to be single or dual encrypted using either the Blowfish or AES-256 cipher all with a specialized packet interface. Both the server and client know the shared secret used to encrypt the underlying data, which contains information on which encryption method is expected. You will either need to use native cipher implementations (rare) or find or write a library with the correct implementation and then write a couple of packet wrapper routines that perform identically to the PHP encrypt/decrypt packet routines. You will also need a JSON encoder and decoder, which any rational, modern, competent language has. You will also need to create a way to obtain secure random bytes of data and may want to port part of the CSPRNG that comes with the SSO client to make life easier for the rest of the port. 11 | -------------------------------------------------------------------------------- /docs/using-custom-api-keys.md: -------------------------------------------------------------------------------- 1 | Using Custom API Keys 2 | ===================== 3 | 4 | Here be dragons. The not recommended last resort workaround for dealing with encountered SSO server endpoint limitations. 5 | 6 | If you have entered the area of custom API keys, then you have reached a limitation of the core SSO server endpoint API. Instead of working with custom API keys and endpoint hooks, it is highly recommended to find another approach to accomplishing the task. There are very few reasons to ever use a custom API key when other options almost always exist that will work better. Custom API keys should only be used as an absolute last resort. 7 | 8 | Creating a custom API key is a process that involves multiple steps: 9 | 10 | * First, create an API key in the SSO server admin and edit it. The "Type" dropdown contains three options - Normal, Remote, and Custom. Select "Custom" and save the API key. This creates the custom API key that will be used later. 11 | * Create a file called 'endpoint_hook.php' in the same directory as 'endpoint.php'. The endpoint will automatically load this file if it exists. 12 | * Write a function called `EndpointHook_CustomHandler()`. The function takes no options and is expected to return a boolean to the caller that indicates whether or not the "action" was handled. 13 | * Within EndpointHook_CustomHandler(), write the custom code to do the task you want. The handler should handle the "action" you plan to use with SSO_Client::SendRequest(). It is highly recommended to actually open and read 'endpoint.php' to gain a proper understanding of how the SSO server endpoint functions for the operations it handles natively. 14 | * From the application using the SSO client, call `SSO_Client::SendRequest()` (e.g. `$sso_client->SendRequest()`) with the action you wish to run and the options you wish to send. Be sure to check the return value. The API key and secret must be the custom API key and secret from earlier (normal and remote API keys won't work). Read the documentation on `SSO_Client::SendRequest()` and look at the source code in 'support/sso_functions.php' in the client to see how this low-level function works. 15 | 16 | Those are the basic guidelines for working with custom API keys. The best way to work with them is to crack open the relevant source code of the SSO server and client to understand what is going on. Help with working with custom API keys is limited and falls outside the general scope of support. 17 | -------------------------------------------------------------------------------- /providers/sso_facebook/facebook.php: -------------------------------------------------------------------------------- 1 | true, "code" => true, "access_token" => true, "user_id" => true); 21 | 22 | // Original function is poorly written. 23 | protected function establishCSRFTokenState() 24 | { 25 | global $sso_rng; 26 | 27 | if (!isset($this->state)) 28 | { 29 | $this->state = $sso_rng->GenerateString(); 30 | $this->setPersistentData("state", $this->state); 31 | } 32 | } 33 | 34 | protected function setPersistentData($key, $value) 35 | { 36 | global $sso_session_info; 37 | 38 | if (!isset(self::$allowed[$key])) 39 | { 40 | self::errorLog("Unsupported key passed to setPersistentData."); 41 | 42 | return; 43 | } 44 | 45 | if (!isset($sso_session_info["sso_facebook"])) $sso_session_info["sso_facebook"] = array(); 46 | $sso_session_info["sso_facebook"][$key] = $value; 47 | 48 | if (!SSO_SaveSessionInfo()) self::errorLog("Unable to save session info."); 49 | } 50 | 51 | protected function getPersistentData($key, $default = false) 52 | { 53 | global $sso_session_info; 54 | 55 | if (!isset(self::$allowed[$key])) 56 | { 57 | self::errorLog("Unsupported key passed to setPersistentData."); 58 | 59 | return; 60 | } 61 | 62 | return (isset($sso_session_info["sso_facebook"]) && isset($sso_session_info["sso_facebook"][$key]) ? $sso_session_info["sso_facebook"][$key] : $default); 63 | } 64 | 65 | protected function clearPersistentData($key) 66 | { 67 | global $sso_session_info; 68 | 69 | if (!isset(self::$allowed[$key])) 70 | { 71 | self::errorLog("Unsupported key passed to setPersistentData."); 72 | 73 | return; 74 | } 75 | 76 | unset($sso_session_info["sso_facebook"][$key]); 77 | 78 | SSO_SaveSessionInfo(); 79 | } 80 | 81 | protected function clearAllPersistentData() 82 | { 83 | global $sso_session_info; 84 | 85 | unset($sso_session_info["sso_facebook"]); 86 | 87 | SSO_SaveSessionInfo(); 88 | } 89 | } 90 | ?> -------------------------------------------------------------------------------- /support/install.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #FFFFFF none repeat scroll 0 0; 3 | color: #000000; 4 | margin: 0; 5 | } 6 | 7 | img { 8 | border: 0px none; 9 | margin: 0px; 10 | } 11 | 12 | form { 13 | border: 0px none; 14 | margin: 0px; 15 | } 16 | 17 | a { 18 | color: #035488; 19 | text-decoration: none; 20 | } 21 | 22 | a:hover { 23 | color: #444444; 24 | text-decoration: underline; 25 | } 26 | 27 | #main { 28 | width: 600px; 29 | margin-left: auto; 30 | margin-right: auto; 31 | } 32 | 33 | div.box { 34 | margin-top: 5px; 35 | margin-bottom: 15px; 36 | width: 600px; 37 | background-color: #F1F1F1; 38 | border: 1px solid black; 39 | border-radius: 5px; 40 | -moz-border-radius: 5px; 41 | -webkit-border-radius: 5px; 42 | } 43 | 44 | div.box h1 { 45 | text-align: center; 46 | font-size: 1.5em; 47 | width: 100%; 48 | color: #322F34; 49 | background-color: #B7CDE0; 50 | margin: 0; 51 | border-bottom: 1px dashed #322F34; 52 | border-top-right-radius: 3px; 53 | border-top-left-radius: 3px; 54 | -moz-border-radius-topleft: 3px; 55 | -moz-border-radius-topright: 3px; 56 | -webkit-border-top-right-radius: 3px; 57 | -webkit-border-top-left-radius: 3px; 58 | } 59 | 60 | div.box h3 { 61 | margin: 5px 5px 15px 5px; 62 | color: #666666; 63 | font-size: 1.2em; 64 | } 65 | 66 | div.box div.boxmain { 67 | margin: 15px 15px 15px 30px; 68 | font-size: 0.9em; 69 | } 70 | 71 | div.indent { 72 | font-size: 1.2em; 73 | font-style: italic; 74 | margin-left: 30px; 75 | } 76 | 77 | div.box div.boxbuttons { 78 | font-size: 1.3em; 79 | font-weight: bold; 80 | text-align: right; 81 | margin: 5px 15px 10px 0px; 82 | } 83 | 84 | table { 85 | border: 1px solid #E5E5E5; 86 | } 87 | 88 | td, th { 89 | padding-left: 3px; 90 | padding-right: 3px; 91 | } 92 | 93 | tr.head { 94 | background-color: #CFD6DD; 95 | } 96 | 97 | tr.row { 98 | background-color: #EFEFEF; 99 | } 100 | 101 | tr.altrow { 102 | background-color: #ECECEC; 103 | } 104 | 105 | tr.row td { 106 | vertical-align: top; 107 | } 108 | 109 | .success { 110 | color: #008800; 111 | font-weight: bold; 112 | } 113 | 114 | .error { 115 | color: #880000; 116 | font-weight: bold; 117 | } 118 | 119 | div.formfields { 120 | border: 1px solid #E5E5E5; 121 | background-color: #F5F5F5; 122 | padding-left: 10px; 123 | padding-right: 10px; 124 | } 125 | 126 | div.formfields div.formitem { 127 | margin-top: 10px; 128 | margin-bottom: 10px; 129 | } 130 | 131 | div.formfields div.formitem div.formitemtitle { 132 | font-weight: bold; 133 | margin-bottom: 2px; 134 | } 135 | 136 | div.formfields div.formitem input.text { 137 | margin-left: 7px; 138 | width: 95%; 139 | border: 1px solid #C5C5C5; 140 | } 141 | 142 | div.formfields div.formitem input.checkbox { 143 | margin-left: 7px; 144 | border: 1px solid #C5C5C5; 145 | } 146 | 147 | div.formfields div.formitem input.submit { 148 | margin-top: 15px; 149 | margin-left: 225px; 150 | background-color: #FFFFFF; 151 | border: 2px solid #A5A5A5; 152 | } 153 | 154 | div.formfields div.formitem select { 155 | margin-left: 7px; 156 | width: 95%; 157 | border: 1px solid #C5C5C5; 158 | } 159 | 160 | div.formfields div.formitem div.formitemdesc { 161 | margin-left: 15px; 162 | font-size: 0.9em; 163 | } 164 | 165 | div.testresult { 166 | margin-left: 25px; 167 | margin-right: 25px; 168 | border: 1px solid #C5C5C5; 169 | padding: 5px; 170 | display: none; 171 | } 172 | -------------------------------------------------------------------------------- /support/csdb/db_mysql_lite.php: -------------------------------------------------------------------------------- 1 | Query("SET", "NAMES 'utf8mb4'"); 25 | } 26 | 27 | public function GetInsertID($name = null) 28 | { 29 | return $this->GetOne("SELECT", array("LAST_INSERT_ID()")); 30 | } 31 | 32 | public function QuoteIdentifier($str) 33 | { 34 | return "`" . str_replace(array("`", "?"), array("``", ""), $str) . "`"; 35 | } 36 | 37 | protected function GenerateSQL(&$master, &$sql, &$opts, $cmd, $queryinfo, $args, $subquery) 38 | { 39 | switch ($cmd) 40 | { 41 | case "SELECT": 42 | { 43 | $supported = array( 44 | "PRECOLUMN" => array("DISTINCT" => "bool", "HIGH_PRIORITY" => "bool", "SUBQUERIES" => true), 45 | "FROM" => array("SUBQUERIES" => true), 46 | "WHERE" => array("SUBQUERIES" => true), 47 | "GROUP BY" => true, 48 | "HAVING" => true, 49 | "ORDER BY" => true, 50 | "LIMIT" => ", " 51 | ); 52 | 53 | return $this->ProcessSELECT($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 54 | } 55 | case "INSERT": 56 | { 57 | $supported = array( 58 | "PREINTO" => array("LOW_PRIORITY" => "bool", "DELAYED" => "bool", "HIGH_PRIORITY" => "bool", "IGNORE" => "bool"), 59 | "SELECT" => true, 60 | "BULKINSERT" => true 61 | ); 62 | 63 | return $this->ProcessINSERT($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 64 | } 65 | case "UPDATE": 66 | { 67 | $supported = array( 68 | "PRETABLE" => array("LOW_PRIORITY" => "bool", "IGNORE" => "bool"), 69 | "WHERE" => array("SUBQUERIES" => true), 70 | "ORDER BY" => true, 71 | "LIMIT" => ", " 72 | ); 73 | 74 | return $this->ProcessUPDATE($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 75 | } 76 | case "DELETE": 77 | { 78 | $supported = array( 79 | "PREFROM" => array("LOW_PRIORITY" => "bool", "QUICK" => "bool", "IGNORE" => "bool"), 80 | "WHERE" => array("SUBQUERIES" => true), 81 | "ORDER BY" => true, 82 | "LIMIT" => ", " 83 | ); 84 | 85 | return $this->ProcessDELETE($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 86 | } 87 | case "SET": 88 | { 89 | $sql = "SET " . $queryinfo; 90 | 91 | return array("success" => true); 92 | } 93 | case "USE": 94 | { 95 | $sql = "USE " . $this->QuoteIdentifier($queryinfo); 96 | 97 | return array("success" => true); 98 | } 99 | case "TRUNCATE TABLE": 100 | { 101 | $master = true; 102 | 103 | $sql = "TRUNCATE TABLE " . $this->QuoteIdentifier($queryinfo[0]); 104 | 105 | return array("success" => true); 106 | } 107 | } 108 | 109 | return array("success" => false, "error" => CSDB::DB_Translate("Unknown query command '%s'.", $cmd), "errorcode" => "unknown_query_command"); 110 | } 111 | } 112 | ?> -------------------------------------------------------------------------------- /docs/upgrade.md: -------------------------------------------------------------------------------- 1 | Upgrading the SSO Server/Client 2 | =============================== 3 | 4 | These upgrade instructions only apply to those who have previously installed an older version of the SSO Server/Client. The SSO Server and SSO Client communicate with a tightly bound protocol. Major version number jumps in the software package (e.g. 1.0 to 2.0) indicate that the underlying protocol and/or database schema has changed significantly and that all server and client components have to be upgraded. Smaller version jumps (e.g. 2.0 to 2.1) indicate that there have been no protocol/database schema changes and upgrading just the server and whatever clients need to use a new feature will be necessary. 5 | 6 | If you use the "Security Through Obscurity (STO)" feature of the SSO server, the 'admin.php' and 'endpoint.php' files will need to be uploaded separately to their respective directories on the web server when uploading files (i.e. 'admin_...' and 'endpoint_...'). If the files are uploaded by accident to the root SSO server directory with STO enabled, they won't function at the default location and the SSO client may not work properly because they will still be using the previous version of the endpoint. STO makes it harder for a hacker to find the location of the admin and endpoint but also adds a little extra time to the upgrade process. 7 | 8 | Major Version Upgrades 9 | ---------------------- 10 | 11 | Major version upgrades take time to complete and can be a little nerve-wracking. You should schedule time for the SSO server and client to be offline/unavailable in which to work. Total time for a major version upgrade depends on a lot of factors, including how large the database is, if there are schema changes, how many clients need to be upgraded, etc. 12 | 13 | The upgrade procedure is as follows: 14 | 15 | * Back up your database in case something goes horribly wrong. 16 | * Read the official post carefully to understand the impact. 17 | * Download the latest SSO Server/Client package, extract, and upload the 'server' files to the correct locations. It is safe to overwrite existing files. 18 | * You may wish to write an 'upgrade_hook.php' file to restrict how and where it may be run (e.g. by IP address). For medium-sized databases, the recommendation is to restrict the upgrade tool to only run from the command-line. 19 | * Run 'upgrade.php' either via a web browser or the command-line. The command-line is generally more reliable because scripts can't get killed off by the web server for running too long. 20 | * Delete 'upgrade.php' off the web server. It technically shouldn't be possible for it to run again, but it is better to be safe than sorry. 21 | * Upload the 'client' files to every client installation's directory. Again, it is safe to overwrite existing files. 22 | * Test each client to make sure nothing broke during the upgrade. 23 | 24 | And that's it. If you encounter any problems upgrading, open an issue on the issue tracker. 25 | 26 | Minor Version Upgrades 27 | ---------------------- 28 | 29 | Minor version upgrades take less time and planning to execute than major version upgrades. The upgrade procedure is similar to major version upgrades: 30 | 31 | * Back up your database in case something goes horribly wrong. 32 | * Download the latest SSO Server/Client package, extract, and upload the 'server' files to the correct locations. It is safe to overwrite existing files. Don't upload 'upgrade.php'. 33 | * Upload the 'client' files to every client installation's directory that needs access to new features/changes. Again, it is safe to overwrite existing files. 34 | * Test each client to make sure nothing broke during the upgrade. 35 | 36 | And that's it. If you encounter any problems upgrading, open an issue on the issue tracker. 37 | -------------------------------------------------------------------------------- /cron.php: -------------------------------------------------------------------------------- 1 | array( 32 | "d" => "debug", 33 | "v" => "verbose", 34 | "?" => "help" 35 | ), 36 | "rules" => array( 37 | "debug" => array("arg" => false), 38 | "verbose" => array("arg" => false), 39 | "help" => array("arg" => false) 40 | ) 41 | ); 42 | $sso_args = CLI::ParseCommandLine($options); 43 | 44 | if (count($sso_args["params"]) != 0 || isset($sso_args["opts"]["help"])) 45 | { 46 | echo "SSO Server Cron Interface\n"; 47 | echo "Purpose: Run cleanup operations that need to be executed periodically.\n"; 48 | echo "\n"; 49 | echo "Syntax: " . $args["file"] . " [options]\n"; 50 | echo "Options:\n"; 51 | echo "\t-d Output debugging information.\n"; 52 | echo "\t-v Verbose output.\n"; 53 | echo "\t-? This help documentation.\n"; 54 | 55 | exit(); 56 | } 57 | 58 | $sso_debug = isset($sso_args["opts"]["debug"]); 59 | $sso_verbose = isset($sso_args["opts"]["verbose"]); 60 | 61 | // Initialize language settings. 62 | BB_InitLangmap(SSO_ROOT_PATH . "/" . SSO_LANG_PATH . "/", SSO_DEFAULT_LANG); 63 | BB_SetLanguage(SSO_ROOT_PATH . "/" . SSO_LANG_PATH . "/", SSO_ADMIN_LANG); 64 | 65 | // Initialize the global CSPRNG instance. 66 | $sso_rng = new CSPRNG(); 67 | 68 | // Connect to the database and generate database globals. 69 | SSO_DBConnect(true); 70 | 71 | // Load in fields without admin select. 72 | SSO_LoadFields(false); 73 | 74 | // Load in $sso_settings and initialize it. 75 | SSO_LoadSettings(); 76 | 77 | // Get system clock drift. 78 | $sso_clockdrift = (isset($sso_settings[""]["clock_drift"]) ? $sso_settings[""]["clock_drift"] : 300); 79 | 80 | // Allow developers to inject code here. 81 | if (file_exists(SSO_ROOT_PATH . "/cron_hook.php")) require_once SSO_ROOT_PATH . "/cron_hook.php"; 82 | 83 | // Run cleanup queries. 84 | try 85 | { 86 | $sso_db->Query("DELETE", array($sso_db_temp_sessions, "WHERE" => "updated < ?"), CSDB::ConvertToDBTime(time() - 60 * 60)); 87 | $sso_db->Query("DELETE", array($sso_db_temp_sessions, "WHERE" => "heartbeat = ? AND updated < ?"), SSO_HEARTBEAT_LIMIT, CSDB::ConvertToDBTime(time() - $sso_clockdrift)); 88 | $sso_db->Query("DELETE", array($sso_db_user_sessions, "WHERE" => "updated < ?"), CSDB::ConvertToDBTime(time() - $sso_clockdrift)); 89 | $sso_db->Query("DELETE", array($sso_db_ipcache, "WHERE" => "created < ?"), CSDB::ConvertToDBTime(time() - 24 * 60 * 60 * $sso_settings[""]["iprestrict"]["ip_cache_len"])); 90 | } 91 | catch (Exception $e) 92 | { 93 | echo "Database query error. " . $e->getMessage() . "\n\n"; 94 | } 95 | 96 | if ($sso_verbose) 97 | { 98 | echo "Time taken: " . number_format(microtime(true) - $sso_start, 2) . " sec\n"; 99 | if (function_exists("memory_get_peak_usage")) echo "Maximum RAM used: " . number_format(memory_get_peak_usage(), 0) . "\n"; 100 | 101 | echo "Done.\n"; 102 | } 103 | ?> -------------------------------------------------------------------------------- /support/aes.php: -------------------------------------------------------------------------------- 1 | setKey($key); 23 | if (isset($options["iv"])) $aes->setIV($options["iv"]); 24 | $aes->disablePadding(); 25 | if (strlen($data) % 16 != 0) $data = str_pad($data, strlen($data) + (16 - (strlen($data) % 16)), "\x00"); 26 | $data = $aes->encrypt($data); 27 | 28 | if (isset($options["key2"])) 29 | { 30 | $data = substr($data, -1) . substr($data, 0, -1); 31 | 32 | if (isset($options["iv2"])) $options["iv"] = $options["iv2"]; 33 | else unset($options["iv"]); 34 | 35 | if ($options["mode"] != "ECB" && (!isset($options["iv"]) || $options["iv"] == "")) return false; 36 | 37 | $aes->setKey($options["key2"]); 38 | if (isset($options["iv"])) $aes->setIV($options["iv"]); 39 | $data = $aes->encrypt($data); 40 | } 41 | 42 | return $data; 43 | } 44 | 45 | // Uses AES to extract the data from an encapsulated data packet and validates the data. Does not support streams. 46 | static function ExtractDataPacket($data, $key, $options = array()) 47 | { 48 | $data = (string)$data; 49 | 50 | if (!isset($options["mode"])) $options["mode"] = "ECB"; 51 | if ($options["mode"] != "ECB" && (!isset($options["iv"]) || $options["iv"] == "")) return false; 52 | 53 | if (!class_exists("Crypt_AES", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/phpseclib/AES.php"; 54 | 55 | if (isset($options["key2"])) 56 | { 57 | $options2 = $options; 58 | if (isset($options["iv2"])) $options["iv"] = $options["iv2"]; 59 | else unset($options["iv"]); 60 | 61 | $aes = new Crypt_AES($options["mode"] == "CBC" ? CRYPT_AES_MODE_CBC : CRYPT_AES_MODE_ECB); 62 | $aes->setKey($options["key2"]); 63 | if (isset($options["iv"])) $aes->setIV($options["iv"]); 64 | $aes->disablePadding(); 65 | $data = $aes->decrypt($data); 66 | 67 | $data = substr($data, 1) . substr($data, 0, 1); 68 | $options = $options2; 69 | } 70 | 71 | $aes = new Crypt_AES($options["mode"] == "CBC" ? CRYPT_AES_MODE_CBC : CRYPT_AES_MODE_ECB); 72 | $aes->setKey($key); 73 | if (isset($options["iv"])) $aes->setIV($options["iv"]); 74 | $aes->disablePadding(); 75 | $data = $aes->decrypt($data); 76 | 77 | if ($data === false) return false; 78 | 79 | $pos = strpos($data, "\n"); 80 | if ($pos === false) return false; 81 | $data = substr($data, $pos + 1); 82 | 83 | $pos = strpos($data, "\n"); 84 | if ($pos === false) return false; 85 | $check = substr($data, 0, $pos); 86 | $data = substr($data, $pos + 1); 87 | 88 | $pos = strrpos($data, "\n"); 89 | if ($pos === false) return false; 90 | $data = substr($data, 0, $pos); 91 | 92 | if (!isset($options["lightweight"]) || !$options["lightweight"]) 93 | { 94 | if ($check !== strtolower(sha1($data))) return false; 95 | } 96 | else if ($check !== strtolower(dechex(crc32($data)))) return false; 97 | 98 | return $data; 99 | } 100 | } 101 | ?> -------------------------------------------------------------------------------- /support/csdb/db_sqlite_lite.php: -------------------------------------------------------------------------------- 1 | dbprefix = ""; 24 | 25 | parent::Connect($dsn, $username, $password, $options); 26 | } 27 | 28 | public function GetInsertID($name = null) 29 | { 30 | return $this->GetOne("SELECT", array("LAST_INSERT_ROWID()")); 31 | } 32 | 33 | public function QuoteIdentifier($str) 34 | { 35 | return "\"" . str_replace(array("\"", "?"), array("\"\"", ""), $str) . "\""; 36 | } 37 | 38 | protected function GenerateSQL(&$master, &$sql, &$opts, $cmd, $queryinfo, $args, $subquery) 39 | { 40 | switch ($cmd) 41 | { 42 | case "SELECT": 43 | { 44 | $supported = array( 45 | "DBPREFIX" => $this->dbprefix, 46 | "PRECOLUMN" => array("DISTINCT" => "bool", "SUBQUERIES" => true), 47 | "FROM" => array("SUBQUERIES" => true), 48 | "WHERE" => array("SUBQUERIES" => true), 49 | "GROUP BY" => true, 50 | "HAVING" => true, 51 | "ORDER BY" => true, 52 | "LIMIT" => ", " 53 | ); 54 | 55 | return $this->ProcessSELECT($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 56 | } 57 | case "INSERT": 58 | { 59 | $supported = array( 60 | "DBPREFIX" => $this->dbprefix, 61 | "PREINTO" => array("LOW_PRIORITY" => "bool", "DELAYED" => "bool", "HIGH_PRIORITY" => "bool", "IGNORE" => "bool"), 62 | "SELECT" => true, 63 | "BULKINSERT" => true, 64 | "BULKINSERTLIMIT" => 900, 65 | ); 66 | 67 | return $this->ProcessINSERT($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 68 | } 69 | case "UPDATE": 70 | { 71 | $supported = array( 72 | "DBPREFIX" => $this->dbprefix, 73 | "PRETABLE" => array("LOW_PRIORITY" => "bool", "IGNORE" => "bool"), 74 | "WHERE" => array("SUBQUERIES" => true), 75 | "ORDER BY" => true, 76 | "LIMIT" => ", " 77 | ); 78 | 79 | return $this->ProcessUPDATE($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 80 | } 81 | case "DELETE": 82 | { 83 | $supported = array( 84 | "DBPREFIX" => $this->dbprefix, 85 | "PREFROM" => array("LOW_PRIORITY" => "bool", "QUICK" => "bool", "IGNORE" => "bool"), 86 | "WHERE" => array("SUBQUERIES" => true), 87 | "ORDER BY" => true, 88 | "LIMIT" => ", " 89 | ); 90 | 91 | return $this->ProcessDELETE($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 92 | } 93 | case "SET": 94 | { 95 | return array("success" => false, "errorcode" => "skip_sql_query"); 96 | } 97 | case "USE": 98 | { 99 | $this->dbprefix = $this->GetDBPrefix($queryinfo); 100 | 101 | return array("success" => false, "errorcode" => "skip_sql_query"); 102 | } 103 | case "TRUNCATE TABLE": 104 | { 105 | $supported = array( 106 | "DBPREFIX" => $this->dbprefix, 107 | "PREFROM" => array() 108 | ); 109 | 110 | $queryinfo = array($queryinfo[0]); 111 | 112 | return $this->ProcessDELETE($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 113 | } 114 | } 115 | 116 | return array("success" => false, "error" => CSDB::DB_Translate("Unknown query command '%s'.", $cmd), "errorcode" => "unknown_query_command"); 117 | } 118 | 119 | private function GetDBPrefix($str) 120 | { 121 | $str = preg_replace('/\s+/', "_", trim(str_replace("_", " ", $str))); 122 | 123 | return ($str != "" ? $str . "__" : ""); 124 | } 125 | } 126 | ?> -------------------------------------------------------------------------------- /support/request.php: -------------------------------------------------------------------------------- 1 | $val) 12 | { 13 | if (is_string($val)) $_REQUEST[$key] = trim($val); 14 | else if (is_array($val)) 15 | { 16 | $_REQUEST[$key] = array(); 17 | foreach ($val as $key2 => $val2) $_REQUEST[$key][$key2] = (is_string($val2) ? trim($val2) : $val2); 18 | } 19 | else $_REQUEST[$key] = $val; 20 | } 21 | } 22 | 23 | // Cleans up all PHP input issues so that $_REQUEST may be used as expected. 24 | public static function Normalize() 25 | { 26 | self::ProcessSingleInput($_COOKIE); 27 | self::ProcessSingleInput($_GET); 28 | self::ProcessSingleInput($_POST); 29 | } 30 | 31 | public static function IsSSL() 32 | { 33 | return ((isset($_SERVER["HTTPS"]) && ($_SERVER["HTTPS"] == "on" || $_SERVER["HTTPS"] == "1")) || (isset($_SERVER["SERVER_PORT"]) && $_SERVER["SERVER_PORT"] == "443") || (isset($_SERVER["REQUEST_URI"]) && str_replace("\\", "/", strtolower(substr($_SERVER["REQUEST_URI"], 0, 8))) == "https://")); 34 | } 35 | 36 | // Returns 'http[s]://www.something.com[:port]' based on the current page request. 37 | public static function GetHost($protocol = "") 38 | { 39 | $protocol = strtolower($protocol); 40 | $ssl = ($protocol == "https" || ($protocol == "" && self::IsSSL())); 41 | if ($protocol == "") $type = "def"; 42 | else if ($ssl) $type = "https"; 43 | else $type = "http"; 44 | 45 | if (!isset(self::$hostcache)) self::$hostcache = array(); 46 | if (isset(self::$hostcache[$type])) return self::$hostcache[$type]; 47 | 48 | $url = "http" . ($ssl ? "s" : "") . "://"; 49 | 50 | $str = (isset($_SERVER["REQUEST_URI"]) ? str_replace("\\", "/", $_SERVER["REQUEST_URI"]) : "/"); 51 | $pos = strpos($str, "?"); 52 | if ($pos !== false) $str = substr($str, 0, $pos); 53 | $str2 = strtolower($str); 54 | if (substr($str2, 0, 7) == "http://") 55 | { 56 | $pos = strpos($str, "/", 7); 57 | if ($pos === false) $str = ""; 58 | else $str = substr($str, 7, $pos); 59 | } 60 | else if (substr($str2, 0, 8) == "https://") 61 | { 62 | $pos = strpos($str, "/", 8); 63 | if ($pos === false) $str = ""; 64 | else $str = substr($str, 8, $pos); 65 | } 66 | else $str = ""; 67 | 68 | if ($str != "") $host = $str; 69 | else if (isset($_SERVER["HTTP_HOST"])) $host = $_SERVER["HTTP_HOST"]; 70 | else $host = $_SERVER["SERVER_NAME"] . ":" . (int)$_SERVER["SERVER_PORT"]; 71 | 72 | $pos = strpos($host, ":"); 73 | if ($pos === false) $port = 0; 74 | else 75 | { 76 | $port = (int)substr($host, $pos + 1); 77 | $host = substr($host, 0, $pos); 78 | } 79 | if ($port < 1 || $port > 65535) $port = ($ssl ? 443 : 80); 80 | $url .= preg_replace('/[^a-z0-9.\-]/', "", strtolower($host)); 81 | if ($protocol == "" && ((!$ssl && $port != 80) || ($ssl && $port != 443))) $url .= ":" . $port; 82 | else if ($protocol == "http" && !$ssl && $port != 80) $url .= ":" . $port; 83 | else if ($protocol == "https" && $ssl && $port != 443) $url .= ":" . $port; 84 | 85 | self::$hostcache[$type] = $url; 86 | 87 | return $url; 88 | } 89 | 90 | public static function GetURLBase() 91 | { 92 | $str = (isset($_SERVER["REQUEST_URI"]) ? str_replace("\\", "/", $_SERVER["REQUEST_URI"]) : "/"); 93 | $pos = strpos($str, "?"); 94 | if ($pos !== false) $str = substr($str, 0, $pos); 95 | if (strncasecmp($str, "http://", 7) == 0 || strncasecmp($str, "https://", 8) == 0) 96 | { 97 | $pos = strpos($str, "/", 8); 98 | if ($pos === false) $str = "/"; 99 | else $str = substr($str, $pos); 100 | } 101 | 102 | return $str; 103 | } 104 | 105 | public static function GetFullURLBase($protocol = "") 106 | { 107 | return self::GetHost($protocol) . self::GetURLBase(); 108 | } 109 | 110 | public static function PrependHost($url, $protocol = "") 111 | { 112 | // Handle protocol-only. 113 | if (strncmp($url, "//", 2) == 0) 114 | { 115 | $host = self::GetHost($protocol); 116 | $pos = strpos($host, ":"); 117 | if ($pos === false) return $url; 118 | 119 | return substr($host, 0, $pos + 1) . $url; 120 | } 121 | 122 | if (strpos($url, ":") !== false) return $url; 123 | 124 | // Handle relative paths. 125 | if ($url === "" || $url[0] !== "/") return rtrim(self::GetFullURLBase($protocol), "/") . "/" . $url; 126 | 127 | // Handle absolute paths. 128 | $host = self::GetHost($protocol); 129 | 130 | return $host . $url; 131 | } 132 | } 133 | ?> -------------------------------------------------------------------------------- /support/crc32_stream.php: -------------------------------------------------------------------------------- 1 | 0x04C11DB7, "start" => 0xFFFFFFFF, "xor" => 0xFFFFFFFF, "refdata" => 1, "refcrc" => 1); 14 | 15 | public function __construct() 16 | { 17 | $this->open = false; 18 | } 19 | 20 | public function Init($options = false) 21 | { 22 | if ($options === false && function_exists("hash_init")) $this->hash = hash_init("crc32b"); 23 | else 24 | { 25 | if ($options === false) $options = self::$default; 26 | 27 | $this->hash = false; 28 | $this->crctable = array(); 29 | $poly = $this->LIM32($options["poly"]); 30 | for ($x = 0; $x < 256; $x++) 31 | { 32 | $c = $this->SHL32($x, 24); 33 | for ($y = 0; $y < 8; $y++) $c = $this->SHL32($c, 1) ^ ($c & 0x80000000 ? $poly : 0); 34 | $this->crctable[$x] = $c; 35 | } 36 | 37 | $this->datareflect = $options["refdata"]; 38 | $this->crcreflect = $options["refcrc"]; 39 | $this->firstcrc = $options["start"]; 40 | $this->currcrc = $options["start"]; 41 | $this->finalxor = $options["xor"]; 42 | } 43 | 44 | $this->open = true; 45 | } 46 | 47 | public function AddData($data) 48 | { 49 | if (!$this->open) return false; 50 | 51 | if ($this->hash !== false) hash_update($this->hash, $data); 52 | else 53 | { 54 | $y = strlen($data); 55 | 56 | for ($x = 0; $x < $y; $x++) 57 | { 58 | if ($this->datareflect) $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ self::$revlookup[ord($data[$x])]]; 59 | else $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ ord($data[$x])]; 60 | } 61 | } 62 | 63 | return true; 64 | } 65 | 66 | public function Finalize() 67 | { 68 | if (!$this->open) return false; 69 | 70 | if ($this->hash !== false) 71 | { 72 | $result = hexdec(hash_final($this->hash)); 73 | 74 | $this->hash = hash_init("crc32b"); 75 | } 76 | else 77 | { 78 | if ($this->crcreflect) 79 | { 80 | $tempcrc = $this->currcrc; 81 | $this->currcrc = self::$revlookup[$this->SHR32($tempcrc, 24)] | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 16) & 0xFF], 8) | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 8) & 0xFF], 16) | $this->SHL32(self::$revlookup[$this->LIM32($tempcrc & 0xFF)], 24); 82 | } 83 | $result = $this->currcrc ^ $this->finalxor; 84 | 85 | $this->currcrc = $this->firstcrc; 86 | } 87 | 88 | return $result; 89 | } 90 | 91 | // These functions are a hacky, but effective way of enforcing unsigned 32-bit integers onto a generic signed int. 92 | // Allow bitwise operations to work across platforms. Minimum integer size must be 32-bit. 93 | private function SHR32($num, $bits) 94 | { 95 | $num = (int)$num; 96 | if ($bits < 0) $bits = 0; 97 | 98 | if ($num < 0 && $bits) 99 | { 100 | $num = ($num >> 1) & 0x7FFFFFFF; 101 | $bits--; 102 | } 103 | 104 | return $this->LIM32($num >> $bits); 105 | } 106 | 107 | private function SHL32($num, $bits) 108 | { 109 | if ($bits < 0) $bits = 0; 110 | 111 | return $this->LIM32((int)$num << $bits); 112 | } 113 | 114 | private function LIM32($num) 115 | { 116 | return (int)((int)$num & 0xFFFFFFFF); 117 | } 118 | } 119 | ?> -------------------------------------------------------------------------------- /support/str_basics.php: -------------------------------------------------------------------------------- 1 | $val) 10 | { 11 | if (is_string($val)) $_REQUEST[$key] = trim($val); 12 | else if (is_array($val)) 13 | { 14 | $_REQUEST[$key] = array(); 15 | foreach ($val as $key2 => $val2) $_REQUEST[$key][$key2] = (is_string($val2) ? trim($val2) : $val2); 16 | } 17 | else $_REQUEST[$key] = $val; 18 | } 19 | } 20 | 21 | // Cleans up all PHP input issues so that $_REQUEST may be used as expected. 22 | public static function ProcessAllInput() 23 | { 24 | self::ProcessSingleInput($_COOKIE); 25 | self::ProcessSingleInput($_GET); 26 | self::ProcessSingleInput($_POST); 27 | } 28 | 29 | public static function ExtractPathname($dirfile) 30 | { 31 | $dirfile = str_replace("\\", "/", $dirfile); 32 | $pos = strrpos($dirfile, "/"); 33 | if ($pos === false) $dirfile = ""; 34 | else $dirfile = substr($dirfile, 0, $pos + 1); 35 | 36 | return $dirfile; 37 | } 38 | 39 | public static function ExtractFilename($dirfile) 40 | { 41 | $dirfile = str_replace("\\", "/", $dirfile); 42 | $pos = strrpos($dirfile, "/"); 43 | if ($pos !== false) $dirfile = substr($dirfile, $pos + 1); 44 | 45 | return $dirfile; 46 | } 47 | 48 | public static function ExtractFileExtension($dirfile) 49 | { 50 | $dirfile = self::ExtractFilename($dirfile); 51 | $pos = strrpos($dirfile, "."); 52 | if ($pos !== false) $dirfile = substr($dirfile, $pos + 1); 53 | else $dirfile = ""; 54 | 55 | return $dirfile; 56 | } 57 | 58 | public static function ExtractFilenameNoExtension($dirfile) 59 | { 60 | $dirfile = self::ExtractFilename($dirfile); 61 | $pos = strrpos($dirfile, "."); 62 | if ($pos !== false) $dirfile = substr($dirfile, 0, $pos); 63 | 64 | return $dirfile; 65 | } 66 | 67 | // Makes an input filename safe for use. 68 | // Allows a very limited number of characters through. 69 | public static function FilenameSafe($filename) 70 | { 71 | return preg_replace('/\s+/', "-", trim(trim(preg_replace('/[^A-Za-z0-9_.\-]/', " ", $filename), "."))); 72 | } 73 | 74 | public static function ReplaceNewlines($replacewith, $data) 75 | { 76 | $data = str_replace("\r\n", "\n", $data); 77 | $data = str_replace("\r", "\n", $data); 78 | $data = str_replace("\n", $replacewith, $data); 79 | 80 | return $data; 81 | } 82 | 83 | public static function LineInput($data, &$pos) 84 | { 85 | $CR = ord("\r"); 86 | $LF = ord("\n"); 87 | 88 | $result = ""; 89 | $y = strlen($data); 90 | if ($pos > $y) $pos = $y; 91 | while ($pos < $y && ord($data[$pos]) != $CR && ord($data[$pos]) != $LF) 92 | { 93 | $result .= $data[$pos]; 94 | $pos++; 95 | } 96 | if ($pos + 1 < $y && ord($data[$pos]) == $CR && ord($data[$pos + 1]) == $LF) $pos++; 97 | if ($pos < $y) $pos++; 98 | 99 | return $result; 100 | } 101 | 102 | // Constant-time string comparison. Ported from CubicleSoft C++ code. 103 | public static function CTstrcmp($secret, $userinput) 104 | { 105 | $sx = 0; 106 | $sy = strlen($secret); 107 | $uy = strlen($userinput); 108 | $result = $sy - $uy; 109 | for ($ux = 0; $ux < $uy; $ux++) 110 | { 111 | $result |= ord($userinput[$ux]) ^ ord($secret[$sx]); 112 | $sx = ($sx + 1) % $sy; 113 | } 114 | 115 | return $result; 116 | } 117 | 118 | public static function ConvertUserStrToBytes($str) 119 | { 120 | $str = trim($str); 121 | $num = (double)$str; 122 | if (strtoupper(substr($str, -1)) == "B") $str = substr($str, 0, -1); 123 | switch (strtoupper(substr($str, -1))) 124 | { 125 | case "P": $num *= 1024; 126 | case "T": $num *= 1024; 127 | case "G": $num *= 1024; 128 | case "M": $num *= 1024; 129 | case "K": $num *= 1024; 130 | } 131 | 132 | return $num; 133 | } 134 | 135 | public static function ConvertBytesToUserStr($num) 136 | { 137 | $num = (double)$num; 138 | 139 | if ($num < 0) return "0 B"; 140 | if ($num < 1024) return number_format($num, 0) . " B"; 141 | if ($num < 1048576) return str_replace(".0 ", "", number_format($num / 1024, 1)) . " KB"; 142 | if ($num < 1073741824) return str_replace(".0 ", "", number_format($num / 1048576, 1)) . " MB"; 143 | if ($num < 1099511627776.0) return str_replace(".0 ", "", number_format($num / 1073741824.0, 1)) . " GB"; 144 | if ($num < 1125899906842624.0) return str_replace(".0 ", "", number_format($num / 1099511627776.0, 1)) . " TB"; 145 | 146 | return str_replace(".0 ", "", number_format($num / 1125899906842624.0, 1)) . " PB"; 147 | } 148 | 149 | public static function JSSafe($data) 150 | { 151 | return str_replace(array("'", "\r", "\n"), array("\\'", "\\r", "\\n"), $data); 152 | } 153 | } 154 | ?> -------------------------------------------------------------------------------- /support/csdb/db_pgsql_lite.php: -------------------------------------------------------------------------------- 1 | lastid = 0; 24 | 25 | parent::Connect($dsn, $username, $password, $options); 26 | 27 | // Set Unicode support. 28 | $this->Query("SET", "client_encoding TO 'UTF-8'"); 29 | } 30 | 31 | public function GetInsertID($name = null) 32 | { 33 | return $this->lastid; 34 | } 35 | 36 | public function QuoteIdentifier($str) 37 | { 38 | return "\"" . str_replace(array("\"", "?"), array("\"\"", ""), $str) . "\""; 39 | } 40 | 41 | protected function GenerateSQL(&$master, &$sql, &$opts, $cmd, $queryinfo, $args, $subquery) 42 | { 43 | switch ($cmd) 44 | { 45 | case "SELECT": 46 | { 47 | $supported = array( 48 | "PRECOLUMN" => array("DISTINCT" => "bool", "SUBQUERIES" => true), 49 | "FROM" => array("SUBQUERIES" => true), 50 | "WHERE" => array("SUBQUERIES" => true), 51 | "GROUP BY" => true, 52 | "HAVING" => true, 53 | "ORDER BY" => true, 54 | "LIMIT" => " OFFSET " 55 | ); 56 | 57 | return $this->ProcessSELECT($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 58 | } 59 | case "INSERT": 60 | { 61 | $supported = array( 62 | "PREINTO" => array(), 63 | "POSTVALUES" => array("RETURNING" => "key_identifier"), 64 | "SELECT" => true, 65 | "BULKINSERT" => true 66 | ); 67 | 68 | // To get the last insert ID via GetInsertID(), the field that contains a 'serial' (auto increment) field must be specified. 69 | if (isset($queryinfo["AUTO INCREMENT"])) $queryinfo["RETURNING"] = $queryinfo["AUTO INCREMENT"]; 70 | 71 | $this->lastid = 0; 72 | $result = $this->ProcessINSERT($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 73 | if ($result["success"] && isset($queryinfo["AUTO INCREMENT"])) $result["filter_opts"] = array("mode" => "INSERT", "queryinfo" => $queryinfo); 74 | 75 | return $result; 76 | } 77 | case "UPDATE": 78 | { 79 | // No ORDER BY or LIMIT support. 80 | $supported = array( 81 | "PRETABLE" => array("ONLY" => "bool"), 82 | "WHERE" => array("SUBQUERIES" => true) 83 | ); 84 | 85 | return $this->ProcessUPDATE($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 86 | } 87 | case "DELETE": 88 | { 89 | // No ORDER BY or LIMIT support. 90 | $supported = array( 91 | "PREFROM" => array("ONLY" => "bool"), 92 | "WHERE" => array("SUBQUERIES" => true) 93 | ); 94 | 95 | return $this->ProcessDELETE($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 96 | } 97 | case "SET": 98 | { 99 | $sql = "SET " . $queryinfo; 100 | 101 | return array("success" => true); 102 | } 103 | case "USE": 104 | { 105 | // Fake multiple databases with PostgreSQL schemas. 106 | // http://www.postgresql.org/docs/7.3/static/ddl-schemas.html 107 | $sql = "SET search_path TO " . ($queryinfo != "" ? $this->QuoteIdentifier($queryinfo) . "," : "") . "\"\$user\",public"; 108 | 109 | return array("success" => true); 110 | } 111 | case "TRUNCATE TABLE": 112 | { 113 | $master = true; 114 | 115 | $sql = "TRUNCATE TABLE " . $this->QuoteIdentifier($queryinfo[0]); 116 | 117 | return array("success" => true); 118 | } 119 | } 120 | 121 | return array("success" => false, "error" => CSDB::DB_Translate("Unknown query command '%s'.", $cmd), "errorcode" => "unknown_query_command"); 122 | } 123 | 124 | protected function RunStatementFilter(&$stmt, &$filteropts) 125 | { 126 | if ($filteropts["mode"] == "INSERT") 127 | { 128 | // Force the last ID value to be extracted for INSERT queries. 129 | $result = new CSDB_PDO_Statement($this, $stmt, $filteropts); 130 | $row = $result->NextRow(); 131 | 132 | $stmt = false; 133 | } 134 | 135 | parent::RunStatementFilter($stmt, $filteropts); 136 | } 137 | 138 | public function RunRowFilter(&$row, &$filteropts, &$fetchnext) 139 | { 140 | switch ($filteropts["mode"]) 141 | { 142 | case "INSERT": 143 | { 144 | if ($row !== false) 145 | { 146 | $key = $filteropts["queryinfo"]["AUTO INCREMENT"]; 147 | $this->lastid = $row->$key; 148 | } 149 | 150 | break; 151 | } 152 | } 153 | 154 | if (!$fetchnext) parent::RunRowFilter($row, $filteropts, $fetchnext); 155 | } 156 | } 157 | ?> -------------------------------------------------------------------------------- /support/blowfish.php: -------------------------------------------------------------------------------- 1 | setKey($key); 23 | if (isset($options["iv"])) $bf->setIV($options["iv"]); 24 | $bf->disablePadding(); 25 | if (strlen($data) % 8 != 0) $data = str_pad($data, strlen($data) + (8 - (strlen($data) % 8)), "\x00"); 26 | $data = $bf->encrypt($data); 27 | 28 | if (isset($options["key2"])) 29 | { 30 | $data = substr($data, -1) . substr($data, 0, -1); 31 | 32 | if (isset($options["iv2"])) $options["iv"] = $options["iv2"]; 33 | else unset($options["iv"]); 34 | 35 | if ($options["mode"] != "ECB" && (!isset($options["iv"]) || $options["iv"] == "")) return false; 36 | 37 | $bf->setKey($options["key2"]); 38 | if (isset($options["iv"])) $bf->setIV($options["iv"]); 39 | $data = $bf->encrypt($data); 40 | } 41 | 42 | return $data; 43 | } 44 | 45 | // Uses Blowfish to extract the data from an encapsulated data packet and validates the data. Does not support streams. 46 | static function ExtractDataPacket($data, $key, $options = array()) 47 | { 48 | $data = (string)$data; 49 | 50 | if (!isset($options["mode"])) $options["mode"] = "ECB"; 51 | if ($options["mode"] != "ECB" && (!isset($options["iv"]) || $options["iv"] == "")) return false; 52 | 53 | if (!class_exists("Crypt_Blowfish", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/phpseclib/Blowfish.php"; 54 | 55 | if (isset($options["key2"])) 56 | { 57 | $options2 = $options; 58 | if (isset($options["iv2"])) $options["iv"] = $options["iv2"]; 59 | else unset($options["iv"]); 60 | 61 | $bf = new Crypt_Blowfish($options["mode"] == "CBC" ? CRYPT_BLOWFISH_MODE_CBC : CRYPT_BLOWFISH_MODE_ECB); 62 | $bf->setKey($options["key2"]); 63 | if (isset($options["iv"])) $bf->setIV($options["iv"]); 64 | $bf->disablePadding(); 65 | $data = $bf->decrypt($data); 66 | 67 | $data = substr($data, 1) . substr($data, 0, 1); 68 | $options = $options2; 69 | } 70 | 71 | $bf = new Crypt_Blowfish($options["mode"] == "CBC" ? CRYPT_BLOWFISH_MODE_CBC : CRYPT_BLOWFISH_MODE_ECB); 72 | $bf->setKey($key); 73 | if (isset($options["iv"])) $bf->setIV($options["iv"]); 74 | $bf->disablePadding(); 75 | $data = $bf->decrypt($data); 76 | 77 | if ($data === false) return false; 78 | 79 | $pos = strpos($data, "\n"); 80 | if ($pos === false) return false; 81 | $data = substr($data, $pos + 1); 82 | 83 | $pos = strpos($data, "\n"); 84 | if ($pos === false) return false; 85 | $check = substr($data, 0, $pos); 86 | $data = substr($data, $pos + 1); 87 | 88 | $pos = strrpos($data, "\n"); 89 | if ($pos === false) return false; 90 | $data = substr($data, 0, $pos); 91 | 92 | if (!isset($options["lightweight"]) || !$options["lightweight"]) 93 | { 94 | if ($check !== strtolower(sha1($data))) return false; 95 | } 96 | else if ($check !== strtolower(dechex(crc32($data)))) return false; 97 | 98 | return $data; 99 | } 100 | 101 | // Uses Blowfish to create a hash of some data. Typically used to securely hash passwords. 102 | // The recommended minimum number of rounds is 16. Powers of two are preferred. 103 | // The recommended minimum amount of time is 250 (milliseconds). Ignored when $mintime is 0. 104 | static function Hash($data, $rounds, $mintime) 105 | { 106 | $data = (string)$data; 107 | if ($data == "") return array("success" => false, "error" => "No data."); 108 | 109 | if (!class_exists("Crypt_Blowfish", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/phpseclib/Blowfish.php"; 110 | 111 | // Expand data. 112 | $origdata = $data; 113 | while (strlen($data) < 56) $data .= $origdata; 114 | $maxpos = strlen($data); 115 | $data .= $data; 116 | 117 | // Run through Blowfish. 118 | $result = ""; 119 | for ($x = 0; $x < 32; $x++) $result .= chr($x); 120 | $x = 0; 121 | $ts = microtime(true) + $mintime / 1000; 122 | $totalrounds = 0; 123 | 124 | $bf = new Crypt_Blowfish(); 125 | $bf->disablePadding(); 126 | 127 | while ($rounds > 0) 128 | { 129 | $key = substr($data, $x, 56); 130 | $x = ($x + 56) % $maxpos; 131 | 132 | $bf->setKey($key); 133 | $result = $bf->encrypt($result); 134 | 135 | $result = substr($result, -1) . substr($result, 0, -1); 136 | 137 | $rounds--; 138 | $totalrounds++; 139 | if (!$rounds && $mintime > 0 && microtime(true) < $ts) $rounds++; 140 | } 141 | 142 | return array("success" => true, "hash" => $result, "rounds" => $totalrounds); 143 | } 144 | } 145 | ?> -------------------------------------------------------------------------------- /providers/sso_login/modules/sso_tos.php: -------------------------------------------------------------------------------- 1 | "Terms of Service/Privacy Policy", 9 | "desc" => "Adds a standard terms of service and/or privacy policy checkbox during registration." 10 | ); 11 | 12 | class sso_login_module_sso_tos extends sso_login_ModuleBase 13 | { 14 | public function DefaultOrder() 15 | { 16 | return 200; 17 | } 18 | 19 | private function GetInfo() 20 | { 21 | global $sso_settings; 22 | 23 | $info = $sso_settings["sso_login"]["modules"]["sso_tos"]; 24 | if (!isset($info["terms"])) $info["terms"] = ""; 25 | if (!isset($info["privacy"])) $info["privacy"] = ""; 26 | 27 | return $info; 28 | } 29 | 30 | public function ConfigSave() 31 | { 32 | global $sso_settings; 33 | 34 | $info = $this->GetInfo(); 35 | $info["terms"] = trim($_REQUEST["sso_tos_terms"]); 36 | $info["privacy"] = trim($_REQUEST["sso_tos_privacy"]); 37 | 38 | $sso_settings["sso_login"]["modules"]["sso_tos"] = $info; 39 | } 40 | 41 | public function Config(&$contentopts) 42 | { 43 | $info = $this->GetInfo(); 44 | $contentopts["fields"][] = array( 45 | "title" => "Terms Of Service", 46 | "type" => "textarea", 47 | "height" => "300px", 48 | "name" => "sso_tos_terms", 49 | "value" => BB_GetValue("sso_tos_terms", $info["terms"]), 50 | "desc" => "The URL of your terms of service or the document's text itself." 51 | ); 52 | $contentopts["fields"][] = array( 53 | "title" => "Privacy Policy", 54 | "type" => "textarea", 55 | "height" => "300px", 56 | "name" => "sso_tos_privacy", 57 | "value" => BB_GetValue("sso_tos_privacy", $info["privacy"]), 58 | "desc" => "The URL of your privacy policy or the document's text itself." 59 | ); 60 | } 61 | 62 | public function SignupCheck(&$result, $ajax, $admin) 63 | { 64 | if ($admin) return; 65 | 66 | if (!$ajax) 67 | { 68 | $info = $this->GetInfo(); 69 | if ($info["terms"] != "" || $info["privacy"] != "") 70 | { 71 | $field = SSO_FrontendFieldValue("sso_login_tos"); 72 | if ($field != "yes") 73 | { 74 | $terms = Str::ReplaceNewlines("\n", trim($info["terms"])); 75 | $privacy = Str::ReplaceNewlines("\n", trim($info["privacy"])); 76 | if ($terms != "" && $privacy != "") $result["errors"][] = BB_Translate("Agree to the Terms of Service and Privacy Policy to continue."); 77 | else if ($terms != "") $result["errors"][] = BB_Translate("Agree to the Terms of Service to continue."); 78 | else $result["errors"][] = BB_Translate("Agree to the Privacy Policy to continue."); 79 | } 80 | } 81 | } 82 | } 83 | 84 | public function GenerateSignup($admin) 85 | { 86 | if ($admin) return false; 87 | 88 | $info = $this->GetInfo(); 89 | if ($info["terms"] != "" || $info["privacy"] != "") 90 | { 91 | $terms = Str::ReplaceNewlines("\n", trim($info["terms"])); 92 | if ($terms != "") 93 | { 94 | if (strpos($terms, "\n") === false && (strtolower(substr($terms, 0, 7)) == "http://" || strtolower(substr($terms, 0, 8)) == "https://")) 95 | { 96 | $termsurl = "" . htmlspecialchars(BB_Translate("Terms of Service")) . ""; 97 | } 98 | else 99 | { 100 | $termsurl = htmlspecialchars(BB_Translate("Terms of Service")); 101 | ?> 102 |
103 |
104 |
105 |
106 | " . htmlspecialchars(BB_Translate("Privacy Policy")) . ""; 116 | } 117 | else 118 | { 119 | $privacyurl = htmlspecialchars(BB_Translate("Privacy Policy")); 120 | ?> 121 |
122 |
123 |
124 |
125 | 133 |
134 |
" name="" value="yes" />
135 |
136 | 142 | -------------------------------------------------------------------------------- /docs/reserved-global-variables.md: -------------------------------------------------------------------------------- 1 | Reserved Variables 2 | ================== 3 | 4 | To minimize naming conflicts, the SSO server and client have very specific naming conventions so as to not interfere with the proper operation of applications, each other, and the various providers. There is a lot to the core system, so avoiding conflicts is important. 5 | 6 | There are two reserved prefixes: '$sso_' and '$bb_'. 7 | 8 | When creating global variable names for providers, a '$g_' prefix is recommended for throwaway variables. For a permanent global, a '$g_providername_' prefix is recommended. In general, correctly written providers will not need to use global variables. 9 | 10 | Both the SSO server and client have a set of define()'d constants. These are carefully constructed so they won't conflict with each other as well as third-party applications such that there is seamless integration. The constants are defined in 'config.php', which is generated by each installer. 11 | 12 | SSO Server Globals 13 | ------------------ 14 | 15 | * $sso_db - The database class instance for running queries. 16 | * $sso_db_apikeys - A string containing the table name for running API key queries. 17 | * $sso_db_fields - A string containing the table name for running field information queries. 18 | * $sso_db_users - A string containing the table name for running user information queries. 19 | * $sso_db_user_tags - A string containing the table name for running user tag information queries. 20 | * $sso_db_user_sessions - A string containing the table name for running user session information queries. 21 | * $sso_db_temp_sessions - A string containing the table name for running temporary session information queries. 22 | * $sso_db_tags - A string containing the table name for running tag information queries. 23 | * $sso_db_ipcache - A string containing the table name for running IP address cache information queries. 24 | * $sso_fields - An array containing key-value pairs for identifying enabled fields (key) and whether or not the field is encrypted (value). 25 | * $sso_select_fields - An array containing key-value pairs for displaying select dropdowns used throughout the admin interface. 26 | * $sso_settings - An array containing the global configuration, including provider configurations. 27 | * $sso_randomwords - An array containing internal information about the dictionary for exclusive use by the SSO_GetRandomWord() function. 28 | * $sso_namespaces - An array containing possibly validated sessions in the various namespaces that have been signed into. 29 | * $sso_rng - An instance of the CSPRNG class. 30 | * $sso_ipaddr - An array containing normalized information about the remote IP address for use with IP address related operations. 31 | * $bb_usertoken - A string containing a valid user token. Required to be set by 'admin_hook.php' for access to the admin interface. 32 | * $sso_site_admin - A boolean indicating whether or not the user has access to the full admin interface or just a subset. 33 | * $sso_user_id - An optional integer that defines a SSO server user ID of the user in the admin interface for identifying who did what. Generally only useful for larger organizations where multiple users have the SSO site admin privilege and the SSO client is used to sign in to the admin interface. 34 | * $sso_menuopts - An array containing menu options for passing to Admin Pack to generate the navigation for the admin interface. 35 | * $sso_providers - An array of key-value pairs that map an internal provider name to an instance of the provider. 36 | * $sso_provider - A string containing an internal provider name. 37 | * $sso_header - A string containing the header for the end user interface. Obtained by capturing the output of 'header.php'. 38 | * $sso_footer - A string containing the footer for the end user interface. Obtained by capturing the output of 'footer.php'. 39 | * $sso_apirow - An object containing raw database API key information. 40 | * $sso_apikey_info - An array containing extracted API key information. 41 | * $sso_session_id - An array containing two elements - a string and an integer - for identifying a session. 42 | * $sso_sessionrow - An object containing raw database session information. 43 | * $sso_session_info - An array containing extracted session information. 44 | * $sso_session_id2 - An array containing two elements - a string and an integer - for identifying a second session. 45 | * $sso_sessionrow2 - An object containing raw database second session information. 46 | * $sso_session_info2 - An array containing extracted second session information. 47 | * $sso_protectedfields - An array containing provider protected SSO field mapping information. 48 | * $sso_automate - A boolean that specifies if the validation step should be bypassed if possible. 49 | * $sso_userrow - An object containing raw database user information. 50 | * $sso_user_info - An array containing decrypted user information. 51 | * $sso_missingfields - An array containing calculated missing fields for use by an 'index_hook.php' script. 52 | * $sso_target_url - A string containing a base URL for conveniently generating URLs that have a similar target. 53 | * $sso_selectors_url - A string containing a URL to use to return to the selection screen when multiple providers are available. 54 | * $sso_skipsleep - A boolean in the endpoint that indicates whether or not to skip the timing attack defense logic. Only usable by custom API keys. 55 | 56 | The list of globals above are not exhaustive. 57 | 58 | PHP SSO Client Globals 59 | ---------------------- 60 | 61 | * $sso_removekeys - An array containing names of $_GET options that should be removed if they appear for the signed in user. This array should be defined by the application before including the 'client/functions.php' file. The SSO client will automatically redirect the browser to a URL without the specified options. 62 | * $sso__client - An instance of the 'SSO_Client' class that gets created when the 'client/functions.php' file is included. 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Single Sign On (SSO) Server 2 | =========================== 3 | 4 | Do you need a PHP login system that rocks? Well, you found it. 5 | 6 | This is Barebones SSO Server. An awesome, scalable, secure, flexible login system. 7 | 8 | ![Example SSO server login screen](https://user-images.githubusercontent.com/1432111/39400265-0eab58a4-4ae2-11e8-88a8-b712df213468.png) 9 | 10 | [![Donate](https://cubiclesoft.com/res/donate-shield.png)](https://cubiclesoft.com/donate/) [![Discord](https://img.shields.io/discord/777282089980526602?label=chat&logo=discord)](https://cubiclesoft.com/product-support/github/) 11 | 12 | Features 13 | -------- 14 | 15 | * Cross-domain and cross-server capable. The SSO server can reside on its own domain and host. 16 | * Massively scalable architecture. Scale out to as many boxes/virtuals as you have available. 17 | * Resilient architecture. Authenticated users can continue to work even if the server becomes unavailable. 18 | * Resource friendly. Small memory footprint. 19 | * Enables partial to complete compliance with various bodies of rules and laws including HIPAA, GDPR, PCI. Work in progress to achieve complete compliance. 20 | * Integrates with a variety of backend databases via [CSDB](https://github.com/cubiclesoft/csdb). 21 | * And much, much more. See the [full feature list](https://github.com/cubiclesoft/sso-server/blob/master/docs/all-features.md). 22 | * Also has a liberal open source license. MIT or LGPL, your choice. 23 | * Designed for relatively painless integration into your project. 24 | * Sits on GitHub for all of that pull request and issue tracker goodness to easily submit changes and ideas respectively. 25 | 26 | SSO Clients 27 | ----------- 28 | 29 | * [PHP](https://github.com/cubiclesoft/sso-client-php) 30 | * [ASP.NET (C#)](https://github.com/cubiclesoft/sso-client-aspnet) 31 | * [Go (Golang)](https://github.com/gbl08ma/ssoclient) 32 | 33 | Getting Started 34 | --------------- 35 | 36 | The fastest way to get started without reading a lot of documentation is to download/'git pull' the server from this repository and a SSO client from the list above and then follow along with the four part video tutorial series: 37 | 38 | [![SSO server/client tutorial series](https://user-images.githubusercontent.com/1432111/39399682-1ac2d3de-4ad7-11e8-8ba7-6f1bf284e0c0.png)](https://www.youtube.com/watch?v=xjPp_YVGttw&list=PLIvucSFZRDjgiSfsm707zn-bqKd64Eikb) 39 | 40 | And use the [installation documentation](https://github.com/cubiclesoft/sso-server/blob/master/docs/install.md) as necessary. 41 | 42 | According to users of this software, it takes about 3 hours to get a functional SSO server/client setup for the first time. Building an equivalent system from scratch would take approximately six months for a team of several people, have less flexibility, and probably have multiple security vulnerabilities. 43 | 44 | Related Projects 45 | ---------------- 46 | 47 | * [Native app framework/API](https://github.com/cubiclesoft/sso-native-apps) 48 | * [Native app demos](http://barebonescms.com/sso_native_app_demos.zip) - Precompiled versions of the above 49 | * [Disqus provider](https://github.com/khachin/sso-disqus-provider) 50 | * [MyBB plugin](http://barebonescms.com/MyBB_SSOClient-2.5.zip) - Direct download 51 | 52 | More Information 53 | ---------------- 54 | 55 | * [The PHP SSO client](https://github.com/cubiclesoft/sso-server/blob/master/docs/php-sso-client.md) - Official documentation for the the PHP SSO client. 56 | * [Upgrading](https://github.com/cubiclesoft/sso-server/blob/master/docs/upgrade.md) - Important information regarding upgrades. 57 | * [Integrating SSO clients with third-party software](https://github.com/cubiclesoft/sso-server/blob/master/docs/integrating-with-third-party-software.md) - Instructions for integrating with forums, CMS products, etc. Dealing with any software that comes with its own login system. 58 | * [Import existing user accounts](https://github.com/cubiclesoft/sso-server/blob/master/docs/import-existing-user-accounts.md) - Instructions for migrating from another product or a homegrown login system. 59 | * [Enabling user impersonation](https://github.com/cubiclesoft/sso-server/blob/master/docs/user-impersonation.md) - For managing hopeless users who regularly forget their sign in information and require constant password resets. 60 | * [Remote Login Provider documentation](https://github.com/cubiclesoft/sso-server/blob/master/docs/remote-login-provider-setup.md) - Set up "remote" API keys to allow trusted hosts with their own login system and users (e.g. Active Directory/LDAP), to sign in. 61 | * [Creating a SSO server provider](https://github.com/cubiclesoft/sso-server/blob/master/docs/creating-providers.md) - The high-level interface for developing a new provider. 62 | * [Creating a Generic Login module](https://github.com/cubiclesoft/sso-server/blob/master/docs/creating-generic-login-modules.md) - Modules extend the Generic Login provider to allow it to do more. 63 | * [Porting the SSO client](https://github.com/cubiclesoft/sso-server/blob/master/docs/porting-the-sso-client.md) - Instructions on porting the official PHP client to your preferred programming/scripting language. 64 | * [Endpoint API](https://github.com/cubiclesoft/sso-server/blob/master/docs/endpoint-api.md) - The SSO server endpoint API. 65 | * [Using custom API keys](https://github.com/cubiclesoft/sso-server/blob/master/docs/using-custom-api-keys.md) - Here be dragons. The not recommended last resort workaround for dealing with encountered SSO server endpoint limitations. 66 | * [Reserved global variables](https://github.com/cubiclesoft/sso-server/blob/master/docs/reserved-global-variables.md) - Global variables defined by the SSO server and some clients. Useful information for provider and module developers. 67 | * [SSO server global functions](https://github.com/cubiclesoft/sso-server/blob/master/docs/server-global-functions.md) - Global functions defined by the SSO server. Useful information for provider and module developers. 68 | -------------------------------------------------------------------------------- /upgrade.php: -------------------------------------------------------------------------------- 1 | \n"; 52 | } 53 | 54 | function UpgradeError($str) 55 | { 56 | echo BB_Translate($str) . "
\n"; 57 | 58 | exit(); 59 | } 60 | 61 | if (!isset($sso_settings[""]["dbversion"])) $sso_settings[""]["dbversion"] = 1; 62 | if ($sso_settings[""]["dbversion"] == 3) UpgradeError("You already have the latest database version. Upgrade completed. You should delete this file off of the server."); 63 | 64 | // Begin upgrade. 65 | DisplayMessage("Beginning upgrade to latest version."); 66 | 67 | if ($sso_settings[""]["dbversion"] == 1) 68 | { 69 | // Add the namespace column to the API keys table. 70 | DisplayMessage("Adding 'namespace' column."); 71 | try 72 | { 73 | $sso_db->Query("ADD COLUMN", array($sso_db_apikeys, "namespace", array("STRING", 1, 20, "NOT NULL" => true), "AFTER" => "user_id")); 74 | } 75 | catch (Exception $e) 76 | { 77 | UpgradeError("Unable to update the database table '" . htmlspecialchars($sso_db_apikeys) . "'. " . htmlspecialchars($e->getMessage())); 78 | } 79 | 80 | // Add an index on the namespace column. 81 | DisplayMessage("Adding 'namespace' index."); 82 | try 83 | { 84 | $sso_db->Query("ADD INDEX", array($sso_db_apikeys, array("KEY", array("namespace"), "NAME" => "namespace"))); 85 | } 86 | catch (Exception $e) 87 | { 88 | UpgradeError("Unable to update the database table '" . htmlspecialchars($sso_db_apikeys) . "'. " . htmlspecialchars($e->getMessage())); 89 | } 90 | 91 | // Wipe all existing sessions. 92 | DisplayMessage("Resetting all sessions."); 93 | try 94 | { 95 | $sso_db->Query("TRUNCATE TABLE", array($sso_db_user_sessions)); 96 | $sso_db->Query("TRUNCATE TABLE", array($sso_db_temp_sessions)); 97 | } 98 | catch (Exception $e) 99 | { 100 | UpgradeError("Unable to wipe sessions. " . htmlspecialchars($e->getMessage())); 101 | } 102 | 103 | $sso_settings[""]["dbversion"] = 2; 104 | 105 | // Save the settings so the database version is saved. 106 | SSO_SaveSettings(); 107 | } 108 | 109 | if ($sso_settings[""]["dbversion"] == 2) 110 | { 111 | // Generate random seeds. 112 | if (!defined("SSO_BASE_RAND_SEED8")) 113 | { 114 | $rng = new CSPRNG(true); 115 | for ($x = 0; $x < 10; $x++) 116 | { 117 | $seed = $rng->GenerateToken(128); 118 | if ($seed === false) UpgradeError("Seed generation failed."); 119 | 120 | define("SSO_BASE_RAND_SEED" . ($x + 5), $seed); 121 | } 122 | 123 | $data = file_get_contents("config.php"); 124 | $data .= "<" . "?php\n"; 125 | $data .= "\tdefine(\"SSO_BASE_RAND_SEED5\", " . var_export(SSO_BASE_RAND_SEED5, true) . ");\n"; 126 | $data .= "\tdefine(\"SSO_BASE_RAND_SEED6\", " . var_export(SSO_BASE_RAND_SEED6, true) . ");\n"; 127 | $data .= "\tdefine(\"SSO_BASE_RAND_SEED7\", " . var_export(SSO_BASE_RAND_SEED7, true) . ");\n"; 128 | $data .= "\tdefine(\"SSO_BASE_RAND_SEED8\", " . var_export(SSO_BASE_RAND_SEED8, true) . ");\n"; 129 | $data .= "\tdefine(\"SSO_BASE_RAND_SEED9\", " . var_export(SSO_BASE_RAND_SEED9, true) . ");\n"; 130 | $data .= "\tdefine(\"SSO_BASE_RAND_SEED10\", " . var_export(SSO_BASE_RAND_SEED10, true) . ");\n"; 131 | $data .= "\tdefine(\"SSO_BASE_RAND_SEED11\", " . var_export(SSO_BASE_RAND_SEED11, true) . ");\n"; 132 | $data .= "\tdefine(\"SSO_BASE_RAND_SEED12\", " . var_export(SSO_BASE_RAND_SEED12, true) . ");\n"; 133 | $data .= "\tdefine(\"SSO_BASE_RAND_SEED13\", " . var_export(SSO_BASE_RAND_SEED13, true) . ");\n"; 134 | $data .= "\tdefine(\"SSO_BASE_RAND_SEED14\", " . var_export(SSO_BASE_RAND_SEED14, true) . ");\n"; 135 | $data .= "\n"; 136 | $data .= "\tdefine(\"SSO_PRIMARY_CIPHER\", \"aes256\");\n"; 137 | $data .= "\tdefine(\"SSO_DUAL_ENCRYPT\", true);\n"; 138 | $data .= "?" . ">"; 139 | if (file_put_contents("config.php", $data) === false) UpgradeError("Unable to update the server configuration file."); 140 | } 141 | 142 | // Regenerate namespace keys. 143 | SSO_GenerateNamespaceKeys(); 144 | 145 | $sso_settings[""]["dbversion"] = 3; 146 | } 147 | 148 | // Save the settings so the database version is saved. 149 | SSO_SaveSettings(); 150 | 151 | // Upgrade is done. 152 | DisplayMessage("Upgrade completed successfully. You should delete this file off of the server."); 153 | ?> -------------------------------------------------------------------------------- /docs/all-features.md: -------------------------------------------------------------------------------- 1 | SSO Server/Client Features 2 | ========================== 3 | 4 | The following is an extensive but not exhaustive list of features for both the SSO server, specific providers that have features worth mentioning (e.g. Generic Login), and the official PHP client. 5 | 6 | SSO Server Features 7 | ------------------- 8 | 9 | * Cross-domain and cross-server capable. The SSO server can reside on its own domain and host. 10 | * Massively scalable architecture. The server understands master-slave database replication and generally delays execution of change queries until the end of a request to minimize queries against a master database. It is built to easily scale out to as many boxes as you have available. 11 | * Resilient architecture. The server can go offline or become unavailable and SSO client authenticated users can continue to work without negatively affecting the integrity of this system. 12 | * Resource friendly. Each frontend user (someone signing in) uses an average footprint of 4MB RAM per connection in the out-of-the-box install. The endpoint uses an average of 1MB RAM. The server includes tips on how to keep the system running lean and mean under high-performance scenarios. 13 | * Easy to use administrative interface. Point and click to set up and manage fields, tags, providers, API keys, and user accounts. 14 | * Enables partial to complete compliance with various rules and laws including PCI, HIPAA, GDPR. Work in progress to achieve complete compliance. 15 | * Integrates with a variety of backend databases via [CSDB](https://github.com/cubiclesoft/csdb). MySQL, Maria DB, PostgreSQL, etc. 16 | * A 'cron' interface is available for scheduled, automated database cleanup. 17 | * Set up your own branded header and footer. The examples include a stylesheet with a modern, clean look. 18 | * Versioned accounts. Display special messages to users such as new Terms of Service, a new product or newsletter or other marketing messages, and/or have the user fill in missing information after authenticating but before returning to the SSO client. Doubles as an anti-bot measure. 19 | * Anti-bot dynamic form field support. Form fields are randomly named based on the randomly generated session ID. Since the order of most fields is controlled by the admin interface, this becomes a bot operator's nightmare. 20 | * Encrypted data storage of private data. Protects against successful hacking attempts that only dump the database. 21 | * Multiple encryption ciphers and optional [dual encryption mode](http://cubicspot.blogspot.com/2013/02/extending-block-size-of-any-symmetric.html) support. 22 | * Set up and use tags as a permissions system or for special account flags for any purpose. 23 | * Field setup and field mapping architecture allow for quickly managing user account fields. 24 | * Simple API key setup and usage. Easily map server fields to expected client fields. API keys can be revoked or renewed in the event of a security breach. 25 | * API key namespaces allow an active sign in to be shared across applications. 26 | * User impersonation support. One-click sign in. Disabled by default but straightforward to set up if needed. 27 | * Comes with several sign in providers: Generic Login, Facebook, Google, LinkedIn, LDAP (Active Directory), and Remote Login. 28 | * The Remote Login provider allows for signing in using a trusted host behind a firewall. For example, sign in with LDAP or Active Directory via VPN and push the user's information to the SSO Server via a native SSO Client call. 29 | * Supports simple third-party software integration via an OAuth2 shim. 30 | * Carefully crafted defenses to deal with [CSRF/XSRF attacks](http://en.wikipedia.org/wiki/Cross-site_request_forgery), [timing attacks](http://en.wikipedia.org/wiki/Timing_attack), [session fixation attacks](http://en.wikipedia.org/wiki/Session_fixation), etc. 31 | * HTTP [DNSRBL](http://en.wikipedia.org/wiki/DNSBL) IP address banning support. 32 | * Geolocation IP address banning and automatic location mapping support (requires uploading an extra 15MB+ database). 33 | * Trusted upstream proxy support. 34 | * IPv4 and IPv6 filtering support. 35 | * Multilingual support. 36 | * A simple, easy-to-use installer. 37 | 38 | Generic Login Provider Features 39 | ------------------------------- 40 | 41 | * AJAX live checking. 42 | * Strong 'bcrypt'-style password hashing. 43 | * E-mail verification. 44 | * Account recovery via e-mail and [SMS via e-mail](https://github.com/cubiclesoft/email_sms_mms_gateways). 45 | * [Two-factor authentication](http://en.wikipedia.org/wiki/Two-factor_authentication) (2FA). Works with [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en) (Android and iOS), [Microsoft Authenticator](http://go.microsoft.com/fwlink/?LinkId=279710), [WinAuth](https://winauth.com/), and e-mail are supported 2FA options. 46 | * Password expiration. 47 | * Minimum required password strength. Backed by a dictionary against weak password selection. 48 | * reCAPTCHA support. 49 | * Remember me support. 50 | * Anti-phishing string support. 51 | * Rate limiting. 52 | * Blacklisting. 53 | * Progressive enhancement. 54 | 55 | SSO Client Features 56 | ------------------- 57 | 58 | * Average memory footprint. About 1MB RAM per connection. 59 | * Classes and functions are carefully named to avoid naming conflicts with third-party software. Makes integrating with third-party software a breeze. 60 | * When authentication is required prior to executing some task (e.g. posting a comment), the SSO client encrypts and sends the current request data ($_GET, $_POST, etc.) to the SSO server for later retrieval and will resume exactly where it left off in most cases (e.g. the comment is posted). File uploads are lost during this procedure. 61 | * Encrypts communications over the network (even HTTP). 62 | * Cookies are encrypted. 63 | * Communicates with the server on a schedule set by the client. Allows for significantly reduced network overhead without affecting system integrity. 64 | * Supports both encrypted cookie (default 50 bytes max per key-value pair) and optional local database storage (virtually unlimited). 65 | * Simple enough to port to other scripting and programming languages. Currently available for: PHP, ASP.NET (C#), and Go (Golang). 66 | * A simple, easy-to-use installer. 67 | -------------------------------------------------------------------------------- /providers/sso_login/modules/sso_email_two_factor.php: -------------------------------------------------------------------------------- 1 | "E-mail Two-Factor Authentication", 11 | "desc" => "Adds two-factor authentication for e-mail." 12 | ); 13 | } 14 | 15 | class sso_login_module_sso_email_two_factor extends sso_login_ModuleBase 16 | { 17 | public function DefaultOrder() 18 | { 19 | return 200; 20 | } 21 | 22 | private function GetInfo() 23 | { 24 | global $sso_settings; 25 | 26 | $info = $sso_settings["sso_login"]["modules"]["sso_email_two_factor"]; 27 | if (!isset($info["email_from"])) $info["email_from"] = ""; 28 | if (!isset($info["email_subject"])) $info["email_subject"] = ""; 29 | if (!isset($info["email_msg"])) $info["email_msg"] = ""; 30 | if (!isset($info["email_msg_text"])) $info["email_msg_text"] = ""; 31 | if (!isset($info["window"])) $info["window"] = 300; 32 | if (!isset($info["clock_drift"])) $info["clock_drift"] = 60; 33 | 34 | return $info; 35 | } 36 | 37 | public function ConfigSave() 38 | { 39 | global $sso_settings; 40 | 41 | $info = $this->GetInfo(); 42 | $info["email_from"] = $_REQUEST["sso_email_two_factor_email_from"]; 43 | $info["email_subject"] = trim($_REQUEST["sso_email_two_factor_email_subject"]); 44 | $info["email_msg"] = $_REQUEST["sso_email_two_factor_email_msg"]; 45 | $info["email_msg_text"] = SMTP::ConvertHTMLToText($_REQUEST["sso_email_two_factor_email_msg"]); 46 | $info["window"] = (int)$_REQUEST["sso_email_two_factor_window"]; 47 | $info["clock_drift"] = (int)$_REQUEST["sso_email_two_factor_clock_drift"]; 48 | 49 | if (stripos($info["email_msg"], "@TWOFACTOR@") === false) BB_SetPageMessage("error", "The E-mail Two-Factor Authentication 'E-mail Message' field does not contain '@TWOFACTOR@'."); 50 | else if ($info["window"] < 30 || $info["window"] > 300) BB_SetPageMessage("error", "The E-mail Two-Factor Authentication 'Window Size' field contains an invalid value."); 51 | else if ($info["clock_drift"] < 0 || $info["clock_drift"] > $info["window"]) BB_SetPageMessage("error", "The E-mail Two-Factor Authentication 'Window Size' field contains an invalid value."); 52 | 53 | $sso_settings["sso_login"]["modules"]["sso_email_two_factor"] = $info; 54 | } 55 | 56 | public function Config(&$contentopts) 57 | { 58 | $info = $this->GetInfo(); 59 | $contentopts["fields"][] = array( 60 | "title" => "From Address", 61 | "type" => "text", 62 | "name" => "sso_email_two_factor_email_from", 63 | "value" => BB_GetValue("sso_email_two_factor_email_from", $info["email_from"]), 64 | "desc" => "The from address for the e-mail message to send to users with the two-factor authentication code. Leave blank for the server default." 65 | ); 66 | $contentopts["fields"][] = array( 67 | "title" => "Subject Line", 68 | "type" => "text", 69 | "name" => "sso_email_two_factor_email_subject", 70 | "value" => BB_GetValue("sso_email_two_factor_email_subject", $info["email_subject"]), 71 | "desc" => "The subject line for the e-mail message to send to users with their two-factor authentication code." 72 | ); 73 | $contentopts["fields"][] = array( 74 | "title" => "HTML Message", 75 | "type" => "textarea", 76 | "height" => "300px", 77 | "name" => "sso_email_two_factor_email_msg", 78 | "value" => BB_GetValue("sso_email_two_factor_email_msg", $info["email_msg"]), 79 | "desc" => "The HTML e-mail message to send to users with their two-factor authentication code. @USERNAME@, @EMAIL@, and @TWOFACTOR@ are special strings that will be replaced with user and system generated values. @TWOFACTOR@ is required." 80 | ); 81 | $contentopts["fields"][] = array( 82 | "title" => "Window Size", 83 | "type" => "text", 84 | "name" => "sso_email_two_factor_window", 85 | "value" => BB_GetValue("sso_email_two_factor_window", $info["window"]), 86 | "desc" => "The length of time, in seconds, each authentication code is valid for. Valid range is 30 to 300. Default is 300." 87 | ); 88 | $contentopts["fields"][] = array( 89 | "title" => "Clock Drift", 90 | "type" => "text", 91 | "name" => "sso_email_two_factor_clock_drift", 92 | "value" => BB_GetValue("sso_email_two_factor_clock_drift", $info["clock_drift"]), 93 | "desc" => "The amount of clock drift, in seconds, to allow for each authentication code. Valid range is 0 to the window size. Default is 60." 94 | ); 95 | } 96 | 97 | public function TwoFactorCheck(&$result, $userinfo) 98 | { 99 | if ($userinfo !== false && $userinfo["two_factor_method"] == "sso_email_two_factor") 100 | { 101 | $info = $this->GetInfo(); 102 | $code = SSO_FrontendFieldValue("two_factor_code", ""); 103 | $twofactor = sso_login::GetTimeBasedOTP($userinfo["two_factor_key"], time() / $info["window"]); 104 | $twofactor2 = sso_login::GetTimeBasedOTP($userinfo["two_factor_key"], (time() - $info["clock_drift"]) / $info["window"]); 105 | $twofactor3 = sso_login::GetTimeBasedOTP($userinfo["two_factor_key"], (time() + $info["clock_drift"]) / $info["window"]); 106 | if ($code !== $twofactor && $code !== $twofactor2 && $code !== $twofactor3) $result["errors"][] = BB_Translate("Invalid two-factor authentication code."); 107 | } 108 | } 109 | 110 | public function GetTwoFactorName() 111 | { 112 | return BB_Translate("E-mail"); 113 | } 114 | 115 | public function SendTwoFactorCode(&$result, $userrow, $userinfo) 116 | { 117 | // Send the two-factor authentication e-mail. 118 | $info = $this->GetInfo(); 119 | $fromaddr = BB_PostTranslate($info["email_from"] != "" ? $info["email_from"] : SSO_SMTP_FROM); 120 | $subject = BB_Translate($info["email_subject"]); 121 | $twofactor = sso_login::GetTimeBasedOTP($userinfo["two_factor_key"], time() / $info["window"]); 122 | $htmlmsg = str_ireplace(array("@USERNAME@", "@EMAIL@", "@TWOFACTOR@"), array(htmlspecialchars($userrow->username), htmlspecialchars($userrow->email), htmlspecialchars($twofactor)), BB_PostTranslate($info["email_msg"])); 123 | $textmsg = str_ireplace(array("@USERNAME@", "@EMAIL@", "@TWOFACTOR@"), array($userrow->username, $userrow->email, $twofactor), BB_PostTranslate($info["email_msg_text"])); 124 | 125 | $result2 = SSO_SendEmail($fromaddr, $userrow->email, $subject, $htmlmsg, $textmsg); 126 | if (!$result2["success"]) $result["errors"][] = BB_Translate("Login exists but a fatal error occurred. Fatal error: Unable to send two-factor authentication e-mail. %s", $result["error"]); 127 | } 128 | } 129 | ?> -------------------------------------------------------------------------------- /docs/creating-providers.md: -------------------------------------------------------------------------------- 1 | Creating a SSO Server Provider 2 | ============================== 3 | 4 | Suppose a popular method of logging in is not already a part of the SSO server. This is where creating a new SSO server provider comes into play. This can be quite the undertaking and the simplest solution is probably to request it. That said, it is best to look at the source code to LDAP provider. The LDAP provider is about as simple as the average provider gets and it took less than a day to create and test the LDAP provider. On the opposite end of the spectrum is the Generic Login provider which is very complex due to its flexibility. 5 | 6 | There are three aspects to every SSO server provider: The configuration, the admin interface functions, and the user interface functions. The SSO server manages all aspects of loading and calling the correct functions at the appropriate times. 7 | 8 | All SSO server providers are classes that derive from the base class 'SSO_ProviderBase'. The default functions in the base class don't do much of anything. Since there is a base class, not every function must be defined in the derived class. 9 | 10 | To create a new provider, create a directory in the 'providers' directory with the name of the provider. Limit the characters of the name to lowercase letters and underscores. The 'sso_' prefix is reserved for official providers. Inside the new directory, create an 'index.php' file. The class name must be the same name as the directory (hence the restrictions on the directory name). 11 | 12 | The rest of this section is a breakdown of each function and what it is expected to do. 13 | 14 | SSO_ProviderBase::Init() 15 | ------------------------ 16 | 17 | Access: public 18 | 19 | Parameters: None. 20 | 21 | Returns: Nothing. 22 | 23 | This function is expected to initialize the class settings in preparation for other calls. The global variable `$sso_settings` is guaranteed to at least contain a key-value pair of class name and an empty array. Most providers use this function as an opportunity to initialize an 'iprestrict' option with the results of a `SSO_InitIPFields()` call. 24 | 25 | SSO_ProviderBase::DisplayName() 26 | ------------------------------- 27 | 28 | Access: public 29 | 30 | Parameters: None. 31 | 32 | Returns: A translated string containing the name of the provider to display to a user. 33 | 34 | This function is expected to return a translated string that will be displayed to the user. This function is called both within the admin interface and the frontend - primarily for a selector when more than one provider is enabled. 35 | 36 | SSO_ProviderBase::DefaultOrder() 37 | -------------------------------- 38 | 39 | Access: public 40 | 41 | Parameters: None. 42 | 43 | Returns: An integer containing the default display order of the provider. 44 | 45 | This function is expected to return the default display order for the provider in relation to other providers when more than one is available/enabled. The default order can be overridden by changing the global configuration in the admin interface. 46 | 47 | SSO_ProviderBase::MenuOpts() 48 | ---------------------------- 49 | 50 | Access: public 51 | 52 | Parameters: None. 53 | 54 | Returns: An array containing "name" and "items" keys that map to a string and array of links to be displayed respectively. 55 | 56 | This function is expected to generate a set of items to display in the admin interface and a section name for the items. Most providers differentiate between users with 'sso_site_admin' and 'sso_admin' privileges here and show only relevant options to the user. The array returned is ordered by the display order before being included into the global $sso_menuopts array. URLs are generally generated with the `SSO_CreateConfigURL()` function. 57 | 58 | SSO_ProviderBase::Config() 59 | -------------------------- 60 | 61 | Access: public 62 | 63 | Parameters: None. 64 | 65 | Returns: Nothing. 66 | 67 | This function is expected to take request inputs and generate a standard [Admin Pack](https://github.com/cubiclesoft/admin-pack) compliant interface. Be sure to check for permissions and errors before executing any command. 68 | 69 | SSO_ProviderBase::IsEnabled() 70 | ----------------------------- 71 | 72 | Access: public 73 | 74 | Parameters: None. 75 | 76 | Returns: A boolean of true if the user should be able to see the provider, false otherwise. 77 | 78 | This function is expected to run a series of tests to make sure that the provider is enabled for a specific user. Tests can range from checking for specific PHP functions and configuration settings to verifying that the user isn't coming from a spammer IP address. 79 | 80 | SSO_ProviderBase::GetProtectedFields() 81 | -------------------------------------- 82 | 83 | Access: public 84 | 85 | Parameters: None. 86 | 87 | Returns: An array containing key-value pairs. 88 | 89 | This function is expected to return a mapping of SSO field names to a boolean value of whether the field is protected or not. Protected fields are not able to be modified by the user except possibly in the provider itself. The only provider that currently offers direct editing of protected fields is the Generic Login provider. 90 | 91 | SSO_ProviderBase::FindUsers() 92 | ----------------------------- 93 | 94 | Access: public 95 | 96 | Parameters: None. 97 | 98 | Returns: Nothing. 99 | 100 | This function is expected to modify the global `$contentopts` to add the results of a search for users. Primarily used by the Generic Login provider to find users who exist but have not activated in search results. 101 | 102 | SSO_ProviderBase::GetEditUserLinks($id) 103 | --------------------------------------- 104 | 105 | Access: public 106 | 107 | Parameters: 108 | 109 | * $id - The internal provider ID that identifies the user. 110 | 111 | Returns: An array of links. 112 | 113 | This function is expected to return an array of links if it supports editing of protected fields. The `$this->Config()` function is expected to be able to handle the actual editing. 114 | 115 | SSO_ProviderBase::AddIPCacheInfo() 116 | ---------------------------------- 117 | 118 | Access: public 119 | 120 | Parameters: None. 121 | 122 | Returns: Nothing. 123 | 124 | This function is expected to modify the global `$contentopts` to introduce additional IP address cache information when displaying information about an IP address. Primarily used by various Generic Login rate limiting modules. 125 | 126 | SSO_ProviderBase::GenerateSelector() 127 | ------------------------------------ 128 | 129 | Access: public 130 | 131 | Parameters: None. 132 | 133 | Returns: Nothing. 134 | 135 | This function is expected to output HTML for a selector for the frontend. Only called when there is more than one enabled provider that the user can choose from. 136 | 137 | SSO_ProviderBase::ProcessFrontend() 138 | ----------------------------------- 139 | 140 | Access: public 141 | 142 | Parameters: None. 143 | 144 | Returns: Nothing. 145 | 146 | This function is expected to perform all frontend tasks required before the user can proceed to the next step. A login system of some sort is generally implemented by this function. `SSO_ActivateUser()` moves the user to the next step. 147 | -------------------------------------------------------------------------------- /docs/endpoint-api.md: -------------------------------------------------------------------------------- 1 | Endpoint API 2 | ============ 3 | 4 | The SSO server endpoint serves as the initiator of all things related to the SSO server and the only method by which a SSO client can use to talk directly to the SSO server. The endpoint API is an important aspect of all SSO server related communication. This documentation is only relevant to those implementing custom endpoint API extensions to the SSO server and those porting the SSO client to other languages. 5 | 6 | Access to the endpoint API is, by default, restricted to those who know the endpoint API URL, have a valid API key, and are accessing the endpoint API from a matching IP address according to the API key's IP address pattern restrictions. The endpoint also supports hooks in a couple of locations but is fairly limited. The actions listed below are reserved for the SSO server and can't be used in the `EndpointHook_CustomHandler()` callback. 7 | 8 | The endpoint API will return errors to the client whenever a situation is encountered that is deemed a failure condition. Most error messages are encrypted but it is possible to get back an unencrypted response (e.g. an invalid API key or the wrong version of the SSO client is being used). The SSO client is responsible for handling both scenarios. All communication is done with encoded JSON objects. 9 | 10 | Once an API key has been validated and loaded, the next step is to locate the correct 'action', verify that the API key is allowed to perform that action, and then execute the action. 11 | 12 | Action: 'test' 13 | --------------- 14 | 15 | API key types: normal, remote, custom 16 | 17 | Inputs: None 18 | 19 | Returns: 20 | 21 | * success - A boolean value of true. 22 | 23 | This action is used by the SSO client installer to verify that the endpoint API URL and API key and secret are working as expected. Diagnosing endpoint URL/API key issues post-installation is a bit more difficult. 24 | 25 | Action: 'canautologin' 26 | ----------------------- 27 | 28 | API key type: normal 29 | 30 | Inputs: 31 | 32 | * ns - A string containing an encrypted namespace cookie. Specifically, the 'sso_server_ns2' cookie. 33 | 34 | Returns: 35 | 36 | * success - A boolean value of true. 37 | 38 | This action is used by the SSO client during the `CanAutoLogin()` call to check the 'sso_server_ns2' cookie and see if the user can be automatically be logged in. 39 | 40 | Action: 'initlogin' 41 | -------------------- 42 | 43 | API key type: normal 44 | 45 | Inputs: 46 | 47 | * url - An optional string containing the URL to return to once the user is activated and validated. 48 | * files - An optional integer specifying whether or not files were uploaded as part of the request. This will display a warning to the user that uploaded files were lost when the browser is redirected to the sign in page. 49 | * initmsg - An optional string to display to the user. If the string is 'insufficient_permissions', the behavior of the server changes so that an infinite loop doesn't occur. 50 | * extra - An optional object of key-value pairs used as part of the return URL. 51 | * info - An optional string, preferably encrypted, containing data to return to the client later when it supplies the recovery ID. 52 | * appurl - An optional string containing the URL of the application to redirect to if the user presses the back button later on in their web browser. 53 | 54 | Returns: 55 | 56 | * success - A boolean value of true. 57 | * url - A string containing the URL that the client should redirect to. 58 | * rid - A string containing the recovery ID. 59 | 60 | This action is used by the SSO client to initiate a sign in. The SSO client is expected to save the recovery ID to retrieve 'info' later and then redirect the browser to the returned URL. 61 | 62 | Action: 'setlogin' 63 | ------------------- 64 | 65 | API key type: remote 66 | 67 | Inputs: 68 | 69 | * sso_id - A string containing the session ID. 70 | * token - A string containing the validation token. 71 | * user_id - A string containing the user ID. 72 | * updateinfo - A string containing a JSON encoded object that contains field mapping information. 73 | 74 | Returns: 75 | 76 | * success - A boolean value of true. 77 | * url - A string containing the URL that the client should redirect to. 78 | 79 | This action is used by the SSO client to authorize a remote sign in. The SSO client is expected to redirect the browser to the returned URL. See the remote provider for details. 80 | 81 | Action: 'getlogin' 82 | ------------------- 83 | 84 | API key type: normal 85 | 86 | Inputs: 87 | 88 | * sso_id - A string containing a session ID or temporary session token. 89 | * expires - An integer containing the number of seconds a session is valid for. 90 | * updateinfo - An optional string containing a JSON encoded object that contains field mapping information. 91 | * delete_old - An optional integer that specifies whether or not the original session should be deleted. 92 | * sso_id2 - An optional string containing the previous session ID. 93 | * rid - A string containing the recovery ID. 94 | 95 | Returns: 96 | 97 | * success - A boolean value of true. 98 | * sso_id - A string containing the session ID. 99 | * id - A string containing the user ID. 100 | * extra - A string containing a constant base token for the user that is intended for use in security nonce calculations. 101 | * field_map - An object containing the field map for the user and API key. 102 | * writable - An object containing a list of writable fields. 103 | * tag_map - An object containing a list of mapped tags associated with the user. 104 | * admin - A boolean that specifies whether or not the user is a site admin. 105 | * rinfo - A string containing the data to return to the client that was submitted with 'initlogin'. 106 | 107 | This action retrieves user sign in information and request recovery information that was sent when the 'initlogin' action was called. When 'delete_old' is specified, the returned object only contains 'success' and is intended to be executed shortly after the first request returns so that the original session information is deleted from the SSO server. When 'sso_id2' and 'rid' are specified, the original recovery data is returned via 'rinfo' and the real session ID via 'sso_id'. The SSO client is responsible for restoring the state of the application as best as possible to the original state so that processing may continue where it left off when the original redirect happened to avoid data loss since data loss results in frustrated users. 108 | 109 | Ideally, this action is called twice by the SSO client: The first call is to retrieve the user's sign in information and application recovery information. The second call is to delete the original session off the server and therefore secure the sign in. Two steps are necessary because a correctly written SSO client will retry an operation multiple times to counteract any server communication failures because failures do happen. The SSO client should never reveal the real session ID across the wire. 110 | 111 | Action: 'logout' 112 | ----------------- 113 | 114 | API key type: normal 115 | 116 | Inputs: 117 | 118 | * sso_id - A string containing a session ID. 119 | 120 | Returns: 121 | 122 | * success - A boolean value of true. 123 | 124 | This action signs out the user from the SSO server across all sign ins within the same namespace as the session specified by sso_id. 125 | -------------------------------------------------------------------------------- /providers/sso_facebook/facebook-sdk-src/facebook.php: -------------------------------------------------------------------------------- 1 | initSharedSession(); 65 | 66 | // re-load the persisted state, since parent 67 | // attempted to read out of non-shared cookie 68 | $state = $this->getPersistentData('state'); 69 | if (!empty($state)) { 70 | $this->state = $state; 71 | } else { 72 | $this->state = null; 73 | } 74 | 75 | } 76 | } 77 | 78 | /** 79 | * Supported keys for persistent data 80 | * 81 | * @var array 82 | */ 83 | protected static $kSupportedKeys = 84 | array('state', 'code', 'access_token', 'user_id'); 85 | 86 | /** 87 | * Initiates Shared Session 88 | */ 89 | protected function initSharedSession() { 90 | $cookie_name = $this->getSharedSessionCookieName(); 91 | if (isset($_COOKIE[$cookie_name])) { 92 | $data = $this->parseSignedRequest($_COOKIE[$cookie_name]); 93 | if ($data && !empty($data['domain']) && 94 | self::isAllowedDomain($this->getHttpHost(), $data['domain'])) { 95 | // good case 96 | $this->sharedSessionID = $data['id']; 97 | return; 98 | } 99 | // ignoring potentially unreachable data 100 | } 101 | // evil/corrupt/missing case 102 | $base_domain = $this->getBaseDomain(); 103 | $this->sharedSessionID = md5(uniqid(mt_rand(), true)); 104 | $cookie_value = $this->makeSignedRequest( 105 | array( 106 | 'domain' => $base_domain, 107 | 'id' => $this->sharedSessionID, 108 | ) 109 | ); 110 | $_COOKIE[$cookie_name] = $cookie_value; 111 | if (!headers_sent()) { 112 | $expire = time() + self::FBSS_COOKIE_EXPIRE; 113 | setcookie($cookie_name, $cookie_value, $expire, '/', '.'.$base_domain); 114 | } else { 115 | // @codeCoverageIgnoreStart 116 | self::errorLog( 117 | 'Shared session ID cookie could not be set! You must ensure you '. 118 | 'create the Facebook instance before headers have been sent. This '. 119 | 'will cause authentication issues after the first request.' 120 | ); 121 | // @codeCoverageIgnoreEnd 122 | } 123 | } 124 | 125 | /** 126 | * Provides the implementations of the inherited abstract 127 | * methods. The implementation uses PHP sessions to maintain 128 | * a store for authorization codes, user ids, CSRF states, and 129 | * access tokens. 130 | */ 131 | 132 | /** 133 | * {@inheritdoc} 134 | * 135 | * @see BaseFacebook::setPersistentData() 136 | */ 137 | protected function setPersistentData($key, $value) { 138 | if (!in_array($key, self::$kSupportedKeys)) { 139 | self::errorLog('Unsupported key passed to setPersistentData.'); 140 | return; 141 | } 142 | 143 | $session_var_name = $this->constructSessionVariableName($key); 144 | $_SESSION[$session_var_name] = $value; 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | * 150 | * @see BaseFacebook::getPersistentData() 151 | */ 152 | protected function getPersistentData($key, $default = false) { 153 | if (!in_array($key, self::$kSupportedKeys)) { 154 | self::errorLog('Unsupported key passed to getPersistentData.'); 155 | return $default; 156 | } 157 | 158 | $session_var_name = $this->constructSessionVariableName($key); 159 | return isset($_SESSION[$session_var_name]) ? 160 | $_SESSION[$session_var_name] : $default; 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | * 166 | * @see BaseFacebook::clearPersistentData() 167 | */ 168 | protected function clearPersistentData($key) { 169 | if (!in_array($key, self::$kSupportedKeys)) { 170 | self::errorLog('Unsupported key passed to clearPersistentData.'); 171 | return; 172 | } 173 | 174 | $session_var_name = $this->constructSessionVariableName($key); 175 | if (isset($_SESSION[$session_var_name])) { 176 | unset($_SESSION[$session_var_name]); 177 | } 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | * 183 | * @see BaseFacebook::clearAllPersistentData() 184 | */ 185 | protected function clearAllPersistentData() { 186 | foreach (self::$kSupportedKeys as $key) { 187 | $this->clearPersistentData($key); 188 | } 189 | if ($this->sharedSessionID) { 190 | $this->deleteSharedSessionCookie(); 191 | } 192 | } 193 | 194 | /** 195 | * Deletes Shared session cookie 196 | */ 197 | protected function deleteSharedSessionCookie() { 198 | $cookie_name = $this->getSharedSessionCookieName(); 199 | unset($_COOKIE[$cookie_name]); 200 | $base_domain = $this->getBaseDomain(); 201 | setcookie($cookie_name, '', 1, '/', '.'.$base_domain); 202 | } 203 | 204 | /** 205 | * Returns the Shared session cookie name 206 | * 207 | * @return string The Shared session cookie name 208 | */ 209 | protected function getSharedSessionCookieName() { 210 | return self::FBSS_COOKIE_NAME . '_' . $this->getAppId(); 211 | } 212 | 213 | /** 214 | * Constructs and returns the name of the session key. 215 | * 216 | * @see setPersistentData() 217 | * @param string $key The key for which the session variable name to construct. 218 | * 219 | * @return string The name of the session key. 220 | */ 221 | protected function constructSessionVariableName($key) { 222 | $parts = array('fb', $this->getAppId(), $key); 223 | if ($this->sharedSessionID) { 224 | array_unshift($parts, $this->sharedSessionID); 225 | } 226 | return implode('_', $parts); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /support/sdk_twilio.php: -------------------------------------------------------------------------------- 1 | sid = false; 12 | $this->token = false; 13 | $this->apibase = false; 14 | } 15 | 16 | public function SetAccessInfo($sid, $token, $apibase = "https://api.twilio.com/2010-04-01") 17 | { 18 | $this->sid = $sid; 19 | $this->token = $token; 20 | $this->apibase = $apibase; 21 | } 22 | 23 | // Drop-in replacement for hash_hmac() on hosts where Hash is not available. 24 | // Only supports HMAC-MD5 and HMAC-SHA1. 25 | public static function hash_hmac_internal($algo, $data, $key, $raw_output = false) 26 | { 27 | if (function_exists("hash_hmac")) return hash_hmac($algo, $data, $key, $raw_output); 28 | 29 | $algo = strtolower($algo); 30 | $size = 64; 31 | $opad = str_repeat("\x5C", $size); 32 | $ipad = str_repeat("\x36", $size); 33 | 34 | if (strlen($key) > $size) $key = $algo($key, true); 35 | $key = str_pad($key, $size, "\x00"); 36 | 37 | $y = strlen($key) - 1; 38 | for ($x = 0; $x < $y; $x++) 39 | { 40 | $opad[$x] = $opad[$x] ^ $key[$x]; 41 | $ipad[$x] = $ipad[$x] ^ $key[$x]; 42 | } 43 | 44 | $result = $algo($opad . $algo($ipad . $data, true), $raw_output); 45 | 46 | return $result; 47 | } 48 | 49 | public function ValidateWebhookRequest($checksig = true) 50 | { 51 | if ($this->sid === false || $this->token === false) 52 | { 53 | http_response_code(403); 54 | 55 | echo "Account SID or Token not set."; 56 | 57 | exit(); 58 | } 59 | 60 | if (!class_exists("Str", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/str_basics.php"; 61 | if (!class_exists("Request", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/request.php"; 62 | 63 | $valid = ((isset($_POST["AccountSid"]) && Str::CTstrcmp($this->sid, $_POST["AccountSid"]) === 0) || (isset($_GET["AccountSid"]) && Str::CTstrcmp($this->sid, $_GET["AccountSid"]) === 0)); 64 | 65 | if (!$valid) 66 | { 67 | http_response_code(403); 68 | 69 | echo "Missing or invalid account SID."; 70 | 71 | exit(); 72 | } 73 | 74 | if (!$checksig) return; 75 | 76 | $url = Request::GetFullURLBase(); 77 | if (isset($_SERVER["QUERY_STRING"]) && $_SERVER["QUERY_STRING"] !== "") $url .= "?" . $_SERVER["QUERY_STRING"]; 78 | 79 | if (isset($_SERVER["REQUEST_METHOD"]) && $_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST)) 80 | { 81 | ksort($_POST); 82 | 83 | foreach ($_POST as $key => $val) 84 | { 85 | $url .= $key . $val; 86 | } 87 | } 88 | 89 | if (!isset($_SERVER["HTTP_X_TWILIO_SIGNATURE"]) || Str::CTstrcmp(base64_encode(self::hash_hmac_internal("sha1", $url, $this->token, true)), $_SERVER["HTTP_X_TWILIO_SIGNATURE"]) !== 0) 90 | { 91 | http_response_code(403); 92 | 93 | echo "Missing or invalid signature."; 94 | 95 | exit(); 96 | } 97 | } 98 | 99 | public static function StartXMLResponse() 100 | { 101 | header("Content-Type: text/xml"); 102 | 103 | echo "<" . "?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n"; 104 | self::OpenXMLTag("Response"); 105 | } 106 | 107 | public static function OpenXMLTag($tagname, $attrs = array(), $void = false) 108 | { 109 | echo "<" . $tagname; 110 | 111 | foreach ($attrs as $key => $val) 112 | { 113 | echo " " . htmlspecialchars($key) . "=\"" . htmlspecialchars($val, ENT_QUOTES | ENT_XML1, "UTF-8") . "\""; 114 | } 115 | 116 | if ($void) echo " /"; 117 | 118 | echo ">"; 119 | } 120 | 121 | public static function AppendXMLData($str) 122 | { 123 | if ($str !== "") echo htmlspecialchars($str, ENT_QUOTES | ENT_XML1, "UTF-8"); 124 | } 125 | 126 | public static function CloseXMLTag($tagname) 127 | { 128 | echo "\n"; 129 | } 130 | 131 | public static function OutputXMLTag($tagname, $attrs = array(), $str = "") 132 | { 133 | if ($str === "") self::OpenXMLTag($tagname, $attrs, true); 134 | else 135 | { 136 | self::OpenXMLTag($tagname, $attrs); 137 | self::AppendXMLData($str); 138 | self::CloseXMLTag($tagname); 139 | } 140 | } 141 | 142 | public static function EndXMLResponse() 143 | { 144 | self::CloseXMLTag("Response"); 145 | } 146 | 147 | public function Internal_DownloadRecordingCallback($response, $data, $opts) 148 | { 149 | if ($response["code"] == 200) 150 | { 151 | fwrite($opts, $data); 152 | } 153 | 154 | return true; 155 | } 156 | 157 | public function DownloadRecording($sid, $format = ".wav", $filename = false) 158 | { 159 | $options = array(); 160 | 161 | if ($filename !== false) 162 | { 163 | $fp = @fopen($filename, "wb"); 164 | if ($fp === false) return array("success" => false, "error" => self::Twilio_Translate("Unable to create file for storing the recording."), "errorcode" => "fopen_failed", "info" => $filename); 165 | 166 | $options["read_body_callback"] = array($this, "Internal_DownloadRecordingCallback"); 167 | $options["read_body_callback_opts"] = $fp; 168 | } 169 | 170 | return $this->RunAPI("GET", "Recordings/" . $sid, array(), $options, 200, false, ($format !== ".wav" ? $format : "")); 171 | } 172 | 173 | public function RunAPI($method, $apipath, $postvars = array(), $options = array(), $expected = 200, $decodebody = true, $extension = ".json") 174 | { 175 | if ($this->sid === false || $this->token === false) return array("success" => false, "error" => self::Twilio_Translate("Account SID or Token not set."), "errorcode" => "missing_account_sid_or_token"); 176 | if ($this->apibase === false) return array("success" => false, "error" => self::Twilio_Translate("API base not set."), "errorcode" => "missing_apibase"); 177 | 178 | if (!class_exists("WebBrowser", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/web_browser.php"; 179 | 180 | $url = $this->apibase . "/Accounts/" . $this->sid . "/" . $apipath . $extension; 181 | 182 | $options2 = array( 183 | "method" => $method, 184 | "headers" => array( 185 | "Authorization" => "Basic " . base64_encode($this->sid . ":" . $this->token) 186 | ) 187 | ); 188 | 189 | if ($method === "POST") 190 | { 191 | $options2["postvars"] = $postvars; 192 | 193 | foreach ($options as $key => $val) 194 | { 195 | if (isset($options2[$key]) && is_array($options2[$key])) $options2[$key] = array_merge($options2[$key], $val); 196 | else $options2[$key] = $val; 197 | } 198 | } 199 | else 200 | { 201 | $options2 = array_merge($options2, $options); 202 | } 203 | 204 | $web = new WebBrowser(); 205 | 206 | $result = $web->Process($url, $options2); 207 | 208 | if (!$result["success"]) return $result; 209 | 210 | if ($result["response"]["code"] != $expected) return array("success" => false, "error" => self::Twilio_Translate("Expected a %d response from the Twilio API. Received '%s'.", $expected, $result["response"]["line"]), "errorcode" => "unexpected_twilio_api_response", "info" => $result); 211 | 212 | if ($decodebody) 213 | { 214 | $data = json_decode($result["body"], true); 215 | if (!is_array($data)) return array("success" => false, "error" => self::Twilio_Translate("Unable to decode the server response as JSON."), "errorcode" => "expected_json", "info" => $result); 216 | 217 | $result = array( 218 | "success" => true, 219 | "data" => $data 220 | ); 221 | } 222 | 223 | return $result; 224 | } 225 | 226 | protected static function Twilio_Translate() 227 | { 228 | $args = func_get_args(); 229 | if (!count($args)) return ""; 230 | 231 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 232 | } 233 | } 234 | ?> -------------------------------------------------------------------------------- /examples/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | A simple layout for the SSO server. 3 | (C) 2014 Cubiclesoft. All Rights Reserved. 4 | The message icons are from the FamFamFam Silk set (http://www.famfamfam.com/lab/icons/silk/) 5 | The social media selector icons are used in good faith. 6 | SSO Generic Login icon is (C) CubicleSoft. 7 | */ 8 | 9 | body { 10 | font-family: Verdana, Arial, Helvetica, sans-serif; 11 | } 12 | 13 | div.sso_server_message_wrap { 14 | max-width: 600px; 15 | overflow: hidden; 16 | padding: 0.5em 0 0.8em 0.7em; 17 | border-bottom: 1px dashed #CCCCCC; 18 | margin: 0 auto 0.8em; 19 | } 20 | 21 | div.sso_server_message_wrap_nosplit { 22 | border-bottom: none; 23 | margin: 0 auto; 24 | } 25 | 26 | div.sso_server_message_wrap div.sso_server_error { 27 | background: url('error.png') 0 0.1em no-repeat; 28 | padding-left: 25px; 29 | } 30 | 31 | div.sso_server_message_wrap div.sso_server_warning { 32 | background: url('warn.png') 0 0.1em no-repeat; 33 | padding-left: 25px; 34 | } 35 | 36 | div.sso_selector_wrap { 37 | max-width: 600px; 38 | margin: 0 auto; 39 | overflow: hidden; 40 | } 41 | 42 | div.sso_selector_wrap div.sso_selector_wrap_inner { 43 | margin: 3px; 44 | } 45 | 46 | div.sso_selector_wrap div.sso_selector_header { 47 | font-size: 1.3em; 48 | font-weight: bold; 49 | color: #333333; 50 | } 51 | 52 | div.sso_selector_wrap div.sso_selectors { 53 | margin-top: 0.7em; 54 | } 55 | 56 | div.sso_selector_wrap div.sso_selector { 57 | margin: 0.8em 1px 1px 1px; 58 | } 59 | 60 | div.sso_selector_wrap div.sso_selector a, div.sso_selector_wrap div.sso_selector a:visited, div.sso_selector_wrap div.sso_selector a:link { 61 | display: block; 62 | border: 0 none; 63 | border-radius: 3px; 64 | padding: 21px 6px 21px 66px; 65 | color: #333333; 66 | text-decoration: none; 67 | background-repeat: no-repeat; 68 | background-position: 6px 6px; 69 | } 70 | 71 | div.sso_selector_wrap div.sso_selector a:hover { 72 | border: 1px solid #CCCCCC; 73 | padding: 20px 5px 20px 65px; 74 | background-color: #F8F8F8; 75 | background-position: 5px 5px; 76 | } 77 | 78 | div.sso_selector_wrap div.sso_selector a:active { 79 | border: 1px solid #C1C1C1; 80 | padding: 21px 4px 19px 66px; 81 | background-color: #F1F1F1; 82 | background-position: 6px 6px; 83 | } 84 | 85 | div.sso_selector_wrap div.sso_selector a.sso_login { 86 | background-image: url('sso_login.png'); 87 | } 88 | 89 | div.sso_selector_wrap div.sso_selector a.sso_facebook { 90 | background-image: url('sso_facebook.png'); 91 | } 92 | 93 | div.sso_selector_wrap div.sso_selector a.sso_google { 94 | background-image: url('sso_google.png'); 95 | } 96 | 97 | div.sso_selector_wrap div.sso_selector a.sso_linkedin { 98 | background-image: url('sso_linkedin.png'); 99 | } 100 | 101 | div.sso_selector_wrap div.sso_selector a.sso_ldap { 102 | background-image: url('sso_ldap.png'); 103 | } 104 | 105 | div.sso_selector_wrap div.sso_selector a.sso_remote { 106 | background-image: url('sso_remote.png'); 107 | } 108 | 109 | div.sso_main_wrap { 110 | max-width: 600px; 111 | margin: 0 auto; 112 | overflow: hidden; 113 | } 114 | 115 | div.sso_main_wrap div.sso_main_wrap_inner { 116 | margin: 3px; 117 | } 118 | 119 | div.sso_main_wrap div.sso_main_messages_wrap { 120 | padding-bottom: 0.8em; 121 | border-bottom: 1px dashed #CCCCCC; 122 | margin-bottom: 0.8em; 123 | } 124 | 125 | div.sso_main_wrap div.sso_main_messages_wrap div.sso_main_messages_header { 126 | font-weight: bold; 127 | } 128 | 129 | div.sso_main_wrap div.sso_main_messages_wrap div.sso_main_messages { 130 | margin: 0.5em 0 0 0.7em; 131 | } 132 | 133 | div.sso_main_wrap div.sso_main_messages_wrap div.sso_main_messages div.sso_main_messageerror { 134 | background: url('error.png') 0 0.1em no-repeat; 135 | padding-left: 25px; 136 | } 137 | 138 | div.sso_main_wrap div.sso_main_messages_wrap div.sso_main_messages div.sso_main_messagewarning { 139 | background: url('warn.png') 0 0.1em no-repeat; 140 | padding-left: 25px; 141 | } 142 | 143 | div.sso_main_wrap div.sso_main_messages_wrap div.sso_main_messages div.sso_main_messageokay { 144 | background: url('ok.png') 0 0.1em no-repeat; 145 | padding-left: 25px; 146 | } 147 | 148 | div.sso_main_wrap div.sso_main_info { 149 | } 150 | 151 | div.sso_main_wrap div.sso_main_form_wrap { 152 | margin-top: 0.7em; 153 | } 154 | 155 | div.sso_main_wrap div.sso_main_form_header { 156 | font-size: 1.3em; 157 | font-weight: bold; 158 | color: #333333; 159 | } 160 | 161 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem { 162 | margin-top: 0.8em; 163 | } 164 | 165 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formtitle { 166 | color: #222222; 167 | margin-bottom: 0.2em; 168 | font-weight: bold; 169 | } 170 | 171 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formdata input.sso_main_text { 172 | width: 95%; 173 | font-size: 0.9em; 174 | padding: 0.3em; 175 | border: 1px solid #BBBBBB; 176 | } 177 | 178 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formdata input.sso_main_text:focus { 179 | border: 1px solid #888888; 180 | } 181 | 182 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formdata input.sso_main_text:hover { 183 | border: 1px solid #888888; 184 | } 185 | 186 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formdata select.sso_main_dropdown { 187 | width: 97%; 188 | font-size: 0.9em; 189 | border: 1px solid #BBBBBB; 190 | } 191 | 192 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formdata select.sso_main_dropdown:focus { 193 | border: 1px solid #888888; 194 | } 195 | 196 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formdata select.sso_main_dropdown:hover { 197 | border: 1px solid #888888; 198 | } 199 | 200 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formdata div.sso_main_static { 201 | margin-left: 0.5em; 202 | font-size: 0.9em; 203 | } 204 | 205 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formdesc { 206 | color: #333333; 207 | margin-bottom: 0.2em; 208 | margin-left: 0.5em; 209 | font-size: 0.9em; 210 | } 211 | 212 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formshowhide { 213 | margin-top: 0.2em; 214 | margin-left: 0.5em; 215 | font-size: 0.8em; 216 | } 217 | 218 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formtwofactorreset { 219 | margin-top: 0.2em; 220 | margin-left: 0.5em; 221 | font-size: 0.8em; 222 | } 223 | 224 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formresult { 225 | margin-left: 0.5em; 226 | } 227 | 228 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formresult div.sso_main_formchecking { 229 | background: url('wait.png') 0 0.1em no-repeat; 230 | padding-left: 25px; 231 | } 232 | 233 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formresult div.sso_main_formerror { 234 | background: url('error.png') 0 0.1em no-repeat; 235 | padding-left: 25px; 236 | } 237 | 238 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formresult div.sso_main_formwarning { 239 | background: url('warn.png') 0 0.1em no-repeat; 240 | padding-left: 25px; 241 | } 242 | 243 | div.sso_main_wrap form.sso_main_form div.sso_main_formitem div.sso_main_formresult div.sso_main_formokay { 244 | background: url('ok.png') 0 0.1em no-repeat; 245 | padding-left: 25px; 246 | } 247 | 248 | div.sso_main_wrap form.sso_main_form div.sso_main_formsubmit { 249 | margin-top: 1.2em; 250 | } 251 | 252 | div.sso_main_wrap form.sso_main_form div.sso_main_formsubmit input { 253 | padding: 0.2em 0.5em; 254 | font-weight: bold; 255 | font-size: 1.0em; 256 | color: #1F1F1F; 257 | } 258 | 259 | div.sso_main_wrap div.sso_login_recover_changeinfo { 260 | margin-top: 1.5em; 261 | font-size: 0.8em; 262 | } 263 | 264 | div.sso_has_js { display: block; } 265 | div.sso_no_js { display: none; } 266 | -------------------------------------------------------------------------------- /support/ipaddr.php: -------------------------------------------------------------------------------- 1 | $segment) 40 | { 41 | $segment = trim($segment); 42 | if ($segment != "") $ipaddr2[] = $segment; 43 | else if ($foundpos === false && count($ipaddr) > $num + 1 && $ipaddr[$num + 1] != "") 44 | { 45 | $foundpos = count($ipaddr2); 46 | $ipaddr2[] = "0000"; 47 | } 48 | } 49 | // Convert ::ffff:123.123.123.123 format. 50 | if (strpos($ipaddr2[count($ipaddr2) - 1], ".") !== false) 51 | { 52 | $x = count($ipaddr2) - 1; 53 | if ($ipaddr2[count($ipaddr2) - 2] != "ffff") $ipaddr2[$x] = "0"; 54 | else 55 | { 56 | $ipaddr = explode(".", $ipaddr2[$x]); 57 | if (count($ipaddr) != 4) $ipaddr2[$x] = "0"; 58 | else 59 | { 60 | $ipaddr2[$x] = str_pad(strtolower(dechex($ipaddr[0])), 2, "0", STR_PAD_LEFT) . str_pad(strtolower(dechex($ipaddr[1])), 2, "0", STR_PAD_LEFT); 61 | $ipaddr2[] = str_pad(strtolower(dechex($ipaddr[2])), 2, "0", STR_PAD_LEFT) . str_pad(strtolower(dechex($ipaddr[3])), 2, "0", STR_PAD_LEFT); 62 | } 63 | } 64 | } 65 | $ipaddr = array_slice($ipaddr2, 0, 8); 66 | if ($foundpos !== false && count($ipaddr) < 8) array_splice($ipaddr, $foundpos, 0, array_fill(0, 8 - count($ipaddr), "0000")); 67 | foreach ($ipaddr as $num => $segment) 68 | { 69 | $ipaddr[$num] = substr(str_pad(strtolower(dechex(hexdec($segment))), 4, "0", STR_PAD_LEFT), -4); 70 | } 71 | $ipv6addr = implode(":", $ipaddr); 72 | 73 | // Extract IPv4 address. 74 | if (substr($ipv6addr, 0, 30) == "0000:0000:0000:0000:0000:ffff:") $ipv4addr = hexdec(substr($ipv6addr, 30, 2)) . "." . hexdec(substr($ipv6addr, 32, 2)) . "." . hexdec(substr($ipv6addr, 35, 2)) . "." . hexdec(substr($ipv6addr, 37, 2)); 75 | 76 | // Make a short IPv6 address. 77 | $shortipv6 = $ipv6addr; 78 | $pattern = "0000:0000:0000:0000:0000:0000:0000"; 79 | do 80 | { 81 | $shortipv6 = str_replace($pattern, ":", $shortipv6); 82 | $pattern = substr($pattern, 5); 83 | } while (strlen($shortipv6) == 39 && $pattern != ""); 84 | $shortipv6 = explode(":", $shortipv6); 85 | foreach ($shortipv6 as $num => $segment) 86 | { 87 | if ($segment != "") $shortipv6[$num] = strtolower(dechex(hexdec($segment))); 88 | } 89 | $shortipv6 = implode(":", $shortipv6); 90 | 91 | return array("ipv6" => $ipv6addr, "shortipv6" => $shortipv6, "ipv4" => $ipv4addr); 92 | } 93 | 94 | public static function GetRemoteIP($proxies = array()) 95 | { 96 | $ipaddr = self::NormalizeIP(isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : "127.0.0.1"); 97 | 98 | // Check for trusted proxies. Stop at first untrusted IP in the chain. 99 | if (isset($proxies[$ipaddr["ipv6"]]) || ($ipaddr["ipv4"] != "" && isset($proxies[$ipaddr["ipv4"]]))) 100 | { 101 | $xforward = (isset($_SERVER["HTTP_X_FORWARDED_FOR"]) ? explode(",", $_SERVER["HTTP_X_FORWARDED_FOR"]) : array()); 102 | $clientip = (isset($_SERVER["HTTP_CLIENT_IP"]) ? explode(",", $_SERVER["HTTP_CLIENT_IP"]) : array()); 103 | 104 | do 105 | { 106 | $found = false; 107 | 108 | if (isset($proxies[$ipaddr["ipv6"]])) $header = $proxies[$ipaddr["ipv6"]]; 109 | else $header = $proxies[$ipaddr["ipv4"]]; 110 | 111 | $header = strtolower($header); 112 | if ($header == "xforward" && count($xforward) > 0) 113 | { 114 | $ipaddr = self::NormalizeIP(array_pop($xforward)); 115 | $found = true; 116 | } 117 | else if ($header == "clientip" && count($clientip) > 0) 118 | { 119 | $ipaddr = self::NormalizeIP(array_pop($clientip)); 120 | $found = true; 121 | } 122 | } while ($found && (isset($proxies[$ipaddr["ipv6"]]) || ($ipaddr["ipv4"] != "" && isset($proxies[$ipaddr["ipv4"]])))); 123 | } 124 | 125 | return $ipaddr; 126 | } 127 | 128 | public static function IsMatch($pattern, $ipaddr) 129 | { 130 | if (is_string($ipaddr)) $ipaddr = self::NormalizeIP($ipaddr); 131 | 132 | if (strpos($pattern, ":") !== false) 133 | { 134 | // Pattern is IPv6. 135 | $pattern = explode(":", strtolower($pattern)); 136 | $ipaddr = explode(":", $ipaddr["ipv6"]); 137 | if (count($pattern) != 8 || count($ipaddr) != 8) return false; 138 | foreach ($pattern as $num => $segment) 139 | { 140 | $found = false; 141 | $pieces = explode(",", $segment); 142 | foreach ($pieces as $piece) 143 | { 144 | $piece = trim($piece); 145 | $piece = explode(".", $piece); 146 | if (count($piece) == 1) 147 | { 148 | $piece = $piece[0]; 149 | 150 | if ($piece == "*") $found = true; 151 | else if (strpos($piece, "-") !== false) 152 | { 153 | $range = explode("-", $piece); 154 | $range[0] = hexdec($range[0]); 155 | $range[1] = hexdec($range[1]); 156 | $val = hexdec($ipaddr[$num]); 157 | if ($range[0] > $range[1]) $range[0] = $range[1]; 158 | if ($val >= $range[0] && $val <= $range[1]) $found = true; 159 | } 160 | else if ($piece === $ipaddr[$num]) $found = true; 161 | } 162 | else if (count($piece) == 2) 163 | { 164 | // Special IPv4-like notation. 165 | $found2 = false; 166 | $found3 = false; 167 | $val = hexdec(substr($ipaddr[$num], 0, 2)); 168 | $val2 = hexdec(substr($ipaddr[$num], 2, 2)); 169 | 170 | if ($piece[0] == "*") $found2 = true; 171 | else if (strpos($piece[0], "-") !== false) 172 | { 173 | $range = explode("-", $piece[0]); 174 | if ($range[0] > $range[1]) $range[0] = $range[1]; 175 | if ($val >= $range[0] && $val <= $range[1]) $found2 = true; 176 | } 177 | else if ($piece[0] == $val) $found2 = true; 178 | 179 | if ($piece[1] == "*") $found3 = true; 180 | else if (strpos($piece[1], "-") !== false) 181 | { 182 | $range = explode("-", $piece[1]); 183 | if ($range[0] > $range[1]) $range[0] = $range[1]; 184 | if ($val >= $range[0] && $val <= $range[1]) $found3 = true; 185 | } 186 | else if ($piece[1] == $val2) $found3 = true; 187 | 188 | if ($found2 && $found3) $found = true; 189 | } 190 | 191 | if ($found) break; 192 | } 193 | 194 | if (!$found) return false; 195 | } 196 | } 197 | else 198 | { 199 | // Pattern is IPv4. 200 | $pattern = explode(".", strtolower($pattern)); 201 | $ipaddr = explode(".", $ipaddr["ipv4"]); 202 | if (count($pattern) != 4 || count($ipaddr) != 4) return false; 203 | foreach ($pattern as $num => $segment) 204 | { 205 | $found = false; 206 | $pieces = explode(",", $segment); 207 | foreach ($pieces as $piece) 208 | { 209 | $piece = trim($piece); 210 | 211 | if ($piece == "*") $found = true; 212 | else if (strpos($piece, "-") !== false) 213 | { 214 | $range = explode("-", $piece); 215 | if ($range[0] > $range[1]) $range[0] = $range[1]; 216 | if ($ipaddr[$num] >= $range[0] && $ipaddr[$num] <= $range[1]) $found = true; 217 | } 218 | else if ($piece == $ipaddr[$num]) $found = true; 219 | 220 | if ($found) break; 221 | } 222 | 223 | if (!$found) return false; 224 | } 225 | } 226 | 227 | return true; 228 | } 229 | } 230 | ?> -------------------------------------------------------------------------------- /support/csdb/db_oci_lite.php: -------------------------------------------------------------------------------- 1 | lastid = 0; 43 | 44 | parent::Connect($dsn, $username, $password, $options); 45 | 46 | // Convert DB NULL values into empty strings for use in code. 47 | $this->dbobj->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_TO_STRING); 48 | 49 | // Converts all uppercase table names into lowercase table names. 50 | $this->dbobj->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); 51 | 52 | // Set Oracle session variables to use standard date formats. 53 | $this->Query("SET", "NLS_DATE_FORMAT='YYYY-MM-DD'"); 54 | $this->Query("SET", "NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); 55 | 56 | // Set Unicode support. 57 | // TODO: Figure out unicode support for Oracle 58 | //$this->Query("SET", "NLS_LANGUAGE='UTF8'"); 59 | } 60 | 61 | public function GetVersion() 62 | { 63 | $tableExists = $this->TableExists("SSO_USER"); 64 | 65 | return $this->GetOne("SELECT",array( 66 | "banner", 67 | "FROM" => "v\$version", 68 | "WHERE" => "banner LIKE 'Oracle%'" 69 | )); 70 | } 71 | 72 | public function GetInsertID($name = null) 73 | { 74 | return $this->lastid; 75 | } 76 | 77 | public function TableExists($name) 78 | { 79 | return ($this->GetOne("SHOW TABLES", array("LIKE" => $name)) === false ? false : true); 80 | } 81 | 82 | public function QuoteIdentifier($str) 83 | { 84 | return preg_replace('/[^A-Za-z0-9_]/', "_", $str); 85 | } 86 | 87 | // This function is used to get the last inserted sequence value by table name. 88 | // 89 | // Uses automatically geneerated sequences as part of the Oracle 12c IDENTITY 90 | // column. This is not available in 11g and older Oracle databases. 91 | // See the ProcessColumnDefinition() function for more detail. 92 | private function GetOracleInsertID($tableName) 93 | { 94 | // Query the "all_tab_columns" for the oracle IDENTITY column and identify the sequence 95 | $seqName = $this->GetOne("SELECT", array( 96 | "data_default", 97 | "FROM" => "all_tab_columns", 98 | "WHERE" => "identity_column = 'YES' AND table_name = ?" 99 | ), strtoupper($tableName)); 100 | 101 | // The previous query returned "nextval" with the sequence name, 102 | // however we need the current sequence value 103 | $seqName = str_replace(".nextval", ".CURRVAL", $seqName); 104 | 105 | // This grabs the current value from the sequence identified above 106 | $retVal = $this->GetOne("SELECT", array( 107 | $seqName, 108 | "FROM" => "DUAL" 109 | )); 110 | 111 | // Return the current sequence value 112 | return $retVal; 113 | } 114 | 115 | protected function GenerateSQL(&$master, &$sql, &$opts, $cmd, $queryinfo, $args, $subquery) 116 | { 117 | $mystr = "test"; 118 | switch ($cmd) 119 | { 120 | case "SELECT": 121 | { 122 | $supported = array( 123 | "PRECOLUMN" => array("DISTINCT" => "bool", "SUBQUERIES" => true), 124 | "FROM" => array("SUBQUERIES" => true), 125 | "WHERE" => array("SUBQUERIES" => true), 126 | "GROUP BY" => true, 127 | "HAVING" => true, 128 | "ORDER BY" => true, 129 | // Haven't figured out the LIMIT problem yet 130 | // TODO: Figure out how to use Oracle's ROWNUM where clause functionalitty 131 | // instead of the LIMIT function 132 | //"LIMIT" => " OFFSET " 133 | ); 134 | 135 | // Oracle does not support aliasing table names in the FROM clause. 136 | // However, alias' are supported in COLUMN names. 137 | // AS is used in the Oracle FROM clause to process nested queries, 138 | // but does not support alias'. 139 | $queryinfo["FROM"] = str_replace("? AS ", "? ", $queryinfo["FROM"]); 140 | 141 | return $this->ProcessSELECT($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 142 | } 143 | case "INSERT": 144 | { 145 | $supported = array( 146 | "PREINTO" => array(), 147 | "POSTVALUES" => array("RETURNING" => "key_identifier"), 148 | "SELECT" => true, 149 | "BULKINSERT" => false 150 | ); 151 | 152 | $result = $this->ProcessINSERT($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 153 | if ($result["success"] && isset($queryinfo["AUTO INCREMENT"])) $result["filter_opts"] = array("mode" => "INSERT", "queryinfo" => $queryinfo); 154 | 155 | // Handle bulk insert by rewriting the queries because, well, Oracle. 156 | // http://stackoverflow.com/questions/39576/best-way-to-do-multi-row-insert-in-oracle 157 | if ($result["success"] && is_array($sql)) 158 | { 159 | $sql2 = "INSERT ALL"; 160 | foreach ($sql as $entry) $sql2 .= substr($entry, 6); 161 | $sql2 .= " SELECT 1 FROM DUAL"; 162 | $sql = $sql2; 163 | } 164 | 165 | return $result; 166 | } 167 | case "UPDATE": 168 | { 169 | // No ORDER BY or LIMIT support. 170 | $supported = array( 171 | "PRETABLE" => array("ONLY" => "bool"), 172 | "WHERE" => array("SUBQUERIES" => true) 173 | ); 174 | 175 | return $this->ProcessUPDATE($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 176 | } 177 | case "DELETE": 178 | { 179 | // No ORDER BY or LIMIT support. 180 | $supported = array( 181 | "PREFROM" => array("ONLY" => "bool"), 182 | "WHERE" => array("SUBQUERIES" => true) 183 | ); 184 | 185 | return $this->ProcessDELETE($master, $sql, $opts, $queryinfo, $args, $subquery, $supported); 186 | } 187 | case "SET": 188 | { 189 | $sql = "ALTER SESSION SET " . $queryinfo; 190 | 191 | return array("success" => true); 192 | } 193 | 194 | case "USE": 195 | { 196 | // Fake multiple databases with Oracle schemas. 197 | // SCHEMA is already selected with user 198 | // $sql = "SELECT 1 FROM DUAL"; 199 | 200 | return array("success" => false, "errorcode" => "skip_sql_query"); 201 | } 202 | case "TRUNCATE TABLE": 203 | { 204 | $master = true; 205 | 206 | $sql = "TRUNCATE TABLE " . $this->QuoteIdentifier($queryinfo[0]); 207 | 208 | return array("success" => true); 209 | } 210 | } 211 | 212 | return array("success" => false, "error" => CSDB::DB_Translate("Unknown query command '%s'.", $cmd), "errorcode" => "unknown_query_command"); 213 | } 214 | 215 | protected function RunStatementFilter(&$stmt, &$filteropts) 216 | { 217 | if ($filteropts["mode"] == "INSERT") 218 | { 219 | // Force the last ID value to be extracted for INSERT queries. 220 | // Unable to find a way to get Oracle to return a row without 221 | // Using PL/SQL functions. 222 | $result = new CSDB_PDO_Statement($this, $stmt, $filteropts); 223 | $row = $result->NextRow(); 224 | 225 | $stmt = false; 226 | } 227 | 228 | parent::RunStatementFilter($stmt, $filteropts); 229 | } 230 | 231 | public function RunRowFilter(&$row, &$filteropts, &$fetchnext) 232 | { 233 | switch ($filteropts["mode"]) 234 | { 235 | case "INSERT": 236 | { 237 | // Use the private function provided above to get the Last Inserted ID 238 | $this->lastid = $this->GetOracleInsertID($filteropts["queryinfo"][0]); 239 | 240 | break; 241 | } 242 | } 243 | 244 | if (!$fetchnext) parent::RunRowFilter($row, $filteropts, $fetchnext); 245 | } 246 | } 247 | ?> -------------------------------------------------------------------------------- /providers/sso_login/modules/sso_google_authenticator.php: -------------------------------------------------------------------------------- 1 | "Google Authenticator", 9 | "desc" => "Adds two-factor authentication that is compatible with Google Authenticator (IETF RFC 6238)." 10 | ); 11 | 12 | class sso_login_module_sso_google_authenticator extends sso_login_ModuleBase 13 | { 14 | public function DefaultOrder() 15 | { 16 | return 30; 17 | } 18 | 19 | private function GetInfo() 20 | { 21 | global $sso_settings; 22 | 23 | $info = $sso_settings["sso_login"]["modules"]["sso_google_authenticator"]; 24 | if (!isset($info["generate_qr_codes"])) $info["generate_qr_codes"] = true; 25 | if (!isset($info["clock_drift"])) $info["clock_drift"] = 5; 26 | 27 | return $info; 28 | } 29 | 30 | public function ConfigSave() 31 | { 32 | global $sso_settings; 33 | 34 | $info = $this->GetInfo(); 35 | $info["generate_qr_codes"] = ($_REQUEST["sso_google_authenticator_generate_qr_codes"] > 0); 36 | $info["clock_drift"] = (int)$_REQUEST["sso_google_authenticator_clock_drift"]; 37 | 38 | if ($info["clock_drift"] < 0 || $info["clock_drift"] > 30) BB_SetPageMessage("error", "The Google Authenticator 'Clock Drift' field contains an invalid value."); 39 | 40 | $sso_settings["sso_login"]["modules"]["sso_google_authenticator"] = $info; 41 | } 42 | 43 | public function Config(&$contentopts) 44 | { 45 | $info = $this->GetInfo(); 46 | $contentopts["fields"][] = array( 47 | "title" => "Generate QR Codes", 48 | "type" => "select", 49 | "name" => "sso_google_authenticator_generate_qr_codes", 50 | "options" => array(1 => "Yes", 0 => "No"), 51 | "select" => BB_GetValue("sso_google_authenticator_generate_qr_codes", (string)(int)$info["generate_qr_codes"]), 52 | "desc" => "Displays a Google Authenticator compatible QR code to the user during sign up and account recovery." 53 | ); 54 | $contentopts["fields"][] = array( 55 | "title" => "Clock Drift", 56 | "type" => "text", 57 | "name" => "sso_google_authenticator_clock_drift", 58 | "value" => BB_GetValue("sso_google_authenticator_clock_drift", (string)(int)$info["clock_drift"]), 59 | "desc" => "The amount of clock drift, in seconds, to allow for each authentication code. Range is 0 to 30. Default is 5." 60 | ); 61 | } 62 | 63 | public function TwoFactorCheck(&$result, $userinfo) 64 | { 65 | if ($userinfo !== false && $userinfo["two_factor_method"] == "sso_google_authenticator") 66 | { 67 | $info = $this->GetInfo(); 68 | $code = SSO_FrontendFieldValue("two_factor_code", ""); 69 | $twofactor = sso_login::GetTimeBasedOTP($userinfo["two_factor_key"], time() / 30); 70 | $twofactor2 = sso_login::GetTimeBasedOTP($userinfo["two_factor_key"], (time() - $info["clock_drift"]) / 30); 71 | $twofactor3 = sso_login::GetTimeBasedOTP($userinfo["two_factor_key"], (time() + $info["clock_drift"]) / 30); 72 | if ($code !== $twofactor && $code !== $twofactor2 && $code !== $twofactor3) $result["errors"][] = BB_Translate("Invalid two-factor authentication code."); 73 | } 74 | } 75 | 76 | private function SignupUpdateCheck(&$result, $update, $userrow) 77 | { 78 | global $sso_target_url, $sso_session_info; 79 | 80 | // Generate the QR code. 81 | $info = $this->GetInfo(); 82 | if ($info["generate_qr_codes"]) 83 | { 84 | if (isset($_REQUEST["sso_google_authenticator_qr_u"]) && isset($_REQUEST["sso_google_authenticator_qr_h"]) && isset($_REQUEST["sso_google_authenticator_qr_k"])) 85 | { 86 | require_once SSO_ROOT_PATH . "/" . SSO_SUPPORT_PATH . "/phpqrcode.php"; 87 | 88 | $url = "otpauth://totp/" . urlencode($_REQUEST["sso_google_authenticator_qr_u"]) . "@" . urlencode($_REQUEST["sso_google_authenticator_qr_h"]) . "?secret=" . $_REQUEST["sso_google_authenticator_qr_k"]; 89 | 90 | QRcode::png($url, false, QR_ECLEVEL_Q, 4); 91 | } 92 | else 93 | { 94 | if ($update) $username = SSO_FrontendFieldValue("update_username", ($userrow !== false ? $userrow->username : "")); 95 | else $username = SSO_FrontendFieldValue("username", ""); 96 | if ($username == "") 97 | { 98 | if ($update) $email = SSO_FrontendFieldValue("update_email", ($userrow !== false ? $userrow->email : "")); 99 | else $email = SSO_FrontendFieldValue("email", ""); 100 | if ($email != "") 101 | { 102 | $pos = strpos($email, "@"); 103 | if ($pos !== false) $username = substr($email, 0, $pos); 104 | } 105 | } 106 | 107 | require_once SSO_ROOT_PATH . "/" . SSO_SUPPORT_PATH . "/http.php"; 108 | 109 | $host = BB_GetRequestHost(); 110 | $result2 = HTTP::ExtractURL($host); 111 | $host = $result2["host"]; 112 | 113 | $key = (isset($sso_session_info["sso_login_two_factor_key"]) ? $sso_session_info["sso_login_two_factor_key"] : ""); 114 | 115 | if ($username != "" && $host != "" && $key != "") 116 | { 117 | $url = $sso_target_url . "&sso_login_action=" . ($update ? "update_info&sso_v=" . urlencode($_REQUEST["sso_v"]) : "signup_check") . "&sso_ajax=1&sso_google_authenticator_qr_u=" . urlencode($username) . "&sso_google_authenticator_qr_h=" . urlencode($host) . "&sso_google_authenticator_qr_k=" . urlencode($key); 118 | 119 | ?> 120 | 123 | 128 | 131 | SignupUpdateCheck($result, false, false); 140 | } 141 | 142 | private function DisplaySignup($userrow, $userinfo, $admin) 143 | { 144 | global $sso_target_url, $sso_session_info; 145 | 146 | $info = $this->GetInfo(); 147 | if ($admin) 148 | { 149 | $result = array( 150 | array( 151 | "title" => "Google Authenticator Key", 152 | "type" => "static", 153 | "value" => $_REQUEST["two_factor_key"], 154 | "desc" => "The manual key to use with Google Authenticator and compatible applications." 155 | ) 156 | ); 157 | 158 | return $result; 159 | } 160 | else 161 | { 162 | ?> 163 |
164 |
165 |
166 |
167 |
168 | 172 |
173 | SignupUpdateCheck($result, ($userinfo !== false), $userrow); 176 | } 177 | ?> 178 |
179 | 191 | DisplaySignup(false, false, $admin); 198 | } 199 | 200 | public function GetTwoFactorName() 201 | { 202 | return BB_Translate("Google Authenticator"); 203 | } 204 | 205 | public function UpdateInfoCheck(&$result, $userinfo, $ajax) 206 | { 207 | if ($ajax && $userinfo !== false) $this->SignupUpdateCheck($result, true, false); 208 | } 209 | 210 | public function GenerateUpdateInfo($userrow, $userinfo) 211 | { 212 | $this->DisplaySignup($userrow, $userinfo, false); 213 | } 214 | } 215 | ?> -------------------------------------------------------------------------------- /support/jquery.tablednd-20140418.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t,n,r){var i="ontouchstart"in n.documentElement,s=i?"touchstart":"mousedown",o=i?"touchmove":"mousemove",u=i?"touchend":"mouseup";i&&e.each("touchstart touchmove touchend".split(" "),function(t,n){e.event.fixHooks[n]=e.event.mouseHooks});e(n).ready(function(){function t(e){var t={},n=e.match(/([^;:]+)/g)||[];while(n.length)t[n.shift()]=n.shift().trim();return t}e("table").each(function(){if(e(this).data("table")=="dnd"){e(this).tableDnD({onDragStyle:e(this).data("ondragstyle")&&t(e(this).data("ondragstyle"))||null,onDropStyle:e(this).data("ondropstyle")&&t(e(this).data("ondropstyle"))||null,onDragClass:e(this).data("ondragclass")==r&&"tDnD_whileDrag"||e(this).data("ondragclass"),onDrop:e(this).data("ondrop")&&new Function("table","row",e(this).data("ondrop")),onDragStart:e(this).data("ondragstart")&&new Function("table","row",e(this).data("ondragstart")),scrollAmount:e(this).data("scrollamount")||5,sensitivity:e(this).data("sensitivity")||10,hierarchyLevel:e(this).data("hierarchylevel")||0,indentArtifact:e(this).data("indentartifact")||'
 
',autoWidthAdjust:e(this).data("autowidthadjust")||true,autoCleanRelations:e(this).data("autocleanrelations")||true,jsonPretifySeparator:e(this).data("jsonpretifyseparator")||" ",serializeRegexp:e(this).data("serializeregexp")&&new RegExp(e(this).data("serializeregexp"))||/[^\-]*$/,serializeParamName:e(this).data("serializeparamname")||false,dragHandle:e(this).data("draghandle")||null})}})});t.jQuery.tableDnD={currentTable:null,dragObject:null,mouseOffset:null,oldX:0,oldY:0,build:function(t){this.each(function(){this.tableDnDConfig=e.extend({onDragStyle:null,onDropStyle:null,onDragClass:"tDnD_whileDrag",onDrop:null,onDragStart:null,scrollAmount:5,sensitivity:10,hierarchyLevel:0,indentArtifact:'
 
',autoWidthAdjust:true,autoCleanRelations:true,jsonPretifySeparator:" ",serializeRegexp:/[^\-]*$/,serializeParamName:false,dragHandle:null},t||{});e.tableDnD.makeDraggable(this);this.tableDnDConfig.hierarchyLevel&&e.tableDnD.makeIndented(this)});return this},makeIndented:function(t){var n=t.tableDnDConfig,r=t.rows,i=e(r).first().find("td:first")[0],s=0,o=0,u,a;if(e(t).hasClass("indtd"))return null;a=e(t).addClass("indtd").attr("style");e(t).css({whiteSpace:"nowrap"});for(var f=0;fe.vertical&&this.dragObject.parentNode.insertBefore(this.dragObject,t.nextSibling)||00&&e(n).find("td:first").children(":first").remove()&&e(n).data("level",--i);0>t.horizontal&&i=i&&e(n).children(":first").prepend(r.indentArtifact)&&e(n).data("level",++i)},mousemove:function(t){var n=e(e.tableDnD.dragObject),r=e.tableDnD.currentTable.tableDnDConfig,i,s,o,u,a;t&&t.preventDefault();if(!e.tableDnD.dragObject)return false;t.type=="touchmove"&&event.preventDefault();r.onDragClass&&n.addClass(r.onDragClass)||n.css(r.onDragStyle);s=e.tableDnD.mouseCoords(t);u=s.x-e.tableDnD.mouseOffset.x;a=s.y-e.tableDnD.mouseOffset.y;e.tableDnD.autoScroll(s);i=e.tableDnD.findDropTargetRow(n,a);o=e.tableDnD.findDragDirection(u,a);e.tableDnD.moveVerticle(o,i);e.tableDnD.moveHorizontal(o,i);return false},findDragDirection:function(e,t){var n=this.currentTable.tableDnDConfig.sensitivity,r=this.oldX,i=this.oldY,s=r-n,o=r+n,u=i-n,a=i+n,f={horizontal:e>=s&&e<=o?0:e>r?-1:1,vertical:t>=u&&t<=a?0:t>i?-1:1};if(f.horizontal!=0)this.oldX=e;if(f.vertical!=0)this.oldY=t;return f},findDropTargetRow:function(t,n){var r=0,i=this.currentTable.rows,s=this.currentTable.tableDnDConfig,o=0,u=null;for(var a=0;ao-r&&n1&&e(this.currentTable.rows).each(function(){s=e(this).data("level");if(s>1){i=e(this).prev().data("level");while(s>i+1){e(this).find("td:first").children(":first").remove();e(this).data("level",--s)}}});t.onDragClass&&e(r).removeClass(t.onDragClass)||e(r).css(t.onDropStyle);this.dragObject=null;t.onDrop&&this.originalOrder!=this.currentOrder()&&e(r).hide().fadeIn("fast")&&t.onDrop(this.currentTable,r);this.currentTable=null},mouseup:function(t){t&&t.preventDefault();e.tableDnD.processMouseup();return false},jsonize:function(e){var t=this.currentTable;if(e)return JSON.stringify(this.tableData(t),null,t.tableDnDConfig.jsonPretifySeparator);return JSON.stringify(this.tableData(t))},serialize:function(){return e.param(this.tableData(this.currentTable))},serializeTable:function(e){var t="";var n=e.tableDnDConfig.serializeParamName||e.id;var r=e.rows;for(var i=0;i0)t+="&";var s=r[i].id;if(s&&e.tableDnDConfig&&e.tableDnDConfig.serializeRegexp){s=s.match(e.tableDnDConfig.serializeRegexp)[0];t+=n+"[]="+s}}return t},serializeTables:function(){var t=[];e("table").each(function(){this.id&&t.push(e.param(this.tableData(this)))});return t.join("&")},tableData:function(t){var n=t.tableDnDConfig,r=[],i=0,s=0,o=null,u={},a,f,l,c;if(!t)t=this.currentTable;if(!t||!t.id||!t.rows||!t.rows.length)return{error:{code:500,message:"Not a valid table, no serializable unique id provided."}};c=n.autoCleanRelations&&t.rows||e.makeArray(t.rows);f=n.serializeParamName||t.id;l=f;a=function(e){if(e&&n&&n.serializeRegexp)return e.match(n.serializeRegexp)[0];return e};u[l]=[];!n.autoCleanRelations&&e(c[0]).data("level")&&c.unshift({id:"undefined"});for(var h=0;hi){r.push([l,i]);l=a(c[h-1].id)}else if(s=i)r[p][1]=0}}i=s;if(!e.isArray(u[l]))u[l]=[];o=a(c[h].id);o&&u[l].push(o)}else{o=a(c[h].id);o&&u[l].push(o)}}return u}};t.jQuery.fn.extend({tableDnD:e.tableDnD.build,tableDnDUpdate:e.tableDnD.updateTables,tableDnDSerialize:e.proxy(e.tableDnD.serialize,e.tableDnD),tableDnDSerializeAll:e.tableDnD.serializeTables,tableDnDData:e.proxy(e.tableDnD.tableData,e.tableDnD)})}(window.jQuery,window,window.document) -------------------------------------------------------------------------------- /docs/import-existing-user-accounts.md: -------------------------------------------------------------------------------- 1 | Import Existing User Accounts 2 | ============================= 3 | 4 | Let's suppose you already have a large database of users and want to import them into the SSO server. While this is possible, this is a fairly advanced task and you are somewhat on your own as far as programming goes. There is some code later on in this section to give you an idea of how to proceed but you'll still ultimately have to do your own thing. 5 | 6 | There are several approaches you can take when dealing with the issue of importing users. You'll have to first decide which one is right for your situation. 7 | 8 | * One option is to author a provider that allows existing users to sign into their existing account. The LDAP provider is a simple enough model to follow and there is plenty of documentation on the topic of creating a new provider. You don't necessarily have to allow users to create or recover their account, just sign in. This approach can be useful when your existing system's passwords are hashed or encrypted. The provider approach can also be an "Import old account" method that simply migrates their account to the Generic Login provider but doesn't actually sign them in. 9 | * Another option is to not import anything. Users recreate their account in the new system and then the application using the SSO client operates on e-mail addresses. You don't have to do much to make this method work. This has the added benefits of cleaning up the user database and when a former user signs in with their old e-mail address, the new account will be linked automatically to their old user regardless of how they arrive from the SSO server. 10 | * The last option is to import accounts directly into the Generic Login provider. If this approach is used with passwords that are already hashed or encrypted, the user will have to recover their account before they can access the system. Doing that will be weird from an end-user perspective but you could reset their password for them and send it via e-mail during the import process. If the passwords are plain-text, while you shouldn't have been doing that, this method will significantly upgrade your existing system and users will be able to sign in without having to recover their account. 11 | 12 | The rest of this section is dedicated to importing user accounts into the Generic Login provider. 13 | 14 | The Generic Login provider is quite versatile but it is also hard to integrate with because of both its flexibility and the security measures taken to prevent a data breach. This is intentional but it does make it difficult to import accounts from other systems into this provider. The recommended approach for importing large numbers of accounts in one go is to write a command-line script. The following is an example to get you started. The code is borrowed from both 'cron.php' and the Generic Login provider: 15 | 16 | ```php 17 | query("SELECT * FROM yourusers"); 59 | while ($row = $result->getrow()) 60 | { 61 | // Put your code here to get the username, e-mail, and (optional) password out of your database row. 62 | $username = $row->username; 63 | $email = $row->email; 64 | $password = ""; 65 | 66 | // Load up $mapinfo with field data. Keys must match field names in the server. 67 | // Don't worry about e-mail address and username. Those are dealt with later. 68 | $mapinfo = array(); 69 | 70 | // Do not modify anything below this line unless you really know what you are doing. 71 | if ($sso_settings["sso_login"]["install_type"] == "email_username" || $sso_settings["sso_login"]["install_type"] == "email") 72 | { 73 | $result2 = SMTP::MakeValidEmailAddress($email); 74 | if (!$result2["success"]) 75 | { 76 | echo BB_Translate("Invalid e-mail address. %s\n", $email["error"]); 77 | continue; 78 | } 79 | 80 | $email = $result2["email"]; 81 | } 82 | 83 | // Create the new user in the Generic Login database. 84 | $userinfo = array(); 85 | $phrase = ""; 86 | for ($x = 0; $x < 4; $x++) $phrase .= " " . SSO_GetRandomWord(); 87 | $phrase = preg_replace('/\s+/', " ", trim($phrase)); 88 | if (SET_PASSWORD_MODE == 0) $phrase = $password; 89 | 90 | $salt = $sso_rng->GenerateString(); 91 | $data = $username . ":" . $email . ":" . $salt . ":" . $phrase; 92 | $userinfo["extra"] = $sso_rng->GenerateString(); 93 | if (SET_PASSWORD_MODE == 0 || SET_PASSWORD_MODE == 1) 94 | { 95 | $passwordinfo = Blowfish::Hash($data, $sso_settings["sso_login"]["password_minrounds"], $sso_settings["sso_login"]["password_mintime"]); 96 | if (!$passwordinfo["success"]) BB_SetPageMessage("error", "Unexpected cryptography error."); 97 | else 98 | { 99 | $userinfo["salt"] = $salt; 100 | $userinfo["rounds"] = (int)$passwordinfo["rounds"]; 101 | $userinfo["password"] = bin2hex($passwordinfo["hash"]); 102 | 103 | echo BB_Translate("Initial password for '%s' - '%s' has been set to '%s'.\n", $username, $email, $phrase); 104 | } 105 | } 106 | else 107 | { 108 | $userinfo["salt"] = ""; 109 | $userinfo["rounds"] = 0; 110 | $userinfo["password"] = ""; 111 | } 112 | 113 | $sso_db_sso_login_users = SSO_DB_PREFIX . "p_sso_login_users"; 114 | $userinfo2 = SSO_EncryptDBData($userinfo); 115 | 116 | try 117 | { 118 | if ($sso_settings["sso_login"]["install_type"] == "email_username") 119 | { 120 | $sso_db->Query("INSERT", array($sso_db_sso_login_users, array( 121 | "username" => $username, 122 | "email" => $email, 123 | "verified" => (int)$verified, 124 | "created" => CSDB::ConvertToDBTime(time()), 125 | "info" => $userinfo2, 126 | ), "AUTO INCREMENT" => "id")); 127 | } 128 | else if ($sso_settings["sso_login"]["install_type"] == "email") 129 | { 130 | $sso_db->Query("INSERT", array($sso_db_sso_login_users, array( 131 | "email" => $email, 132 | "verified" => (int)$verified, 133 | "created" => CSDB::ConvertToDBTime(time()), 134 | "info" => $userinfo2, 135 | ), "AUTO INCREMENT" => "id")); 136 | } 137 | else if ($sso_settings["sso_login"]["install_type"] == "username") 138 | { 139 | $sso_db->Query("INSERT", array($sso_db_sso_login_users, array( 140 | "username" => $username, 141 | "created" => CSDB::ConvertToDBTime(time()), 142 | "info" => $userinfo2, 143 | ), "AUTO INCREMENT" => "id")); 144 | } 145 | else 146 | { 147 | echo BB_Translate("Fatal error: Login system is broken.\n"); 148 | exit(); 149 | } 150 | 151 | $userid = $sso_db->GetInsertID(); 152 | 153 | $userrow = $sso_db->GetRow("SELECT", array( 154 | "*", 155 | "FROM" => "?", 156 | "WHERE" => "id = ?", 157 | ), $sso_db_sso_login_users, $userid); 158 | } 159 | catch (Exception $e) 160 | { 161 | echo BB_Translate("Database query error. %s\n", $e->getMessage()); 162 | continue; 163 | } 164 | 165 | // Activate the user. 166 | if ($sso_settings["sso_login"]["install_type"] == "email_username" || $sso_settings["sso_login"]["install_type"] == "email") $mapinfo[$sso_settings["sso_login"]["map_email"]] = $userrow->email; 167 | if ($sso_settings["sso_login"]["install_type"] == "email_username" || $sso_settings["sso_login"]["install_type"] == "username") $mapinfo[$sso_settings["sso_login"]["map_username"]] = $userrow->username; 168 | 169 | SSO_ActivateUser($userrow->id, $userinfo["extra"], $mapinfo, false, false); 170 | 171 | $numrows++; 172 | } 173 | ?> 174 | ``` 175 | 176 | That code should provide a sufficient starting point. Just make the necessary modifications to integrate with an existing system to import and activate each account. The script is intended to be run from a command-line in the SSO server root directory. 177 | -------------------------------------------------------------------------------- /support/geoip/Reader/Decoder.php: -------------------------------------------------------------------------------- 1 | 'extended', 19 | 1 => 'pointer', 20 | 2 => 'utf8_string', 21 | 3 => 'double', 22 | 4 => 'bytes', 23 | 5 => 'uint16', 24 | 6 => 'uint32', 25 | 7 => 'map', 26 | 8 => 'int32', 27 | 9 => 'uint64', 28 | 10 => 'uint128', 29 | 11 => 'array', 30 | 12 => 'container', 31 | 13 => 'end_marker', 32 | 14 => 'boolean', 33 | 15 => 'float', 34 | ); 35 | 36 | public function __construct( 37 | $fileStream, 38 | $pointerBase = 0, 39 | $pointerTestHack = false 40 | ) { 41 | $this->fileStream = $fileStream; 42 | $this->pointerBase = $pointerBase; 43 | $this->pointerTestHack = $pointerTestHack; 44 | 45 | $this->switchByteOrder = $this->isPlatformLittleEndian(); 46 | } 47 | 48 | 49 | public function decode($offset) 50 | { 51 | list(, $ctrlByte) = unpack( 52 | 'C', 53 | Util::read($this->fileStream, $offset, 1) 54 | ); 55 | $offset++; 56 | 57 | $type = $this->types[$ctrlByte >> 5]; 58 | 59 | // Pointers are a special case, we don't read the next $size bytes, we 60 | // use the size to determine the length of the pointer and then follow 61 | // it. 62 | if ($type == 'pointer') { 63 | list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset); 64 | 65 | // for unit testing 66 | if ($this->pointerTestHack) { 67 | return array($pointer); 68 | } 69 | 70 | list($result) = $this->decode($pointer); 71 | 72 | return array($result, $offset); 73 | } 74 | 75 | if ($type == 'extended') { 76 | list(, $nextByte) = unpack( 77 | 'C', 78 | Util::read($this->fileStream, $offset, 1) 79 | ); 80 | 81 | $typeNum = $nextByte + 7; 82 | 83 | if ($typeNum < 8) { 84 | throw new InvalidDatabaseException( 85 | "Something went horribly wrong in the decoder. An extended type " 86 | . "resolved to a type number < 8 (" 87 | . $this->types[$typeNum] 88 | . ")" 89 | ); 90 | } 91 | 92 | $type = $this->types[$typeNum]; 93 | $offset++; 94 | } 95 | 96 | list($size, $offset) = $this->sizeFromCtrlByte($ctrlByte, $offset); 97 | 98 | return $this->decodeByType($type, $offset, $size); 99 | } 100 | 101 | private function decodeByType($type, $offset, $size) 102 | { 103 | switch ($type) { 104 | case 'map': 105 | return $this->decodeMap($size, $offset); 106 | case 'array': 107 | return $this->decodeArray($size, $offset); 108 | case 'boolean': 109 | return array($this->decodeBoolean($size), $offset); 110 | } 111 | 112 | $newOffset = $offset + $size; 113 | $bytes = Util::read($this->fileStream, $offset, $size); 114 | switch ($type) { 115 | case 'utf8_string': 116 | return array($this->decodeString($bytes), $newOffset); 117 | case 'double': 118 | $this->verifySize(8, $size); 119 | return array($this->decodeDouble($bytes), $newOffset); 120 | case 'float': 121 | $this->verifySize(4, $size); 122 | return array($this->decodeFloat($bytes), $newOffset); 123 | case 'bytes': 124 | return array($bytes, $newOffset); 125 | case 'uint16': 126 | return array($this->decodeUint16($bytes), $newOffset); 127 | case 'uint32': 128 | return array($this->decodeUint32($bytes), $newOffset); 129 | case 'int32': 130 | return array($this->decodeInt32($bytes), $newOffset); 131 | case 'uint64': 132 | return array($this->decodeUint64($bytes), $newOffset); 133 | case 'uint128': 134 | return array($this->decodeUint128($bytes), $newOffset); 135 | default: 136 | throw new InvalidDatabaseException( 137 | "Unknown or unexpected type: " . $type 138 | ); 139 | } 140 | } 141 | 142 | private function verifySize($expected, $actual) 143 | { 144 | if ($expected != $actual) { 145 | throw new InvalidDatabaseException( 146 | "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)" 147 | ); 148 | } 149 | } 150 | 151 | private function decodeArray($size, $offset) 152 | { 153 | $array = array(); 154 | 155 | for ($i = 0; $i < $size; $i++) { 156 | list($value, $offset) = $this->decode($offset); 157 | array_push($array, $value); 158 | } 159 | 160 | return array($array, $offset); 161 | } 162 | 163 | private function decodeBoolean($size) 164 | { 165 | return $size == 0 ? false : true; 166 | } 167 | 168 | private function decodeDouble($bits) 169 | { 170 | // XXX - Assumes IEEE 754 double on platform 171 | list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits)); 172 | return $double; 173 | } 174 | 175 | private function decodeFloat($bits) 176 | { 177 | // XXX - Assumes IEEE 754 floats on platform 178 | list(, $float) = unpack('f', $this->maybeSwitchByteOrder($bits)); 179 | return $float; 180 | } 181 | 182 | private function decodeInt32($bytes) 183 | { 184 | $bytes = $this->zeroPadLeft($bytes, 4); 185 | list(, $int) = unpack('l', $this->maybeSwitchByteOrder($bytes)); 186 | return $int; 187 | } 188 | 189 | private function decodeMap($size, $offset) 190 | { 191 | 192 | $map = array(); 193 | 194 | for ($i = 0; $i < $size; $i++) { 195 | list($key, $offset) = $this->decode($offset); 196 | list($value, $offset) = $this->decode($offset); 197 | $map[$key] = $value; 198 | } 199 | 200 | return array($map, $offset); 201 | } 202 | 203 | private $pointerValueOffset = array( 204 | 1 => 0, 205 | 2 => 2048, 206 | 3 => 526336, 207 | 4 => 0, 208 | ); 209 | 210 | private function decodePointer($ctrlByte, $offset) 211 | { 212 | $pointerSize = (($ctrlByte >> 3) & 0x3) + 1; 213 | 214 | $buffer = Util::read($this->fileStream, $offset, $pointerSize); 215 | $offset = $offset + $pointerSize; 216 | 217 | $packed = $pointerSize == 4 218 | ? $buffer 219 | : (pack('C', $ctrlByte & 0x7)) . $buffer; 220 | 221 | $unpacked = $this->decodeUint32($packed); 222 | $pointer = $unpacked + $this->pointerBase 223 | + $this->pointerValueOffset[$pointerSize]; 224 | 225 | return array($pointer, $offset); 226 | } 227 | 228 | 229 | private function decodeUint16($bytes) 230 | { 231 | // No big-endian unsigned short format 232 | return $this->decodeUint32($bytes); 233 | } 234 | 235 | private function decodeUint32($bytes) 236 | { 237 | list(, $int) = unpack('N', $this->zeroPadLeft($bytes, 4)); 238 | return $int; 239 | } 240 | 241 | private function decodeUint64($bytes) 242 | { 243 | return $this->decodeBigUint($bytes, 8); 244 | } 245 | 246 | private function decodeUint128($bytes) 247 | { 248 | return $this->decodeBigUint($bytes, 16); 249 | } 250 | 251 | private function decodeBigUint($bytes, $size) 252 | { 253 | $numberOfLongs = $size / 4; 254 | $integer = 0; 255 | $bytes = $this->zeroPadLeft($bytes, $size); 256 | $unpacked = array_merge(unpack("N$numberOfLongs", $bytes)); 257 | foreach ($unpacked as $part) { 258 | // No bitwise operators with bcmath :'-( 259 | $integer = bcadd(bcmul($integer, bcpow(2, 32)), $part); 260 | } 261 | return $integer; 262 | } 263 | 264 | private function decodeString($bytes) 265 | { 266 | // XXX - NOOP. As far as I know, the end user has to explicitly set the 267 | // encoding in PHP. Strings are just bytes. 268 | return $bytes; 269 | } 270 | 271 | private function sizeFromCtrlByte($ctrlByte, $offset) 272 | { 273 | $size = $ctrlByte & 0x1f; 274 | $bytesToRead = $size < 29 ? 0 : $size - 28; 275 | $bytes = Util::read($this->fileStream, $offset, $bytesToRead); 276 | $decoded = $this->decodeUint32($bytes); 277 | 278 | if ($size == 29) { 279 | $size = 29 + $decoded; 280 | } elseif ($size == 30) { 281 | $size = 285 + $decoded; 282 | } elseif ($size > 30) { 283 | 284 | $size = ($decoded & (0x0FFFFFFF >> (32 - (8 * $bytesToRead)))) 285 | + 65821; 286 | } 287 | 288 | return array($size, $offset + $bytesToRead); 289 | } 290 | 291 | private function zeroPadLeft($content, $desiredLength) 292 | { 293 | return str_pad($content, $desiredLength, "\x00", STR_PAD_LEFT); 294 | } 295 | 296 | private function maybeSwitchByteOrder($bytes) 297 | { 298 | return $this->switchByteOrder ? strrev($bytes) : $bytes; 299 | } 300 | 301 | private function isPlatformLittleEndian() 302 | { 303 | $testint = 0x00FF; 304 | $packed = pack('S', $testint); 305 | return $testint === current(unpack('v', $packed)); 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /docs/integrating-with-third-party-software.md: -------------------------------------------------------------------------------- 1 | Integrating With Third-Party Software 2 | ===================================== 3 | 4 | Building a brand new application that uses the official SSO client is one thing. The earlier 'test_oo.php' and 'test_flat.php' examples are great starting points for managing signed in users in a brand new application. However, there are quite a few popular software applications out there that implement their own login systems. With a bit of work, the SSO server can be used with many of these products. 5 | 6 | There are currently two approaches to integrating the SSO server with third-party software. Both approaches have their pros and cons. 7 | 8 | Integrating With The OAuth2 Shim 9 | -------------------------------- 10 | 11 | OAuth2 is a protocol that lets users sign into many different systems. Integrating with the OAuth2 shim for SSO server is by far the easiest integration method requiring no coding and usually only takes a few minutes to get it working. Most third-party software products offer various OAuth2 integrations (Google, Facebook, Twitter, etc). You may have already noticed that API key configurations reference the OAuth2 shim that ships with SSO server. 12 | 13 | Before getting into the setup of OAuth2, here are the downsides of the OAuth2 shim: 14 | 15 | * No session control management from the SSO server. Sessions are controlled by the calling application whereas the official SSO clients always abide by the SSO server session lifetime. 16 | * Very limited tags/permissions support. Mapped SSO tags are passed back with a `tag:` prefix, but only a custom OAuth2 provider could interpret them and do something. At that point, integrating the regular SSO client may make more sense. 17 | * Disabled namespace support. Namespaces can still be enabled but could result in an infinite login loop since an OAuth2 provider can't detect a loop of this nature. 18 | * No request continuation. HTML forms that were filled out will probably have to be filled out again unless the application saved them prior to initiating the login. Admittedly, this is fairly minor. 19 | 20 | The simplest approach is to find a "generic" OAuth2 client plugin for the third-party software and install it. This is the most flexible solution as there will be freeform fields that allow for the various endpoint URLs that are needed to complete the OAuth2 flow. 21 | 22 | If there isn't a generic OAuth2 client plugin available, a Google OAuth2 plugin for the third-party software is the next best bet. A Google OAuth2 login sequence is quite simple compared to other OAuth2 providers. Download and set up the Google OAuth2 provider. Before enabling the provider though, locate URLs with `google.com` and `googleapis.com` in the source code. There should be three: One 'auth', one 'token', and one 'userinfo'. Replace each URLs with the OAuth2 URL from a SSO server API key. Now you have created your own OAuth2 plugin for the third-party software that interfaces with the SSO server. Once the various tokens and bits are set up, it is just a matter of mapping SSO server API key fields to what Google OAuth2 emits (not all fields have to be mapped - email is probably the most important though): 23 | 24 | * name - Full name 25 | * given_name - First name 26 | * family_name - Last name 27 | * gender - Gender 28 | * picture - Profile photo URL 29 | * locale - Locale 30 | * timezone - Timezone 31 | * email - E-mail address 32 | * email_verified - Whether or not the e-mail address is verified. Making a static field mapping here is a good idea. 33 | 34 | Of course, the button or link that goes to the SSO server may say something like "Login with Google". To avoid confusing users, find the relevant string and/or icon and change it. 35 | 36 | An OAuth2 provider requires the following pieces of information to function: 37 | 38 | * A redirect/callback URI. This is usually generated by the third-party software for you and it just has to be added to the `OAuth2 Redirect URIs` box of the relevant API key. 39 | * A client ID. This value is the `API key`. 40 | * A client secret. This value is the `OAuth secret` box. 41 | * Authorize endpoint. This value is the `OAuth2 URL`. This URL supports the usual extra parameters (`lang`, `use_namespaces`, etc). 42 | * Token endpoint. This value is also the `OAuth2 URL`. Extra parameters are not supported. 43 | * User info endpoint. This value is also the `OAuth2 URL` plus an optional `access_token` parameter (e.g. `http://localhost/sso/server/oauth2/?access_token=`). Some libraries pass a Bearer token Authorization header instead of URL parameters. 44 | 45 | It is recommended to use an isolated API key for OAuth2. To avoid getting logged out elsewhere, using a custom namespace for OAuth2 is also recommended. 46 | 47 | When using the Apache with Bearer tokens, the following should be added to the VirtualHost configuration: 48 | 49 | ``` 50 | SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 51 | ``` 52 | 53 | By default, Apache only passes properly formatted Basic Authorization headers to PHP. 54 | 55 | Integrating With An Official SSO Client 56 | --------------------------------------- 57 | 58 | Integrating the SSO client with a third-party software product requires detailed knowledge of how that product's internals work as well as a healthy knowledge of the scripting/programming language that the software is written in. There is usually a "users" database table, which contains information about users and permissions that those users have. Trying to replace all of the code that references that database table would be a huge undertaking and would result in massive modifications, making upgrades to the product an impossible task later on. The inability to upgrade a product will eventually result in the system getting taken over through some security vulnerability in the product. 59 | 60 | A better solution is to fake it. The goal of "faking it" is to override the target login system and keep the third-party software mostly oblivious to that fact. What is meant by this is to locate the earliest common point in the software product that results in the fewest modifications (if any) to the software to override the login system transparently with the SSO client. Essentially this involves writing some software "glue" between two distinct sign in systems. Many third-party software products support what are known as "plugins" (also known as "hooks" or "extensions"). Plugins introduce additional features into a product in such a way that the core of that software product is not modified. The end result is the ability to more easily upgrade the core product whenever there are new releases of that software. 61 | 62 | An example plugin is the [MyBB plugin](https://github.com/cubiclesoft/sso-server). It is fairly fancy in that it includes a nice admin interface to support the installation of the SSO client and make installation happen as smoothly as possible. However, it does several other things that are generally useful and common concepts among plugins that implement the SSO client: 63 | 64 | * The plugin loads the SSO client software very early in the process. This allows it to restore most POST data/requests when returning from the SSO server. However, since MyBB doesn't offer a hook early enough in the process, it had to be written in a slightly different way from most MyBB plugins to get past that problem. The worst case scenario if this doesn't happen is that POST data is lost and the user loses some work when they come back from the SSO server. 65 | * The plugin uses a secondary database table to map SSO server IDs to local user IDs. Whenever someone signs in and returns from the SSO server, the secondary table is checked to see if that SSO user ID has been seen before. If so, it looks up the target user and signs the person into the forums as the user they signed in as before. The extra table helps with system performance and avoids modifying the main MyBB users table structure. If the user doesn't exist, it is created in the MyBB users table just like it would be created via MyBB itself. The plugin actually relies on e-mail address as a key, so that it is possible for two users with different SSO server user IDs with the same e-mail address to end up mapping to the same MyBB account - this can happen if two different providers are used. 66 | * The plugin emulates session cookies. The same functions that MyBB uses to set up a user session are also called by the plugin. The plugin actually manages the session based on what the SSO client says. MyBB, however, is completely unaware that the login system has been replaced. It sees the user in the users table and the session cookies and thinks that all is well. 67 | * The plugin uses SSO server fields and tags to implement permissions. This allows permissions for the application to be managed at the SSO server level rather than the application level. The plugin has a bit of extra work to do to keep permissions in sync, but it is worth the effort for a seamless experience - at least from the perspective of the SSO server admin. 68 | * The plugin completely overrides both the 'login' and 'register' paths. The plugin sees any request to login or register and uses the SSO client to redirect the request to the SSO server. 69 | * The plugin ignores the 'upgrade' path. When upgrading the software, the MyBB plugin gets out of the way and the original login system is restored. This happens because upgrades are more delicate than the normal MyBB software execution path. The upgrade system also doesn't always load plugins in the first place. 70 | 71 | Other plugins for third-party software products will have a similar sort of approach. The downside is that developing a plugin that uses the full power of the SSO client/server and overrides an existing login system takes time and effort. 72 | 73 | If the software being integrated with is already in use, then the next step after creating/installing a plugin might be to import those user accounts into the SSO server. See the documentation on [importing existing user accounts](https://github.com/cubiclesoft/sso-server/blob/master/docs/import-existing-user-accounts.md). If the existing login system relies, for example, on e-mail address as a unique key in the users table, the plugin could be authored to take advantage of that fact and skip most of difficult bits of account migration for most users with the main exception being admin users. 74 | 75 | If you don't know how to write a plugin to integrate with a specific third-party software product, you can try asking. 76 | --------------------------------------------------------------------------------