29

I recently migrated to Laravel 5, and now CSRF check is on every post submission. I thought about removing it but I want to follow the best practices, so I'll keep it that way.

On the other hand, I'm problems submitting ajax requests.. my page has multiple forms and some submissions are not even from forms, just plain ajax calls. My idea is to have one single hidden "token" input on the page and attach it to every submission. Are there any drawbacks on having that universal single token input?

Also, how can I output the token? Would it be ok to just create a hidden input on the page footer?

Jeff Puckett
  • 37,464
  • 17
  • 118
  • 167
sigmaxf
  • 7,998
  • 15
  • 65
  • 125

7 Answers7

34

I don't see any drawbacks. You can easily create a global token field in your layout file:

<input type="hidden" name="_token" id="csrf-token" value="{{ Session::token() }}" />

Or if you use the form builder:

{!! Form::token() !!}

In jQuery you could use something like this to attach the token to every request.

Community
  • 1
  • 1
lukasgeiter
  • 147,337
  • 26
  • 332
  • 270
32

There is a helper to add the form token inside forms. You can just use

{!! csrf_field() !!}

inside the forms. It will add the hidden input and the token.

Arda
  • 6,756
  • 3
  • 47
  • 67
8

You can use something like this at the bottom of the page:

$('form').append('{{csrf_field()}}');

This will append a hidden input to all your forms:

<input type="hidden" name="_token" value="yIcHUzipr2Y2McGE3EUk5JwLOPjxrC3yEBetRtlV">

And for all your AJAX requests:

$.ajaxSetup({
    beforeSend: function (xhr, settings) {
        //////////// Only for your domain
        if (settings.url.indexOf(document.domain) >= 0) {
            xhr.setRequestHeader("X-CSRF-Token", "{{csrf_token()}}");
        }
    }
});
tomloprod
  • 7,472
  • 6
  • 48
  • 66
3

Here are some excerpts of how I got my CSRF working for all the different scenarios in my jQuery Mobile application that I recently upgraded to use Laravel 5:

I added an encrypted csrf token in a variable that will be passed to my views in my main base controller: app\Http\Controllers\MyController.php

$this->data['encrypted_csrf_token'] = Crypt::encrypt(csrf_token());

Then, I added the meta tag in my main view header: resources\views\partials\htmlHeader.blade.php

<meta name="_token" content="{!! $encrypted_csrf_token !!}"/>

Then, I also added this jquery snippet as suggested in some forums:

    $(function () {
            $.ajaxSetup({
                    headers: {
                            'X-XSRF-TOKEN': $('meta[name="_token"]').attr('content')
                    }
            });
    });

But, the key (for my setup at least) was the addition of the check for the XSRF-TOKEN cookie in my custom VerifyCsrfToken middleware: app\Http\Middleware\VerifyCsrfToken.php:

    /**
     * Determine if the session and input CSRF tokens match.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    protected function tokensMatch($request)
    {       
            $token = $request->session()->token();

            $header = $request->header('X-XSRF-TOKEN');

            $cookie = $request->cookie('XSRF-TOKEN');

            return StringUtils::equals($token, $request->input('_token')) ||
                   ($header && StringUtils::equals($token, $this->encrypter->decrypt($header))) ||
                    ($cookie && StringUtils::equals($token, $cookie));
    }

Before I added that, just about all of my AJAX POSTs (including form submissions and lazyloading listviews) were failing due to a TokenMismatchException.

EDIT: On second thought, I'm not sure how much sense it makes to compare the session token with the one set in the cookie (which would have come from the session token in the first place right?). That may have just been bypassing the security of it all.

I think my main issue was with the jquery snippet above which was supposed to be adding the X-XSRF-TOKEN header to every ajax request. That wasn't working for me in my in jQuery Mobile app (specifically, in my lazyloader plugin) until I added some options for the plugin itself. I added a new default selector csrf (which would be meta[name="_token"] in this case) and a new default setting csrfHeaderKey (which would be X-XSRF-TOKEN in this case). Basically, during initialization of the plugin, a new _headers property is initialized with the CSRF token if one is locatable by the csrf selector (default or user-defined). Then, in the 3 different places where an ajax POST can be fired off (when resetting session variables or when lazyloading a listview) the headers option of $.ajax is set with whatever is in _headers.

Anyway, since the X-XSRF-TOKEN received on the server-side comes from the encrypted meta _token, I think the CSRF protection is now working as it should.

My app\Http\Middleware\VerifyCsrfToken.php now looks like this (which is essentially back to the default implementation provided by Laravel 5 - LOL):

    /**
     * Determine if the session and input CSRF tokens match.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    protected function tokensMatch($request)
    {
            $token = $request->session()->token();

            $_token = $request->input('_token');

            $header = $request->header('X-XSRF-TOKEN');

            return StringUtils::equals($token, $_token) ||
                   ($header && StringUtils::equals($token, $this->encrypter->decrypt($header)));
    }
dcarrith
  • 7,510
  • 2
  • 18
  • 9
2

I think you can do something like this (not tested will update if I get a chance)

$(document).on('submit', 'form', function(e)
      $(this).append('<input name="_token" value="{{{ Session::token() }}}">);
});

you actually might want to store token in a variable that you reupdate as it expires.

The benefit of appending it on submit is if you append elements via ajax I think it'll still work without having to add anything else.

EDIT: Here's a great article on using Rails UJS with Laravel (which includes this auto CRSF token functionality): https://medium.com/@barryvdh/unobtrusive-javascript-with-jquery-ujs-and-laravel-e05f444d3439

Sabrina Leggett
  • 9,079
  • 7
  • 47
  • 50
1

You need to pass along the header X-XSRF-TOKEN which contains an encrypted version of the csrf-token.

There are two ways which this can be done that I am aware of. You can encrypt the token and pass it along to the view:

$xsrfToken = app('Illuminate\Encryption\Encrypter')->encrypt(csrf_token());

return view('some.ajax.form.view')->with('xsrf_token', $xsrfToken);

Or you can grab the token from cookies using JavaScript (Angular makes this easy). In vanilla JS you might do something like this:

function getCookie(name) {
    var pattern = RegExp(name + "=.[^;]*")
    matched = document.cookie.match(pattern)
    if (matched) {
        var cookie = matched[0].split('=')
        return decodeURIComponent(cookie[1])
    }
    return false
}

In jQuery you might then do something like this for the ajax request:

$.ajax({
    // your request
    //
    beforeSend: function(request) {
        return request.setRequestHeader('X-XSRF-TOKEN', getCookie('XSRF-TOKEN'));
    }
});
tplaner
  • 8,363
  • 3
  • 31
  • 47
1

All the answer do not cover JS files. What if we are not using blade and we want to use JS files. blade syntax will not work there.

so here is the code which will work everywhere for forms and for ajax.

var csrf = document.querySelector('meta[name="csrf-token"]').content;
var csrf_field = '<input type="hidden" name="_token" value=“'+csrf+'”>';

$('form').append(csrf_field);

$.ajaxSetup({
    beforeSend: function (xhr, settings) {
        if (settings.url.indexOf(document.domain) >= 0) {
            xhr.setRequestHeader("X-CSRF-Token", csrf);
        }
    }
});
rameezmeans
  • 830
  • 1
  • 10
  • 21
  • 1
    this worked for me, I was receiving 419 unknown status error. Everything worked directly inside the blade file prior. But stopped when I sent to external js file. I used {{ csrf_field() }} in my forms and used your "csrf" value directly in my js file and everything work well :) – Dlaw Nov 14 '22 at 13:07