br.com.vpsa.oauth2android.token.MacTokenTypeDefinition.java Source code

Java tutorial

Introduction

Here is the source code for br.com.vpsa.oauth2android.token.MacTokenTypeDefinition.java

Source

/*
 * The MIT License (MIT)
 * Copyright (c) 2011 Christoph Gerstner <development@christoph-gerstner.de>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * Note: For questions or suggestions don't hesitate to contact me under the
 * above email address.
 */
package br.com.vpsa.oauth2android.token;

import android.util.Base64;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.http.Header;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;

import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import br.com.vpsa.oauth2android.Client;
import br.com.vpsa.oauth2android.Server;
import br.com.vpsa.oauth2android.common.Connection;
import br.com.vpsa.oauth2android.common.Util;
import br.com.vpsa.oauth2android.exception.InvalidTokenTypeException;

/**
 * The <code>MacTokenTypeDefinition</code>  serves as the configuration class to define attributes of
 * MacTokens.<br>
 * The OAuth 2.0 protokoll allows to be extended with different types of tokens.
 * The MAC Tokens have a more complex handling than the standard bearer token.
 * In addition to the basic attributes they hold a <italic>token secret</italic> wich is used in combination
 * with a current timestamp and a randomly generated String to create a signature and sign the messages.
 * @author Christoph Gerstner <development@christoph-gerstner.de>
 *  @see org.gerstner.oauth2android.token.BearerTokenTypeDefinition
 */
public class MacTokenTypeDefinition extends TokenTypeDefinition {

    private final String TAG = "TokenTypeDef";
    private static final int NONCE_LENGTH = 15;

    @Override
    public Token getEmptyToken() {
        return new MacToken();
    }

    @Override
    public String getName() {
        return "mac";
    }

    @Override
    public String getHttpAuthenticationScheme() {
        return "MAC";
    }

    @Override
    public String requestProtectedResource(Token token, Client client, Server server, String resourceEndpoint,
            String body) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    /**
     * This method returns a String containing the complete requests signature.<br>
     * You might not need to call this method directly from your application.
     * But if you don't want to use {@link org.gerstner.oauth2android.OAuth#executeProtectedResourceRequest(java.lang.String, java.util.List)}
     * (wich is actually calling this method indirectly to sign the request),
     * you can call {@link #getAuthorizedHttpPost(java.util.List, java.lang.String, org.gerstner.oauth2android.Server, org.gerstner.oauth2android.Client, boolean)}
     * or similar to get a complete, signed request.
     *<br>
     * <br>
     * Here is an example on how the return string might look like (the example is from the
     * draft-ietf-oauth-v2-http-mac-00) (line breaks are for displaying purposes only):
     * <pre>
     * <code>
     *  MAC id="jd93dh9dh39D"
     *      nonce="273156:di3hvdf8",
     *      bodyhash="k9kbtCIy0CkI3/FEfpS/oIDjk6k=",
     *      mac="W7bdMZbv9UWOTadASIQHagZyirA="
     * </code>
     * </pre>
     * @param client <code>Client</code> of this application
     * @param server <code>Server</code> for this application
     * @param requestUri <code>String</code> containing this requests endpoint
     * @param requestMethod <code>String</code> value of the http method used (GET, POST)
     * @param ext <code>String</code> value of the extension parameters
     * @param body <code>String</code> representation of the complete requests body
     * @return <code>String</code> with complete signature
     * @throws InvalidTokenTypeException thrown if the token is not of the mac type or containes incorrect mac credentials
     */
    public String constructAuthorization(Client client, Server server, String requestUri, String requestMethod,
            String ext, String body) throws InvalidTokenTypeException {
        MacToken macToken = castToken(client);
        String nonce = System.currentTimeMillis() - macToken.getCreated() + ":"
                + Util.getRandomString(NONCE_LENGTH);
        String bodyhash = calculateBodyHash(body,
                getHashAlgorithm(macToken.getAdditionalParameters().get("algorithm")));
        ext = (ext == null) ? "" : ext;
        String host = server.getResourceServer().split("://")[1];
        String port = server.getResourceServer().split("://")[0];
        if (port.equalsIgnoreCase("http")) {
            port = "80";
        } else if (port.equalsIgnoreCase("https")) {
            port = "443";
        }

        String normalizedString = constructNormalizedString(nonce, requestMethod, requestUri, host, port, bodyhash,
                ext);
        String mac = calculateMAC(macToken.getAdditionalParameters().get("secret"), normalizedString,
                getMACAlgorithm(macToken.getAdditionalParameters().get("algorithm")));
        return constructAuthorizationHeaderField(macToken.getToken(), nonce, bodyhash, ext, mac);

    }

    /**
     * This method constructs the normalized string wich is used to calculate the
     * mac for the request. To get a valid mac it might be neccessary to calculate
     * the bodyhash for the request first. See {@link #calculateBodyHash(java.lang.String, java.lang.String) }<br>
     * The resulting normalized string might look similar to this (example taken from the draft-ietf-oauth-v2-http-mac-00):<br>
     * <pre>
     * <code>
     *  273156:di3hvdf8\n
     *   POST\n
     *   /request\n
     *   example.net
     *   80\n
     *   k9kbtCIy0CkI3/FEfpS/oIDjk6k=\n
     *   \n
     * </code>
     * </pre>
     *
     * @param nonce <code>String</code> containing the tokens age and a random string (e.g.  "273156:di3hvdf8")
     * @param requestMethod <code>String</code> value of the http method (e.g. "POST")
     * @param requestUri <code>String</code> of the requests uri (e.g. "/request")
     * @param requestHost <code>String</code> representation of the hosts uri as contained in the requests header (e.g. "example.net")
     * @param requestPort <code>String</code> the requests port (e.g. "80" for http or "443" for https)
     * @param bodyhash <code>String</code> the previously calculated bodyhash (remember even an empty body produces a bodyhash)
     * @param ext <code>String</code> some extension parameters for the request
     * @return <code>String</code> of the complete normalized String
     */
    private String constructNormalizedString(String nonce, String requestMethod, String requestUri,
            String requestHost, String requestPort, String bodyhash, String ext) {
        String newline = "\n";
        String normalizedString = nonce + newline + requestMethod.toUpperCase() + newline + requestUri + newline
                + requestHost + newline // request Header
                + requestPort + newline // wie im request Header
                + bodyhash + newline + ext + newline; // ext Header ... ????
        return normalizedString;
    }

    /**
     * This method constructs the actual http authorization header field with the signature.<br>
     * All included parameters need to be calculated first. <br>
     * Here is an example on how the return string might look like (the example is from the
     * draft-ietf-oauth-v2-http-mac-00) (line breaks are for displaying purposes only):
     * <pre>
     * <code>
     *  MAC id="jd93dh9dh39D"
     *      nonce="273156:di3hvdf8",
     *      bodyhash="k9kbtCIy0CkI3/FEfpS/oIDjk6k=",
     *      mac="W7bdMZbv9UWOTadASIQHagZyirA="
     * </code>
     * </pre>
     * @param macKeyIdentifier <code>String </code> the token identifier
     * @param nonce <code>String</code> containing the tokens age and a random string (e.g.  "273156:di3hvdf8")
     * @param bodyhash <code>String </code> the requests bodyhash. See {@link #calculateBodyHash(java.lang.String, java.lang.String)}
     * @param ext <code>String</code> some extension parameters for the request
     * @param mac <code>String</code> the calculated mac. See {@link #calculateMAC(java.lang.String, java.lang.String, java.lang.String)}
     * @return <code>String</code> with the authorized header field value
     *
     * @see #constructAuthorization(org.gerstner.oauth2android.Client, org.gerstner.oauth2android.Server, java.lang.String, java.lang.String, java.lang.String, java.lang.String)
     */
    private String constructAuthorizationHeaderField(String macKeyIdentifier, String nonce, String bodyhash,
            String ext, String mac) {
        String authorizationHeaderField = this.getHttpAuthenticationScheme() + " ";
        authorizationHeaderField += "id=\"" + macKeyIdentifier + "\"," + "nonce=\"" + nonce + "\","
                + ((!bodyhash.equalsIgnoreCase("")) ? "bodyhash=\"" + bodyhash + "\"," : "")
                + ((!ext.equalsIgnoreCase("")) ? "ext=\"" + ext + "\"," : "") + "mac=\"" + mac + "\"";

        return authorizationHeaderField;
    }

    /**
     * Seperates the Hash algorithm (sha-1 or sha-256) from the hmac algorithm string.<br>
     * e.g "hmac-sha-1" produces "sha-1"<br>
     * @param algorithm
     * @return
     */
    private String getHashAlgorithm(String algorithm) {
        return algorithm.substring(algorithm.indexOf("sha", 0));
    }

    /**
     * Constructs a java readable algorithm designation.<br>
     * e.g. "hmac-sha-1" produces "hmacsha1".
     * @param algorithm
     * @return <code>String</code> of the algorithms name
     */
    private static String getMACAlgorithm(String algorithm) {
        return algorithm.replace("-", "").toUpperCase();
    }

    private String calculateBodyHash(String body, String algorithm) throws InvalidTokenTypeException {
        try {
            MessageDigest md = MessageDigest.getInstance(algorithm);
            String bodyhash = Base64.encodeToString(md.digest(body.getBytes()), Base64.DEFAULT);
            return bodyhash;
        } catch (NoSuchAlgorithmException ex) {
            throw new InvalidTokenTypeException(
                    "This token contains no or an invalid algorithm to calculate the bodyhash");
        }
    }

    private static String calculateMAC(String key, String normalizedString, String algorithm) {
        String macString = "";
        try {
            System.out.println("algorithm=" + algorithm);
            Mac mac = Mac.getInstance(algorithm);
            mac.init(new SecretKeySpec(key.getBytes(), algorithm));
            macString = Base64.encodeToString(mac.doFinal(normalizedString.getBytes()), Base64.DEFAULT);
        } catch (InvalidKeyException ex) {
            Logger.getLogger(MacTokenTypeDefinition.class.getName()).log(Level.SEVERE, null, ex);
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(MacTokenTypeDefinition.class.getName()).log(Level.SEVERE, null, ex);
        }
        return macString;
    }

    @Override
    public List<String> getAdditionalTokenParameters() {
        List<String> tokenAttributes = new ArrayList<String>(1);
        tokenAttributes.add("secret");
        tokenAttributes.add("algorithm");
        return tokenAttributes;
    }

    @Override
    public HttpGet getAuthorizedHttpGet(List<NameValuePair> additionalParameter, String requestUri, Server server,
            Client client, boolean useHeader) throws InvalidTokenTypeException {
        String ext = castToken(client).getExt();

        String url = server.getResourceServer() + (requestUri.startsWith("/") ? requestUri : "/" + requestUri);
        if (additionalParameter != null && !additionalParameter.isEmpty()) {
            if (url.contains("?")) {
                url += "&";
            } else {
                url += "?";
            }
            for (Iterator<NameValuePair> it = additionalParameter.iterator(); it.hasNext();) {
                NameValuePair nameValuePair = it.next();
                url += nameValuePair.getName() + "=" + nameValuePair.getValue();
                if (it.hasNext()) {
                    url += "&";
                }
            }
        }

        HttpGet httpGet = new HttpGet(url);
        Header header = new BasicHeader("Authorization", constructAuthorization(client, server,
                httpGet.getRequestLine().getUri(), Connection.HTTP_METHOD_GET, ext, ""));
        httpGet.addHeader(header);
        return httpGet;
    }

    @Override
    public HttpDelete getAuthorizedHttpDelete(List<NameValuePair> additionalParameter, String requestUri,
            Server server, Client client, boolean useHeader) throws InvalidTokenTypeException {
        String ext = castToken(client).getExt();

        String url = server.getResourceServer() + (requestUri.startsWith("/") ? requestUri : "/" + requestUri);
        if (additionalParameter != null && !additionalParameter.isEmpty()) {
            if (url.contains("?")) {
                url += "&";
            } else {
                url += "?";
            }
            for (Iterator<NameValuePair> it = additionalParameter.iterator(); it.hasNext();) {
                NameValuePair nameValuePair = it.next();
                url += nameValuePair.getName() + "=" + nameValuePair.getValue();
                if (it.hasNext()) {
                    url += "&";
                }
            }
        }

        HttpDelete httpDelete = new HttpDelete(url);
        Header header = new BasicHeader("Authorization", constructAuthorization(client, server,
                httpDelete.getRequestLine().getUri(), Connection.HTTP_METHOD_DELETE, ext, ""));
        httpDelete.addHeader(header);
        return httpDelete;
    }

    @Override
    public HttpPost getAuthorizedHttpPost(List<NameValuePair> additionalParameter, String requestUri, Server server,
            Client client, boolean useHeader) throws InvalidTokenTypeException {
        String ext = castToken(client).getExt();
        String url = server.getResourceServer() + (requestUri.startsWith("/") ? requestUri : "/" + requestUri);
        HttpPost httpPost = new HttpPost(url);
        String body = "";
        if (additionalParameter != null && !additionalParameter.isEmpty()) {
            httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded");
            try {
                httpPost.setEntity(new UrlEncodedFormEntity(additionalParameter));
                try {
                    body = EntityUtils.toString(httpPost.getEntity());
                } catch (IOException ex) {
                    Logger.getLogger(MacTokenTypeDefinition.class.getName()).log(Level.SEVERE, null, ex);
                } catch (ParseException ex) {
                    Logger.getLogger(MacTokenTypeDefinition.class.getName()).log(Level.SEVERE, null, ex);
                }
            } catch (UnsupportedEncodingException ignored) {
            }
        }
        Header header = new BasicHeader("Authorization", constructAuthorization(client, server,
                httpPost.getRequestLine().getUri(), Connection.HTTP_METHOD_POST, ext, body));
        httpPost.addHeader(header);

        return httpPost;
    }

    @Override
    public HttpPut getAuthorizedHttpPut(List<NameValuePair> additionalParameter, String requestUri, Server server,
            Client client, boolean useHeader) throws InvalidTokenTypeException {
        String ext = castToken(client).getExt();
        String url = server.getResourceServer() + (requestUri.startsWith("/") ? requestUri : "/" + requestUri);
        HttpPut httpPut = new HttpPut(url);
        String body = "";
        if (additionalParameter != null && !additionalParameter.isEmpty()) {
            httpPut.addHeader("Content-Type", "application/x-www-form-urlencoded");
            try {

                httpPut.setEntity(new UrlEncodedFormEntity(additionalParameter));
                try {
                    body = EntityUtils.toString(httpPut.getEntity());
                } catch (IOException ex) {
                    Logger.getLogger(MacTokenTypeDefinition.class.getName()).log(Level.SEVERE, null, ex);
                } catch (ParseException ex) {
                    Logger.getLogger(MacTokenTypeDefinition.class.getName()).log(Level.SEVERE, null, ex);
                }
            } catch (UnsupportedEncodingException ex) {
                // TODO: invalid url exceptions ????
            }
        }
        Header header = new BasicHeader("Authorization", constructAuthorization(client, server,
                httpPut.getRequestLine().getUri(), Connection.HTTP_METHOD_PUT, ext, body));
        httpPut.addHeader(header);

        return httpPut;
    }

    /**
     * Casts the clients token into a token of the mac type (if it acutally is one).
     * @param client <code>Client</code> client containing the token
     * @return <code>MacToken<code> the clients token as a Mac-Type
     * @throws InvalidTokenTypeException if the token can't be casted
     */
    private MacToken castToken(Client client) throws InvalidTokenTypeException {
        MacToken token;
        try {
            token = (MacToken) client.getAccessToken();
        } catch (ClassCastException e) {
            throw new InvalidTokenTypeException("The token used for this request is not a MAC token");
        }
        if (!client.getAccessToken().getType().equalsIgnoreCase("Mac")) {
            throw new InvalidTokenTypeException(
                    "The token used for this request is not a MAC token. It is of the type:" + token.getType());
        }
        return token;
    }
}