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 | 
9 |
10 | [](https://cubiclesoft.com/donate/) [](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 | [](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 "" . $tagname . ">\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 |
',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 |
--------------------------------------------------------------------------------