/* OpenSAM Single Sign On Module * * * This is a BSD style permissive license. * This module is original work by the author. * * Copyright (c) 2007, iNetOffice, Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of iNetOffice nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY iNetOffice ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL iNetOffice BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * Author: Tom Snyder, iNetOffice, Inc. */ /* -- OpenSAM_SSO.java -- * -- Version 1.00.a - updated 9/13/07. -- * * SEE: http://opensam.org/4%20SSO%20and%20Provisioning.html * * This module contains a utility routine that implements OpenSAM Single Sign On. * * Customize and integrate this module with your authentication routines. */ /* #################################################################### * TO USE: * * --- 1. Install and Compile --- * Install and compile this module into the classes dir of your webapp. * For example: * /srv/www/tomcat5/base/webapps/ROOT/WEB-INF/classes/org/opensam/sso/OpenSAM_SSO.java * javac OpenSAM_SSO.java * * --- 2. Import into your "Unauthenticated Entry Point" JSP or servlet file: * <%@ page import = "org.opensam.sso.*" %> * * --- 3. Call the authenticate() method ---- OpenSAM_SSO sso = new OpenSAM_SSO(); out.println("...calling .authenticate()"); int auth_result = OpenSAM_SSO.resultERROR; // simple value meaning "none yet". try { auth_result = sso.authenticate( request.getParameterMap() ); } catch( Exception exc ) { out.println("Exception: " + exc); } * * --- 4. Check the result --- * if( auth_result == OpenSAM_SSO.resultNOOP ) { * // No OpenSAM CGI params were found and no attempt at login was made. * // Proceed with your normal no-login session handling. * } else if( auth_result == OpenSAM_SSO.resultSUCCESS ) { * // CHECK the OUT_StorageDomainAndPath to ensure it is valid for the * // OUT_StorageUserName account. * // Establish your logged in session using OUT_StorageUserName. * // Save all the OUT_ class values to your session data for future use to make * // requests to the WebDAV server. * } * else { * // Login attempt was made but failed. Print some diagnostic info for the engineer. * out.println("...failed, result code: "+auth_result+", Err msg: "+sso.get_errorMessage() ); * } * * The possible results are: * 1 OpenSAM_SSO.resultNOOP * 2 OpenSAM_SSO.resultMISSING_PARAMS * 3 OpenSAM_SSO.resultINVALID_USERID_PASSWORD * 4 OpenSAM_SSO.resultFAILED_CHECK_STATUS_CODE * 5 OpenSAM_SSO.resultERROR * 6 OpenSAM_SSO.resultSUCCESS * * Other errors, such as a malformed StorageServerUrl, will throw the typical exceptions. */ package org.opensam.sso; import java.net.*; import java.io.*; import java.util.*; import java.util.regex.Pattern; import java.util.regex.Matcher; import org.apache.commons.httpclient.*; import org.apache.commons.httpclient.methods.*; import org.apache.commons.httpclient.auth.AuthScope; public class OpenSAM_SSO { // RESULT CODES ----------------------------------------------------- // // The authenticate() method can return these normal result codes: // // No login was attempted because no OpenSAM CGI params were found. // Simply proceed normally. public static final int resultNOOP = 1; // Some, but not enough OpenSAM CGI params were found. Report the // get_errorMessage() to the user so they can get it fixed. public static final int resultMISSING_PARAMS = 2; // A login attempt was made but rejected by the StorageServerUrl server via status 401 // Tell the user the login failed and offer them a chance to re-enter their username/password. // Once entered, resubmit to the authenticate() routine with the RequestCGIParameters MAP // holding the StorageUserName and StoragePassword filled in with the user's values. // BE SURE THIS MAP THAT IS RE-SUBMITTED DOES NOT CONTAIN ANY ORIGINAL CGI PARAMS (just delete // the CGI one and create a new one from scratch). public static final int resultINVALID_USERID_PASSWORD = 3; // Authentication failed via some HTTP status other than the usual 401. May be a // server problem. Report to user so they can relay to their support folks. public static final int resultFAILED_CHECK_STATUS_CODE = 4; // Some error occurred, probably an exception was thrown, catch the exception. public static final int resultERROR = 5; // The login attempt to the StorageServerUrl server succeeded. // This is the only value that indicates a successful login. public static final int resultSUCCESS = 6; // Used internally here. Should never be returned to a caller. private static final int resultPERFORM_THIS_MODE = 7; // // END RESULT CODES ----------------------------------------------------- // PRIVATE MEMBERS: // These to-login-or-not-to-login values are managed closely by this class: private int lastAuthenticateResult; private int httpStatusCode; // HTTP status code related to last authenticate() attempt. private String errorMessage; // Handy get-functions to retrieve various tidbits of info: // Returns the same code as the last .authenticate() call did. public int get_lastAuthenticationResult() { return( this.lastAuthenticateResult ); } // Returns the HTTP status code of the last request - 200, 207, 301, 404, etc. public int get_httpStatusCode() { return( this.httpStatusCode ); } // Returns an error message string that elaborates on any error. Not always set. public String get_errorMessage() { return( this.errorMessage ); } // OUT MEMBERS: // These members are commonly queried by the call after an authenticate() and might // be updated by the client for their own bookkeeping. public String OUT_StorageServerUrl; public String OUT_StorageUserName; public String OUT_StorageSessionId; public String OUT_StoragePassword; public String OUT_determinedWebDAVUrlParameters; public String OUT_determinedStorageDomainAndPath; // Create safe defaults: public OpenSAM_SSO() { this.lastAuthenticateResult = resultNOOP; this.httpStatusCode = 0; this.errorMessage = ""; // default value. this.OUT_StorageServerUrl = ""; // default value. this.OUT_StorageUserName = ""; // default value. this.OUT_StorageSessionId = ""; // default value. this.OUT_StoragePassword = ""; // default value. this.OUT_determinedWebDAVUrlParameters = ""; // default value. this.OUT_determinedStorageDomainAndPath = ""; // default value. } // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // // MAIN ENTRY POINT: // // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public int authenticate( Map RequestCGIParameters ) throws Exception { // // FIRST // // Analyze CGI parameters and determine what Single Sign On type we are // attempting, if any. // boolean hasStorageServerUrl = RequestCGIParameters.containsKey("StorageServerUrl"); // Salesforce SSO? this.lastAuthenticateResult = this.checkforSalesforceSSO( RequestCGIParameters ); if( this.lastAuthenticateResult == this.resultMISSING_PARAMS ) return( this.resultMISSING_PARAMS ); if( this.lastAuthenticateResult != this.resultPERFORM_THIS_MODE ) { // Now try StorageSessionId based SSO: this.lastAuthenticateResult = this.checkforSessionIdSSO(RequestCGIParameters); } if( this.lastAuthenticateResult == this.resultMISSING_PARAMS ) return( this.resultMISSING_PARAMS ); if( this.lastAuthenticateResult != this.resultPERFORM_THIS_MODE ) { // Now try UserName/Password based SSO: this.lastAuthenticateResult = this.checkforUserNamePasswordSSO(RequestCGIParameters); } if( this.lastAuthenticateResult != this.resultPERFORM_THIS_MODE ) { // No attempt to be made. Our final result is the result of that last check. if( hasStorageServerUrl && this.lastAuthenticateResult == this.resultNOOP ) { this.lastAuthenticateResult = this.resultMISSING_PARAMS; } return( this.lastAuthenticateResult ); } // All SSO types require StorageServerUrl: if( !hasStorageServerUrl ) { this.errorMessage = "Missing CGI param StorageServerUrl"; return (this.lastAuthenticateResult = this.resultMISSING_PARAMS); } // FIll in a safe, default result until we find out otherwise. This is the result // that will remain in our obj if we throw an exception. this.lastAuthenticateResult = this.resultERROR; // We've identified an SSO type and it has all the required CGI parameter values. // Our OUT_determinedWebDAVUrlParameters contains the extra pass-to-webdav CGI params we need. // // SECOND // // Misconfigured Server Pre-test // If the server allows an unauthenticated request, then it is not acting as an // HTTP authenticating server and cannot be trusted. // String StorageServerUrl_touse = this.get_the_one_cgi_param(RequestCGIParameters,"StorageServerUrl"); try { HttpClient client = new HttpClient(); System.out.println("m_StorServ: " + StorageServerUrl_touse); GetMethod method = new GetMethod(StorageServerUrl_touse); method.setFollowRedirects(true); // Execute the GET method this.httpStatusCode = client.executeMethod(method); if (this.httpStatusCode != 401) { throw new Exception("LOGIN FAILED: Misconfigured server pre-test failed, server returned status " + this.httpStatusCode + " when no credentials were provided"); } } catch( Exception exc ) { throw new Exception( "LOGIN FAILED: Misconfigured server pre-test threw exception: "+exc+ " (ServerUrl="+StorageServerUrl_touse+")" ); } // // Third // // Prepare for actual authenticated request: // FIll in the webdav CGI params. They may be "". String StorageServerUrl_withOptionalParams = StorageServerUrl_touse + this.OUT_determinedWebDAVUrlParameters; String StorageUserName_touse = this.get_the_one_cgi_param(RequestCGIParameters,"StorageUserName"); String StoragePassword_touse = ""; try { HttpClient client = new HttpClient(); // If we are doing normal Username/Password, then plug those values in: if (RequestCGIParameters.containsKey("StoragePassword")) { StoragePassword_touse = this.get_the_one_cgi_param(RequestCGIParameters,"StoragePassword"); Credentials logincreds = new UsernamePasswordCredentials(StorageUserName_touse, StoragePassword_touse); client.getState().setCredentials(AuthScope.ANY, logincreds); } GetMethod method = new GetMethod(StorageServerUrl_withOptionalParams); method.setFollowRedirects(true); // Execute the GET method this.httpStatusCode = client.executeMethod(method); } catch( Exception exc ) { throw new Exception( "LOGIN FAILED: Authenticated remote request threw exception: "+exc+ " (ServerUrl="+StorageServerUrl_withOptionalParams+")" ); } // // FOURTH // // Analyze the result: if (this.httpStatusCode == 401) { // typical bad username/password indicator: return (this.lastAuthenticateResult = resultINVALID_USERID_PASSWORD); } if (this.httpStatusCode >= 200 && this.httpStatusCode <= 299) { // SUCCESSFULL LOGIN // Return the good values in the out params: this.OUT_StorageServerUrl = StorageServerUrl_touse; this.OUT_StorageUserName = StorageUserName_touse; this.OUT_StorageSessionId = this.get_the_one_cgi_param(RequestCGIParameters,"StorageSessionId"); this.OUT_StoragePassword = StoragePassword_touse; this.OUT_determinedStorageDomainAndPath = this.determineStorageDomainAndPath( StorageServerUrl_touse ); return (this.lastAuthenticateResult = this.resultSUCCESS); } return (this.lastAuthenticateResult = this.resultFAILED_CHECK_STATUS_CODE); } private String get_the_one_cgi_param(Map RequestCGIParameters, String key) { String[] valueArray = (String[])RequestCGIParameters.get(key); if (valueArray == null) return (null); if (valueArray.length < 1) return (null); return (valueArray[0].trim()); } // check the CGI params for hints of SalesforceSSO: // See table 4.2 at http://opensam.org/4%20SSO%20and%20Provisioning.html. private int checkforSalesforceSSO(Map RequestCGIParameters) throws UnsupportedEncodingException { // Build a bit mask of what SalesForce SSO params are available. int bitmask = 0; if( RequestCGIParameters.containsKey("StorageSForceUrl") ) bitmask |= 1; if( RequestCGIParameters.containsKey("StorageSForceSessionId") ) bitmask |= 2; if( RequestCGIParameters.containsKey("StorageSForceOrg") ) bitmask |= 4; if( bitmask == 0 ) return( this.resultNOOP ); // No sforce params at all. if( bitmask != 7 ) { // We're missing an SForce param: this.errorMessage = ""; if( (bitmask & 1) == 0 ) this.errorMessage += "Missing CGI param StorageSForceUrl "; if( (bitmask & 2) == 0 ) this.errorMessage += "Missing CGI param StorageSForceSessionId "; if( (bitmask & 4) == 0 ) this.errorMessage += "Missing CGI param StorageSForceOrg "; return( this.resultMISSING_PARAMS ); } // We have all we need. Now fasion the WebDAV URL CGI params for subsequent requests: // .trimming the CGI param values is convenient since constructing the Launch Link can sometimes result in // misc spaces within the OpenSAM CGI params. Esp. with SalesForce.com's 'custom' link tools. this.OUT_determinedWebDAVUrlParameters = "?StorageSForceUrl=" + URLEncoder.encode(this.get_the_one_cgi_param(RequestCGIParameters,"StorageSForceUrl"),"UTF-8") + "&StorageSForceSessionId=" + URLEncoder.encode(this.get_the_one_cgi_param(RequestCGIParameters, "StorageSForceSessionId"), "UTF-8") + "&StorageSForceOrg=" + URLEncoder.encode(this.get_the_one_cgi_param(RequestCGIParameters, "StorageSForceOrg"), "UTF-8"); return( this.resultPERFORM_THIS_MODE ); } private int checkforSessionIdSSO(Map RequestCGIParameters) throws UnsupportedEncodingException { if (!RequestCGIParameters.containsKey("StorageSessionId")) { return (this.resultNOOP); // no SessionId means don't try this mode at all. } if (!RequestCGIParameters.containsKey("StorageUserName")) { // SessionId mode requires the UserName. this.errorMessage = "Missing CGI param StorageUserName"; return (this.resultMISSING_PARAMS); } // We have all we need. Now fasion the WebDAV URL CGI params for subsequent requests: this.OUT_determinedWebDAVUrlParameters = "?StorageUserName=" + URLEncoder.encode(this.get_the_one_cgi_param(RequestCGIParameters, "StorageUserName"), "UTF-8") + "&StorageSessionId=" + URLEncoder.encode(this.get_the_one_cgi_param(RequestCGIParameters, "StorageSessionId"), "UTF-8"); return (this.resultPERFORM_THIS_MODE); } private int checkforUserNamePasswordSSO(Map RequestCGIParameters) { // Build a bit mask of what SalesForce SSO params are available. int bitmask = 0; if (RequestCGIParameters.containsKey("StorageUserName")) bitmask |= 1; if (RequestCGIParameters.containsKey("StoragePassword")) bitmask |= 2; if (bitmask == 0) return (this.resultNOOP); // No sforce params at all. if (bitmask != 3) { // We're missing an SForce param: if ( (bitmask & 1) == 0 ) this.errorMessage = "Missing CGI param StorageUserName"; else this.errorMessage = "Missing CGI param StoragePassword"; return (this.resultMISSING_PARAMS); } return (this.resultPERFORM_THIS_MODE); } // The caller must confirm that the used WebDAV server is correct for the account. // This helper extracts out the domain and path portions which are significant. // RETURNS: string containing the domain name, tld, and path portions of the URL. // FAILURE: returns null. private String determineStorageDomainAndPath( String StorageServerUrl_touse ) { Pattern pattern_stripprotocol = Pattern.compile("^([^:]+:)?(//)?([^?]*)"); Matcher matcher_stripprotocol = pattern_stripprotocol.matcher(StorageServerUrl_touse); if( matcher_stripprotocol.find() ) { // Note that this regex is anchored at the end of the string $ so it will // skip any number of this.that hostname prefixes on the domain name. Pattern pattern_domandpath = Pattern.compile("([^./]+)\\.([^./]+)(/.*)?$"); Matcher matcher_domandpath = pattern_domandpath.matcher(matcher_stripprotocol.group(3)); if( matcher_domandpath.find() ) { String strDomain = (matcher_domandpath.group(1)!=null ? matcher_domandpath.group(1) : ""); String strTld = (matcher_domandpath.group(2)!=null ? matcher_domandpath.group(2) : ""); String strPath = (matcher_domandpath.group(3)!=null ? matcher_domandpath.group(3) : ""); return( strDomain + "." + strTld + strPath ); } } return (null); } };