How to retrieve laravel CSRF token using separated vue frontend - laravel

Is there a way to pass Laravel csrf token to Vue given that the Laravel backend and the Vue frontend are separated from each other (in diferent directory and in diferent subdomains) ??
I'm building an application and would like to have separated backend and frontend for organization purposes and for because it facilitates team work. So, it would be something like:
api.mydomainexample.com (Laravel backend)
mydomainexample.com (Vue frontend for public)
admin.mydomainexample.com (Vue Frontend for admin only)
Is this possible? What I thought was maybe running a nodejs server for the frontend project and make the nodejs server communicate with the laravel server. Don't know how to do that.
I found similiar stackoverflow questions, but the responses from them do not solve my problem. The best thing I found was this, which proposes to use Laravel passport. But, is the proposal the only one that works? If so, does Laravel passport protect users from CSRF attacks?
Actually, if there is an workaround which enables to have separated backend and frontend while protecting against CSRF tokens, then that would be perfect!

Use Sanctum
LARAVEL BACKEND
Install Sanctum via Composer
composer require laravel/sanctum
Publish the Sanctum configuration and migration files
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
Run your migrations - Sanctum will add a table to store API tokens
php artisan migrate
Add Sanctum's middleware to your api middleware group in your App/Http/Kernel.php
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
'api' => [
EnsureFrontendRequestsAreStateful::class,
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
Configure which domains your SPA will be making requests from. From the docs:
You may configure these domains using the stateful configuration option in your sanctum configuration file. This configuration setting determines which domains will maintain "stateful" authentication using Laravel session cookies when making requests to your API.
So - Update your config\sanctum.php to include something like this:
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,localhost:8000,127.0.0.1,127.0.0.1:8000,::1')),
Configure your config/cors.php
<?php
return [
/*
|--------------------------------------------------------------------------
| Cross-Origin Resource Sharing (CORS) Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for cross-origin resource sharing
| or "CORS". This determines what cross-origin operations may execute
| in web browsers. You are free to adjust these settings as needed.
|
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
*/
'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', '*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];
Configure your config/session.php
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| Here you may change the domain of the cookie used to identify a session
| in your application. This will determine which domains the cookie is
| available to in your application. A sensible default has been set.
|
*/
'domain' => env('SESSION_DOMAIN', null),
In your .env, add the following:
// Change .your-site.local to whatever domain you are using
// Please note the leading '.'
SESSION_DOMAIN=.your-site.local
SANCTUM_STATEFUL_DOMAINS=your-site.local:8000
CORS_ALLOWED_ORIGINS=http://app.your-site.local:8000
Run a php artisan config:clear
VUE FRONTEND
In your front-end, create the following folder/file structure
#/src/services/api.js
api.js:
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_URL,
withCredentials: true,
});
export default apiClient;
In the root directory, place an .env file with the following in it:
VUE_APP_API_URL= 'http://api.your-site.local'
To authenticate, your SPA should first make a request to the /sanctum/csrf-cookie. This sets the XSRF-TOKEN cookie. This token needs to be sent on subsequent requests ( axios will handle this for you automatically ). Immediately after, you'll want to send a POST request to your Laravel /login route.
On your Vue front-end login component:
import Vue from 'vue'
import apiClient from '../services/api';
export default {
data() {
return {
email: null,
password: null,
loading: false,
};
},
methods: {
async login() {
this.loading = true; // can use this to triggle a :disabled on login button
this.errors = null;
try {
await apiClient.get('/sanctum/csrf-cookie');
await apiClient.post('/login', {
email: this.email,
password: this.password
});
// Do something amazing
} catch (error) {
this.errors = error.response && error.response.data.errors;
}
this.loading = false;
},
},

Related

Laravel Sanctum - sanctum/csrf-cookie (204 "No content")

Laravel sanctum has been a bit of a headache for me as i have spent hours trying to figure out why sanctum/csrf-cookie route returns no content. initially the same route return 404 not found but after adding 'prefix' => 'api/sanctum' config/sanctum.php it seems to work except that it outputs nothing and no cookie is set in my browser.
Here are some of my codes
.env
SANCTUM_STATEFUL_DOMAINS=localhost:8080
SPA_URL=http://localhost:8080
SESSION_DOMAIN=localhost
--config/cors.php
'paths' => [
'api/*',
'login',
'logout',
'register',
'user/password',
'forgot-password',
'reset-password',
'sanctum/csrf-cookie',
'user/profile-information',
'email/verification-notification',
],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
axios
export const authClient = axios.create({
baseURL: process.env.VUE_APP_API_URL,
withCredentials: true, // required to handle the CSRF token
});
and having done all of that, if i tried to generate a token using
axios.get('/sanctum/csrf-cookie').then(response => {
// Login...
});
i get 204 no content response
i have also added the api middleware in kernel.php as instructed in the doucmentation but still wont set the cookie. and when i try to make request to another route protected by sanctum i 419 token mismatch.
i have also run a fresh installation of laravel, php artisan optimize, cleared my brower history, checked the endpoints in postman but still thesame 204 and 419 exceptions
I was struggling with the same issue for days and then founded a way that worked.
so :
The '/sanctum/csrf-cookie' route return a 204 response when successfull, the then you have to send your post request with credentials. i used to get a 419 reponse status.
so after followed laravel docs here is what i added :
SPA
make sure you set the withCredentials header to true
API
.env
SESSION_DRIVER=cookie
SESSION_DOMAIN='.localhost'
SANCTUM_STATEFUL_DOMAINS='localhost,127.0.0.1'
.kernel.php
add in your middleware array : \Illuminate\Session\Middleware\StartSession::class
Hey I don't know whether you've found the answer or not but that request meant to have empty response body. Your CSRF token is available in the header of the response ;D
From HTTP Specification:
The HTTP 204 No Content success status response code indicates that a request has succeeded, but that the client doesn't need to navigate away from its current page. This might be used, for example, when implementing "save and continue editing" functionality for a wiki site.
According to specification 204 is the exptected response from server.
You can insert in bootstrap.js file
import axios from 'axios';
window._ = _;
window.axios = axios;
window.axios.defaults.baseURL = "/api/";
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

Can't get auth user with laravel passport, keep getting "Unauthenticated" error

I can't get the information of the authenticated user in a Laravel passport app with JWT and vue.
I've installed laravel passport. Ive done everything in the documentation and added:
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
To consume it with js for a SPA app.
I've protected my routes with the auth:api middleware, but i keep getting:
{"Status":{"api_status":0,"Code":401,"Message":"Unauthenticated"}}
When i use postman to manually insert the CSRF-TOKEN in Authorization Bearer Token. It does give me the auth user.
Whatever i do, i keep getting null on Auth::user(); in my controllers and routes
Laravel V5.7
Node V10.15.3
Npm V.6.9.0
You need to send a POST request (using Postman/Insomnia) with the details of the user you want to log in as to /oauth/token in your app which will respond with an API token. You save this api token locally, and then add add it as a Header variable to your guzzle/axios/whatever function's http calls (every one of them!) to your API.
$http = new GuzzleHttp\Client;
$data = 'Whatever';
$response = $http->request('POST','api/user',[
'data' => $data,
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer xxxxxxxxxxxxThis is the long authentication string returned from the Postman requestxxxxxxxxxxxxxx'
]
]);
dd(json_decode((string) $response->getBody())); // To view the response
From: https://laravel.com/docs/5.5/passport#creating-a-password-grant-client

CORS Laravel VueJS

I'm trying to do a get with axios from VueJS to Laravel which is my API.
I got this error :
Access to XMLHttpRequest at 'http://api.test/api/events/1/' from origin >'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control->Allow-Origin' header is present on the requested resource.
Uncaught (in promise) Error: Network Error
at createError (createError.js?2d83:16)
at XMLHttpRequest.handleError (xhr.js?b50d:87)
I've tried to create a middleware named 'cors' like here but it's not working for me or maybe I'm doing it badly ?
Strange thing ? is that's working with Postman.
Thank for the help ! :)
Servers are used to host web pages, applications, images, fonts, and
much more. When you use a web browser, you are likely attempting to
access a distinct website (hosted on a server). Websites often request
these hosted resources from different locations (servers) on the
Internet. Security policies on servers mitigate the risks associated
with requesting assets hosted on different server. Let's take a look
at an example of a security policy: same-origin.
The same-origin policy is very restrictive. Under this policy, a
document (i.e., like a web page) hosted on server A can only interact
with other documents that are also on server A. In short, the
same-origin policy enforces that documents that interact with each
other have the same origin.
Check this CORS library made for Laravel usage.
Installation is easy:
$ composer require barryvdh/laravel-cors
$ php artisan vendor:publish --provider="Barryvdh\Cors\ServiceProvider"
The defaults are set in config/cors.php
return [
/*
|--------------------------------------------------------------------------
| Laravel CORS
|--------------------------------------------------------------------------
|
| allowedOrigins, allowedHeaders and allowedMethods can be set to array('*')
| to accept any value.
|
*/
'supportsCredentials' => false,
'allowedOrigins' => ['*'],
'allowedHeaders' => ['Content-Type', 'X-Requested-With'],
'allowedMethods' => ['*'], // ex: ['GET', 'POST', 'PUT', 'DELETE']
'exposedHeaders' => [],
'maxAge' => 0,
];
allowedOrigins, allowedHeaders and allowedMethods can be set to array('*') to accept any value.
To allow CORS for all your routes, add the HandleCors middleware in the $middleware property of app/Http/Kernel.php class:
protected $middleware = [
// ...
\Barryvdh\Cors\HandleCors::class,
];
If you want to allow CORS on a specific middleware group or route, add the HandleCors middleware to your group:
protected $middlewareGroups = [
'web' => [
// ...
],
'api' => [
// ...
\Barryvdh\Cors\HandleCors::class,
],
];
https://www.codecademy.com/articles/what-is-cors
Tried sending Axios request from Vue app to Laravel backend.
I had CORS Error and I couldn't find the solution.
After following request execution in vendor files I found out that I was simply testing it wrong.
I was testing on url: http://localhost/api, but in config/cors.php there is:
'paths' => ['api/*', 'sanctum/csrf-cookie'],
So all I had to do was to change request to http://localhost/api/... and it started working.
Another solution is adding 'api' to paths array in config/cors.php if you want to use http://localhost/api
The problem you are facing is with the same origin policy. you can read about it on the Mozilla site (https://developer.mozilla.org/en-US/docs/Web/HTTP/Server-Side_Access_Control).
it is basally to proven un authorized access to web servers. you can change the way your web server reacts and i also in that link i have included.

Laravel Passport: CreateFreshApiToken in App: Authorization Failed

I'm attempting to install Laravel Passport on Laravel 5.7.18 using PHP 7.2.13.
My application consumes the API within itself using JavaScript (Axios with Vue)
I'm getting a 401 Unauthorized error within the JavaScript web application. I've read the documentation and added CreateFreshApiToken to the web Kernel. The laravel_token cookie is in fact setting itself. However, the oauth tables are clean in the database.
Http/Kernel:
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
JavaScript:
axios.get("api/users/" + id).then(({ data }) => {
this.user = data;
});
Auth.php:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
Routes (Api.php):
Route::middleware('auth:api')->group(function () {
Route::resource('users', 'UserController');
Route::resource('groups', 'GroupController');
// .. plus more resources
});
Axios Config:
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
var token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
Headers in Browser that returns 401:
Working Request with Postman:
If you are using a username and password for initial login then the assumption is you are building a First Party application that has the right to make a user/pass login attempt.
It is highly recommended that if you are building a Reactive app using
Angular, React.js or Vue.js then an SPA (Single Page Application)
approach will yield a much more robust product.
https://en.wikipedia.org/wiki/Single-page_application
You should note that with this particular method, if your application
makes a static (none ajax request) and thus reloads in the browser,
you will loose the auth token. In this case you are not playing host
to an app that is by it's very definition an SPA, so if you need to retain the token between
static request reloads then you need to store the token in a cookie, I
suggest using a cookie rather than localStorage because the availability of
localStorage is not 100% guaranteed to be at your disposal in all web browsers.
If your application is on the same domain, you do not need to use
Passport. Instead native session cookie auth is perfectly fine, all
you have to do is make sure you are passing the CSRF Token for post
requests.
For user/pass token grants you should follow this guideline:
https://laravel.com/docs/5.7/passport#password-grant-tokens
From that guide, when you make a successful request to /oauth/token, the returned token should be set in your application as an Authorization header with Bearer token.
The token request response looks like this:
{
"token_type": "Bearer",
"expires_in": 31536000,
"access_token": "eyJ0eXAiOiJKVJhb...nheKL-fuTlM",
"refresh_token": "def502008d6313e...94508f1cb"
}
You should request and handle that JSON object as follows:
axios.post('/oauth/token', {
grant_type: "password",
client_id: "1",
client_secret: "zkI40Y.......KvPNH8",
username:"email#address.com",
password:"my-password"
}).then( response => {
axios.defaults.headers.common['Authorization'] = `Bearer ${response.data.access_token}`
} );
The values for client_id (id) and client_secret come from oauth_clients table, there should already be an entry in there.
If not then run php artisan passport:client --password
Don't forget that you will have to configure some headers, look to this post has some relevant information for the Oauth Authorization header:
How to send authorization header with axios
If you say your tables are clean in the database, try to run this command again:
php artisan passport:install
This command will create the encryption keys needed to generate secure access tokens. In addition, the command will create "personal access" and "password grant" clients which will be used to generate access tokens:
Laravel 5.7 Passport docs

How to authenticate Vue.js / Axios request of an API route in Laravel

I'm in Laravel 5.6. I have all my API routes built out and properly responding to requests from my REST client (Paw). I'm trying to build a simple front end to access those routes.
I'm trying to use Laravel's out-of-the-box features as much as possible, so I'm using Axios to call those routes from a blade template using Vue.js. It works if I disable auth middleware on the test route, but I get 401 errors on the console when auth middleware is enabled for the route.
The problem seems obvious enough... The auth:api guard on my /api routes wants to see an oauth token in the header, but when I log in with the web page it does session authentication. I assume there's a simple way to resolve this without having to spoof an oauth token request in the web frontend, right? Do I need to somehow pass the session token in my request with Axios? And, if so, do I also need to change the auth:api guard in my api routes file?
I solved it! I'm a bit embarrassed because the answer was actually in the Laravel docs, but I will say I tried this before posting the question here and it wasn't working. Perhaps something else was broken at the time.
Per the Laravel docs:
All you need to do is add the CreateFreshApiToken middleware to your
web middleware group in your app/Http/Kernel.php file:
'web' => [
// Other middleware...
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
This Passport middleware will attach a laravel_token cookie to your
outgoing responses. This cookie contains an encrypted JWT that
Passport will use to authenticate API requests from your JavaScript
application. Now, you may make requests to your application's API
without explicitly passing an access token...
You will probably want to use Larvel Passport or a JWT auth mechanism for obtain the Authorization token.
Seeing as how you're using axios, add a request interceptor to attach the access token to every request once you successfully authenticate. A simple example:
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// assume your access token is stored in local storage
// (it should really be somewhere more secure but I digress for simplicity)
let token = localStorage.getItem('access_token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
to use the auth:api first you need api_token inside your users table
Schema::table('users', function ($table) {
$table->string('api_token', 80)->after('password')
->unique()
->nullable()
->default(null);
});
also you can create a user for testing as follows
User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'api_token' => Str::random(60),
]);
in your layout use the following before #yield('content')
<script>
window.Laravel = <?php echo json_encode(['api_token' => (Auth::user())->api_token]); ?>
</script>
now you can use window.laravel.api_token inside your vue js to use it in headers
heres an example
var methods = new Vue({
el: '#tabs_lists',
data: {
config: {
headers: {
Authorization: 'Bearer ' + window.Laravel.api_token,
Accept: 'application/json'
}
},
data: []
},
methods: {
test: function (link) {
axios.get(link, this.config)
.then(response => (this.data = response.data)).catch(function (error) {
// handle error
console.log(error);
});
}
}
}
)

Resources