====== Single Sign-On (SSO) ======
===== About Single Sign-On =====
FoxyCart's SSO allows [[.:customers|customers]] who are already logged into your website to proceed through to checkout without needing to re-enter their username and password. This allows for far greater integration options, and can provide for a significantly improved checkout flow if you have customers who may already be logged into an external system. In order to prevent possible security issues, SSO checkouts still require the customer to enter the [[wp>Card_Security_Code|CSC]] when using a saved credit card - unless the CSC is set to only be required for new cards within the "Customize the payment card security code (CSC) usage" option on the "configuration" page of the Foxy admin.
When a user has been authenticated, they won't have to enter their email address or password to authenticate, and it looks something like this on the checkout:
{{:v:2.0:qa_2.0_secure_checkout.png?direct&300|}}
==== Outgoing ("Reverse") SSO (Foxy -> Your System) ====
FoxyCart also supports the same approach, but in reverse. So an authentication token can be generated from the customer's FoxyCart-hosted receipt page, and that token can then be passed along to your own website. This allows you to log a new user into your site after they've completed a transaction from FoxyCart.
===== Alternate Uses of SSO =====
Because of how FoxyCart's SSO functionality works it actually allows for a few interesting options in besides straight single sign-on:
* Checkout can be made entirely "private", requiring authentication (through your own system) prior to allowing purchase. This could allow for members-only stores or other secure ordering or customer pre-screening opportunities.
* Prior to passing a customer on to the checkout the customer's cart could be retrieved (using the JSONP functionality) and validated to ensure they:
* aren't ordering more or less items than may be required;
* aren't getting a discount or price range they aren't authorized to get;
* have entered any additional information (referrer values, coupon codes, etc.);
* have registered through your system's own registration process;
* have been presented with cross-sell or up-sell opportunities.
* Upsells. Using the "reverse SSO" functionality, post-transaction upsells are possible. See the example below for more info.
===== Requirements =====
* A functional site + FoxyCart setup, using [[.:api|the API]] to synchronize customer records with your own user database.
===== How It Works =====
==== The Basic Idea ====
The basic idea of SSO is as follows:
- User creates an account or logs into SITE.
- On any user creation or modification, the SITE synchs the user's information with FoxyCart's customer records, using the [[api|API]].
- When the user attempts to load the SITE's FoxyCart checkout (ie. from clicking "checkout" on the cart, from a direct to checkout request, etc.), the checkout redirects the user back to the SSO endpoint as configured in your FoxyCart store settings.
- The SITE's endpoint checks the current user's authentication status (on the SITE). This is possible because the endpoint is (probably) on the same domain as the SITE (where the initial authentication and any cookie-based sessions reside), and thus can access whatever session information might be available (such as the ''COOKIE'' headers).
- Based on the SITE's shared-authentication endpoint, the script can:
* Redirect the user to the checkout, authenticated;
* Redirect the user to the checkout, //not// authenticated; or
* Take other action, such as redirect the user to a login or registration page, or deny checkout altogether.
==== Related Functionality ====
* [[.:webhooks|Webhooks]] (for creating new users from the FoxyCart checkout, or updating existing users).
* [[.:api|The API]], for creating users from your system -> FoxyCart.
* [[.:customers|Customers]], since that's what we actually care about. Pay particular attention to the password hashing section.
==== Best Practices ====
Before we get to [[#the_details|the details of an SSO implementation]], it's important to understand what we consider "best practices" when it comes to SSO. The most important piece that people often miss is that // NOTE: We recommend using the official Foxy SDK instead of rolling your own, as below.
// Find it here: https://sdk.foxy.dev/modules/_backend_index_.html#createssourl
const crypto = require('crypto');
module.exports.generateSsoUri = function (customerId, timestamp, secret, sessionId) {
if (!customerId || !timestamp || !secret) {
return false;
}
let stringToSign = `${customerId}|${timestamp}|${secret}`;
let token = crypto.createHash('sha1').update("" + stringToSign).digest('hex');
let uri = `https://${storeDomain}/checkout?fc_customer_id=${customerId}×tamp=${timestamp}&fc_auth_token=${token}`;
if (sessionId && validator.isAlphanumeric(sessionId)) {
uri += `&fcsid=${sessionId}`;
}
return uri;
}
Here's what it might look like in PHP:
$auth_token = sha1($customer_id . '|' . $timestamp . '|' . $foxycart_secret_key);
or in Ruby: Digest::SHA1.hexdigest("#{customer_id}|#{timestamp}|#{foxycart_secret_key}")
or JavaScript
* It is critically important to note that the ''timestamp'' value you hash must match the ''timestamp'' value you send in the clear (below). Again, the ''timestamp'' provided //to// your endpoint must not be used when passed back to FoxyCart, as that timestamp will already be in the past.
* ''fcsid'': The FoxyCart session ID. This is necessary to prevent issues with users with 3rd party cookies disabled and stores that are not using a custom subdomain.
* ''fc_customer_id'': INTEGER. The customer ID, as determined and stored when the user is first created or synched using the [[api|API]]. **NOTE**: If a customer is not authenticated and you would like to allow them through to checkout, enter a customer ID of ''0'' (the number).
* ''timestamp'': INTEGER, epoch time. The future time that this authentication token will expire. If a customer makes a checkout request with an expired authentication token, then FoxyCart will redirect them to the endpoint in order to generate a new token. You can make use of the ''timestamp'' value you received to your endpoint in the ''GET'' parameters, and add additional time to it for how long you want it to be valid for. For example, adding 3600 to the timestamp will extend it by 3600 seconds, or 60 minutes.
The completed redirect might look something like this (in PHP):
$redirect_complete = 'https://yourdomain.foxycart.com/checkout?fc_auth_token=' . $auth_token . '&fcsid=' . $fcsid . '&fc_customer_id=' . $customer_id . '×tamp=' . $timestamp;
header('Location: ' . $redirect_complete);
=== What Happens on Error ===
* If the SHA-1 hash passed to FoxyCart does is invalid or doesn't match the supplied cleartext values, the user is redirected back to the store's URL (as configured in the store settings). The only way this should happen is if your SSO endpoint is configured incorrectly, or if a malicious user is attempting to manipulate the redirect URL.
* If the SSO timestamp has expired, the user will be redirected back to the shared-authentication endpoint. This //may// result in the user then being bounced immediately back to the checkout, which can be problematic as it will appear that there was an error on checkout.
* Set the timestamp/expiration far enough in the future (an hour or more, perhaps) to prevent issues.
* Add a javascript alert on a ''setTimeout()'' to match your expiration setting alerting your user that their checkout session is about to expire to let your customers know that their session is about to (or has already) expired.
=== Troubleshooting ===
If you're convinced that FoxyCart is broken because you can't get SSO working, it's possible, but unlikely. Check a few common causes:
* Consult your store's error log, accessible from the FoxyCart administration, as SSO errors are logged there for additional context.
* What's your timestamp value? It must be in the future, but don't set a date in the year 24800 or something crazy. If it's in the past, that's your problem.
* The customer ID must match the //FoxyCart// customer ID, as retrieved in the webhooks or API. This isn't (usually) the ID of the customer in your database.
* The customer ID is for a //non-guest// customer record (the ''is_anonymous'' bit should be 0). You cannot send the checkout a guest user, since guest users cannot be reused (by design).
* The URL is pointing to ''checkout'' and not ''cart''.
* The URL is wellformed, and doesn't have any syntax errors like missing ''&'' or ''=''.
* The hash you're sending (in the URL) is indeed what you get by manually hashing the pieces you're sending.
* To prevent an infinite loop, FoxyCart will redirect to your homepage on the fourth checkout attempt that does not make it to the checkout page. So if your SSO strategy involves an intermediate page, be aware that you can only hit that page three times before the SSO endpoint will bump you to the store's homepage. Once you hit the checkout page successfully, this counter gets reset.
If you still can't get it working, please [[http://forum.foxycart.com|post in our forum]] and we'll be happy to help.
===== Best Practices: How To Approach a SSO Integration =====
One of the things we often hear is this workflow:
When the user goes to checkout I want them to be required to login or already be logged in. For new customers I want them to be redirected to the registration page on my site.We generally recommend allowing a checkout as a guest (or if not enabling guest mode, at least allowing checkout through the FoxyCart-powered checkout page without first registering elsewhere). That allows for the most streamlined approach, and puts the least number of hurdles between your customer and a successful transaction. So if possible, you should allow checkout //without// first registering on your site. Once the order's done, you can create the user in your systems, so the end result is still a synchronized user. (Of course, you may have very legitimate reasons for requiring registration first, but if possible we recommend allowing an unauthenticated checkout.) In any case, there are three main pieces to dig into: - Creating and updating users from your system -> FoxyCart. - Creating and updating users from FoxyCart -> your system. - The SSO endpoint (FoxyCart -> your system -> FoxyCart). #1 would generally be code in your system that attaches to specific events that your system provides, which might look like ''OnUserChangePassword'', ''OnUserSave'', or other events where users are created or modified. Consult your systems documentation for what those events are, and on those events, just do a quick FoxyCart [[.:api|API]] call to create/update the user as needed. #2 would be an endpoint on your system that accepted and processed the [[.:webhooks|webhooks]] in order to create or update the user on your system. #3 would be another endpoint on your end to handle the Single Sign-On functionality. These three pieces are the foundation for a fully synchronized userbase between FoxyCart and your system of choice. If you have any questions about this process, [[http://forum.foxycart.com|just ask]]. ==== Identifying Customers: Email vs ID ==== FoxyCart treats a customer's email as a unique identifier - so no two customers within a given store can share the same email. This means you can use the email address of the customer as an identifier when performing API requests. It is possible for a customer's email address to be changed though using the API. If your system allows a customer to update their email address, this could mean that some API calls won't return full results - such as ''transaction_list'' or ''subscription_list'' - any where you're filtering by email address. For example, while historically the user may have purchased with ''john.doe@example.com'', if their customer email is updated to ''johnd@example.com'', then any future purchases will be bought from there instead. Filtering transactions by either email will only show the transactions placed for that email. With this in mind, if your user database supports storing custom attributes, we recommend storing the FoxyCart customer ID as well. That way, as the ID won't ever change for a single customer record, you can use it as the definitive identifier for the customer record even if the customer email changes. ==== The different ways to sync data for SSO ==== ; Webhooks : **Triggered by:** Automatically from checkout completion, updateinfo request and subscription modification, cancel or past due payments. : **Actions:** * User accounts need to be created if they don't exist. * Passwords should be updated each time to ensure it's up to date with any changes from the FoxyCart checkout * A subscription modification can result in the subscription being moved to a different email address. If that is the case, you may need to take action on the subscription's previous email and create a new one for this email. If this could be an issue for your store, you can track the subscriptions for a given email by the sub-token ; Subscription Webhook : **Triggered by:** Automatically as subscriptions are cancelled : **Actions:** * User accounts may need to be deleted or modified depending on the subscriptions status and your usage of it. ; Your Website : **Triggered by:** User signing up for a new account or modifying an existing account : **Actions:** * If a user creates an account, create a linked FoxyCart account using the API. * If a user modifies an existing account, update their linked FoxyCart account using the API. ; SSO Endpoint : **Triggered by:** Customer checking out from cart : **Actions:** * If a user is logged in, use the API to ensure the FC customer account exists for that email, and if not - create it before redirecting over to the checkout with the customer authenticated. * If no user is logged in, either redirect the user to the websites login/register page, or push them through to the checkout as a guest. ===== Outgoing SSO: Logging a customer into your own site from FoxyCart =====
{% if first_receipt_display == 1 and is_anonymous == 0 %}
{% endif %}
This example does a few things: It only outputs this code on the very first time a receipt is viewed. So if a customer revisits their receipt (from their email link), this won't output. It then adds a product to the cart, and when that's done it immediately redirects. You'd probably want to have this happen on click instead of just automatically on pageload.
There are a few important things to note:
* Since this really should only ever be used immediately after a customer has made a transaction, you'll want to use the ''first_receipt_display'' flag to ensure you're not generating tokens willy nilly, potentially far after the fact. (That said, the ''timestamp'' does offer protection against this as well.)
* Because this example is passing tokens in the request URI, this should only be used if you're redirecting to an ''https'' location.
* The ''timestamp'' is generated by you. Since this token is a substitute for password-based authentication, you probably don't want to set this to much longer than a few minutes (or shorter if you're doing an automatic redirect.) The idea is that you don't want this any longer than absolutely necessary.
* The ''customer_id'' is available to your Twig templates, as documented on [[.:cheat_sheet|the cheat sheet]].
* The ''generate_sso_token()'' is the piece that generates the SSO token. It accepts a single argument (''timestamp'').
* A guest customer cannot use SSO, so this only works if you're forcing accounts. Hence the ''is_anonymous'' check.
==== Example 2: Automatically logging the user in on YOUR website ====
Another great use of the Reverse SSO functionality is to automatically log new accounts in on your own website after they checkout. This way people who sign up on the checkout with a new account can continue to your website and already be authenticated with their new login. The steps would look like this:
- Update the continue link on the receipt to point to your Reverse SSO endpoint.
- On your endpoint, confirm the customer is legitimate using the ''fc_auth_token'' and if so fetch the customer information using the API to create the customer and log them in.
- Forward the customer on to your website as an authenticated user.
Step 1 is completed within your receipt template with some Twig, and step 2 and 3 would be a server-side script on your side.
=== Redirecting from the receipt ===
The Twig code for your receipt would look something like this, and would be pasted at the top of your custom "receipt" template:
{% set reverse_sso_url = "" %}
{% if first_receipt_display and not is_anonymous %}
{% set timestamp = checkout_date|date_modify("+120 seconds")|date("U") %}
{% set reverse_sso_url = "https://www.YOURWEBSITE.com/reversesso.php?fc_auth_token=" ~ generate_sso_token(timestamp) ~ "×tamp=" ~ timestamp ~ "&fc_customer_id=" ~ customer_id %}
{% set continue_url = reverse_sso_url %}
{% endif %}
This example only runs this code on the very first time a receipt is viewed, and updates the continue button on the receipt to forward to your reverse SSO endpoint. So if a customer revisits their receipt (from their email link), the reverse SSO redirect to your website won't happen again. If it is the first time, and the user is not a guest, it updates the continue URL to your reverse sso endpoint.
There are a few important things to note:
* Since this really should only ever be used immediately after a customer has made a transaction, you'll want to use the ''first_receipt_display'' flag to ensure you're not generating tokens willy nilly, potentially far after the fact. (That said, the ''timestamp'' does offer protection against this as well.)
* The ''timestamp'' is generated by you based on the checkout date. Since this token is a substitute for password-based authentication, you probably don't want to set this to much longer than a few minutes (or shorter if you're doing an automatic redirect.) The idea is that you don't want this any longer than absolutely necessary.
* The ''customer_id'' is available to your Twig templates, as documented on [[.:cheat_sheet|the cheat sheet]].
* The ''generate_sso_token()'' is the piece that generates the SSO token. It accepts a single argument (''timestamp'').
* A guest customer cannot use SSO, so this only works if you're forcing accounts. Hence the ''is_anonymous'' check.
* If you wanted to add in an additional or larger link for the customer to continue to your reverse sso endpoint, you can make use of the ''%%{%%{ reverse_sso_url %%}%%}'' Twig variable that the above script created to provide the URL and edit the receipt template to add in the custom HTML.
* Similarly - if you wanted to skip the FoxyCart receipt entirely and just forward straight to your reverse SSO URL, you would replace the line ''{% set continue_url = reverse_sso_url %}'' with ''''.
=== Logging the user in ===
Once the user has been redirected from the receipt to your endpoint (the reversesso.php file in the example script above, but you can call it whatever you want), you then need to check the parameters to confirm it's legit. On your endpoint - you need to grab the parameters included in the URL for the token, timestamp and customer ID and handle them in a similar way to the normal SSO endpoint. Instead of you generating it for our servers to check though, you're generating it to check that the token you're receiving is correct. This check ensures that the raw customer ID value you're receiving is legitimate and the timestamp is still valid - otherwise anyone could conceivably send a customer ID and timestamp over to automatically log someone in.
In PHP, that would look something like this:
$local_timestamp) {
// Everything checks out, log the user in
// Enter your logic here
}
// Redirect the customer on to your own website
header('Location: http://www.yourwebsite.com');
Important things to note:
* The API token at the start of the script will need to be set to your [[v:2.0:store_secret|store's secret key]], and similarly the URL to redirect to at the bottom of the script will also need to be updated.
* The check for the auth token and the timestamp act as the validation that this request is legitimately from the receipt - it's important that these checks are completed
* Not included above is the actual code to log the user in on your website. This will be different depending on the authentication system you're using. Consult the documentation for your system for how to approach that.
* When logging the customer in to your website - this assumes that they've been entered into your database already from the [[.:webhooks|webhooks]]. The webhook is sent to your endpoint after the customer completes the transaction successfully. This means that by the time the customer clicks the link to continue to your endpoint - your webhook endpoint //should// have processed the transaction and entered the user into your database. If not - you can use the [[.:api|the API]] to fetch their information using their customer ID.
===== Caveats and Gotchas =====
*