Skip to content
Permalink
Browse files

Added OAuth2 shim and relevant support.

  • Loading branch information...
cubiclesoft committed Mar 20, 2019
1 parent 4162c1b commit bd2e12ebff4979d40d5f53e82639a9dc06b3e0e6
Showing with 597 additions and 66 deletions.
  1. +44 −28 admin.php
  2. +50 −6 docs/integrating-with-third-party-software.md
  3. +30 −3 docs/server-global-functions.md
  4. +18 −3 endpoint.php
  5. +382 −0 oauth2/index.php
  6. +73 −26 support/sso_functions.php
@@ -1051,10 +1051,7 @@ function SSO_ConfigRedirect($action2, $extra = array(), $msgtype = "", $msg = ""
if ($row)
{
$info = unserialize($row->info);
if (!isset($info["type"])) $info["type"] = "normal";
if (!isset($info["impersonation"])) $info["impersonation"] = false;
if (!isset($info["clock_drift"])) $info["clock_drift"] = 0;
$info = SSO_LoadAPIKeyInfo(unserialize($row->info));
if (isset($_REQUEST["purpose"]))
{
@@ -1080,17 +1077,17 @@ function SSO_ConfigRedirect($action2, $extra = array(), $msgtype = "", $msg = ""
}
}
$info = array(
"key" => $secretkey,
"type" => $_REQUEST["type"],
"purpose" => $_REQUEST["purpose"],
"url" => $_REQUEST["url"],
"impersonation" => (bool)(int)$_REQUEST["impersonation"],
"clock_drift" => (int)$_REQUEST["clock_drift"],
"field_map" => array(),
"tag_map" => array(),
"patterns" => $_REQUEST["patterns"]
);
$info["key"] = $secretkey;
$info["type"] = $_REQUEST["type"];
$info["purpose"] = $_REQUEST["purpose"];
$info["url"] = $_REQUEST["url"];
$info["impersonation"] = (bool)(int)$_REQUEST["impersonation"];
$info["clock_drift"] = (int)$_REQUEST["clock_drift"];
$info["field_map"] = array();
$info["tag_map"] = array();
$info["patterns"] = $_REQUEST["patterns"];
$info["oauth2_redirects"] = $_REQUEST["oauth2_redirects"];
$info["static_field_map"] = $_REQUEST["static_field_map"];
foreach ($sso_fields as $key => $encrypted)
{
@@ -1137,6 +1134,11 @@ function SSO_ConfigRedirect($action2, $extra = array(), $msgtype = "", $msg = ""
"type" => "static",
"value" => (function_exists("AdminHook_GetEndpointURL") ? AdminHook_GetEndpointURL() : SSO_ENDPOINT_URL)
),
array(
"title" => "OAuth2 URL",
"type" => "static",
"value" => SSO_LOGIN_URL . "oauth2/"
),
array(
"title" => "API Key",
"type" => "static",
@@ -1145,8 +1147,13 @@ function SSO_ConfigRedirect($action2, $extra = array(), $msgtype = "", $msg = ""
array(
"title" => "Secret Key",
"type" => "custom",
"value" => "<div class=\"textareawrap\"><textarea class=\"text\" style=\"background-color: #EEEEEE;\" rows=\"3\" readonly>" . htmlspecialchars($info["key"]) . "</textarea></div>",
"htmldesc" => "<input type=\"checkbox\" id=\"reset_key\" name=\"reset_key\" value=\"yes\" /> <label for=\"reset_key\">" . BB_Translate("Generate new secret key") . "</label>"
"value" => "<div class=\"textareawrap\"><textarea class=\"text\" style=\"background-color: #EEEEEE;\" rows=\"3\" readonly>" . htmlspecialchars($info["key"]) . "</textarea></div>"
),
array(
"title" => "OAuth2 Client Secret",
"type" => "custom",
"value" => "<div class=\"textareawrap\"><textarea class=\"text\" style=\"background-color: #EEEEEE;\" rows=\"3\" readonly>" . htmlspecialchars(hash_hmac((function_exists("hash_algos") && in_array("sha256", hash_algos()) ? "sha256" : "sha1"), $row->apikey . "-" . $row->id, $info["key"])) . "</textarea></div>",
"htmldesc" => "<input type=\"checkbox\" id=\"reset_key\" name=\"reset_key\" value=\"yes\" /> <label for=\"reset_key\">" . BB_Translate("Generate new secret keys") . "</label>"
),
array(
"title" => "Symmetric Cipher",
@@ -1215,6 +1222,22 @@ function SSO_ConfigRedirect($action2, $extra = array(), $msgtype = "", $msg = ""
"name" => "patterns",
"value" => BB_GetValue("patterns", $info["patterns"]),
"desc" => "A whitelist of IP address patterns that allows access to this API key. One pattern per line. (e.g. '10.0.0-15,17.*')"
),
array(
"title" => "OAuth2 Redirect URIs",
"type" => "textarea",
"height" => "150px",
"name" => "oauth2_redirects",
"value" => BB_GetValue("oauth2_redirects", $info["oauth2_redirects"]),
"desc" => "A whitelist of allowed redirect URIs. One redirect URI per line. The OAuth2 shim is disabled for an API key when this field is empty."
),
array(
"title" => "Static Fields",
"type" => "textarea",
"height" => "150px",
"name" => "static_field_map",
"value" => BB_GetValue("static_field_map", $info["static_field_map"]),
"desc" => "A set of key-value pairs that always present the same values for this API key. One key-value pair per line. Useful for delivering expected fields to third-party applications that don't have a specific field in this system."
)
),
"submit" => "Save",
@@ -1299,17 +1322,10 @@ function SSO_ConfigRedirect($action2, $extra = array(), $msgtype = "", $msg = ""
$secretkey .= ":" . $sso_rng->GenerateToken($_REQUEST["cipher"] == "aes256" ? 32 : 8);
}
$info = array(
"key" => $secretkey,
"type" => "normal",
"purpose" => $_REQUEST["purpose"],
"url" => $_REQUEST["url"],
"impersonation" => false,
"clock_drift" => 0,
"field_map" => array(),
"tag_map" => array(),
"patterns" => "*:*:*:*:*:*:*:*"
);
$info = SSO_LoadAPIKeyInfo(array());
$info["key"] = $secretkey;
$info["purpose"] = $_REQUEST["purpose"];
$info["url"] = $_REQUEST["url"];
$sso_db->Query("INSERT", array($sso_db_apikeys, array(
"user_id" => $sso_user_id,
@@ -1,11 +1,55 @@
Integrating SSO Clients With Third-Party Software
=================================================
Integrating With Third-Party Software
=====================================

Building a brand new application that uses the 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 client can be used with many of these products.
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.

There are currently two approaches to integrating the SSO server with third-party software. Both approaches have their pros and cons.

Integrating With The OAuth2 Shim
--------------------------------

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.

Before getting into the setup of OAuth2, here are the downsides of the OAuth2 shim:

* No session control management from the SSO server. Sessions are controlled by the calling application whereas the SSO client always abides by the SSO server session lifetime.
* 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.
* 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.
* 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.

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.

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):

* name - Full name
* given_name - First name
* family_name - Last name
* gender - Gender
* picture - Profile photo URL
* locale - Locale
* timezone - Timezone
* email - E-mail address
* email_verified - Whether or not the e-mail address is verified. Making a static field mapping here is a good idea.

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.

An OAuth2 provider requires the following pieces of information to function:

* 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.
* A client ID. This value is the `API key`.
* A client secret. This value is the `OAuth secret` box.
* Authorize endpoint. This value is the `OAuth2 URL`. This URL supports the usual extra parameters (`lang`, `use_namespaces`, etc).
* Token endpoint. This value is also the `OAuth2 URL`. Extra parameters are not supported.
* User info endpoint. This value is also the `OAuth2 URL` plus an `access_token` parameter (e.g. `http://localhost/sso/server/oauth2/?access_token=`).

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.

Integrating With An Official SSO Client
---------------------------------------

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.

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 easily upgrade the core product whenever there are new releases of that software.
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.

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:

@@ -16,8 +60,8 @@ An example plugin is the [MyBB plugin](https://github.com/cubiclesoft/sso-server
* 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.
* 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.

Other plugins for third-party software products will have a similar sort of approach.
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.

If the software being integrated with is already in use, then the next step after 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.
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.

If you don't know how to write a plugin to integrate with a specific third-party software product, you can try asking about it on the forums.
@@ -583,16 +583,43 @@ Returns: An array containing the namespace session information.

This global function reads the encrypted browser cookie or submitted data and attempts to decrypt it into a namespace array. The SSO server only cookie (sso_server_ns) contains a complete session ID while the optional subdomain cookie (sso_server_ns2) only contains the integer portion of the session ID.

SSO_SaveNamespaces($realnamespaces, $exposednamespaces)
-------------------------------------------------------

Access: global

Parameters:

* $realnamespaces - An array containing the results of a `SSO_LoadNamespaces(true)` call.
* $exposednamespaces - An array containing the results of a `SSO_LoadNamespaces(false)` call.

Returns: Nothing.

This global function saves the signed in namespaces to cookies if the relevant features are enabled/relevant.

SSO_RemoveSavedNamespace($namespace)
------------------------------------

Access: global

Parameters:

* $namespace - A string containing a namespace to remove.

Returns: Nothing.

This global function removes a signed in namespace and calls `SSO_SaveNamespaces()` to update the browser cookies.

SSO_ActivateNamespaceUser()
---------------------------

Access: global

Parameters: None.

Returns: A boolean of false if the namespace is invalid or activation fails. The browser is redirected on success.
Returns: A boolean of false if the namespace is invalid or activation fails. The browser is redirected on success.

This global function locates the namespace of the current API key, looks for an existing session in the same namespace, and activates a new session if a match is found.
This global function locates the namespace of the current API key, looks for an existing session in the same namespace, and activates a new session if a match is found. The behavior can be overridden by setting `use_namespaces` to 0 in the request.

SSO_ActivateUser($id, $entropy, $info, $created = false, $automate = false, $activatesession = true)
----------------------------------------------------------------------------------------------------
@@ -602,7 +629,7 @@ Access: global
Parameters:

* $id - A string containing the provider ID for the user being activated.
* $entropy - A string containing optional, extra entropy.
* $entropy - A string containing optional, extra entropy. No longer used by this function and MAY be removed in the future.
* $info - An array containing user information.
* $created - A boolean of false or a UNIX timestamp (that will be GMT encoded) to set the 'created' field mapping with.
* $automate - A boolean that specifies if the validation phase should be automated (Default is false).
Oops, something went wrong.

0 comments on commit bd2e12e

Please sign in to comment.
You can’t perform that action at this time.