Cookies are not sent to the server via getServerSideProps, here is the code in the front-end:
export async function getServerSideProps() {
const res = await axios.get("http://localhost:5000/api/auth", {withCredentials: true});
const data = await res.data;
return { props: { data } }
}
On the server I have a strategy that checks the access JWT token.
export class JwtStrategy extends PassportStrategy(Strategy, "jwt") {
constructor() {
super({
ignoreExpiration: false,
secretOrKey: "secret",
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
console.log(request.cookies) // [Object: null prototype] {}
let data = request.cookies['access'];
return data;
}
]),
});
}
async validate(payload: any){
return payload;
}
}
That is, when I send a request via getServerSideProps cookies do not come to the server, although if I send, for example via useEffect, then cookies come normally.
That's because the request inside getServerSideProps doesn't run in the browser - where cookies are automatically sent on every request - but actually gets executed on the server, in a Node.js environment.
This means you need to explicitly pass the cookies to the axios request to send them through.
export async function getServerSideProps({ req }) {
const res = await axios.get("http://localhost:5000/api/auth", {
withCredentials: true,
headers: {
Cookie: req.headers.cookie
}
});
const data = await res.data;
return { props: { data } }
}
The same principle applies to requests made from API routes to external APIs, cookies need to be explicitly passed as well.
export default function handler(req, res) {
const res = await axios.get("http://localhost:5000/api/auth", {
withCredentials: true,
headers: {
Cookie: req.headers.cookie
}
});
const data = await res.data;
res.status(200).json(data)
}
Related
I am using NextJS Request helpers to implement a proxy api.
Upon successful login I set an access_token within the getServerSideProps using cookies-next
export async function getServerSideProps(ctx) {
let login = await CompleteLogin(ctx.query.session);
if (login?.type === 200) {
deleteCookie("token", ctx);
setCookie("access_token", login.data.token, {
secure: true,
httpOnly: true,
res: ctx.res,
req: ctx.req,
expires: new Date(login.data.expiry),
path: "/",
});
}
return {
props: {
...returnProps,
},
};
}
The cookie set's no problem, I can see it being stored.
Subsequent requests then go via the proxy handler /api/[...path].ts, however within the proxy handler I am unable to get the access_token cookie.
import { getCookie } from "cookies-next";
import type { NextApiRequest, NextApiResponse } from "next";
import httpProxyMiddleware from "next-http-proxy-middleware";
export const config = {
api: {
bodyParser: false,
externalResolver: true,
},
};
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const access_token = getCookie("access_token", { req, res });
console.log("token api", access_token); // undefined
httpProxyMiddleware(req, res, {
headers: {
Authorization: `Bearer ${token}`,
},
target: process.env.API_URL,
});
};
export default handler;
Any idea where I am going wrong? I have implement this proxy style before and had no issues so I am assuming I have missed something quite obvious?
I have tried different ways of setting the cookie, custom proxy handler, but the only way I have been able to get it to work is not setting the cookie to be HTTPOnly, which defeats the need of the proxy.
I have a Next.js api like this (/api/events/index.js):
import EventSource from 'eventsource';
import { getAccessToken, withApiAuthRequired } from '#auth0/nextjs-auth0';
export default withApiAuthRequired(async function handler(req, res) {
if (req.method === 'GET') {
const { accessToken } = await getAccessToken(req, res);
res.writeHead(200, {
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream',
});
const eventSource = new EventSource(
`http://localhost:8000/v1/event-stream`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
eventSource.onmessage = (e) => {
res.write(`data: ${e.data}\n\n`);
// res.end();
};
}
});
Essentially, I'm trying to make a request on a backend API SSE endpoint that requires an authorization bearer token (preferably, the access token should be available only on Next.js API side). After an event is received, it should be written in response to of a new event stream.
In React client code, I simply subscribe to these SSE's like this:
const IndexPage = () => {
useEffect(() => {
const eventSource = new EventSource(`http://localhost:3000/api/events`);
eventSource.onmessage = (e) => {
console.log(e);
};
return () => {
eventSource.close();
};
}, []);
return <div></div>;
}
Where's the problem? Nothing is received on the client side.
When I try to call res.end() on the API side, an event is received on the client side, but the event stream keeps reconnecting... like, every second. Basically, the connection is not kept alive. How can I forward the result of my SSE?
I am trying to authorize a Next app using the existing Nodejs backend using a manual JWT strategy.
My backend issues an access token, and I'm trying to sign each request with accessToken using axios.interceptor where I set Bearer ${token}
// utils/axios.ts
const axiosPrivate = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
timeout: 1000,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
});
axiosPrivate.interceptors.request.use(
(config: AxiosRequestConfig): AxiosRequestConfig => {
if (config.headers === undefined) {
config.headers = {};
}
// no way to access sessionStorage or document.cookies
config.headers['Authorization'] = `Bearer ${accessToken}`
return config;
}
);
export { axiosPrivate }
My question is if there is some way to grab access token without Redux?
I want it in the interceptor because it will let me do SSR like that:
// pages/dashboard.tsx
export const getServerSideProps: GetServerSideProps = async (context) => {
const res = await axiosPrivate.get('/dashboard'); // request already signed
return {
props: {
dashboard: res.data,
},
};
};
You can pass the accessToken in the params for the axios request.
<code>await axiosPrivate.get('/dashboard', {
params: {
accessToken
}
});
</code>
After reading the following post, https://dev.to/chrsgrrtt/easy-user-authentication-with-next-js-18oe and consulting the following question Using next-iron-session's "withIronSession" with Next.JS to perform simple authentication, I am still unable to access the session using req.session.get('user'). Below is my implementation in a Next.js project:
Create a util
import {withIronSession} from 'next-iron-session';
const cookie = {
cookieName: process.env.COOKIE_NAME,
password: process.env.COOKIE_SECRET,
cookieOptions: {secure: process.env.NODE_ENV === 'production'},
};
export const guard = (handler) => {
return withIronSession(handler, cookie);
};
export default cookie;
Create an API endpoint
const zlib = require('zlib');
import cookie from '#/utils/cookie';
const fetch = require('node-fetch');
import {withIronSession} from 'next-iron-session';
export default withIronSession(
async (req, res) => {
if (req.method === 'POST') {
try {
const request = await fetch(
process.env.NEXT_PUBLIC_API_BASE_URL + '/api/login',
{
method: 'post',
body: req.body,
headers: {
'Content-Type': 'application/json',
'Origin': req.headers.host || req.headers.origin,
},
}
);
const response = await request.text();
const {success, data, message} = JSON.parse(response);
// set JWT in session
compressor(data, (x) => req.session.set('user', x));
// persist session value
await req.session.save();
// console.log(req.session.get('user'));
return res.status(201).json({success, message});
} catch (error) {
console.log(error);
}
}
return res.status(404).json('Not found');
},
cookie
);
Access session data in a page
export const getServerSideProps = guard(async (ctx) => {
const {req} = ctx;
const session = req.session.get();
console.log({session});
return {redirect: {destination: '/sign-in', permanent: false}};
});
The above terminal log gives an empty object. Is there something am doing wrong??
Try the following:
export const getServerSideProps = guard(async function ({
req,
res,
query,
}) {
//Assuming you have "user" session object
const user = req.session.get("user");
...
});
Harel
Currently, when I authenticate a user with firebase, I store their auth token in localStorage to be used later to connect to my backend like so:
const httpLink = new HttpLink({uri: 'http://localhost:9000/graphql'})
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization token to the headers
const token = localStorage.getItem(AUTH_TOKEN) || null
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : ''
}
})
return forward(operation)
})
const authAfterware = onError(({networkError}) => {
if (networkError.statusCode === 401) AuthService.signout()
})
function createApolloClient() {
return new ApolloClient({
cache: new InMemoryCache(),
link: authMiddleware.concat(authAfterware).concat(httpLink)
})
}
My problem with this is that I have no way to refresh the token once it expires. So I tried to use the following to set the authorization token for apollo:
const httpLink = new HttpLink({uri: 'http://localhost:9000/graphql'})
const asyncAuthLink = setContext(
() => {
return new Promise((success, reject) => {
firebase.auth().currentUser.getToken().then(token => {
success({
headers: {
authorization: token ? `Bearer ${token}` : ''
}
})
}).catch(error => {
reject(error)
})
})
}
)
const authAfterware = onError(({networkError}) => {
if (networkError.statusCode === 401) AuthService.signout()
})
function createApolloClient() {
return new ApolloClient({
cache: new InMemoryCache(),
link: asyncAuthLink.concat(authAfterware.concat(httpLink))
})
}
This works when the user first authenticates, but once the user refreshes the page, firebase is no longer initialized when my graphql queries are sent to my backend, so the token is not sent with it. Is there a way I can asynchronously wait for firebase.auth().currentUser so this will work? Or is there another approach I should take entirely? As far as I know (100% sure) currentUser.getIdToken only makes a network call if the current token is no longer valid. I think this is acceptable as in cases where the token is not valid, the backend can't respond anyway, so I will need to wait for a token refresh to continue.
Some other ideas I thought of:
Continue to use localStorage to store the auth token, refresh it in authAfterware if my backend sends a 401 response back and retry the request.
Set a delay on getting the auth token (not desirable)
Any other ideas?
Thanks!
I know is a bit late but I was stuck on that as well and found a way to solve it. Maybe is not the best one but at least it works.
My approach is to create a Next api endpoint to retrieve the user token using the getUserFromCookies method:
import { NextApiRequest, NextApiResponse } from "next";
import { getUserFromCookies } from "next-firebase-auth";
import initAuth from "../../utils/initAuth";
initAuth();
const handler = async (req: NextApiRequest, res: NextApiResponse<any>) => {
try {
const user = await getUserFromCookies({ req, includeToken: true });
const accessToken = await user.getIdToken();
return res.status(200).json({ success: true, accessToken });
} catch (e) {
console.log(`${e}`);
return res.status(500).json({ error: `Unexpected error. ${e}` });
}
};
export default handler;
And then call this endpoint in the apollo client config like that:
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink, concat } from "#apollo/client";
import { setContext } from "#apollo/client/link/context";
import { relayStylePagination } from "#apollo/client/utilities";
const getUserToken = async () => {
const res = await fetch("http://localhost:3000/api/get-user-token");
const { accessToken } = await res.json();
return accessToken;
};
const asyncAuthLink = setContext(async (request) => {
const token = await getUserToken();
return { ...request, headers: { authorization: token ? `Bearer ${token}` : "" } };
});
const httpLink = new HttpLink({ uri: process.env.NEXT_PUBLIC_API_URL });
const client = new ApolloClient({
name: "web",
version: "1.0",
uri: process.env.NEXT_PUBLIC_API_URL,
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: relayStylePagination(),
},
},
},
}),
link: concat(asyncAuthLink, httpLink),
});
export default client;