package com.sap.jam.api.sample; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.security.PrivateKey; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.joda.time.DateTime; import org.opensaml.Configuration; import org.opensaml.DefaultBootstrap; import org.opensaml.common.SAMLVersion; import org.opensaml.saml2.core.Assertion; import org.opensaml.saml2.core.Attribute; import org.opensaml.saml2.core.AttributeStatement; import org.opensaml.saml2.core.AttributeValue; import org.opensaml.saml2.core.Audience; import org.opensaml.saml2.core.AudienceRestriction; import org.opensaml.saml2.core.Conditions; import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.NameID; import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.core.Subject; import org.opensaml.saml2.core.SubjectConfirmation; import org.opensaml.saml2.core.SubjectConfirmationData; import org.opensaml.saml2.core.impl.AssertionBuilder; import org.opensaml.saml2.core.impl.AssertionMarshaller; import org.opensaml.saml2.core.impl.AttributeBuilder; import org.opensaml.saml2.core.impl.AttributeStatementBuilder; import org.opensaml.saml2.core.impl.AudienceBuilder; import org.opensaml.saml2.core.impl.AudienceRestrictionBuilder; import org.opensaml.saml2.core.impl.ConditionsBuilder; import org.opensaml.saml2.core.impl.IssuerBuilder; import org.opensaml.saml2.core.impl.NameIDBuilder; import org.opensaml.saml2.core.impl.SubjectBuilder; import org.opensaml.saml2.core.impl.SubjectConfirmationBuilder; import org.opensaml.saml2.core.impl.SubjectConfirmationDataBuilder; import org.opensaml.xml.ConfigurationException; import org.opensaml.xml.io.MarshallingException; import org.opensaml.xml.schema.XSString; import org.opensaml.xml.security.x509.BasicX509Credential; import org.opensaml.xml.signature.Signature; import org.opensaml.xml.signature.SignatureConstants; import org.opensaml.xml.signature.SignatureException; import org.opensaml.xml.signature.Signer; import org.opensaml.xml.signature.impl.SignatureBuilder; import org.opensaml.xml.util.Base64; import org.opensaml.xml.util.XMLHelper; import org.w3c.dom.Element; /** * This class provides some working Java sample client code to illustrate authentication of the SAP Jam API * using an OAuth2 access token obtained from a SAML2 bearer assertion as described here: * http://help.sap.com/download/documentation/sapjam/developer/index.html#c6813927839541a19e4703c3a2564f1b.html * * This example assumes a Jam deployment with an OAuth client application and a SAML Trusted IDP configured * for the company. * * It is run by invoking the "main" method and supplying command line arguments of the form * key1=value1 key2=value2 key3=value3 ... * * The keys are: * baseUrl - Required. The base url of the Jam site e.g. https://jam4.sapjam.com. * clientKey - Required. The OAuth client key. Used as the the client_id parameter in the POST /api/v1/auth/token call * clientSecret - Optional and not recommended. The OAuth client secret. If supplied the clientKey is not included * in the SAML assertion as an attribute. It is recommended to not supply the clientSecret, but it is included * to show how SAML IDPs that cannot add assertion attributes can work with the OAuth SAML2 bearer flow. * idpId - Required. Identifier for the SAML trusted IDP. * subjectNameId - Required. The identifier for the user. Can be an email address or a unique identifier, depending on the * company type. * subjectNameIdFormat - Required. Either 'email' or 'unspecified' (without quotes). * subjectNameIdQualifier - Optional. For SuccessFactors integrated companies when using the 'unspecified' name id format, * use the value 'www.successfactors.com' (without quotes). For all else, should omit. * idpPrivateKey - Required. Base64 encoded IDP private key. * * As an example (where the idpPrivateKey is shortened) * baseUrl=https://developer.sapjam.com clientKey=i7Gb7qe9hzD4ix8D3vZ4 idpId=bo.ilic.test.idp subjectNameId=blue@berry.com subjectNameIdFormat=email idpPrivateKey=MIIEv...MsR7 * * Depending on the environment, a proxy can be configured using VM arguments supplied to the jvm. For example, * for the environment where this program was developed these are: * -Dhttp.proxyHost=proxy.van.sap.corp * -Dhttp.proxyPort=8080 * -Dhttps.proxyHost=proxy.van.sap.corp * -Dhttps.proxyPort=8080 * * The program will print to the console the OAuth access token. This can then be used easily for subsequent calls * e.g. for baseUrl=https://jam4.sapjam.com if the program produced an OAuth access token of As3UvmSz3qeCpnNvrrHZhIaYEvDXoeREtVMswcBV then * the following curl command gets the profile of the user authenticated by the access token in JSON format: * curl https://jam4.sapjam.com/api/v1/OData/Self -H "Authorization: OAuth As3UvmSz3qeCpnNvrrHZhIaYEvDXoeREtVMswcBV" -H "Accept: application/json" */ public class OAuth2SAMLWorkflowSample { //as obtained from SAP Jam docs private static final String SP_ID_JAM = "cubetree.com"; private static final String ACCESS_TOKEN_URL_PATH = "/api/v1/auth/token"; private static final String SAML2_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:saml2-bearer"; public static void main (String[] args) throws Exception { System.out.println("Proxy settings:"); System.out.println("http.proxyHost=" + System.getProperty("http.proxyHost")); System.out.println("http.proxyPort=" + System.getProperty("http.proxyPort")); System.out.println("https.proxyHost=" + System.getProperty("https.proxyHost")); System.out.println("https.proxyPort=" + System.getProperty("https.proxyPort")); Set<String> allowedKeys = new HashSet<String>(Arrays.asList( "baseUrl", "clientKey", "clientSecret", "idpId", "subjectNameId", "subjectNameIdQualifier", "subjectNameIdFormat", "idpPrivateKey")); Map<String,String> params = new LinkedHashMap<String,String>(); for (String arg : args) { String[] keyValue = arg.split("=",2); if (keyValue.length != 2) { throw new IllegalArgumentException("Command line arguments must be of the form 'key=value' with distinct arguments separated by spaces e.g. client_id=a0m client_secret=oRMFr."); } String key = keyValue[0]; if (!allowedKeys.contains(key)) { throw new IllegalArgumentException("Command line arguments: invalid key '" + key + "'. Allowed keys are: " + allowedKeys.toString()); } if (params.put(key, keyValue[1]) != null) { throw new IllegalArgumentException("Command line arugments: duplicate key '" + key + "'."); } } System.out.println("Command line arguments: " + params); String baseUrl = getRequiredParam(params, "baseUrl"); String clientKey = getRequiredParam(params, "clientKey"); String clientSecret = params.get("clientSecret"); String idpId = getRequiredParam(params, "idpId"); String subjectNameId = getRequiredParam(params, "subjectNameId"); String subjectNameIdFormat = getRequiredParam(params, "subjectNameIdFormat"); if (!subjectNameIdFormat.equals("email") && !subjectNameIdFormat.equals("unspecified")) { throw new IllegalArgumentException("Command line arguments: the value of subjectNameIdFormat must be 'email' or 'unspecified'."); } String subjectNameIdQualifier = params.get("subjectNameIdQualifier"); String idpPrivateKeyString = getRequiredParam(params, "idpPrivateKey"); PrivateKey idpPrivateKey = SignatureUtil.makePrivateKey(idpPrivateKeyString); postOAuth2AccessToken(baseUrl, clientKey, clientSecret, idpId, subjectNameId, subjectNameIdFormat, subjectNameIdQualifier, idpPrivateKey); } private static String getRequiredParam(Map<String,String> params, String key) { String value = params.get(key); if (value == null) { throw new IllegalArgumentException("Required command line argument key '" + key + "' is missing."); } return value; } /** * Creates an OAuth2 access token from a SAML bearer assertion * POST /api/v1/auth/token */ private static String postOAuth2AccessToken( String baseUrl, String clientKey, String clientSecret, String idpId, String subjectNameId, String subjectNameIdFormat, String subjectNameIdQualifier, PrivateKey idpPrivateKey) throws Exception { System.out.println("\n***************************************************************"); String urlString = baseUrl + "/api/v1/auth/token"; System.out.println("POST " + urlString); URL requestUrl = new URL(urlString); Assertion assertion = buildSAML2Assertion(baseUrl, subjectNameId, subjectNameIdFormat, subjectNameIdQualifier, idpId, clientKey, clientSecret == null); String signedAssertion = signAssertion(assertion, idpPrivateKey); System.out.println("Signed assertion: " + signedAssertion); List<Pair<String,String>> postParams = new ArrayList<Pair<String,String>>(); postParams.add(new Pair<String,String>("client_id", URLEncoder.encode(clientKey, "UTF-8"))); if (clientSecret != null) { postParams.add(new Pair<String,String>("client_secret", URLEncoder.encode(clientSecret, "UTF-8"))); } postParams.add(new Pair<String,String>("grant_type", URLEncoder.encode(SAML2_BEARER_GRANT_TYPE, "UTF-8"))); String base64SamlAssertion = new String(Base64.encodeBytes(signedAssertion.getBytes(), Base64.DONT_BREAK_LINES)); postParams.add(new Pair<String,String>("assertion", URLEncoder.encode(base64SamlAssertion, "UTF-8"))); String requestBody = joinPostBodyParams(postParams); System.out.println("Request body: " + requestBody); return postOAuth2AccessTokenHelper(requestUrl,requestBody); } private static Assertion buildSAML2Assertion( String baseUrl, String subjectNameId, String subjectNameIdFormat, String subjectNameIdQualifier, String idpId, String clientKey, boolean includeClientKeyAttribute) { // Bootstrap the OpenSAML library try { DefaultBootstrap.bootstrap(); } catch (ConfigurationException e) { } DateTime issueInstant = new DateTime(); DateTime notOnOrAfter = issueInstant.plusMinutes(10); DateTime notBefore = issueInstant.minusMinutes(10); NameID nameID = (new NameIDBuilder().buildObject()); if (subjectNameIdFormat.equals("email")) { nameID.setFormat(NameIDType.EMAIL); } else if (subjectNameIdFormat.equals("unspecified")) { nameID.setFormat(NameIDType.UNSPECIFIED); } else { throw new IllegalArgumentException("subjectNameIdFormat must be 'email' or 'unspecified'."); } if (subjectNameIdQualifier != null) { nameID.setNameQualifier(subjectNameIdQualifier); } nameID.setValue(subjectNameId); SubjectConfirmationData subjectConfirmationData = (new SubjectConfirmationDataBuilder().buildObject()); subjectConfirmationData.setRecipient(baseUrl + ACCESS_TOKEN_URL_PATH); subjectConfirmationData.setNotOnOrAfter(notOnOrAfter); SubjectConfirmation subjectConfirmation = (new SubjectConfirmationBuilder().buildObject()); subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER); subjectConfirmation.setSubjectConfirmationData(subjectConfirmationData); Subject subject = (new SubjectBuilder().buildObject()); subject.setNameID(nameID); subject.getSubjectConfirmations().add(subjectConfirmation); Issuer issuer = (new IssuerBuilder().buildObject()); issuer.setValue(idpId); Audience audience = (new AudienceBuilder().buildObject()); audience.setAudienceURI(SP_ID_JAM); AudienceRestriction audienceRestriction = (new AudienceRestrictionBuilder().buildObject()); audienceRestriction.getAudiences().add(audience); Conditions conditions = (new ConditionsBuilder().buildObject()); conditions.setNotBefore(notBefore); conditions.setNotOnOrAfter(notOnOrAfter); conditions.getAudienceRestrictions().add(audienceRestriction); Assertion assertion = (new AssertionBuilder().buildObject()); assertion.setID(UUID.randomUUID().toString()); assertion.setVersion(SAMLVersion.VERSION_20); assertion.setIssueInstant(issueInstant); assertion.setIssuer(issuer); assertion.setSubject(subject); assertion.setConditions(conditions); if (includeClientKeyAttribute) { XSString attributeValue = (XSString)Configuration.getBuilderFactory().getBuilder(XSString.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); attributeValue.setValue(clientKey); Attribute attribute = (new AttributeBuilder().buildObject()); attribute.setName("client_id"); attribute.getAttributeValues().add(attributeValue); AttributeStatement attributeStatement = (new AttributeStatementBuilder().buildObject()); attributeStatement.getAttributes().add(attribute); assertion.getAttributeStatements().add(attributeStatement); } return assertion; } /** Signs the assertion and returns the string representation of the signed assertion */ private static String signAssertion(Assertion assertion, PrivateKey privateKey) { // Build the signing credentials BasicX509Credential signingCredential = new BasicX509Credential(); signingCredential.setPrivateKey(privateKey); // Build up the signature SignatureBuilder signatureBuilder = new SignatureBuilder(); Signature signature = signatureBuilder.buildObject(); signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1); signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); signature.setSigningCredential(signingCredential); assertion.setSignature(signature); String assertionString = null; try { // Marshal the assertion AssertionMarshaller marshaller = new AssertionMarshaller(); Element element = marshaller.marshall(assertion); // Finally, sign the assertion - this must be done after marshaling Signer.signObject(signature); assertionString = XMLHelper.nodeToString(element); } catch (SignatureException e) { e.printStackTrace(); } catch (MarshallingException e) { e.printStackTrace(); } return assertionString; } private static String postOAuth2AccessTokenHelper( URL requestUrl, String requestBody) throws Exception { HttpURLConnection connection = createConnection(requestUrl); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setDoOutput(true); OutputStream output = null; try { output = connection.getOutputStream(); output.write(requestBody.getBytes("UTF-8")); } finally { if (output != null) try { output.close(); } catch (IOException e) { e.printStackTrace(); } } int responseCode = connection.getResponseCode(); System.out.println("HTTP response code: " + responseCode); InputStream is; if (connection.getResponseCode() >= 400) { is = connection.getErrorStream(); } else { is = connection.getInputStream(); } StringBuilder result = new StringBuilder(); BufferedReader in = new BufferedReader(new InputStreamReader(is)); String inputLine; while ((inputLine = in.readLine()) != null) { result.append(inputLine); } in.close(); String resultString = result.toString(); System.out.println ("Response body: " + resultString); String oauthToken = null; if (responseCode == 200) { //The OAuth2 spec actually requires a token_type parameter. //{"token_type":"bearer","access_token":"As3UvIaYEvDXoeREtmSz3qeCpnNvrrHZhVMswcBV"} int tokenStartIndex = resultString.indexOf("access_token") + "access_token".length() + 3; int tokenEndIndex = resultString.indexOf('"', tokenStartIndex); oauthToken = resultString.substring(tokenStartIndex, tokenEndIndex); System.out.println ("OAuth access token: " + oauthToken); } return oauthToken; } private static class DefaultTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} @Override public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} @Override public X509Certificate[] getAcceptedIssuers() { return null; } } private static HttpURLConnection createConnection(URL requestUrl) throws Exception { HttpURLConnection connection; if (requestUrl.getProtocol().equals("https")) { //http://stackoverflow.com/questions/1828775/httpclient-and-ssl //Nice trick (for non-production code) to create a SSL Context that accepts any cert. //This lets us avoid configuring any self-signed certificate from a test system. SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(new KeyManager[0], new TrustManager[] {new DefaultTrustManager()}, new SecureRandom()); SSLContext.setDefault(ctx); connection = (HttpsURLConnection)requestUrl.openConnection(); ((HttpsURLConnection )connection).setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String arg0, SSLSession arg1) { return true; } }); } else { connection = (HttpURLConnection)requestUrl.openConnection(); } return connection; } private static String joinPostBodyParams(List<Pair<String,String>> postParams) { StringBuilder sb = new StringBuilder(); boolean first = true; for (Pair<String,String> item : postParams) { if (first) { first = false; } else { sb.append("&"); } sb.append(item.fst()).append("=").append(item.snd()); } return sb.toString(); } }