Single-Sign-On Approaches for AppExchange User Authentication
May 2, 2006
This document is for AppExchange Developers and Partners developing Composite Applications. In the following pages we will describe how you can have your Web application appear inside the salesforce.com UI using a secure means to authenticate an AppExchange user by your application.
All Composite AppExchange applications should implement a form of Single-Sign-On between salesforce.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.
Topics
- User Actions and Your Web Application
- Handling AppExchange User Web Requests
- Developer Notes
- Sample code for C# (ASP.NET) and Java
User Actions and Your Web Application
Figure 1: Custom Web Application in an AppExchange Custom Tab
- AppExchange 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 AppExchange Web service 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 AppExchange User Authentication (AUA) as it relates to an AppExchange composite application.
Developer Notes
- Be sure to use the Partner WSDL to generate your API stubs (do not use the Enterprise WSDL). The partner WSDL is designed to work across orgs and is not dependent upon any custom org meta data.
- The link to download the Partner WSDL is found at Setup|App Setup|Integrate|AppExchange API.
Figure 2: Download WSDL Page
- Always use SSL (https://) for all API transactions.
- The AppExchange API is a key feature of the Enterprise and Unlimited Editions of salesforce.com. Your application will not normally be able to access the AppExchange Web services API of customers that are using other editions of salesforce.com (i.e., Professional Edition). As a benefit of the AppExchange Certification 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 Certification testing process. More on AppExchange Certification.
- 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.
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.
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/"")
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.
Handling AppExchange 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 AppExchange 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
|
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:
- Verifying that the Web request is in fact originating from the salesforce.com service.
- Implementing authorization logic to cross-reference the identified user against your list of authorized users.
Web Request Verification
To ensure that any AppExchange 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 salesforce.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.
Figure 4: Authorization Logic Flow
Sample Code:
The following is sample code that demonstrates this AppExchange User Authentication (Single-Sign-On) approach. A complete .NET sample application that demonstrates this concept can be downloaded from the link below:
.NET sample application (C#/ASP.NET)
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 == "") if (serverURL == null || serverURL == "") 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 = null; SessionHeader sh = new SessionHeader(); sh.setSessionId(sessionId); String ns = serviceLocator.getServiceName().getNamespaceURI(); binding.setHeader(ns, "SessionHeader", sh); // 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; } |
[1] An org is the "virtual" instance of the salesforce.com service that contains the customer specific user, customer and application data owned and managed by the customer.