function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
pathworkspathworks 

How do you make single sign on to your composite app secure?

 
We are providing a composite offering for AppExchange, integrating our own on-demand offering (hosted separately).
 
I have the doc from salesforce about doing single sign on with a composite offering using a web tab.  Basically, it recommends you pass the username, API session token, and API server url to your application.
 
I've built a mapping from sales force user name to accounts in our own application - so given the user name I can log them in.  That's simple.  But...I am not currently doing anything to validate that the single-sign-on request is secure and cannot be spoofed.  How are others solving this problem?  Anybody can post an HTTP request that includes the same parameters - spoofing the user name, session id, and/or url.  The only thing I can imagine is that I'm supposed to make a request to the salesforce url passed (and maybe I should check that it ends in salesforce.com, or somehow validate it is not a 3rd party site?), use the session token to prove that somebody is authenticated, and then make an API request that tells me who the session is for (so I can compare it with the username on the URL and make sure that was not hacked). That's the only route I see.  I don't know what SForce call will tell me who the current user is based on the session token, but I hope it exists.
 
More detail (from a previous email) - which may clarify my question:
 
I understand how the session id that is passed to us is secure from the point of view of making calls back to salesforce.

However, the point of single-sign-on is to allow a second application (in this case, us) to effectively "automatically log the user in" based on trusting a primary application (in this case, salesforce) to assert that a user is authenticated as a particular principal.

I don't see how the tokens passed to us allow us to assure that salesforce has authenticated the user indicated in a secure way.  As stated above, we are going to log them in to our application as that user and give them full access to their data - so we have to know that it is really them, as asserted by salesforce, but in an environment where any machine or person can make arbitrary HTTP requests to our site (since we are not deployed behind a proxy at salesforce, etc.).

For a concrete example, imagine that a bank is signing up to become an AppExchange partner.  They will offer account balances and wire transfers as a tab in salesforce.  On their side, they would do some work to associate a salesforce login name (e.g. bob@companyname.com) with a bank account number or social security number.  Obviously, security is critical - when the tab is clicked on and the accounts page is displayed, it better be the current salesforce user's data!

Now, when salesforce's web tab passes the username, session id, and server url to the bank's web site, the bank has to be able to validate these tokens and "trust" that salesforce has securely authenticated the user.  Once it does that, it can then show the data from the external banking system, and allow operations on that data such as wire transfers.  I know of three methods to do this - one is to use private/public keys to encode the tokens - another is to have the second application make a return call to the originator of the tokens (similar to a kerberos model) - and a third is to put the second server behind a proxy at the same site as the first server (which is not feasible in this case).

Given salesforce's approach, what steps should the bank's application take to validate the tokens?  The username is what indicates the account to use.  However, anybody can make an HTTP request and pass a username - so it's not secure to simply check that.  The session id is interesting, but to prove that it is valid you would have to go back to a salesforce server (i.e. the token originator) to validate that is in active session.  What server serves as the token originator - perhaps that is what the server URL parameter is for?  That gave me the idea that you can use the server URL combined with the session id to verify that the session is valid on that salesforce server.  (However, in this case - how do we make sure that the URL isn't pointing to some non-salesforce server designed to provide bogus validation?)  Once we've determined that the session is indeed valid on a salesforce server, we know that there has been an authenticated session.  However, how do we know it is for the user passed on the URL?  Somebody could just as easily authenticate with the SForce API to salesforce.com, get a valid session token and server URL, and then pass that to our application along with a completely different username.

Somebody must have already built a solution for this....?
 
SuperfellSuperfell
Yes, the reason we say to pass a sessionID and serverUrl on the webtab is so that you can make an API call back to verify the session Id. In addition, as you point out you should also verify that the domain in the serverUrl ends in salesforce.com and is using HTTPS. You can use the getUserInfo call to get details of the user who initiated the session represented by the sessionId you have.
pathworkspathworks
Thanks!  I'd recommend putting this information (and sample code, perhaps) in your documentation on single-sign-on.  The doc I got does not mention that you should do this, and if a partner were to build an application without thinking of this they would have a rather large security hole!
 
Regards,
Dan
 
pathworkspathworks
 
Simon,

I looked into the getUserInfo call.  It returns a GetUserInfoResult (I'm accessing this using the Java bindings).  That object does not seem to contain the user name - at least not the name that is passed by the single-sign-on recommendation.  I see the user full name, the user id, etc., but nothing to compare to the token passed on the URL (the login name).
 
I'll look into either a) making an addition query on the user after calling getUserInfo (i.e. get the ID first, then use that to select the username) or b) passing a different token (user id) on the URL if possible.
 
Dan
 
pathworkspathworks
 
Simon,

I pretty much have this coded up but am running into an error.  My approach was this - grab the server url, session id, and username from the request parameters.  Create a SoapBindingStub using the url, set the session id into a _SessionHeader, and then call getUserInfo.  Once getUserInfo returns, it'll give me the user's ID but not the user name.  However, I can then make a query against the User object given the id and get the user name, which I can then compare to the username passed on the URL.  If that all works fine, then I can trust the authentication.
 
The server URL is:
 
I construct the binding as follows -
  SoapBindingStub binding = (SoapBindingStub)new SforceServiceLocator().getSoap(new URL(server));
and set the session id:
 
  _SessionHeader sh = new _SessionHeader();
  //set the sessionId property on the header object using
  sh.setSessionId(sessionId);
  binding.setHeader("SforceService", "SessionHeader", sh);
when I call getUser Info, I get this exception:
 
Element {urn:enterprise.soap.sforce.com)getUserInfo invalid at this location
Interestingly - if I don't pass the URL to the getSoap() call, the getUserInfo call works.  However, I think I need to pass the specific server URL I got from salesforce.
 
Any help would be appreciated.
 
Thanks,
Dan
 
SuperfellSuperfell
You appear to be building against an old WSDL. did you download the latest WSDL from the app ?
pathworkspathworks
 
I'm using the Java classes from the "getting started" area (e.g. com.sforce.soap.enterprise.*).  I downloaded them just last week.  Are those old already?
 
Thanks,
Dan
 
SuperfellSuperfell
AFAIK that should be ok, but I'd recommend always starting with the WSDL.
pathworkspathworks

Simon,


I mis-spoke - if I don't pass the URL to the SoapBindingStub, it still doesn't work, but I get a different exception on the getUserInfo call:

UNKNOWN_EXCEPTION: Destination URL not reset. The URL returned from login must be set in the SforceService

This is interesting - I don't make a login call, because I'm reusing an existing session token by setting it in the header.  I'm not sure what this means I ought to do...

FWIW, the URL on the SoapBindingStub is this when I don't pass it a URL:

https://www.salesforce.com/services/Soap/c/2.5

The URL I'm passed from the web tab is this:

https://na1.salesforce.com/services/Soap/u/7.0

Does that make a difference?  I'm not sure if the problem is incompatible client bindings (and if so, how to we guarantee forward compatibility once we do have the right version?  We'll always need to connect to the URL passed by the web tab, correct?) or if it I'm just putting the sesion token in the header improperly...or if there is more to it.

Sorry if I'm beeing slow....I'm fairly new to the SForce API.

Dan

 

SuperfellSuperfell
No idea where you got the 2.5 samples from, can you give me the URL, i'll get that fixed. You won't be able to make requests to the v7.0 API with the v2.5 stubs.

When you setup the webtab, you have to pick a version of the API for the merge field, that version # and the version of the WSDL you're using need to match. (I'd recommend downloading the current WSDL, and going forward with v7).
pathworkspathworks
 
No idea where I got it from either...but the file name was java_quickstart.zip - I downloaded it on 3/20.  I see the 7.0 quickstart now.  I'll get that and see if it makes a difference.
 
If I find the URL I used, I'll let you know.
 
Thanks,
Dan
 
pathworkspathworks
 
Simon,

I know you recommend starting from the WSDL - but I downloaded the latest quickstart and now have the 7.0 Java bindings.  Easier for me to get going (...but if somebody wants to show me how to go from WSDL-to-client-bindings in Java/Eclipse 3.1 as easy as you can with Visual Studio .NET, that would be appreciated.).
 
I am now able to set up the session id, specify the salesforce URL, and successfully call getUserInfo().  The GetUserInfoResult does not contain the user name though (any reason why not?), so I have to do one additional query against User.  Username is how we map salesforce users to our own users.  It fails one fails on the binding.query call, with the below error.  It seems to occur when the response is being deserialized.  Any ideas?
 

org.xml.sax.SAXException: Invalid element in com.sforce.soap.enterprise.sobject.SObject - type

I'll post all of my code once I get this working.  Here's the bit up to where it fails....
 
 public String getSingleSignOnUser(HttpServletRequest request) {
  String server = (String)request.getParameter("server");
  String user = (String)request.getParameter("username");
  String session = (String)request.getParameter("session");
  if (server == null || user == null || session == null) {
   return null;
  }
  // make sure the server is a salesforce.com server
  if (!server.startsWith("https://") || server.indexOf("/", 8) < 0) {
   return null; // not https or not valid path after server
  }
  if (!server.substring(0, server.indexOf("/", 8)).endsWith("salesforce.com")) {
   return null; // not a salesforce.com server!
  }
  try {
   SoapBindingStub binding = (SoapBindingStub)new SforceServiceLocator().getSoap(new URL(server));
   //create a session head object
         binding._setProperty(SoapBindingStub.ENDPOINT_ADDRESS_PROPERTY, server);
         // create a session head object
         SessionHeader sh = new SessionHeader();
         // set the sessionId property on the header object using
         sh.setSessionId(session);
         // add the header to the binding stub
         String sforceURI = new SforceServiceLocator().getServiceName().getNamespaceURI();
         binding.setHeader(sforceURI, "SessionHeader", sh);
         // get the current user id
         GetUserInfoResult userInfo = binding.getUserInfo();
         if (userInfo != null) {
          // get the profile id - then query and see if the username matches
          QueryResult qrPerson = binding.query("select Username from User"); //  where id = '" + userInfo.getUserId() + "'");
SuperfellSuperfell
You're using the enterprise stubs, but pointing it to the partner API endpoint. You'll need to change the Api ServerUrl merge field to return you the enterprise endpoint, not the partner endpoint.

I thought there was a WSDL2Java eclipse plug-in now for axis.
pathworkspathworks
 
Thanks for the quick responses.  So the partner API is a sub-set of the enterprise API?  (That would explain why getUserInfo works on the partner endpoint...?)  I'm not sure exactly what change to make to return to the enterprise endpoint before the query (and will the session id still be valid when I do that?  It needs to be - remember that I'm not logging in.)  Would you mind giving me a quick pointer or code sample?
 
If it helps, I'll post the whole solution when it's done.  I have to imagine every single composite application needs to do this....so it should be very reusable.
 
Dan
 
SuperfellSuperfell
Its not in the code, its in the definition of your webtab. In the Link URL for your webtab, you probalby have something like

http://my.server.com/saleforce?serverUrl={!API_Partner_Server_URL_70}&sid={!API_Session_ID}&username={!User_Username}

what you want is

http://my.server.com/saleforce?serverUrl={!API_Enterprise_Server_URL_70}&sid={!API_Session_ID}&username={!User_Username}

pathworkspathworks
 
Ah, never mind - I see.  You were suggesting changing the web tab configuration.   Got it. 
 
Here's the code, for what it's worth - probably wouldn't be a bad idea to include some version of this in your single-sign-on documentation. 
 
This code returns the user name if it is validated and ok - otherwise returns null.
 
 
 public String getSingleSignOnUser(HttpServletRequest request) {
  String server = (String)request.getParameter("server");
  String user = (String)request.getParameter("username");
  String session = (String)request.getParameter("session");
  if (server == null || user == null || session == null) {
   return null;
  }
  // make sure the server is a salesforce.com server
  if (!server.startsWith("https://") || server.indexOf("/", 8) < 0) {
   return null; // not https or not valid path after server
  }
  if (!server.substring(0, server.indexOf("/", 8)).endsWith("salesforce.com")) {
   return null; // not a salesforce.com server!
  }
  try {
   SoapBindingStub binding = (SoapBindingStub)new SforceServiceLocator().getSoap(new URL(server));
   //create a session head object
         binding._setProperty(SoapBindingStub.ENDPOINT_ADDRESS_PROPERTY, server);
         // create a session head object
         SessionHeader sh = new SessionHeader();
         // set the sessionId property on the header object using
         sh.setSessionId(session);
         // add the header to the binding stub
         String sforceURI = new SforceServiceLocator().getServiceName().getNamespaceURI();
         binding.setHeader(sforceURI, "SessionHeader", sh);
         // get the current user id
         GetUserInfoResult userInfo = binding.getUserInfo();
         if (userInfo != null) {
             QueryResult qrPerson = binding.query("select Username from User where id = '" + userInfo.getUserId() + "'");
          SObject [] person = (SObject [])qrPerson.getRecords();
          if (person != null && person.length == 1) {
           if (((User)person[0]).getUsername().equals(user)) {
            return user; // valid - session is for this user
           }
          }
         }
  } catch (Exception err) {
   return null;
  }
  return null;
 }

Message Edited by pathworks on 03-28-2006 02:22 PM