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>
Related
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?
I have a page where users can give themselves a "role" like member or admin. They can go to another route to create messages. I am trying to update user's role from "user" to "admin". It updates req.session to admin role in the admin.js file, but when I go to messages/create.js and try to log req.session, it shows that user still has the "user" role. I am saving the changes I make by calling req.session.save(), but it is not working.
admin.js
import { withIronSessionApiRoute } from "iron-session/next";
import nc from "next-connect";
import { session_config } from "../../lib/config";
import Users from "../../models/user";
import { connectToDatabase } from "../../util/mongodb";
const handler = nc()
handler.post(async (req) => {
if (req.body.password === process.env.ADMIN_PASSWORD) {
await connectToDatabase()
await Users.findOneAndUpdate({ name: req.session.user.name }, { role: "admin" })
const updated_user = { name: req.session.user.name, role: "admin" }
req.session.user = updated_user
await req.session.save()
}
})
export default withIronSessionApiRoute(handler, session_config);
messages/create.js
import { withIronSessionApiRoute } from "iron-session/next";
import nc from "next-connect";
import { session_config } from "../../../lib/config";
const handler = nc()
handler.post(async (req) => {
console.log(req.session.user)
console.log(req.body)
})
export default withIronSessionApiRoute(handler, session_config)
Please let me know what the issue is and how I can fix it. Thank you
The first thing I noticed from looking at the code is that you're not sending a response back to the client. Iron session uses cookies to manage stateless authentication and the way it manages is by setting the response header. Because you're not sending a response, it can't update the session.
Looking further into the API documentation, session.save() - "Saves the session and sets the cookie header to be sent once the response is sent."
Not knowing your full implementation or having a working code example from something like codesandbox.io, I suggest the following code to see if this solves your problem.
// please make sure that `res` is a parameter on the `.post()` function
// on your original code. I've already set it as shown below.
handler.post(async (req, res) => {
if (req.body.password === process.env.ADMIN_PASSWORD) {
await connectToDatabase()
await Users.findOneAndUpdate({ name: req.session.user.name }, { role: "admin" })
const updated_user = { name: req.session.user.name, role: "admin" }
req.session.user = updated_user
await req.session.save()
// response below
res.send({ ok: true })
// or if you don't want to send custom data back, comment the line above,
// and then uncomment the line below
// res.status(200).end()
}
})
Attempt 2
I made an iron session demo on Codesandbox using some of the demo code from the iron session repo NextJs example.
The code example shows:
login
log out
setting a user as an admin
fetching user data from server-side
fetching user data from client-side
fetching using SWR
Some side notes to be aware of: if you are doing something like
const sessionData = req.session.user, then trying to mutate the req.session.user, and then sending the data back, it won't work because the session object will be recreated per request and node cannot store req.session as a reference.
If my demo doesn't help you, then you're going to have to share more info and code, and maybe create a Codesandbox to reproduce what is happening to you.
Apollo is not storing the header from the query dynamically.
pages/index.vue
methods: {
fetchCars() {
const token = Cookies.get('XSRF-TOKEN')
console.log(token) // 🟢 Token is shown in console
this.$apollo.query({
query: gql`
query {
cars {
uuid
name
}
}
`,
headers: {
'X-XSRF-TOKEN': token, // â• Fetch without header
},
})
},
},
Is there a way to set the header value new for every Apollo request?
I have a separate Frontend and Backend. For the Frontend I am using Nuxt.js with Apollo. I want to have a session based communication with my server. For this reason I need to send the CSRF-Token with every Request.
Now the problem: On the first load of the page there is no Cookie set on the browser. I do a GET-Request on every initialization of my Nuxt application.
plugins/csrf.js
fetch('http://127.0.0.1:8000/api/csrf-cookie', {
credentials: 'include',
})
Now I have a valid Cookie set on my side and want to communicate with the GraphQL Server but my header is not set dynamically in the query. Does anyone know how I can solve this?
My Laravel Backend is throwing now a 419 Token Mismatch Exception because I did not send a CSRF-Token with my request.
Link to the repository: https://github.com/SuddenlyRust/session-based-auth
[SOLVED] Working solution: https://github.com/SuddenlyRust/session-based-auth/commit/de8fb9c18b00e58655f154f8d0c95a677d9b685b Thanks to the help of kofh in the Nuxt Apollo discord channel 🎉
In order to accomplish this, we need to access the code that gets run every time a fetch happens. This code lives inside your Apollo client's HttpLink. While the #nuxtjs/apollo module gives us many options, we can't quite configure this at such a high level.
Step 1: Creating a client plugin
As noted in the setup section of the Apollo module's docs, we can supply a path to a plugin that will define a clientConfig:
// nuxt.config.js
{
apollo: {
clientConfigs: {
default: '~/plugins/apollo-client.js'
}
}
}
This plugin should export a function which receives the nuxt context. It should return the configuration to be passed to the vue-cli-plugin-apollo's createApolloClient utility. You don't need to worry about that file, but it is how #nuxtjs/apollo creates the client internally.
Step 2: Creating the custom httpLink
In createApolloClient's options, we see we can disable defaultHttpLink and instead supply our own link. link needs to be the output of Apollo's official createHttpLink utility, docs for which can be found here. The option we're most interested in is the fetch option which as the docs state, is
a fetch compatible API for making a request
This boils down to meaning a function that takes uri and options parameters and returns a Promise that represents the network interaction.
Step 3: Creating the custom fetch method
As stated above, we need a function that takes uri and options and returns a promise. This function will be a simple passthrough to the standard fetch method (you may need to add isomorphic-fetch to your dependencies and import it here depending on your setup).
We'll extract your cookie the same as you did in your question, and then set it as a header. The fetch function should look like this:
(uri, options) => {
const token = Cookies.get('XSRF-TOKEN')
options.headers['X-XSRF-TOKEN'] = token
return fetch(uri, options)
}
Putting it all together
Ultimately, your ~/plugins/apollo-client.js file should look something like this:
import { createHttpLink } from 'apollo-link-http'
import fetch from 'isomorphic-fetch'
export default function(context) {
return {
defaultHttpLink: false,
link: createHttpLink({
uri: '/graphql',
credentials: 'include',
fetch: (uri, options) => {
const token = Cookies.get('XSRF-TOKEN')
options.headers['X-XSRF-TOKEN'] = token
return fetch(uri, options)
}
})
}
}
I'm wondering if it is possible to save the state of localStorage across tests.
Mainly because I want to avoid re-authentication on each test. I realize that I can create a command that sends an API request to our backend to avoid going through the auth flow but for various reasons this won't work in my situation.
I am asking if it possible to have a workflow like this:
Go to login page: Authenticate get back response and save session to local storage
Persist local storage somehow...
Go through other tests as authenticated user
You can use the cypress-localstorage-commands package to persist localStorage between tests, so you'll be able to do login only once:
In support/commands.js:
import "cypress-localstorage-commands";
In your tests:
before(() => {
// Do your login stuff here
cy.saveLocalStorage();
});
beforeEach(() => {
cy.restoreLocalStorage();
});
Here's what I ended up doing:
Go to login page: Authenticate
At this point we have data we want to persist between tests in localStorage but we are not allowed to whitelist localStorage.
However, we are allow to whitelist cookies
I have some code like this inside my support/commands.js that act as helpers
const sessionKeys = {
authTokens: 'auth.tokens',
sessionConfig: 'session.config',
};
// The concatenation of username and cid will be the key to set the session
Cypress.Commands.add('persistSession', (key) => {
const authTokens = localStorage.getItem(key);
cy.setCookie(key, authTokens);
});
Cypress.Commands.add('restoreSession', (key) => {
cy.getCookie(key).then(authTokens => {
localStorage.setItem(key, authTokens.value);
});
});
So we call cy.persistSession(key) after we login, which means we have all the authentication saved as cookies which are whitelisted inside of support/index.js with code.
Like this:
Cypress.Cookies.defaults({
whitelist: function(cookie){
// Persist auth stuff
const reAuthTokens = new RegExp('.*auth\.tokens');
if(reAuthTokens.test(cookie.name)){
return true;
}
return false;
}
});
Now anytime we need our auth tokens inside our other tests before running them we cy.restoreSession(key) and we should be good!
Here is the useful link that solves my problem like yours: Preserve cookies through multiple tests
my code like:
const login = () => {
cy.visit('http://0.0.0.0:8080/#/login');
cy.get('#username').type('username');
cy.get('#password').type('1234password$');
cy.get('#login-button').click();
}
describe('UI', () => {
// beforeEach(login);
beforeEach(() => {
login();
Cypress.Cookies.preserveOnce('session_id', 'remember_token');
});
});
hope can help you.
Anything you can do in JS you can do in a cypress test. If you have some way to store creds (auth token, etc.) in local storage, I see no reason why you can't do that. If cypress is clearing out your local storage between tests, you will have to write a beforeEach hook that saves an authenticated token (hard-coded by you) to local storage before each test.
I'm using Sails.js and trying to do a simple login with saving the user information to the session. Here's my code for the login action:
AuthService.login(req.body.email, req.body.password).then(user => {
// save user information in the session
req.session.user = user;
req.session.authenticated = true;
return res.json({
user:user,
token:token
});
}).catch(err => {
sails.log.error(err);
res.json(403, {
callstack: err.callstack,
error: 'Authentication error',
reason : err.reason
});
});
This is my policy for the other pages (after login):
module.exports = function(req, res, next) {
// User is allowed, proceed to the next policy,
if (req.session.authenticated && req.session.user) {
return next();
}
// User is not allowed
return res.forbidden('You are not permitted to perform this action. (Authenticated)');
};
The problem is that the session data is not saved for the first request meaning I'm getting 403 forbidden only after the first successful login. As a result I must then logout and then login again for the session data to appear.
The issue is not limited to the login (with authentication policy) but also applies for any action that requires the session data with or without policy. And occurs for each new session - not only the first time the server goes up.
I thought the problem was due to using memory session so I've also tried to configure Sail.js to work with Redis as the session store by modifying the config/session.js to the following:
adapter: 'redis',
// host: 'localhost',
// port: 6379,
// ttl: 60*60*20,
// db: 0,
// prefix: 'sess:',
I uncommented the adapter line and tried with and without the optional redis connection parameters but then the req.session was declared undefined everywhere
I'm using the connect-mongo adapter and found a similar problem. The reason is because when you use req.session.user = user the background code is saving your session to DB (in my case, Mongo, in yours Redis), and before it finishes, your code executes the res.json part.
To deal with the DB saving's asynchronous code, try to do something like this (haven't tested your code, but doing that to my case, solved the problem):
AuthService.login(req.body.email, req.body.password).then(user => {
// save user information in the session
// this is on memory
req.session.user = user;
req.session.authenticated = true;
// now the session is saved in DB
req.session.save(function() {
return res.json({
user:user,
token:token
});
}
}).catch(err => {
sails.log.error(err);
res.json(403, {
callstack: err.callstack,
error: 'Authentication error',
reason : err.reason
});
});