I'm trying to use this module but I'm having an issue retrieving the token on the server during first SSR render...
Heres the example:
<script lang="ts" setup>
const { csrf } = useCsrf();
console.log(csrf); // prints undefined
const result = await useCsrfFetch("/api/test", {
method: "post",
body: "",
});
console.log("result :: ", result); // error result running on server (Invalid CSRF token, as its undefined.)
const result = await $fetch('/api/test', {
method:'post',
headres:{
'csrf-token': csrf
},
body:''
}); // fails... csrf token invalid...undefined on server
</script>
Basically, CSRF token is always undefined on the server which isn't great... because it means the fetch has to run on the client side. You can see looking at the page data that the server request always fails.
I found the source of the issue looking at the useCsrf() implementation, it hasn't accounted for SSR scenarios and if process.server=true then the composable always returns null for csrf
The next problem is that the cookie is storing only the random secret, and its not currently possible to get the token from the event using useRequestEvent() composable. Additionally, no context, or headers or anything are set in the nitro plugin so that we can retrieve the token server side.
The nitro plugin currently registers the hook render:html unfortunately the html is already rendered so we can't attach anything to the event here as our rendering page wont get it. My proposed solution to get this working would be the following:
The nitro plugin should be like this:
export default defineNitroPlugin((nitroApp) => {
const csrfConfig = useRuntimeConfig().csurf;
const cookieKey = csrfConfig.cookieKey;
nitroApp.hooks.hook("request", async (event)=>{
let secret = getCookie(event, cookieKey);
if (!secret) {
secret = csrf.randomSecret();
setCookie(event, cookieKey, secret, csrfConfig.cookie);
}
const csrfToken = await csrf.create(secret, await useSecretKey(csrfConfig), csrfConfig.encryptAlgorithm);
// Attach the generated token to the event context and we can access it
// In our pages...
event.context.csrfToken = csrfToken;
});
nitroApp.hooks.hook("render:html", async (html, { event }) => {
if(event.context.csrfToken){
return;
}
html.head.push(`<meta name="csrf-token" content="${event.context.csrfToken}">`);
});
});
Then we can create something like this:
function useCsrfPatched() {
if (process.client) {
return useCsrf();
} else {
return { csrf: useRequestEvent()?.context?.csrfToken };
}
}
At the user level it's not even possible to fix this, for example:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("render:html", async (html, { event }) => {
const csrfTokenMeta = html.head.find((head) =>
head.includes('name="csrf-token"')
);
const match = csrfTokenMeta && csrfTokenMeta.match('content="(.*)"');
if (match) {
const [_, csrfToken] = match;
// Context is set AFTER page has been rendered, so never accessible in pages..
event.context.csrf = csrfToken;
}
});
});
This was probably never encountered, because quite often POST requests aren't being made from server side to a CSRF protected route, typically GET requests are issued which aren't commonly CSRF protected. However, I can see this has use cases for instance:
@Morgbn
I'm trying to use this module but I'm having an issue retrieving the token on the server during first SSR render...
Heres the example:
Basically, CSRF token is always undefined on the server which isn't great... because it means the fetch has to run on the client side. You can see looking at the page data that the server request always fails.
I found the source of the issue looking at the
useCsrf()
implementation, it hasn't accounted for SSR scenarios and ifprocess.server=true
then the composable always returnsnull
forcsrf
The next problem is that the cookie is storing only the random
secret,
and its not currently possible to get the token from the event usinguseRequestEvent()
composable. Additionally, no context, or headers or anything are set in the nitro plugin so that we can retrieve the token server side.The nitro plugin currently registers the hook
render:html
unfortunately the html is already rendered so we can't attach anything to the event here as our rendering page wont get it. My proposed solution to get this working would be the following:The nitro plugin should be like this:
Then we can create something like this:
Which would allow this to work:
At the user level it's not even possible to fix this, for example:
This was probably never encountered, because quite often
POST
requests aren't being made from server side to a CSRF protected route, typicallyGET
requests are issued which aren't commonly CSRF protected. However, I can see this has use cases for instance:useFetch('/pageView, { body:SomeUserContext, method:'post', headers: { csrf } })