Thursday, January 11, 2018

XERO Private Application | XERO Connect Using Private Application Java | Java to Connect to XERO Private Application

At first you have to create an Private Application in XERO. If you didn't yet you can follow the below link:
Connect to Xero Private Application Using Php Application
You already notified that there are two combinations of public/private key file. Public key need to create application and Private key need to sign data before send to XERO. Below is a sample Java script which will connect to XERO private application.

package com.pkm;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.FileReader;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.KeyFactory;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;

/**
 * Created by pritom on 2/01/2018.
 */
public class XeroPrivateApiUtils {
    private static final String NONCE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    private static final String ORGANIZATION_URL = "https://api.xero.com/api.xro/2.0/Organisation";

    private String sbs;
    private String path;
    private String method;
    private Map<Object, Object> configs = new HashMap<>();
    private Map<Object, Object> params = new HashMap<>();
    private Map<Object, Object> sign = new HashMap<>();
    private String KEY_FILE = null;
    private String CONSUMER_KEY = null;
    private String CONSUMER_SECRET = null;

    public static void main(String[] args) throws Exception {
        XeroPrivateApiUtils xero = new XeroPrivateApiUtils();
        HttpJavaClient.Response response = xero.execute("GET", XeroPrivateApiUtils.ORGANIZATION_URL, new HashMap(), new HashMap());
        System.out.println(response.toString());
    }

    private XeroPrivateApiUtils() {
        this.KEY_FILE = "private.key";
        this.CONSUMER_KEY = "IKAEAI8BA..........1MAOR9XBEHU";
        this.CONSUMER_SECRET = "STXPEDM..........0FWMPLIDGB6DS";

        putToMap(this.configs, "consumer_key", CONSUMER_KEY);
        putToMap(this.configs, "consumer_secret", CONSUMER_SECRET);
        putToMap(this.configs, "core_version", "2.0");
        putToMap(this.configs, "payroll_version", "1.0");
        putToMap(this.configs, "file_version", "1.0");
        putToMap(this.configs, "application_type", "Private");
    }

    HttpJavaClient.Response execute(
            String method, String path,
            Map<Object, Object> configs,
            Map<Object, Object> params
    ) throws Exception {
        this.method = method;
        this.path = path;
        this.params = params;
        putToMap(this.configs, configs);
        this.buildParameters();
        this.sign();

        Map<Object, Object> headers = new HashMap<>();
        putToMap(headers, "Accept", "application/json");
        if (this.method.equalsIgnoreCase("get")) {
            return HttpJavaClient.doGet((String) this.sign.get("signed_url"), headers);
        }
        return null;
    }

    private void sign() throws Exception {
        putToMap(this.params, "oauth_signature", this.generateSignature());
        putToMap(this.sign, "parameters", this.params);
        putToMap(this.sign, "signature", escape((String) this.params.get("oauth_signature")));
        putToMap(this.sign, "signed_url", this.path + "?" + normalizeParameters(true));
        putToMap(this.sign, "sbs", this.sbs);
    }

    private String generateSignature() throws Exception {
        switch ((String) this.params.get("oauth_signature_method")) {
            case "RSA-SHA1":
                this.sbs = escape(this.method.toUpperCase());
                this.sbs += "&" + escape(this.path);
                this.sbs += "&" + escape(this.normalizeParameters(false));
                RSAPrivateKey privateKey = getPrivateKey(KEY_FILE);
                Signature sign = Signature.getInstance("SHA1withRSA");
                sign.initSign(privateKey);
                sign.update(this.sbs.getBytes("UTF-8"));
                return encodeBase64(sign.sign());
            case "HMAC-SHA1":
                String secret = this.configs.get("consumer_secret") + "&";
                if (this.configs.get("access_token_secret") != null) {
                    secret = secret + this.configs.get("access_token_secret");
                }
                this.sbs = escape(this.method.toUpperCase());
                this.sbs += "&" + escape(this.path);
                this.sbs += "&" + escape(this.normalizeParameters(false));
                return hmacSha(secret, this.sbs);
            default:
                throw new Exception("Undefined signature method");
        }
    }

    private static String hmacSha(String key, String value) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "HmacSHA1");
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(keySpec);
        byte[] result = mac.doFinal(value.getBytes());
        return encodeBase64(result);
    }

    public static String encodeBase64(String o) {
        BASE64Encoder base64Encoder = new BASE64Encoder();
        return base64Encoder.encodeBuffer(o.getBytes()).replace("\r\n", "").replace("\n", "");
    }

    private static String encodeBase64(byte[] o) {
        BASE64Encoder base64Encoder = new BASE64Encoder();
        return base64Encoder.encodeBuffer(o).replace("\r\n", "").replace("\n", "");
    }

    private static RSAPrivateKey getPrivateKey(String fileName) throws Exception {
        byte[] encoded = decodeBase64(getKey(fileName));
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
        return (RSAPrivateKey) kf.generatePrivate(keySpec);
    }

    private static byte[] decodeBase64(String o) throws Exception {
        BASE64Decoder decoder = new BASE64Decoder();
        return decoder.decodeBuffer(o);
    }

    private String normalizeParameters(Boolean filter) throws Exception {
        String normalized = "";
        for (Object o : this.params.entrySet()) {
            Map.Entry me2 = (Map.Entry) o;
            String key = me2.getKey().toString();
            Object value = me2.getValue();
            Boolean allow = !(key.equalsIgnoreCase("xml") && filter) && !key.endsWith("_secret");
            if (allow) {
                normalized += escape(key) + "=" + escape(value.toString()) + "&";
            }
        }
        return normalized.length() > 0 ? normalized.substring(0, normalized.length() - 1) : normalized;
    }

    private static String escape(String context) throws Exception {
        context = rawUrlEncode(context);
        context = stringReplace("+", "%20", context);
        context = stringReplace("!", "%21", context);
        context = stringReplace("*", "%2A", context);
        context = stringReplace("\'", "%27", context);
        context = stringReplace("(", "%28", context);
        context = stringReplace(")", "%29", context);
        return context;
    }

    private static String rawUrlEncode(String context) throws Exception {
        return URLEncoder.encode(context, "UTF-8");
    }

    private static String rawUrlDecode(String context) throws Exception {
        return URLDecoder.decode(context, "UTF-8");
    }

    private static String stringReplace(String search, String fill, String context) {
        return context.replace(search, fill);
    }

    private void buildParameters() {
        putToMap(this.params, "oauth_nonce", shuffle(5));
        putToMap(this.params, "oauth_token", CONSUMER_KEY);
        putToMap(this.params, "oauth_version", "1.0");
        putToMap(this.params, "oauth_timestamp", "" + ((System.currentTimeMillis()) / 1000));
        putToMap(this.params, "oauth_consumer_key", CONSUMER_KEY);
        putToMap(this.params, "oauth_signature_method", "RSA-SHA1");
        this.params = new TreeMap(this.params); //Sorted map by key
    }

    private static String shuffle(Integer length) {
        List<Character> characters = new ArrayList<Character>();
        for(char c : NONCE.toCharArray()){
            characters.add(c);
        }
        StringBuilder output = new StringBuilder(length);
        while(length != 0) {
            int randPicker = (int) (Math.random() * characters.size());
            output.append(characters.get(randPicker));
            length --;
        }
        return output.toString();
    }

    private static String getKey(String filename) throws Exception {
        String key = "";
        BufferedReader br = new BufferedReader(new FileReader(filename));
        String line;
        while ((line = br.readLine()) != null) {
            if (!line.startsWith("-")) {
                key += line + "\n";
            }
        }
        br.close();
        return key;
    }

    private static void putToMap(Map<Object, Object> map, Map fromMap) {
        for (Object key : fromMap.keySet().toArray()) {
            putToMap(map, key.toString(), fromMap.get(key));
        }
    }

    private static void putToMap(Map<Object, Object> map, Object key, Object value) {
        map.put(key, value);
    }

    private static void println(Object o) {
        System.out.println("" + o);
    }

    private static void exception() throws Exception {
        throw new Exception();
    }
}
You can download a sample Private Key File from this link
And output? Obviously as below:
{
  "Id": "966aed9c-....-4d86-....-c59a6b34eb2c",
  "Status": "OK",
  "ProviderName": "Dev Private",
  "DateTimeUTC": "\/Date(1515683933534)\/",
  "Organisations": [
    {
      "APIKey": "WXP9HDM..........ILMSZHZIUSIA5",
      "Name": "Dev  Organisation",
      "LegalName": "Dev  Organisation",
      "PaysTax": true,
      "Version": "AU",
      "OrganisationType": "COMPANY",
      "BaseCurrency": "AUD",
      "CountryCode": "AU",
      "IsDemoCompany": false,
      "OrganisationStatus": "ACTIVE",
      "FinancialYearEndDay": 30,
      "FinancialYearEndMonth": 6,
      "SalesTaxBasis": "ACCRUALS",
      "SalesTaxPeriod": "QUARTERLY1",
      "DefaultSalesTax": "Tax Exclusive",
      "DefaultPurchasesTax": "Tax Inclusive",
      "CreatedDateUTC": "\/Date(1514794572000)\/",
      "OrganisationEntityType": "COMPANY",
      "Timezone": "AUSEASTERNSTANDARDTIME",
      "ShortCode": "!wnfkR",
      "OrganisationID": "91de7ce4-....-48e8-....-74573ab8b4e0",
      "LineOfBusiness": "tests xero integration",
      "Addresses": [],
      "Phones": [],
      "ExternalLinks": [],
      "PaymentTerms": {}
    }
  ]
}