IMPORTANT!!! This is NOT A SECURE version of this implementation. To understand it's vulnerabilities and workarounds, please view the Secure Coding Single Sign On article.

This document is for Force.com Developers and Partners developing Composite Applications. In the following pages we will describe how you can have your Web application appear inside the force.com UI using a secure means to authenticate an user by your application.

All Composite Force.com applications should implement a form of Single-Sign-On (SSO) between force.com and the external application. Single-Sign-On is a key feature to help drive user adoption, and will be needed later on to implement a seamless Get It Now experience as well.


Contents

User Actions and Your Web Application


Sso image1.jpg

Figure 1: Custom Web Application in an Custom Tab


  • Force.com users will click on a Custom Tab or Custom Link that will invoke your web application via an HTTP Get (hyperlink).
  • The hyperlink URL will contain a user specific SessionId and a ServerURL as query string parameters.
  • Your application (presentation) will appear inside an iFrame that is embedded within the current user context (tab or current page).
  • Users should not be required to enter a username and password to complete this action. Your web application will determine what the calling user's identity is by accessing the Force.com SOAP API. By exploiting the key parameters — current user session id and server url, a simple API call can return user and org [1] attributes for you to validate this user against your list of authorized users.


This approach is referred to as Force.com User Authentication (AUA) as it relates to an composite application.

  • Always use SSL (https://) for all API transactions.
  • The Force.com API is a key feature of the Enterprise and Unlimited Editions of force.com. Your application will not normally be able to access the SOAP API of customers that are using other editions of force.com (i.e., Professional Edition). As a benefit of the Force.com AppExchange Security Review Program, your application will be able to access the API for Professional Edition customers through the use of an assigned API token. The API token is provided to you after successfully completing the Security Review process. See Step 3: Publish for more on Security Review.
  • Always identify a user by their User ID, which cannot be changed once created.
    • It is possible (although rare) for the user to change their username.
    • Note that the username is in the format of an email address, but does not have to match the email address of the user.

Handling User Requests

In the following example, we are going to create a Custom Tab that will invoke our sample web application.


The following example is provided as a standalone Force.com application. To follow along with the example below, install the sample application from this private AppExchange listing into your Developer org to see this application in action.


Configuration of the Custom Tab

  1. Click App Setup|Build|Custom Tabs
  2. Select the Web Tabs node, and then click the New button
  3. Select the page type (use default)
  4. For the Tab Type, use URL; enter the Tab Label and select any of the available Tab Styles. Click Next.
  5. Enter the URL
    1. In the Link URL field, type the website address:
      https://www.my_website.com/appexchange_page.jsp
    2. Our example requires a few more parameters to work properly:
    3. Add "?SessionId=" to the url in the Link URL field
      1. In the Select Field Type dropdown select API Fields
      2. In the Select Field dropdown select API Session ID
      3. Copy {!API_Session_ID} in Copy Merge Field Value and paste it at the end of the Link URL field
    4. Add "&ServerURL=" to the url in the Link URL field
      1. Select API Partner Server URL 9.0 in the Select Field dropdown
      2. Copy {!API_Partner_Server_URL_90} from the Copy Merge Field Value and paste it at the end of the Link URL field
  6. Click Next and then Save for the remaining pages of the wizard.
  7. Your new Tab will appear on the right in the list of tabs, click it to see the results.


Note: There is much more information that you can include in the URL as query parameters such as the UserName or User Id. As you can see in the sample code below, it is not necessary to include other user or org related attributes, as these values can be determined by exercising the GetUserInfo() AppExchange API call as part of your authentication logic. To ensure that the Web request originates from the salesforce.com service, be sure to follow the best practice outlined below.


Now that we know how to construct the hyperlink (URL), we need to think about:

  1. Verifying that the Web request is in fact originating from the salesforce.com service.
  2. Implementing authorization logic to cross-reference the identified user against your list of authorized users.



Web Request Verification

To ensure that any User Web Request is in fact originating from the salesforce.com service, you should use the provided session id query parameter in your service binding and make a call back to the force.com service. A successful result will determine that this Web Request is bona fide

A simple approach to verification is to use the GetUserInfo() call, which will yield key user data, such as the unique AppExchange identifier UserID.

Authorization Logic

With the UserID returned from the Web Request Verification (using the GetUserInfo() call), you can then implement some authorization logic within your service and cross-reference this ID against your list of authorized users. In this way, you will have provided for a seamless integration between salesforce.com and your service — also referred to as Single-Sign-On between salesforce.com and your AppExchange application.

In the event that the authorization logic has failed, you have the opportunity to engage with the user (prospect) and provide access to a free trial (if this capability exists) or display a web page that provides additional information on how they can gain access (purchase) to your application/service.


Sso image2.jpg


Cookies and Session Woes: Introducing P3P

This is all great stuff — when it works. Unfortunately, the creators of Web technologies — and especially browsers — don't always have the application developer's best interests at heart. This was especially true in the late 90s, when the Internet was dominated by consumer content and the sites that served advertising to their users. (It is interesting that new enterprise IT tools and standards now migrate from consumer to corporate applications; until 1994 this process worked in reverse.)

Due to the fact that any well-formed HTTP request contains a referrer, or a site from which the request originated, advertising networks gained the unique position of having their content "embedded" in pages across the Web and were therefore given the ability to track users' activity therein. When they promised to link this "clickstream" data with real-world name and address details, consumer outrage ensued, and cookies quickly became the most politically charged Web technology.

In response, a new W3C standard called P3P, or Platform for Privacy Preferences, was created. In theory, P3P would allow sites to contain and relay meta-data about how cookies and personal information were used and allow browser to intelligently decide if a cookie should be accepted or rejected based on these assertions. In practice, however, P3P is about as simple for Web developers as having to write HTML in assembly language (and about as frequently done). Most developers simply ignored P3P, blissful in their ignorance of the HTTP-header mess they'd avoided.

The red circle icon on the IE status bar means a cookie has been blocked; if you see this when accessing an AppExchange Control or Web tab, you likely need to implement P3P. The problem — and it's a significant one — lies in what happens when a browser (or more specifically, Internet Explorer 5 or later), requests an AppExchange Control. Since AppExchange Controls are technically "embedded content" inside salesforce.com, IE's default configuration treats them not as the useful enterprise applications you are trying to deploy, but as an ad-serving network, trying to collect personal data on users. As a result, your application server's cookie is rejected, and your application server is unable to create a session — a condition that renders itself with difficult-to-diagnose and occasionally bizarre behavior.

Creating Cookies with P3P

As you may have guessed, the solution to this unfortunate situation is to implement P3P in your Web application. Since P3P is a complex and multi-faceted specification, we'll cut to the chase. At its core, to implement P3P a developer must set a specific HTTP header, and an optional XML file, whenever a "Set-Cookie" header is issued. Since session management happens automatically in most app servers, and its not always clear when a cookie is set; in practice this means setting a specific HTTP header on every HTTP request and ignoring the optional XML file, because it is by definition optional.

The specific header that needs to be set is:

DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT

Don't ask what those codes mean because no one is quite sure. Just trust that as long as you've set IE to "Medium" security or lower (more relaxed), the cookie will be accepted (Users who set IE security to "Strong" can't access many Web applications and should consider switching their browsers). In Java, using JSPs, the specific call would be:

response.addHeader("P3P","CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"")

The call would typically be put at the top of each page or in an include file.

To see if you are able to correctly set a cookie, check the lower right hand corner of the IE status bar. An eye with a red circle means the cookie has been rejected, and therefore a session has not been generated. Note that it is sometimes useful to restart IE between tests to ensure correct behavior, and it is always useful to use a tool like TCPMon (or the Live HTTP Headers extension to the Firefox browser) to view the HTTP headers and ensure the P3P header exists and is well formed.

C# Sample Code

 #region Login()
    protected bool Login()
    {
        // from salesforce.com Custom Tab
        string sessionId = Request.QueryString["sessionid"];
        string serverURL = Request.QueryString["serverurl"];

        // declare for all messages below
        string v_message = "";

        // make sure there are values

        if (sessionId == null || sessionId == "")
              v_message += "SessionId is missing or blank.<br>";
        if (serverURL == null || serverURL == "")
              v_message += "ServerURL is missing or blank.<br>";
        if (v_message != "")
        {
            Response.Write(v_message);
            return false;
        }

        try  {
            Uri uri = new Uri(serverURL);
            if (!uri.AbsoluteUri.StartsWith("https://") ||
                    !uri.Host.EndsWith(".salesforce.com") ||
                    !(uri.Query=="") )
            {
                // protect against spoofing web request
                // begins with https
                // host ends in salesforce.com
                // does not have a querystring
                Response.Write("Not a valid API Server URL.<br><br>" + serverURL);
                return false;
            }

            // binding
            AppExchangeAPI.SforceService binding = new AppExchangeAPI.SforceService();
            binding.SessionHeaderValue = new AppExchangeAPI.SessionHeader();
            binding.SessionHeaderValue.sessionId = sessionId;
            binding.Url = serverURL;
 
            // TEST a SOAP call
            AppExchangeAPI.GetUserInfoResult userInfoResult = binding.getUserInfo();

            // string userid = userInfoResult.userId;
            // check if user is in your database
            // if not, you can present a signup screen
            return true;
        } catch (System.Web.Services.Protocols.SoapException exsoap)  {
            if (exsoap.Code.ToString().Contains("API_DISABLED_FOR_ORG"))  {
                v_message += "This edition of salesforce.com does not provide API access.<br>"
                    + "API access is a standard feature of Enterprise "
                    + "and Unlimited Editions.<br>"
                    + "Certify your application to gain API access to "
                    + "Professional Edition as well.<br><br>";
            }
            Response.Write(v_message + exsoap.Message);
        }  catch (System.UriFormatException uriEx) {
            Response.Write("The Server URL is invalid.<br><br>" + uriEx.Message);
        }  catch (Exception ex) {
            Response.Write("Unable to connect to the API.<br><br>" + ex.Message);
        }
        return false;
    }
    #endregion

Java Sample Code

 protected boolean Login(HttpServletRequest request, HttpServletResponse response) {
    // from salesforce.com Custom Tab
    String sessionId = request.getParameter("sessionid");
    String serverURL = request.getParameter("serverurl");
  
    // declare for all messages below
    String v_message = null;
       
    try {
        // make sure there are values
        if (sessionId == null || sessionId == "") v_message +=
             "SessionId is missing or blank.<br>";
        if (serverURL == null || serverURL == "") v_message +=
             "ServerURL is missing or blank.<br>";
        if (v_message != null) {
            response.getWriter().write(v_message);
            return false;
        }
  
        URL uri = new URL(serverURL);
        if (!uri.getProtocol().startsWith("https://") ||
                !uri.getHost().endsWith("salesforce.com") ||
                !(uri.getQuery().equals("")) ) {
            // protect against spoofed requests
            // must begin with https
            // host ends in salesforce.com
            // does not have a querystring
            response.getWriter().write("Not a valid API " +
                                "Server URL.<br><br>" + serverURL);
            return false;
        }
  
        // binding
        SforceServiceLocator serviceLocator = new SforceServiceLocator();
        SoapBindingStub binding = new SoapBindingStub();
        SessionHeader sh = new SessionHeader();
        sh.setSessionId(sessionId);
        String ns = serviceLocator.getServiceName().getNamespaceURI();
        binding.setHeader(ns, "SessionHeader", sh);
        binding._setProperty(SoapBindingStub.ENDPOINT_ADDRESS_PROPERTY, uri.toString());
        // TEST a SOAP call
        GetUserInfoResult userInfoResult = binding.getUserInfo();
 
        // string userid = userInfoResult.userId;
        // check if user is in your database
        // if not, you can present a signup screen
        return true;
           
    } catch (UnexpectedErrorFault e) {
        v_message = "Unable to connect to the API.<br><br>" +
                  e.getExceptionMessage();
    } catch (ApiFault e) {
        if (e.getExceptionCode().equals(ExceptionCode._API_DISABLED_FOR_ORG)) {
            v_message += "This edition of salesforce.com " +
                         "does not provide API access.<br>"
                    + "The API is a standard feature of Enterprise " +
                      "and Unlimited Editions.<br>"
                    + "Certify your application to gain API access " +
                      "to Professional Edition as well.";
 
            v_message += e.getExceptionMessage();
        }
    } catch (RemoteException e) {
        v_message = "Unable to connect to the API.<br><br>" + e.getMessage();
    } catch (MalformedURLException e) {
        v_message = "The Server URL is invalid.<br><br>" + e.getMessage();
    } catch (IOException e) {
        v_message = "Unable to write to response stream.<br><br>" + e.getMessage();
    }
    if (v_message != null) {
        try {
            response.getWriter().write(v_message);
        } catch (IOException e) {
        }
    }
   
    return false;
}