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.
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.
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
|
-
Click App Setup|Build|Custom
Tabs
-
Select the
Web Tabs node, and then click the New
button
-
Select the page type
(use default)
-
For the
Tab Type, use URL; enter the Tab
Label and select any of the available Tab Styles. Click
Next.
-
Enter the URL — see
image below —
-
In the Link URL field, type the website address:
https://appexchange.opsource.net/appexchange.aspx
-
Our example requires a few more parameters to work properly:
-
Add "?session=" to the url in the Link URL field
-
In the Select Field Type dropdown selectAPI Fields
-
In the Select Field dropdown select API Session ID
-
Copy {!API_Partner_Server_URL_70} from the Copy Merge Field Value and paste it at the end of the Link URL field
-
Add "&server=" to the url in the Link URL field
-
Select API Partner Server URL 7.0 in the Select Field dropdown
-
Copy {!User_Session_ID} in Copy Merge Field Value and paste it at the end of the Link URL field
-
Click Next and then Save for the remaining pages of the wizard.
-
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:
-
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 == "")
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 = 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;
}
|
|