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.List;
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;


/**
 * Purpose:
 * - Illustrates authentication of the SAP Jam API using an OAuth2 access token obtained from a SAML2 bearer assertion.
 * - More information at https://help.sap.com/viewer/u_collaboration_dev_help/c6813927839541a19e4703c3a2564f1b.html
 * 
 * Prerequisite:
 * - Jam deployment with an OAuth client application and a SAML Trusted IDP configured for the company.
 * 
 * Result:
 * - Prints the OAuth access token to the console. 
 * - With this access token, you'll be able to make authenticated requests to the SAP Jam API.
 * 
 * Procedure:
 * 1. Configure the REQUIRED and OPTIONAL private static final variables to your SAP Jam Collaboration instance.
 * 2. Run as a "Java Application". An OAuth access token will appear in the console.
 * 3. Copy the access token and use it to make authenticated requests to the SAP Jam API:
 *    - If you are using Cloud Platform, try it at https://developer.sapjam.com/ODataDocs/ui
 *      - e.g. Select GROUP > GET/GROUPS, scroll down to GET/GROUPS, click TRY.
 *    - Try using Postman or cURL as well:
 *      - e.g. (GET /GROUPS)
 *        - curl https://{jam_instance}.sapjam.com/api/v1/OData/Groups -H "Authorization: OAuth {OAuth2 Access Token}" -H "Accept: application/json"
 *    - Try using it in your own programs.
 */
public class OAuth2SAMLWorkflowSample {
	/** 
	 * REQUIRED: Configure to your SAP Jam Collaboration instance.
	 */
	// The base url of your Jam instance (https://{jam_instance}.sapjam.com).
	private static final String BASE_URL = "";
	
	// The OAuth client key. Used as the the client_id parameter in the POST /api/v1/auth/token call
	private static final String CLIENT_KEY = "";
	
	// Identifier for the SAML trusted IDP (https://{SAML_trusted_IDP_URL}).
	private static final String IDP_ID = "";
	
	// The identifier for the user.
	// Valid values: email address or a unique identifier.
	private static final String SUBJECT_NAME_ID = "";
	
	// Valid values: "email" or "unspecified".
	private static final String SUBJECT_NAME_ID_FORMAT = "";
	
	// Base64 encoded IDP private key.
	private static final String IDP_PRIVATE_KEY_STRING = "";
	
	/** 
	 * OPTIONAL: Configure to your SAP Jam Collaboration instance.
	 */	
	// The OAuth client secret (not recommended).
	// - If supplied the CLIENT_KEY 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.
	private static final String clientSecret = null;
	
	// For SuccessFactors integrated companies when using the "unspecified" name id format.
	// Valid value: "www.successfactors.com".
	private static final String subjectNameIdQualifier = null;
	
	/**
	 *  DO NOT CHANGE
	 */ 
    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 {
        
        if (!SUBJECT_NAME_ID_FORMAT.equals("email") && !SUBJECT_NAME_ID_FORMAT.equals("unspecified")) {
            throw new IllegalArgumentException("The value of SUBJECT_NAME_ID_FORMAT must be 'email' or 'unspecified'.");
        }
        
        PrivateKey idpPrivateKey = SignatureUtil.makePrivateKey(IDP_PRIVATE_KEY_STRING);
            
        postOAuth2AccessToken(idpPrivateKey);
    }
       
    /**
     * Creates an OAuth2 access token from a SAML bearer assertion
     * POST /api/v1/auth/token
     */
    private static String postOAuth2AccessToken(PrivateKey idpPrivateKey) throws Exception {
        
        System.out.println("\n***************************************************************");
        String urlString = BASE_URL + "/api/v1/auth/token";
        System.out.println("POST " + urlString);
  
        URL requestUrl = new URL(urlString);
        
        Assertion assertion = buildSAML2Assertion(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(CLIENT_KEY, "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(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 (SUBJECT_NAME_ID_FORMAT.equals("email")) {
            nameID.setFormat(NameIDType.EMAIL);
        } else if (SUBJECT_NAME_ID_FORMAT.equals("unspecified")) {
            nameID.setFormat(NameIDType.UNSPECIFIED);
        } else {
            throw new IllegalArgumentException("SUBJECT_NAME_ID_FORMAT must be 'email' or 'unspecified'.");
        }
        if (subjectNameIdQualifier != null) {
            nameID.setNameQualifier(subjectNameIdQualifier);
        }
        nameID.setValue(SUBJECT_NAME_ID);
        
        SubjectConfirmationData subjectConfirmationData = (new SubjectConfirmationDataBuilder().buildObject());
        subjectConfirmationData.setRecipient(BASE_URL + 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(IDP_ID);
        
        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(CLIENT_KEY);
    
            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();
    }    
    
}