SSO with SAP Logon Tickets and Java

To validate/verify a SAP Logon Ticket in a non-SAP Java environment you have to call into native libraries. Fortunately there’s some Java sample code provided in the SAPSSOEXT library archive.

Go to service.sap.com (you need a valid user and download permissions) -> Download -> Support Packages and Patches -> Entry by Application Group -> Additional Components, follow the SAPSECULIB and SAPSSOEXT links and download the library versions for your operating system. You’ll also need SAPCAR to extract those .SAR files. Both libraries – sapsecu.dll and sapssoext.dll – are needed to validate a SAP Logon Ticket.

What to do if you get a java.lang.UnsatisfiedLinkError: getVersion

Make sure both (sapsecu.dll and sapssoext.dll) libraries are accessible from your “java.library.path” system property. On Windows servers you may want to copy them to c:\windows\system32.
If you’re still seeing this error you may have renamed the class file. You have to leave it at SSO2Ticket. The provided Java sample SSO2Ticket.java doesn’t come with any package statement so make sure you dont’ use a package statement at all or if you don’t like classes in the default package use com.mysap.sso. It won’t work with your own package path as it’s natively tied with JNI to a library.

package com.mysap.sso;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;

public class SSO2Ticket {
...

Ticket and server certificate

The provided sample ticket is useless as it won’t validate without the correct certificate. You have to extract your own sample ticket from a HTTP header (i.e. using the sample below or a browser extension like Live HTTP Headers). You also need the verify.pse certificate which your administrator can extract from the keystore.

Obtaining a SAP Logon Ticket with validation

This code sample receives a SAP Logon Ticket from a SAP Enterprise Portal server and validates it using the SAPSSOEXT and SAPSECU libraries. It uses the SSO2Ticket class for the native calls.

package com.trick77.sapsso;

import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.mysap.sso.SSO2Ticket;

/**
 * Extracts a SAP Logon Ticket by remotely logging into SAP Enterprise Portal and validates the received ticket using
 * native SAPSSOEXT and SAPSECU libraries.
 * 
 * @author Jan
 */
public class TicketSample {

	/**
	 * SAP Enterprise Portal host and port number if needed.
	 */
	static final String PORTAL_HOST = "myepserver:56800";

	/**
	 * Valid Enterprise Portal username.
	 */
	static final String PORTAL_USER_NAME = "username";

	/**
	 * Valid SAP Enterprise Portal password.
	 */
	static final String PORTAL_PASSWORD = "givemeticket;

	/**
	 * Path and filename of the exported certificate.
	 */
	static final String CERTIFICATE_FILENAME = "c:/temp/verify.pse";

	/**
	 * Password needed to extract the certificate (same as used when the certificate was created). Can be null but
	 * shouldn't for security reasons.
	 */
	static final String CERTIFICATE_PASSWORD = null;

	public static void main(String[] args) throws Exception {
		// Skip login dialog by sending login form data directly to the server.
		URL _url = new URL("http://" + PORTAL_HOST
				+ "/irj/portal?login_submit=on&login_do_redirect=1&no_cert_storing=on&j_user=" + PORTAL_USER_NAME
				+ "&j_password=" + PORTAL_PASSWORD + "&j_authscheme=default&uidPasswordLogon=Log+on");
		System.out.print("Establishing connection to remote server...");
		HttpURLConnection _conn = (HttpURLConnection) _url.openConnection();
		try {
			if (_conn.getResponseCode() != 200) {
				throw new Exception("Server responded with status code " + _conn.getResponseCode());
			}
			System.out.println("OK");
		}
		catch (ConnectException cex) {
			throw new Exception("Unable to connect using URL " + _url.toString());
		}

		Collection _cookies = null;
		for (Iterator _iter = _conn.getHeaderFields().entrySet().iterator(); _iter.hasNext();) {
			// Can't use getHeaderField("Set-Cookie") as it won't return duplicate "Path=" entries
			Map.Entry _entry = (Map.Entry) _iter.next();
			// System.out.println(_entry.getKey() + " -> " + _entry.getValue());
			String _key = (String) _entry.getKey();
			if (_key != null && _key.toLowerCase().equals("set-cookie")) {
				_cookies = (Collection) _entry.getValue();
				break;
			}
		}

		// Extracting the SAP Logon Ticket from the cookies.
		String _ticket = null;
		for (Iterator _iter = _cookies.iterator(); _iter.hasNext();) {
			String _cookieStr = (String) _iter.next();
			Pattern _pattern = Pattern.compile("MYSAPSSO2=(.*?);");
			Matcher _matcher = _pattern.matcher(_cookieStr);
			if (_matcher.find()) {
				_ticket = _matcher.group(1);
				break;
			}
		}

		File _pab = new File(CERTIFICATE_FILENAME);
		if (!_pab.exists()) {
			throw new Exception(CERTIFICATE_FILENAME + " does not exist");
		}
		if (_ticket == null) {
			throw new Exception(
					"Ticket not found in HTTP header. Authentication failed? Paste this URL in your web browser to check:\n"
							+ _url.toString());
		}
		System.out.println("Version of SAPSSOEXT: " + SSO2Ticket.getVersion());

		try {
			// You have to declare init public in SSO2Ticket.java in order to access it from here
			SSO2Ticket.init(SSO2Ticket.SECLIBRARY);
			Object _obj[] = SSO2Ticket.evalLogonTicket(_ticket, _pab.getAbsolutePath(), null);
			// You have to declare PrintResults public in SSO2Ticket.java in order to access it from here
			SSO2Ticket.PrintResults((String) _obj[0], (String) _obj[1], (String) _obj[2], SSO2Ticket.parseCertificate(
					(byte[]) _obj[3], SSO2Ticket.ISSUER_CERT_SUBJECT), SSO2Ticket.parseCertificate((byte[]) _obj[3],
					SSO2Ticket.ISSUER_CERT_ISSUER), _ticket, (String) _obj[4], (String) _obj[5], (String) _obj[6]);
		}
		catch (Exception ex) {
			System.out.println(ex);

			// Extract the error codes in the exception and map the codes to an error text
			StringWriter _sw = new StringWriter();
			PrintWriter _ps = new PrintWriter(_sw);
			ex.printStackTrace(_ps);

			int _stdError = -1;
			int _ssfError = -1;
			StringTokenizer _st = new StringTokenizer(_sw.toString());
			while (_st.hasMoreTokens()) {
				String _token = _st.nextToken();
				if (_token.equals("error=")) {
					StringBuffer _numberStr = new StringBuffer(_st.nextToken());
					for (int i = 0; i < _numberStr.length(); i++) {
						if (_numberStr.charAt(i) < '0' || _numberStr.charAt(i) > '9') {
							_numberStr.deleteCharAt(i);
						}
					}
					if (_stdError == -1) {
						_stdError = Integer.parseInt(_numberStr.toString());
					}
					else if (_stdError > -1) {
						_ssfError = Integer.parseInt(_numberStr.toString());
						break;
					}
				}
			}
			System.out.println(getStdErrorMessage(_stdError) + " - " + getSsfErrorMessage(_ssfError));
		}
	}

	static String getStdErrorMessage(int aCode) {
		switch (aCode) {
		case 0:
			return "Ok";
		case 1:
			return "No user provided to function (1)";
		case 3:
			return "Provided buffer too small (3)";
		case 4:
			return "Ticket expired (4)";
		case 5:
			return "Ticket syntactically invalid (5)";
		case 6:
			return "Provided buffer too small (6)";
		case 8:
			return "No ticket provided to function (8)";
		case 9:
			return "Internal error (9)";
		case 11:
			return "Memory allocation failed (11)";
		case 12:
			return "Wrong action (12)";
		case 13:
			return "Tried to call nullpointer function (13)";
		case 14:
			return "Error occurred in security ticket (14)";
		case 15:
			return "Pointer was null (15)";
		case 16:
			return "Incomplete information in ticket (16)";
		case 17:
			return "Couldn't get certificate (17)";
		case 19:
			return "Missing authorization (19)";
		case 20:
			return "Signature couldn't be verified (20)";
		case 21:
			return "Ticket too new for library (21)";
		case 22:
			return "Conversion error (22)";
		default:
			return "Unknown standard error (" + aCode + ")";
		}
	}

	static String getSsfErrorMessage(int aCode) {
		switch (aCode) {
		case 0:
			return "No SSF error (0)";
		case 1:
			return "No security toolkit found (1)";
		case 2:
			return "Unknown wrapper format (2)";
		case 3:
			return "Input data length zero (3)";
		case 4:
			return "Insufficient main memory (4)";
		case 5:
			return "There are signer errors (5)";
		case 6:
			return "No memory for result list (6)";
		case 7:
			return "Private address book (PAB) not found (7)";
		case 8:
			return "Invalid PAB password (8)";
		case 9:
			return "There are recipient errors (9)";
		case 10:
			return "Unknown MD algorithm (10)";
		case 11:
			return "Could not encode output (11)";
		case 12:
			return "Could not decode input (12)";
		case 13:
			return "Unknown security toolkit error (13)";
		case 21:
			return "Security profile is locked (21)";
		case 22:
			return "Unknown signer or recipient (22)";
		case 23:
			return "Security profile not found (23)";
		case 24:
			return "Security profile not usable (24)";
		case 25:
			return "Invalid password for signer (25)";
		case 26:
			return "Certificate not found (26)";
		case 27:
			return "Signature invalid or operation not OK for signer (27)";
		case 51:
			return "No more memory output data (51)";
		case 52:
			return "Signer/recipient ID is empty (52)";
		case 53:
			return "Signer/recipient info empty (53)";
		case 54:
			return "Signer/recipient info list empty (54)";
		default:
			return "Unknown SSF error (" + aCode + ")";
		}
	}
}

Pure Java ticket validation solution

I came across a pure Java SAP Logon Ticket validation solution and looking at the source code it looks pretty well. I haven’t tested it though and even if it works you won’t have any SAP support with this solution. But it might be worth a try.

12 thoughts on “SSO with SAP Logon Tickets and Java

  1. I am getting a return error of 1:

    “No user provided to function (1)”;

    Can’t figure out what the problem could be.

    Any suggestions?

  2. How can you get a SAP logon ticket delivered by the AS Java itselft without a Portal ticket issuer installed? Is this possible?

  3. hi,

    I am getting a return error of 5:
    Ticket syntactically invalid (5);

    What could be the reason ?

    If I copy the ticket content from the Firefox Browser and run the java class from the OS. I am able to extract details pertaining to the user.

  4. HI,
    what I cud make out from this discussion is to use this code and along with it, use the SSO2Ticket.java as supplied by SAP in sapext library.
    I am using eclipse java perspective and used you code as a java class and second java class is sso2ticket.java.
    I put my .dll file in c:/windows/system 32.
    but when i run the code i am getting the following error:
    Establishing connection to remote server…OK
    SAPSSOEXT loaded.
    static part ends.

    java.lang.UnsatisfiedLinkError: getVersion
    at com.mysap.sso.SSO2Ticket.SSO2Ticket.getVersion(Native Method)
    at com.trick77.sapsso.TicketSample.main(TicketSample.java:98)
    Exception in thread “main”
    Please help me with what is missing. I understand that unsatisfiedlink error is coming because of native function declaration in our class. but how to resolve.
    ankur

  5. Jan,
    is it possible for you to explain it further.

    i tried at looking System Property
    file.encoding

    it is specified as UTF-8

    from both the sources.

  6. Jan, would it be possible to eliminate the ticket expired class of error (StdErrMessage 4) if we already had a logon ticket, and continuously pass that to the Enterprise Portal before the ticket expires (by default in 8 hours)? So the user supplies the password only once, your program retrieves a valid ticket, then we continuously get a new logon ticket with the previous logon ticket.

    This should work as long as mysapcomusesso2cookie in transaction SSO2 is set to 1, I believe.

    If true, then another trick77 :-)

  7. Anthony, I’m not sure I understand what you’re trying to achieve. I’m using the LogonTicket (actually, it’s an AssertionTicket which is only valid for 120 seconds) to validate the caller in a non-SAP backend system using web services. I don’t know if SAP EP can be forced to accept a LogonTicket to have some kind of 8h-lasting browser-based SSO. Doesn’t sound to me like something I would want to use in a production environment if that is what you’re up to :-)

    Cheers,
    Jan

  8. We recently upgraded to 64 Bit Windows/Intel-Xeon workstations. My jboss setup was migrated ovber from the 32 bit machine. I seem to get the, java.lang.UnsatisfiedLinkError: no sapssoext in java.library.path, error all the time now. We are looking for a solution in the DLL set of files that should be in the windows/system32 folder. We are on 2.1.8 JCO and cannot find the DLL set for my computer setup.

    ????

  9. Thanks a lot for the post. It has helped me a lot in parsing the token. Please let me know if i could use the Parsing token logic in LoginModule in Java application.
    Because the Web portal that i am developing can be accessed from SAP and also from another Web application.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>