How to persist laravel sanctum api session despite closing browser? - laravel

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?

Related

How to clear local storage when session cookie expires in laravel and vue app

I have a Vue app within a Laravel app. On logging in a user is issued authenticated session cookie by Laravel and after that, I set authenticated to true in the local storage localStorage.setItem("authenticated", "true"). When the session expires authenticated is still true. How can I remove the key when the session expires?
Check session()->get("session_name") in your controller.
You can use this code below, just add in your created() or mounted() option
Check if session expired or not from API and change your localStorage key value;
<script>
export default {
mounted() {
fetch('http://yourserver.com/checkSession').then(async response => {
const status = await response.json();
if(!status.session) {
localStorage.setItem("authenticated", "false")
}
})
}
}
</script>

Cypress cy.visit() is not redirecting to correct URL

I am trying to bypass the UI login by using cy.request() to log a user in and cy.visit() to go to the restricted route. I have followed this doc: https://docs.cypress.io/guides/end-to-end-testing/testing-your-app#Bypassing-your-UI
However, the test fails because the visit URL ('http://localhost:3000/join-or-create') is not loaded and instead the home page URL is loaded (http://localhost:3000/).
This is my test code:
describe('when user is signed in but has not joined a group', () => {
beforeEach(() => {
cy.request('POST', 'http://localhost:5000/api/testing/reset');
const user = {
name: 'Joe Bloggs',
email: 'Joe#Bloggs.com',
password: 'Password',
};
cy.request('POST', 'http://localhost:5000/register', user);
cy.request('POST', 'http://localhost:5000/sign-in', user);
cy.visit('http://localhost:3000/join-or-create');
});
it.only('should allow a logged in user to join or create a group', () => {
cy.contains('Join a group');
cy.contains('Create a group');
});
});
If I change cy.contains('Join a group'); to cy.contains('Welcome'); (which is content on the URL 'http://localhost:3000/') then the test passes.
If I use:
cy.visit('http://localhost:3000');
cy.get('[data-testid="email"]').type('Joe#Bloggs.com');
cy.get('[data-testid="password"]').type('Password');
cy.contains('sign in').click();
instead of cy.visit('http://localhost:3000/join-or-create'); the test passes.
The output of the test body shows that is redirecting to a new URL 'http://localhost:3000/' (as shown in the screenshot below) but I can't figure out why.
Thanks for any help.
In the bypass code, check the response from the POST request
cy.request('POST', 'http://localhost:5000/sign-in', user)
.then(response => console.log(response))
to see what tokens are returned.
Then in the form-login code, look at what happens to the same token after cy.contains('sign in').click() and see where the browser stores same token.
That's probably the step missing in the bypass code. You'll need to add something to do the same, e.g
cy.request('POST', 'http://localhost:5000/sign-in', user)
.then(response => {
const token = response.body.token; // figure out where the token is in response
cy.setCookie(token); // figure out where the app want's the token
})
It's also difficult to tell from the question what URL you need to cy.visit() in the bypass code, but there's only a couple of them so try both permutations.

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?

Laravel Vue SPA using Sanctum response Unauthorized

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')),

Sapper/svelte3 session not synchronizing without page reload

I'm having trouble getting Sapper to synchronize session changes made in my server-side routes without a pageload. My example scenario is that I load my app with no user in the session, my server-side login route sets the user to the session, and I use goto to got to the dashboard.
The problem is that the session argument in the dashboard's preload function isn't populated. If I use window.location.href = '/dashboard', it is, because it's running through Sapper's page_handler. But if I do a client-only redirect, Sapper isn't sending the updated session to the client.
Any way around this? Am I using my tools wrong?
Note: I'm using connect-pg-simple and express-session, setting up sapper like this: sapper.middleware({session: (req, res) => req.session.public}).
I found my answer in the Sapper docs
session contains whatever data was seeded on the server. It is a writable store, meaning you can update it with new data (for example, after the user logs in) and your app will be refreshed.
Reading between the lines, this indicates that your app has to manually synchronize your session data.
The solution here is to manually sync the session data to the client, either with a webhook connection, a response header, or a key in the response data.
I've got a decorator I use to create a server route handler, in which I add the session data to the response. Here's a simplified version:
const createHandler = getData => (req, res) => {
res.status(200).json({data: getData(req.body), session: req.session.public})
}
Obviously there's more to it than that, e.g. error handling, but you get the idea. On the client, I wrap fetch in a helper function that I always use anyway to get my json, set the correct headers, etc. In it, I look at the response, and if there's a session property, I set that to the session store so that it's available in my preloads.
import {stores} from "#sapper/app"
const myFetch = (...args) => fetch(...args).then(r => r.json()).then(body => {
if (body.session) stores().session.set(body.session)
return body.data
})
To put it simply, after your session status changes from the front end (user just logged in, or you just invalidated his login), you should update the session store on the front end.
<script>
import { goto, stores } from '#sapper/app';
const { session } = stores();
const loginBtnHandler = () => {
const req = await fetch('/api/login', {
method: 'POST',
credentials: 'same-origin', // (im using cookies in this example)
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ........ })
});
if (req.ok) {
// here is where you refresh the session on the client right after you log in
$session.loggedIn = true; // or your token or whatever
// next page will properly read the session
goto('/');
return;
}
...
}
</script>

Resources