Laravel Vue SPA using Sanctum response Unauthorized - laravel

The Sanctum Auth system on my local machine works well and I have no errors. But my deployed app is having trouble with authorizing a user. When I login it sends a request to get the user data then redirects. After auth completes you are redirected and the app make a GET request for more data. This GET route is guarded using laravel sanctum. But the backend is not registering that the user has made a successful login attempt so it sends a 401 Unauthorized error. Here is some code...
loadUser action from store.js
actions: {
async loadUser({ commit, dispatch }) {
if (isLoggedIn()) {
try {
const user = (await Axios.get('/user')).data;
commit('setUser', user);
commit('setLoggedIn', true);
} catch (error) {
dispatch('logout');
}
}
},
}
Route Guard on the routs.js checking to see isLoggedIn (which is just a boolean store locally)
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// if (to.meta.requiresAuth) {
if (isLoggedIn()) {
next();
} else {
next({
name: 'home'
});
}
} else {
next();
}
})
It was pointed out that I had forgotten the withCredetials setting for axios in bootstrap.js. I made this addition but my issue still remains.
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.withCredentials = true;
Route middleware guard on the server side (this is where the request is getting turned away)
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('trucks', 'Api\TruckController');
});
In the laravel cors.php config file I changed the "supports_credentials" from false to true
'supports_credentials' => true,
It seems to me that the cookie information is not being over the api call (but I'm really not sure). This setup is working on my local machine but not on the server that I have deployed to.

Needed to add an environment variable to the .env file for SANCTUM_STATEFUL_DOMAINS and made that equal the domain name.
In the laravel sanctum.php config file...
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),

Related

How to persist laravel sanctum api session despite closing browser?

I'm generating a session cookie in a login component and save a 1 (true) value to a global store.auth composable as well as localStorage.auth:
const login = () => {
axios.get('/sanctum/csrf-cookie')
.then(res => {
axios.post('/login', form)
.then(res => {
store.auth = localStorage.auth = 1
store.signInModal = false
})
.catch(er => {
state.errors = er.response.data.errors
})
})
}
This works great until the browser has been closed and the session cookie has expired. Then, the user still can see the frontend auth elements because localStorage.auth is 1 but he can't interact with the backend because the cookie has expired.
How is it done correctly? I'm not entirely sure, how the session cookie is handled in the http-headers because I don't seem to be able to extract it after login.
I've tried to get a cookie whenever the app is mounted (app.vue):
<script setup>
onMounted(() => {
axios.get('/sanctum/csrf-cookie')
.then(res => {
console.log(res)
})
})
</script>
but I seem to be on the wrong path with that because laravel seems to send a new session cookie. The user is still not able to interact with the backend.
Is persisting the cookie even the right thing to do or should I instead look into auto login users?

NextJS getServerSideProps not sending cookies to server during production

I use getServerSideProps to fetch data so that it is available to the user immediately when a user clicks on a link. Sometimes, some data is protected and only available to authenticated users, so I'll need to send an HttpOnly cookie containing the user's JWT to confirm if the user is authenticated or not. This is one of the examples:
export const getSession = async (context: GetServerSidePropsContext) => {
return axios
.get(process.env.NEXT_PUBLIC_API_URL + "/auth/user", {
withCredentials: true,
headers: {
Cookie: context.req.headers.cookie!,
},
})
.then((response) => Promise.resolve(response))
.catch((error) => {
console.log(error);
return null;
});
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context);
if (!session) {
return {
redirect: {
destination: "/login"
},
props: {},
};
}
return {
props: { session.data },
};
};
This works well in development because both my frontend and backend share the same host (localhost).
However, in production, I host my nextjs app on Vercel and my backend on Heroku. Since they now belong to different domains, the ctx object in getServerSideProps no longer has access to the cookies, causing some parts of the website to break. Is there a way to be able to get access to the cookies, or do I need to set up the backend on Heroku as a subdomain of the frontend site?

How to use Auth::check or Auth::user between different domains?

I've got a question regarding Laravel framework (vers. 5.2) and the authentication. I have 2 domains which represent a software. let's call them https://azure.mydomain.com and https://azuresoftware.mydomain.com.
The Laravel application is hosted on the domain https://azuresoftware.mydomain.com. https://azure.mydomain.com is just a CMS framework which is providing some information about the website.
Now I want to display different menus, if the user is logged in or not on https://azure.mydomain.com. I thought, I can do a fetch request to https://azuresoftware.mydomain.com/ and use the Laravel methods Auth::check() to check if the user is already logged in or not. I know that this is a CORS fetch request, but this is not the issue. I've allowed in the IIS webserver requests from https://azure.mydomain.com. The request works fine and also just a simple request. But actually Auth::check() is always returning false, even when I'm logged in on the software side.
This is my code so far:
<script>
fetch('https://azuresoftware.mydomain.com/checkLogin', {
method: 'GET',
headers: {
'Content-Type': 'text/plain'
}
})
.then(function(res) {
return res.json()
})
.then(function(data) {
if(data.isLoggedIn) {
// do some stuff...
}
else
{
// do some other stuff...
}
});
</script>
routes.php:
Route::group(['middleware' => 'web'], function () {
...
Route::get('checkLogin', function() {
return json_encode(['isLoggedIn'=>\Auth::check()]);
});
...
I'm sure, I forgot something essential, why it is not working this way.
This is due to the fact that AJAX calls only send cookies if the url you're calling is on the same domain as your calling script.
See Cross domain POST request is not sending cookie Ajax Jquery for more information.

Vuejs Laravel Passport - what should I do if access token is expired?

I am using Vuejs SPA with Laravel API as backend. I successfully got the personal access token and store in localStorage and Vuex state like below.
token: localStorage.getItem('token') || '',
expiresAt: localStorage.getItem('expiresAt') || '',
I use the access token every time I send axios request to laravel api. Every thing works well. However, initially the token was set to 1 year expiration so when I develop I didn't care about token being expired and today suddenly I thought what is going to happen if token expired. So I set token expiry to 10 seconds in laravel AuthServiceProvier.php.
Passport::personalAccessTokensExpireIn(Carbon::now()->addSecond(10));
and then I logged in and after 10 seconds, every requests stopped working because the token was expired and got 401 unauthorised error.
In this case, how can I know if the token is expired? I would like to redirect the user to login page if token is expired when the user is using the website.
Be as user friendly as possible. Rather than waiting until the token expires, receiving a 401 error response, and then redirecting, set up a token verification check on the mounted hook of your main SPA instance and have it make a ajax call to e.g. /validatePersonalToken on the server, then do something like this in your routes or controller.
Route::get('/validatePersonalToken', function () {
return ['message' => 'is valid'];
})->middleware('auth:api');
This should return "error": "Unauthenticated" if the token is not valid. This way the user will be directed to authenticate before continuing to use the app and submitting data and then potentially losing work (like submitting a form) which is not very user friendly.
You could potentially do this on a component by component basis rather than the main instance by using a Vue Mixin. This would work better for very short lived tokens that might expire while the app is being used. Put the check in the mounted() hook of the mixin and then use that mixin in any component that makes api calls so that the check is run when that component is mounted. https://v2.vuejs.org/v2/guide/mixins.html
This is what I do. Axios will throw error if the response code is 4xx or 5xx, and then I add an if to check if response status is 401, then redirect to login page.
export default {
methods: {
loadData () {
axios
.request({
method: 'get',
url: 'https://mysite/api/route',
})
.then(response => {
// assign response.data to a variable
})
.catch(error => {
if (error.response.status === 401) {
this.$router.replace({name: 'login'})
}
})
}
}
}
But if you do it like this, you have to copy paste the catch on all axios call inside your programs.
The way I did it is to put the code above to a javascript files api.js, import the class to main.js, and assign it to Vue.prototype.$api
import api from './api'
Object.defineProperty(Vue.prototype, '$api', { value: api })
So that in my component, I just call the axios like this.
this.$api.GET(url, params)
.then(response => {
// do something
})
The error is handled on api.js.
This is my full api.js
import Vue from 'vue'
import axios from 'axios'
import router from '#/router'
let config = {
baseURL : process.env.VUE_APP_BASE_API,
timeout : 30000,
headers : {
Accept : 'application/json',
'Content-Type' : 'application/json',
},
}
const GET = (url, params) => REQUEST({ method: 'get', url, params })
const POST = (url, data) => REQUEST({ method: 'post', url, data })
const PUT = (url, data) => REQUEST({ method: 'put', url, data })
const PATCH = (url, data) => REQUEST({ method: 'patch', url, data })
const DELETE = url => REQUEST({ method: 'delete', url })
const REQUEST = conf => {
conf = { ...conf, ...config }
conf = setAccessTokenHeader(conf)
return new Promise((resolve, reject) => {
axios
.request(conf)
.then(response => {
resolve(response.data)
})
.catch(error => {
outputError(error)
reject(error)
})
})
}
function setAccessTokenHeader (config) {
const access_token = Vue.cookie.get('access_token')
if (access_token) {
config.headers.Authorization = 'Bearer ' + access_token
}
return config
}
/* https://github.com/axios/axios#handling-errors */
function outputError (error) {
if (error.response) {
/**
* The request was made and the server responded with a
* status code that falls out of the range of 2xx
*/
if (error.response.status === 401) {
router.replace({ name: 'login' })
return
}
else {
/* other response status such as 403, 404, 422, etc */
}
}
else if (error.request) {
/**
* The request was made but no response was received
* `error.request` is an instance of XMLHttpRequest in the browser
* and an instance of http.ClientRequest in node.js
*/
}
else {
/* Something happened in setting up the request that triggered an Error */
}
}
export default {
GET,
POST,
DELETE,
PUT,
PATCH,
REQUEST,
}
You could use an interceptor with axios. Catch the 401s and clear the local storage when you do then redirect user to appropriate page.

laravel_token is valid for first request but not subsequent requests

I have installed Laravel Passport and configured it according to the documentation. When calling axios.get from my VueJS file, the first call works as expected. the laravel_session Request Cookie is injected into the request, and the authentication passes, returning the resource.
My problem arises when I try to call the axios.get method again. My use case here is a search function. I'm making a call to /api/banking/accounts/search/{search-term} whenever the user types into a text field, using the code below:
remoteMethod(query) {
if (query !== '') {
this.loading = true;
axios.get(
`/api/banking/accounts/search/${escape(query)}`
).then(res => {
this.destinationAccountDirectory = res.data;
this.loading = false;
});
} else {
this.destinationAccountDirectory = [];
}
},
This code works fine without any auth:api middleware on the route, and for the first time with auth:api middleware. As can be seen from the screenshots below, the laravel_token value changes and is rejected on subsequent calls to the API.
**I've tried to removed the \Laravel\Passport\Http\Middleware\CreateFreshApiToken that was added to the web middleware group during passport installation, which seemed to have temporarily solved the issue, until I receive a 419 on a request shortly after. What could be causing the new laravel_tokens to be rejected? **
I solved this by removing the web middleware from my API route. Why it was there in the first place, I have no idea.
I changed my api.php from
Route::group([
'middleware' => [
'web',
'auth:api']], function() {
Route::post('/banking/transactions', 'TransactionController#store');
Route::get('/banking/accounts', 'BankAccountDirectoryController#index');
Route::get('/accounts/{account}', 'BankAccountDirectoryController#show');
Route::get('/banking/accounts/search/{term?}', 'BankAccountDirectoryController#search');
});
to
Route::group([
'middleware' => [
'auth:api']], function() {
Route::post('/banking/transactions', 'TransactionController#store');
Route::get('/banking/accounts', 'BankAccountDirectoryController#index');
Route::get('/accounts/{account}', 'BankAccountDirectoryController#show');
Route::get('/banking/accounts/search/{term?}', 'BankAccountDirectoryController#search');
});
Should the API routes be under the web group to benefit from the middleware, or is it purely for UI? Am I safe to do this?

Resources