Tuesday 11 October 2016

Digest access authentication

Standard
Digest access authentication is one of the agreed-upon methods a web server can use to negotiate credentials, such as username or password, with a user's web browser. This can be used to confirm the identity of a user before sending sensitive information, such as online banking transaction history. It applies a hash function to the username and password before sending them over the network. In contrast, basic access authentication uses the easily reversible Base64 encoding instead of encryption, making it non-secure unless used in conjunction with SSL.

Technically, digest authentication is an application of MD5 cryptographic hashing with usage of nonce values to prevent replay attacks. It uses the HTTP protocol.



Digest access authentication server gives the client a one-time use string (a nonce) that it combines with the username, realm, password and the URI request. The client runs all of those fields through an MD5 hashing method to produce a hash key.

It sends this hash key to the server along with the username and the realm to attempt to authenticate.

Server-side the same method is used to generate a hashkey, only instead of using the password typed in to the browser the server looks up the expected password for the user from its user DB. It looks up the stored password for this username, runs in through the same algorithm and compares it to what the client sent. If they match: access is granted, otherwise it can send back an 401 request to have the user retry or an access denied error (I forget the code sorry).

Digest authentication is standardized in RFC2617. There's a nice overview of it on Wikipedia:

1) Client makes request

2) Client gets back a nonce from the server and a 401 authentication request

3) Client sends back the following response array (username, realm, generate_md5_key(nonce, username, realm, URI, password_given_by_user_to_browser)) (yea, that's very simplified)

4) The server takes username and realm (plus it knows the URI the client is requesting) and it looks up the password for that username. Then it goes and does its own version of generate_md5_key(nonce, username, realm, URI, password_I_have_for_this_user_in_my_db)

5) It compares the output of generate_md5() that it got with the one the client sent, if they match the client sent the correct password. If they don't match the password sent was wrong.





package com.vaquar.auth.example01;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;

/**
 * TODO you need to add servlet mapping into xml 
 * @author Vaquar.khan@gmail.com
 */
public class HttpDigestAuthServlet extends HttpServlet {

    private String AUTH_METHOD = "auth";
    private String USER_NAME = "vaquar";
    private String PASSWORD = "khant5";
    private String RELAM = "vaquarkhan.com";

    public String nonce;
    public ScheduledExecutorService unRefreshExecutor;

    /**
     * Default constructor 
     *
     */
    public HttpDigestAuthServlet() throws IOException, Exception {

        nonce = calculateRandom();//$auth.nc - the value of nonce count parameter from Authorization or Proxy-Authorization header


        unRefreshExecutor = Executors.newScheduledThreadPool(1);

        unRefreshExecutor.scheduleAtFixedRate(new Runnable() {

            public void run() {
                log("Vaquar khan test working....");
                nonce = calculateRandom();
            }
        }, 1, 1, TimeUnit.MINUTES);

    }

    protected void authenticate(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();

        String requestBody = readRequestBody(request);

        try {
            String authHeader = request.getHeader("Authorization");
            if (StringUtils.isBlank(authHeader)) {
                response.addHeader("WWW-Authenticate", getAuthenticateHeader());
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            } else {
                if (authHeader.startsWith("Digest")) {
                    // parse the values of the Authentication header into a hashmap
                    HashMap<String, String> headerValues = parseHeader(authHeader);

                    String method = request.getMethod();

                    String ha1 = DigestUtils.md5Hex(USER_NAME + ":" + RELAM + ":" + PASSWORD);

                    String qop = headerValues.get("qop");//$auth.qop - the value of qop parameter from Authorization or Proxy-Authorization header


                    String ha2;

                    String reqURI = headerValues.get("uri");

                    if (!StringUtils.isBlank(qop) && qop.equals("auth-int")) {
                        String entityBodyMd5 = DigestUtils.md5Hex(requestBody);
                        ha2 = DigestUtils.md5Hex(method + ":" + reqURI + ":" + entityBodyMd5);
                    } else {
                        ha2 = DigestUtils.md5Hex(method + ":" + reqURI);
                    }

                    String serverResponse;

                    if (StringUtils.isBlank(qop)) {
                        serverResponse = DigestUtils.md5Hex(ha1 + ":" + nonce + ":" + ha2);

                    } else {
                        String domain = headerValues.get("realm");

                        String nonceCount = headerValues.get("nc");
                        String clientNonce = headerValues.get("cnonce");

                        serverResponse = DigestUtils.md5Hex(ha1 + ":" + nonce + ":"
                                + nonceCount + ":" + clientNonce + ":" + qop + ":" + ha2);

                    }
                    String clientResponse = headerValues.get("response");

                    if (!serverResponse.equals(clientResponse)) {
                        response.addHeader("WWW-Authenticate", getAuthenticateHeader());
                        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                    }
                } else {
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, " This Servlet only supports Digest Authorization");
                }

            }

            /*
             * out.println("<head>"); out.println("<title>Servlet
             * HttpDigestAuth</title>"); out.println("</head>");
             * out.println("<body>"); out.println("<h1>Servlet HttpDigestAuth at
             * " + request.getContextPath () + "</h1>"); out.println("</body>");
             * out.println("</html>");
             */
        } finally {
            out.close();
        }
    }

    /**
     * Handles the HTTP
     * <code>GET</code> method.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        authenticate(request, response);
    }

    /**
     * Handles the HTTP
     * <code>POST</code> method.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        authenticate(request, response);
    }

    /**
     * Returns a short description of the servlet.
     *
     * @return a String containing servlet description
     */
    @Override
    public String getServletInfo() {
        return "This Servlet Implements The HTTP Digest Auth as per RFC2617";
    }// </editor-fold>

    /**
     * Gets the Authorization header string minus the "AuthType" and returns a
     * hashMap of keys and values
     *
     * @param headerString
     * @return
     */
    private HashMap<String, String> parseHeader(String headerString) {
        // seperte out the part of the string which tells you which Auth scheme is it
        String headerStringWithoutScheme = headerString.substring(headerString.indexOf(" ") + 1).trim();
        HashMap<String, String> values = new HashMap<String, String>();
        String keyValueArray[] = headerStringWithoutScheme.split(",");
        for (String keyval : keyValueArray) {
            if (keyval.contains("=")) {
                String key = keyval.substring(0, keyval.indexOf("="));
                String value = keyval.substring(keyval.indexOf("=") + 1);
                values.put(key.trim(), value.replaceAll("\"", "").trim());
            }
        }
        return values;
    }

    private String getAuthenticateHeader() {
        String header = "";

        header += "Digest realm=\"" + RELAM + "\",";
        if (!StringUtils.isBlank(AUTH_METHOD)) {
            header += "qop=" + AUTH_METHOD + ",";
        }
        header += "nonce=\"" + nonce + "\",";
        header += "opaque=\"" + getOpaque(RELAM, nonce) + "\"";

        return header;
    }

    /**
     * Calculate the nonce based on current time-stamp upto the second, and a
     * random seed
     *
     * @return
     */
    public String calculateRandom() {
        Date d = new Date();
        SimpleDateFormat f = new SimpleDateFormat("yyyy:MM:dd:hh:mm:ss");
        String fmtDate = f.format(d);
        Random rand = new Random(100000);
        Integer randomInt = rand.nextInt();
        return DigestUtils.md5Hex(fmtDate + randomInt.toString());
    }

    private String getOpaque(String domain, String nonce) {
        return DigestUtils.md5Hex(domain + nonce);
    }

    /**
     * Returns the request body as String
     *
     * @param request
     * @return
     * @throws IOException
     */
    private String readRequestBody(HttpServletRequest request) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        try {
            InputStream inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(
                        inputStream));
                char[] charBuffer = new char[128];
                int bytesRead = -1;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            } else {
                stringBuilder.append("");
            }
        } catch (IOException ex) {
            throw ex;
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException ex) {
                    throw ex;
                }
            }
        }
        String body = stringBuilder.toString();
        return body;
    }

}