com.elevenpaths.latch.LatchAuth.java Source code

Java tutorial

Introduction

Here is the source code for com.elevenpaths.latch.LatchAuth.java

Source

/*Latch Java SDK - Set of  reusable classes to  allow developers integrate Latch on their applications.
Copyright (C) 2013 Eleven Paths
    
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
    
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.
    
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA*/

package com.elevenpaths.latch;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.ning.http.util.Base64;

/**
 * This class models an allows the user to make signed request the Latch API
 * Use the methods inside LatchAPI and LatchUser class.
 */
public class LatchAuth {
    protected static final String API_VERSION = "1.0";
    public static String API_HOST = "https://latch.elevenpaths.com";

    //App API
    public static final String API_CHECK_STATUS_URL = "/api/" + API_VERSION + "/status";
    public static final String API_PAIR_URL = "/api/" + API_VERSION + "/pair";
    public static final String API_PAIR_WITH_ID_URL = "/api/" + API_VERSION + "/pairWithId";
    public static final String API_UNPAIR_URL = "/api/" + API_VERSION + "/unpair";
    public static final String API_LOCK_URL = "/api/" + API_VERSION + "/lock";
    public static final String API_UNLOCK_URL = "/api/" + API_VERSION + "/unlock";
    public static final String API_HISTORY_URL = "/api/" + API_VERSION + "/history";
    public static final String API_OPERATION_URL = "/api/" + API_VERSION + "/operation";

    //User API
    public static final String API_APPLICATION_URL = "/api/" + API_VERSION + "/application";
    public static final String API_SUBSCRIPTION_URL = "/api/" + API_VERSION + "/subscription";

    public static final String X_11PATHS_HEADER_PREFIX = "X-11paths-";
    private static final String X_11PATHS_HEADER_SEPARATOR = ":";

    public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
    public static final String DATE_HEADER_NAME = X_11PATHS_HEADER_PREFIX + "Date";
    public static final String AUTHORIZATION_METHOD = "11PATHS";
    private static final String AUTHORIZATION_HEADER_FIELD_SEPARATOR = " ";

    public static final String UTC_STRING_FORMAT = "yyyy-MM-dd HH:mm:ss";

    private static final String HMAC_ALGORITHM = "HmacSHA1";

    private static final String CHARSET_ASCII = "US-ASCII";
    private static final String CHARSET_ISO_8859_1 = "ISO-8859-1";
    private static final String CHARSET_UTF_8 = "UTF-8";
    private static final String HTTP_METHOD_GET = "GET";
    private static final String HTTP_METHOD_DELETE = "DELETE";
    private static final String HTTP_HEADER_CONTENT_LENGTH = "Content-Length";
    private static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type";
    private static final String HTTP_HEADER_CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded";
    private static final String PARAM_SEPARATOR = "&";
    private static final String PARAM_VALUE_SEPARATOR = "=";

    public static void setHost(String host) {
        API_HOST = host;
    }

    /**
     * The custom header consists of three parts, the method, the appId and the signature
     * This method returns the specified part if it exists.
     * @param part The zero indexed part to be returned
     * @param header The HTTP header value from which to extract the part
     * @return the specified part from the header or an empty string if not existent
     */
    private static final String getPartFromHeader(int part, String header) {
        if (header != null) {
            String[] parts = header.split(AUTHORIZATION_HEADER_FIELD_SEPARATOR);
            if (parts.length > part) {
                return parts[part];
            }
        }
        return "";
    }

    /**
     *
     * @param authorizationHeader Authorization HTTP Header
     * @return the Authorization method. Typical values are "Basic", "Digest" or "11PATHS"
     */
    public static final String getAuthMethodFromHeader(String authorizationHeader) {
        return getPartFromHeader(0, authorizationHeader);
    }

    /**
     *
     * @param authorizationHeader Authorization HTTP Header
     * @return the requesting application Id. Identifies the application using the API
     */
    public static final String getAppIdFromHeader(String authorizationHeader) {
        return getPartFromHeader(1, authorizationHeader);
    }

    /**
     *
     * @param authorizationHeader Authorization HTTP Header
     * @return the signature of the current request. Verifies the identity of the application using the API
     */
    public static final String getSignatureFromHeader(String authorizationHeader) {
        return getPartFromHeader(2, authorizationHeader);
    }

    protected String appId;
    protected String secretKey;

    public JsonElement HTTP_GET(String URL, Map<String, String> headers) {
        return HTTP(URL, "GET", headers, null);
    }

    public JsonElement HTTP_POST(String URL, Map<String, String> headers, Map<String, String> data) {
        return HTTP(URL, "POST", headers, data);
    }

    public JsonElement HTTP_DELETE(String URL, Map<String, String> headers) {
        return HTTP(URL, "DELETE", headers, null);
    }

    public JsonElement HTTP_PUT(String URL, Map<String, String> headers, Map<String, String> data) {
        return HTTP(URL, "PUT", headers, data);
    }

    protected LatchResponse HTTP_GET_proxy(String url) {
        try {
            return new LatchResponse(HTTP_GET(API_HOST + url, authenticationHeaders("GET", url, null, null)));
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    protected LatchResponse HTTP_POST_proxy(String url) {
        return HTTP_POST_proxy(url, new HashMap<String, String>());
    }

    protected LatchResponse HTTP_POST_proxy(String url, Map<String, String> data) {
        try {
            return new LatchResponse(
                    HTTP_POST(API_HOST + url, authenticationHeaders("POST", url, null, data), data));
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    protected LatchResponse HTTP_DELETE_proxy(String url) {
        try {
            return new LatchResponse(HTTP_DELETE(API_HOST + url, authenticationHeaders("DELETE", url, null, null)));
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    protected LatchResponse HTTP_PUT_proxy(String url, Map<String, String> data) {
        try {
            return new LatchResponse(HTTP_PUT(API_HOST + url, authenticationHeaders("PUT", url, null, data), data));
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    /**
     *
     * @param data the string to sign
     * @return base64 encoding of the HMAC-SHA1 hash of the data parameter using {@code secretKey} as cipher key.
     */
    private String signData(String data) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), HMAC_ALGORITHM);
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            mac.init(keySpec);
            return Base64.encode(mac.doFinal(data.getBytes(CHARSET_ISO_8859_1))); // data is ASCII except HTTP header values which can be ISO_8859_1
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Calculates the headers to be sent with a request to the API so the server
     * can verify the signature
     * <p>
     * Calls {@link #authenticationHeaders(String, String, Map, Map, String)}
     * with the current date as {@code utc}.
     * @param method The HTTP request method.
     * @param querystring The urlencoded string including the path (from the
     *        first forward slash) and the parameters.
     * @param xHeaders The HTTP request headers specific to the API, excluding
     *        X-11Paths-Date. null if not needed.
     * @param params The HTTP request params. Must be only those to be sent in
     *        the body of the request and must be urldecoded. null if not
     *        needed.
     * @return A map with the {@value AUTHORIZATION_HEADER_NAME} and {@value
     *         DATE_HEADER_NAME} headers needed to be sent with a request to the
     *         API.
     * @throws UnsupportedEncodingException If {@value CHARSET_UTF_8} charset is
     *         not supported.
     */
    private final Map<String, String> authenticationHeaders(String method, String querystring,
            Map<String, String> xHeaders, Map<String, String> params) throws UnsupportedEncodingException {
        return authenticationHeaders(method, querystring, xHeaders, params, getCurrentUTC());
    }

    /**
     *
     * Calculate the authentication headers to be sent with a request to the API
     * @param HTTPMethod the HTTP Method, currently only GET is supported
     * @param queryString the urlencoded string including the path (from the first forward slash) and the parameters
     * @param xHeaders HTTP headers specific to the 11-paths API, excluding X-11Paths-Date. null if not needed.
     * @param utc the Universal Coordinated Time for the X-11Paths-Date HTTP header
     * @return a map with the Authorization and X-11Paths-Date headers needed to sign a Latch API request
     */
    private final Map<String, String> authenticationHeaders(String HTTPMethod, String queryString,
            Map<String, String> xHeaders, Map<String, String> params, String utc)
            throws UnsupportedEncodingException {
        StringBuilder stringToSign = new StringBuilder();
        stringToSign.append(HTTPMethod.toUpperCase().trim());
        stringToSign.append("\n");
        stringToSign.append(utc);
        stringToSign.append("\n");
        stringToSign.append(getSerializedHeaders(xHeaders));
        stringToSign.append("\n");
        stringToSign.append(queryString.trim());
        if (params != null && !params.isEmpty()) {
            String serializedParams = getSerializedParams(params);
            if (serializedParams != null && serializedParams.length() != 0) {
                stringToSign.append("\n");
                stringToSign.append(serializedParams);
            }
        }
        String signedData = signData(stringToSign.toString());
        String authorizationHeader = new StringBuilder(AUTHORIZATION_METHOD)
                .append(AUTHORIZATION_HEADER_FIELD_SEPARATOR).append(this.appId)
                .append(AUTHORIZATION_HEADER_FIELD_SEPARATOR).append(signedData).toString();

        HashMap<String, String> headers = new HashMap<String, String>();
        headers.put(AUTHORIZATION_HEADER_NAME, authorizationHeader);
        headers.put(DATE_HEADER_NAME, utc);
        return headers;
    }

    /**
     * Prepares and returns a string ready to be signed from the 11-paths specific HTTP headers received
     * @param xHeaders a non necessarily ordered map of the HTTP headers to be ordered without duplicates.
     * @return a String with the serialized headers, an empty string if no headers are passed, or null if there's a problem
     * such as non specific 11paths headers
     */
    private String getSerializedHeaders(Map<String, String> xHeaders) {
        if (xHeaders != null) {
            TreeMap<String, String> sortedMap = new TreeMap<String, String>();
            for (String key : xHeaders.keySet()) {
                if (!key.toLowerCase().startsWith(X_11PATHS_HEADER_PREFIX.toLowerCase())) {
                    //TODO: Log this better
                    System.err.println("Error serializing headers. Only specific " + X_11PATHS_HEADER_PREFIX
                            + " headers need to be singed");
                }
                sortedMap.put(key.toLowerCase(), xHeaders.get(key).replace("\n", " "));
            }
            StringBuilder serializedHeaders = new StringBuilder();
            for (String key : sortedMap.keySet()) {
                serializedHeaders.append(key).append(X_11PATHS_HEADER_SEPARATOR).append(sortedMap.get(key))
                        .append(" ");
            }
            return serializedHeaders.toString().trim();
        } else {
            return "";
        }
    }

    /**
     * Prepares and returns a string ready to be signed from the params of an
     * HTTP request
     * <p>
     * The params must be only those included in the body of the HTTP request
     * when its content type is application/x-www-urlencoded and must be
     * urldecoded.
     * @param params The params of an HTTP request.
     * @return A serialized representation of the params ready to be signed.
     *         null if there are no valid params.
     * @throws UnsupportedEncodingException If {@value CHARSET_UTF_8} charset is
     *         not supported.
     */
    private String getSerializedParams(Map<String, String> params) throws UnsupportedEncodingException {
        String rv = null;
        if (params != null && !params.isEmpty()) {
            TreeMap<String, String> sortedParams = new TreeMap<String, String>();
            for (String key : params.keySet()) {
                if (key != null && params.get(key) != null) {
                    sortedParams.put(key, params.get(key));
                }
            }
            StringBuilder serializedParams = new StringBuilder();
            for (String key : sortedParams.keySet()) {
                serializedParams.append(URLEncoder.encode(key, CHARSET_UTF_8));
                serializedParams.append(PARAM_VALUE_SEPARATOR);
                serializedParams.append(URLEncoder.encode(sortedParams.get(key), CHARSET_UTF_8));
                if (!key.equals(sortedParams.lastKey())) {
                    serializedParams.append(PARAM_SEPARATOR);
                }
            }
            if (serializedParams.length() > 0) {
                rv = serializedParams.toString();
            }
        }
        return rv;
    }

    /**
     *
     * @return a string representation of the current time in UTC to be used in a Date HTTP Header
     */
    private final String getCurrentUTC() {
        final SimpleDateFormat sdf = new SimpleDateFormat(UTC_STRING_FORMAT);
        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
        return sdf.format(new Date());

    }

    /**
     * Makes an HTTP request
     * @param URL The request URL.
     * @param method The request method.
     * @param headers Headers to add to the HTTP request.
     * @param data Parameters to add to the HTTP request body.
     * @return The server's JSON response or null if something has gone wrong.
     */
    private JsonElement HTTP(String URL, String method, Map<String, String> headers, Map<String, String> data) {

        JsonElement rv = null;
        InputStream is = null;
        OutputStream os = null;
        InputStreamReader isr = null;

        try {

            URL theURL = new URL(URL);
            HttpURLConnection theConnection = (HttpURLConnection) theURL.openConnection();

            theConnection.setRequestMethod(method);

            if (headers != null && !headers.isEmpty()) {
                Iterator<String> iterator = headers.keySet().iterator();
                while (iterator.hasNext()) {
                    String headerName = iterator.next();
                    theConnection.setRequestProperty(headerName, headers.get(headerName));
                }
            }

            if (!(HTTP_METHOD_GET.equals(method) || HTTP_METHOD_DELETE.equals(method))) {
                StringBuilder sb = new StringBuilder();
                if (data != null && !data.isEmpty()) {
                    String[] paramNames = new String[data.size()];
                    data.keySet().toArray(paramNames);
                    for (int i = 0; i < paramNames.length; i++) {
                        sb.append(URLEncoder.encode(paramNames[i], CHARSET_UTF_8));
                        sb.append(LatchAuth.PARAM_VALUE_SEPARATOR);
                        sb.append(URLEncoder.encode(data.get(paramNames[i]), CHARSET_UTF_8));
                        if (i < paramNames.length - 1) {
                            sb.append(PARAM_SEPARATOR);
                        }
                    }
                }
                byte[] body = sb.toString().getBytes(CHARSET_ASCII);
                theConnection.setRequestProperty(HTTP_HEADER_CONTENT_TYPE,
                        HTTP_HEADER_CONTENT_TYPE_FORM_URLENCODED);
                theConnection.setRequestProperty(HTTP_HEADER_CONTENT_LENGTH, String.valueOf(body.length));
                theConnection.setDoOutput(true);
                os = theConnection.getOutputStream();
                os.write(body);
                os.flush();
            }

            JsonParser parser = new JsonParser();
            is = theConnection.getInputStream();
            isr = new InputStreamReader(is, CHARSET_UTF_8);
            rv = parser.parse(isr);

        } catch (MalformedURLException e) {
            System.err.println("The URL is malformed (" + URL + ")");
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("An exception has been thrown when communicating with Latch backend");
            e.printStackTrace();
        } finally {

            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    System.err.println("An exception has been thrown when trying to close the output stream");
                    e.printStackTrace();
                }
            }

            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException e) {
                    System.err.println("An exception has been thrown when trying to close the input stream reader");
                    e.printStackTrace();
                }
            }

            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    System.err.println("An exception has been thrown when trying to close the input stream");
                    e.printStackTrace();
                }
            }

        }

        return rv;

    }

}