com.sector91.wit.http.CsrfInterceptor.java Source code

Java tutorial

Introduction

Here is the source code for com.sector91.wit.http.CsrfInterceptor.java

Source

// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //

////   ///   /// ///       
//////  ////   // ////  //// 
/// ////  ////  //  ////  //// 
///  //// /////  //        ///  
///  //// ///// //  //// ////// 
//   /// /////  //  ////  ////  
// //// ///// //  ////  ////   
/////////////  ////  ////   
////////////   ///   ///    
///// /////   ////  ////    
///// /////   //// ///// // 
////  ////    /// ///// //  
///// /////   ////////////   
////  ////     ////  ////    

// The Web framework with class.

// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //

// Copyright (c) 2013 Adam R. Nelson
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.sector91.wit.http;

import static com.sector91.wit.html.Builder.*;

import java.security.SecureRandom;
import java.util.Map;
import java.util.Random;

/*>>> import checkers.nullness.quals.Nullable;*/

import org.simpleframework.http.Cookie;
import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import org.simpleframework.http.Status;

import com.esotericsoftware.minlog.Log;
import com.google.common.io.BaseEncoding;
import com.google.common.net.HttpHeaders;
import com.sector91.wit.Interceptor;
import com.sector91.wit.html.Html;
import com.sector91.wit.html.InputEl.Type;

/**
 * <p>Protects non-{@code GET} requests from cross-site request forgery (CSRF)
 * attacks. This works by generating a random token, which will be stored in
 * a cookie, then rejecting all non-{@code GET} requests which do not contain
 * this token by returning an HTTP 403 error. This strategy is based on <a href
 * ="https://docs.djangoproject.com/en/dev/ref/contrib/csrf/">Django's
 * CSRF-protection middleware.</a></p>
 * 
 * <p>In order for this interceptor to work, the token must be obtained with
 * the static {@link #csrfToken(Request, Response)} method, which also creates
 * the cookie if it does not already exist. The token must be stored either in
 * a form field named {@code "csrfToken"} or in an HTTP header named {@code
 * "X-CSRFToken"}. (The convenience method {@link #csrfTokenField(Request,
 * Response)} calls {@code csrfToken} and generates an {@code <input
 * name="csrfToken" type="hidden">} automatically.)</p>
 * 
 * @author Adam R. Nelson
 */
public class CsrfInterceptor implements Interceptor {

    public static final String COOKIE_NAME = "csrftoken";
    public static final String FORM_KEY = "csrfToken";
    public static final String CSRF_HEADER = "X-CSRFToken";

    public static final int CSRF_TOKEN_BITS = 128;

    private static final String TAG = CsrfInterceptor.class.getSimpleName();

    private static final Random rnd = new SecureRandom();
    static {
        Log.debug(TAG, "Seeding SecureRandom for CSRF token generation...");
        long start = System.currentTimeMillis();
        rnd.setSeed(System.currentTimeMillis());
        long timeTaken = System.currentTimeMillis() - start;
        Log.debug(TAG, "SecureRandom setup finished in " + timeTaken + "ms.");
    }

    private static String generateToken() {
        final byte[] bytes = new byte[CSRF_TOKEN_BITS];
        rnd.nextBytes(bytes);
        return BaseEncoding.base64().encode(bytes);
    }

    public static Html csrfTokenField(Request request, Response response) {
        return input.id(FORM_KEY).name(FORM_KEY).type(Type.HIDDEN).value(csrfToken(request, response));
    }

    public static String csrfToken(Request request, Response response) {
        final CookieParams params = (CookieParams) request.getAttribute(CookieParams.class);
        if (params == null)
            throw new IllegalStateException(
                    "Cannot use CsrfInterceptor.csrfToken()" + " on a route that does not have a CsrfInterceptor.");
        Cookie cookie = request.getCookie(params.name);
        String token;
        if (cookie == null) {
            // Generate the token if it's not already stored in a cookie.
            Log.trace(TAG, "Creating new CSRF token.");
            token = generateToken();
            cookie = new Cookie(params.name, token);
            if (params.domain != null)
                cookie.setDomain(params.domain);
            if (params.path != null)
                cookie.setPath(params.path);
            cookie.setSecure(params.secure);
            cookie.setProtected(params.httpOnly);
            response.setCookie(cookie);
        } else {
            token = cookie.getValue();
        }
        response.addValue(HttpHeaders.VARY, "Cookie");
        return token;
    }

    private final CookieParams cookieParams = new CookieParams();

    @Override
    public void intercept(Request request, Response response) throws HttpException {
        // Include information on the CSRF token cookie in the request metadata, so
        // that the csrfToken() method knows what parameters to set on the cookie.
        final Map<Object, Object> requestAttrs = request.getAttributes();
        requestAttrs.put(CookieParams.class, cookieParams);

        final String method = request.getMethod();
        // For "safe" requests (according to RFC 2616), assume no action is
        // performed and therefore no CSRF protection is needed.
        if ("GET".equals(method) || "HEAD".equals(method) || "OPTIONS".equals(method) || "TRACE".equals(method)) {
            return;
        }
        // For POST, PUT, DELETE, etc. requests, check that a CSRF token was
        // received, either in a form field or in an X-CSRFToken header, and
        // throw an HTTP 403 Forbidden if it was not.
        final Cookie cookie = request.getCookie(cookieParams.name);
        if (cookie == null) {
            Log.debug(TAG, "CSRF check failed: Request does not contain CSRF token cookie.");
            throw new HttpException(Status.FORBIDDEN);
        }
        final String token = cookie.getValue();

        if (token.equals(request.getValue(CSRF_HEADER)) || token.equals(request.getParameter(FORM_KEY))) {
            if (request.isSecure()) {
                // If we're using HTTPS, an extra check is required to prevent
                // "man-in-the-middle" attacks. HTTPS requests reliably set the
                // Referer header, so check that this header is present and that
                // the request came from this domain.
                final String referer = request.getValue(HttpHeaders.REFERER);
                if (referer == null) {
                    Log.debug(TAG, "CSRF check failed: HTTPS request has no referer.");
                    throw new HttpException(Status.FORBIDDEN);
                }
                final String safeHost = "https://" + request.getValue(HttpHeaders.HOST);
                if (!referer.startsWith(safeHost)) {
                    Log.debug(TAG, "CSRF check failed: Referer '" + referer + "' is not in HTTPS domain '"
                            + safeHost + "'.");
                    throw new HttpException(Status.FORBIDDEN);
                }
            }
        } else {
            Log.debug(TAG, "CSRF check failed: Missing or invalid token.");
            throw new HttpException(Status.FORBIDDEN);
        }
    }

    public CsrfInterceptor cookieName(String name) {
        cookieParams.name = name;
        return this;
    }

    public CsrfInterceptor cookieDomain(String domain) {
        cookieParams.domain = domain;
        return this;
    }

    public CsrfInterceptor cookiePath(String path) {
        cookieParams.path = path;
        return this;
    }

    public CsrfInterceptor cookieSecure(boolean secure) {
        cookieParams.secure = secure;
        return this;
    }

    public CsrfInterceptor cookieHttpOnly(boolean httpOnly) {
        cookieParams.httpOnly = httpOnly;
        return this;
    }

    private static class CookieParams {
        CookieParams() {
        }

        /*@Nullable*/ String name = COOKIE_NAME;
        /*@Nullable*/ String domain = null;
        /*@Nullable*/ String path = null;
        boolean secure = false;
        boolean httpOnly = true;
    }
}